From eb61fd0dd821bff1d138703a17bcec9678c49aa1 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Fri, 18 Oct 2019 22:52:25 +0300 Subject: [PATCH 01/31] Test and merge feature branch (#52) * Added indication of connected/disconnected copters * Added client removing functionality * Option for automatically remove disconnected copters from table * Renaming copters from QT server table on the go + some improvements --- Drone/FlightLib/FlightLib.py | 2 +- Drone/client.py | 46 ++++-- Drone/client_config.ini | 1 + Drone/copter_client.py | 167 +++++++++++++++++---- Server/copter_table_models.py | 226 ++++++++++++++++++++++------- Server/server.py | 53 ++++--- Server/server_config.ini | 1 + Server/server_gui.py | 6 +- Server/server_gui.ui | 8 +- Server/server_qt.py | 165 ++++++++++++++------- builder/assets/clever-show.service | 4 +- messaging_lib.py | 41 +++++- 12 files changed, 533 insertions(+), 187 deletions(-) diff --git a/Drone/FlightLib/FlightLib.py b/Drone/FlightLib/FlightLib.py index b7dae37..deae299 100644 --- a/Drone/FlightLib/FlightLib.py +++ b/Drone/FlightLib/FlightLib.py @@ -69,7 +69,7 @@ def check(check_name): failures = f(*args, **kwargs) msgs = [] for failure in failures: - msg = "[{}]: Failure: {}".format(check_name, failure) + msg = "[{}]: Err: {}".format(check_name, failure) msgs.append(msg) logger.warning(msg) diff --git a/Drone/client.py b/Drone/client.py index 2b6664b..aed8dc4 100644 --- a/Drone/client.py +++ b/Drone/client.py @@ -47,6 +47,8 @@ class Client(object): global active_client active_client = self + # self._last_ping_time = 0 + def load_config(self): self.config.read(self.config_path) @@ -58,14 +60,16 @@ class Client(object): self.NTP_HOST = self.config.get('NTP', 'host') self.NTP_PORT = self.config.getint('NTP', 'port') - self.files_directory = self.config.get('FILETRANSFER', 'files_directory') + self.files_directory = self.config.get('FILETRANSFER', 'files_directory') # not used?! self.client_id = self.config.get('PRIVATE', 'id') - if self.client_id == 'default': + if self.client_id == '/default': self.client_id = 'copter' + str(random.randrange(9999)).zfill(4) - self.write_config(False, 'PRIVATE', 'id', self.client_id) + self.write_config(False, ConfigOption('PRIVATE', 'id', self.client_id)) elif self.client_id == '/hostname': self.client_id = socket.gethostname() + elif self.client_id == '/ip': + self.client_id = messaging.get_ip_address() def rewrite_config(self): with open(self.config_path, 'w') as file: @@ -103,7 +107,7 @@ class Client(object): try: while True: self._reconnect() - #self._process_connections() + self._process_connections() except (KeyboardInterrupt, ): logger.critical("Caught interrupt, exiting!") @@ -117,6 +121,8 @@ class Client(object): try: self.client_socket = socket.socket() self.client_socket.settimeout(timeout) + self.client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + self.client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.client_socket.connect((self.server_host, self.server_port)) except socket.error as error: if isinstance(error, OSError): @@ -138,15 +144,12 @@ class Client(object): self.broadcast_bind(timeout*2, attempt_limit) attempt_count = 0 - def _connect(self): self.connected = True self.client_socket.setblocking(False) events = selectors.EVENT_READ # | selectors.EVENT_WRITE self.selector.register(self.client_socket, events, data=self.server_connection) self.server_connection.connect(self.selector, self.client_socket, (self.server_host, self.server_port)) - self._process_connections() - def broadcast_bind(self, timeout=3.0, attempt_limit=5): broadcast_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -187,6 +190,9 @@ class Client(object): def _process_connections(self): while True: events = self.selector.select(timeout=1) + # if time.time() - self._last_ping_time > 5: + # self.server_connection.send_message("ping") + # self._last_ping_time = time.time() # logging.debug("tick") for key, mask in events: # TODO add notifier to client! connection = key.data @@ -200,7 +206,7 @@ class Client(object): logger.error( "Exception {} occurred for {}! Resetting connection!".format(error, connection.addr) ) - self.server_connection.close() + self.server_connection._close() self.connected = False if isinstance(error, OSError): @@ -213,20 +219,28 @@ class Client(object): return -@messaging.request_callback("id") -def _response_id(): - return active_client.client_id - -@messaging.request_callback("time") -def _response_time(): - return active_client.time_now() - @messaging.message_callback("config_write") def _command_config_write(*args, **kwargs): options = [ConfigOption(**raw_option) for raw_option in kwargs["options"]] logger.info("Writing config options: {}".format(options)) active_client.write_config(kwargs["reload"], *options) + +@messaging.request_callback("id") +def _response_id(*args, **kwargs): + new_id = kwargs.get("new_id", None) + if new_id is not None: + cfg = ConfigOption("PRIVATE", "id", new_id) + active_client.write_config(True, cfg) + + return active_client.client_id + + +@messaging.request_callback("time") +def _response_time(*args, **kwargs): + return active_client.time_now() + + if __name__ == "__main__": client = Client() client.start() diff --git a/Drone/client_config.ini b/Drone/client_config.ini index 697fbfe..4fa87e1 100644 --- a/Drone/client_config.ini +++ b/Drone/client_config.ini @@ -41,6 +41,7 @@ yaw = -90 [PRIVATE] id = /hostname +restart_dhcpcd = True use_leds = True led_pin = 21 x0 = 0 diff --git a/Drone/copter_client.py b/Drone/copter_client.py index 92cd73b..62d4a73 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -56,6 +56,8 @@ class CopterClient(client.Client): self.USE_LEDS = self.config.getboolean('PRIVATE', 'use_leds') self.LED_PIN = self.config.getint('PRIVATE', 'led_pin') + self.RESTART_DHCPCD = self.config.getboolean('PRIVATE', 'restart_dhcpcd') + def on_broadcast_bind(self): configure_chrony_ip(self.server_host) restart_service("chrony") @@ -97,6 +99,9 @@ class CopterClient(client.Client): def restart_service(name): os.system("systemctl restart {}".format(name)) +def execute_command(command): + os.system(command) + def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1): try: with open(path, 'r') as f: @@ -130,8 +135,114 @@ def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1): return True +def configure_hostname(hostname): + path = "/etc/hostname" + try: + with open(path, 'r') as f: + raw_content = f.read() + except IOError as e: + print("Reading error {}".format(e)) + return False + + current_hostname = str(raw_content) + + if current_hostname != hostname: + content = hostname + '\n' + try: + with open(path, 'w') as f: + f.write(content) + except IOError: + print("Error writing") + return False + + return True + + +def configure_hosts(hostname): + path = "/etc/hosts" + try: + with open(path, 'r') as f: + raw_content = f.read() + except IOError as e: + print("Reading error {}".format(e)) + return False + + index_start = raw_content.find("127.0.1.1", ) + index_stop = raw_content.find("\n", index_start) + + _ip, current_hostname = raw_content[index_start:index_stop].split() + if current_hostname != hostname: + content = raw_content[:index_start] + "{} {}".format(_ip, hostname) + raw_content[index_stop:] + try: + with open(path, 'w') as f: + f.write(content) + except IOError: + print("Error writing") + return False + + return True + +def configure_motd(hostname): + with open("/etc/motd", "w") as f: + f.write("\r\n{}\r\n\r\n".format(hostname)) + +def configure_bashrc(hostname): + path = "/home/pi/.bashrc" + try: + with open(path, 'r') as f: + raw_content = f.read() + except IOError as e: + print("Reading error {}".format(e)) + return False + + index_start = raw_content.find("ROS_HOSTNAME='", ) + 14 + index_stop = raw_content.find("'", index_start) + + current_hostname = raw_content[index_start:index_stop] + if current_hostname != hostname: + content = raw_content[:index_start] + hostname + raw_content[index_stop:] + try: + with open(path, 'w') as f: + f.write(content) + except IOError: + print("Error writing") + return False + + return True + +@messaging.message_callback("execute") +def _execute(*args, **kwargs): + command = kwargs.get("command", None) + if command: + execute_command(command) + +@messaging.message_callback("id") +def _response_id(*args, **kwargs): + new_id = kwargs.get("new_id", None) + if new_id is not None: + old_id = client.active_client.client_id + if new_id != old_id: + cfg = client.ConfigOption("PRIVATE", "id", new_id) + client.active_client.write_config(True, cfg) + if new_id != '/hostname': + if client.active_client.RESTART_DHCPCD: + hostname = client.active_client.client_id + configure_hostname(hostname) + configure_hosts(hostname) + configure_bashrc(hostname) + configure_motd(hostname) + execute_command("reboot") + #execute_command("hostname {}".format(hostname)) + #restart_service("dhcpcd") + #restart_service("avahi-daemon") + #restart_service("smbd") + #restart_service("roscore") + #restart_service("clever") + restart_service("clever-show") + + @messaging.request_callback("selfcheck") -def _response_selfcheck(): +def _response_selfcheck(*args, **kwargs): if check_state_topic(wait_new_status=True): check = FlightLib.selfcheck() return check if check else "OK" @@ -141,7 +252,7 @@ def _response_selfcheck(): @messaging.request_callback("anim_id") -def _response_animation_id(): +def _response_animation_id(*args, **kwargs): # Load animation result = animation.get_id() if result != 'No animation': @@ -163,7 +274,7 @@ def _response_animation_id(): return result @messaging.request_callback("batt_voltage") -def _response_batt(): +def _response_batt(*args, **kwargs): if check_state_topic(wait_new_status=True): return FlightLib.get_telemetry('body').voltage else: @@ -172,7 +283,7 @@ def _response_batt(): @messaging.request_callback("cell_voltage") -def _response_cell(): +def _response_cell(*args, **kwargs): if check_state_topic(wait_new_status=True): return FlightLib.get_telemetry('body').cell_voltage else: @@ -180,37 +291,37 @@ def _response_cell(): return float('nan') @messaging.request_callback("sys_status") -def _response_sys_status(): +def _response_sys_status(*args, **kwargs): return get_sys_status() @messaging.request_callback("cal_status") -def _response_cal_status(): +def _response_cal_status(*args, **kwargs): return get_calibration_status() @messaging.request_callback("position") -def _response_position(): +def _response_position(*args, **kwargs): telem = FlightLib.get_telemetry(client.active_client.FRAME_ID) return "{:.2f} {:.2f} {:.2f} {:.1f} {}".format( telem.x, telem.y, telem.z, math.degrees(telem.yaw), client.active_client.FRAME_ID) @messaging.request_callback("calibrate_gyro") -def _calibrate_gyro(): +def _calibrate_gyro(*args, **kwargs): calibrate('gyro') return get_calibration_status() @messaging.request_callback("calibrate_level") -def _calibrate_level(): +def _calibrate_level(*args, **kwargs): calibrate('level') return get_calibration_status() @messaging.message_callback("test") -def _command_test(**kwargs): +def _command_test(*args, **kwargs): logger.info("logging info test") print("stdout test") @messaging.message_callback("move_start") -def _command_move_start_to_current_position(**kwargs): +def _command_move_start_to_current_position(*args, **kwargs): # Load animation frames = animation.load_animation(os.path.abspath("animation.csv"), x0=client.active_client.X0_COMMON, @@ -232,7 +343,7 @@ def _command_move_start_to_current_position(**kwargs): print ("Start delta: {:.2f} {:.2f}".format(client.active_client.X0, client.active_client.Y0)) @messaging.message_callback("reset_start") -def _command_reset_start(**kwargs): +def _command_reset_start(*args, **kwargs): client.active_client.config.set('PRIVATE', 'x0', 0) client.active_client.config.set('PRIVATE', 'y0', 0) client.active_client.rewrite_config() @@ -240,7 +351,7 @@ def _command_reset_start(**kwargs): print ("Reset start to {:.2f} {:.2f}".format(client.active_client.X0, client.active_client.Y0)) @messaging.message_callback("set_z_to_ground") -def _command_set_z(**kwargs): +def _command_set_z(*args, **kwargs): telem = FlightLib.get_telemetry(client.active_client.FRAME_ID) client.active_client.config.set('PRIVATE', 'z0', telem.z) client.active_client.rewrite_config() @@ -248,7 +359,7 @@ def _command_set_z(**kwargs): print ("Set z offset to {:.2f}".format(client.active_client.Z0)) @messaging.message_callback("reset_z_offset") -def _command_reset_z(**kwargs): +def _command_reset_z(*args, **kwargs): client.active_client.config.set('PRIVATE', 'z0', 0) client.active_client.rewrite_config() client.active_client.load_config() @@ -256,36 +367,36 @@ def _command_reset_z(**kwargs): @messaging.message_callback("update_repo") -def _command_update_repo(**kwargs): +def _command_update_repo(*args, **kwargs): os.system("git reset --hard origin/master") os.system("git fetch") os.system("git pull") os.system("chown -R pi:pi ~/CleverSwarm") @messaging.message_callback("reboot_fcu") -def _command_reboot(): +def _command_reboot(*args, **kwargs): reboot_fcu() @messaging.message_callback("service_restart") -def _command_service_restart(**kwargs): +def _command_service_restart(*args, **kwargs): restart_service(kwargs["name"]) @messaging.message_callback("repair_chrony") -def _command_chrony_repair(): +def _command_chrony_repair(*args, **kwargs): configure_chrony_ip(client.active_client.server_host) restart_service("chrony") @messaging.message_callback("led_test") -def _command_led_test(**kwargs): +def _command_led_test(*args, **kwargs): LedLib.chase(255, 255, 255) time.sleep(2) LedLib.off() @messaging.message_callback("led_fill") -def _command_led_fill(**kwargs): +def _command_led_fill(*args, **kwargs): r = kwargs.get("red", 0) g = kwargs.get("green", 0) b = kwargs.get("blue", 0) @@ -294,11 +405,11 @@ def _command_led_fill(**kwargs): @messaging.message_callback("flip") -def _copter_flip(): +def _copter_flip(*args, **kwargs): FlightLib.flip(frame_id=client.active_client.FRAME_ID) @messaging.message_callback("takeoff") -def _command_takeoff(**kwargs): +def _command_takeoff(*args, **kwargs): task_manager.add_task(time.time(), 0, animation.takeoff, task_kwargs={ "z": client.active_client.TAKEOFF_HEIGHT, @@ -310,7 +421,7 @@ def _command_takeoff(**kwargs): @messaging.message_callback("land") -def _command_land(**kwargs): +def _command_land(*args, **kwargs): task_manager.reset() task_manager.add_task(0, 0, animation.land, task_kwargs={ @@ -323,7 +434,7 @@ def _command_land(**kwargs): @messaging.message_callback("disarm") -def _command_disarm(**kwargs): +def _command_disarm(*args, **kwargs): task_manager.reset() task_manager.add_task(-5, 0, FlightLib.arming_wrapper, task_kwargs={ @@ -333,22 +444,22 @@ def _command_disarm(**kwargs): @messaging.message_callback("stop") -def _command_stop(**kwargs): +def _command_stop(*args, **kwargs): task_manager.reset() @messaging.message_callback("pause") -def _command_pause(**kwargs): +def _command_pause(*args, **kwargs): task_manager.pause() @messaging.message_callback("resume") -def _command_resume(**kwargs): +def _command_resume(*args, **kwargs): task_manager.resume(time_to_start_next_task=kwargs.get("time", 0)) @messaging.message_callback("start") -def _play_animation(**kwargs): +def _play_animation(*args, **kwargs): start_time = float(kwargs["time"]) # Check if animation file is available if animation.get_id() == 'No animation': diff --git a/Server/copter_table_models.py b/Server/copter_table_models.py index b5f9e53..9b85d6b 100644 --- a/Server/copter_table_models.py +++ b/Server/copter_table_models.py @@ -1,32 +1,72 @@ import sys import re import collections +import indexed +from server import ConfigOption from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import Qt as Qt +ModelDataRole = 998 +ModelStateRole = 999 + + class CopterData: - class_attrs = collections.OrderedDict([('copter_id', None), ('anim_id', None), ('batt_v', None), ('batt_p', None), - ('sys_status', None), ('cal_status', None), ('selfcheck', None), ('position', None), ("time_delta", None), - ("client", None), ("checked", 0)], ) + class_basic_attrs = indexed.IndexedOrderedDict([('copter_id', None), ('anim_id', None), + ('batt_v', None), ('batt_p', None), + ('sys_status', None), ('cal_status', None), ('selfcheck', None), + ('position', None), ("time_delta", None), + ("client", None), ]) def __init__(self, **kwargs): - self.attrs = self.class_attrs.copy() - self.attrs.update(kwargs) + self.attrs_dict = self.class_basic_attrs.copy() + self.attrs_dict.update(kwargs) - for attr, value in self.attrs.items(): + for attr, value in self.attrs_dict.items(): setattr(self, attr, value) def __getitem__(self, key): - return getattr(self, list(self.attrs.keys())[key]) + return getattr(self, self.attrs_dict.keys()[key]) def __setitem__(self, key, value): - setattr(self, list(self.attrs.keys())[key], value) + setattr(self, self.attrs_dict.keys()[key], value) + + +class StatedCopterData(CopterData): + class_basic_states = indexed.IndexedOrderedDict([("checked", 0), ("selfchecked", None), ("takeoff_ready", None), + ("copter_id", True), ]) + + def __init__(self, **kwargs): + self.states = CopterData(**self.class_basic_states) + + super(StatedCopterData, self).__init__(**kwargs) + + def __setattr__(self, key, value): + self.__dict__[key] = value + + if key in self.class_basic_attrs.keys(): + try: + self.states.__dict__[key] = \ + Checks.all_checks[self.attrs_dict.keys().index(key)](value) + 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.__dict__["takeoff_ready"] = all( + [self.states[i] for i in Checks.takeoff_checklist] + ) + + +class Checks: + all_checks = {} + takeoff_checklist = (2, 3, 4, 5, 6) class CopterDataModel(QtCore.QAbstractTableModel): - checks = {} selected_ready_signal = QtCore.pyqtSignal(bool) selected_takeoff_ready_signal = QtCore.pyqtSignal(bool) selected_flip_ready_signal = QtCore.pyqtSignal(bool) @@ -35,8 +75,12 @@ class CopterDataModel(QtCore.QAbstractTableModel): def __init__(self, parent=None): super(CopterDataModel, self).__init__(parent) - self.headers = ('copter ID', ' animation ID ', 'batt V', 'batt %', ' system ', 'calibration', 'selfcheck', 'current x y z yaw frame_id', 'time delta') + self.headers = ('copter ID', ' animation ID ', 'batt V', 'batt %', ' system ', + 'calibration', 'selfcheck', 'current x y z yaw frame_id', 'time delta') self.data_contents = [] + + self.on_id_changed = None + self.first_col_is_checked = False def insertRows(self, contents, position='last', parent=QtCore.QModelIndex()): @@ -48,20 +92,28 @@ class CopterDataModel(QtCore.QAbstractTableModel): self.endInsertRows() - def user_selected(self): - return filter(lambda x: x.checked == Qt.Checked, self.data_contents) + def removeRows(self, position, rows=1, index=QtCore.QModelIndex()): + self.beginRemoveRows(QtCore.QModelIndex(), position, position + rows - 1) + self.data_contents = self.data_contents[:position] + self.data_contents[position + rows:] + self.endRemoveRows() + + return True + + 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: all_checks(x), contents) + return filter(lambda x: x.states.selfchecked, contents) def takeoff_ready(self, contents=()): contents = contents or self.data_contents - return filter(lambda x: takeoff_checks(x), contents) + return filter(lambda x: x.states.takeoff_ready, contents) def flip_ready(self, contents=()): contents = contents or self.data_contents - return filter(lambda x: flip_checks(x), contents) + return filter(lambda x: flip_checks(x), contents) # possibly change as takeoff checks def calibrating(self, contents=()): contents = contents or self.data_contents @@ -71,6 +123,22 @@ class CopterDataModel(QtCore.QAbstractTableModel): contents = contents or self.data_contents return filter(lambda x: calibration_ready_check(x), contents) + def get_row_index(self, row_data): + try: + index = 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)) + except StopIteration: + return None + else: + return row_data + def rowCount(self, n=None): return len(self.data_contents) @@ -85,15 +153,19 @@ class CopterDataModel(QtCore.QAbstractTableModel): 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 "" + 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 "" + elif role == ModelDataRole: + return self.data_contents[row][col] elif role == Qt.BackgroundRole: - if col in self.checks.keys(): - item = self.data_contents[row][col] - result = self.checks[col](item) + try: + item = self.data_contents[row] + result = item.states[col] + except KeyError: + return QtGui.QBrush(Qt.white) + else: if result is None: return QtGui.QBrush(Qt.yellow) if result: @@ -102,61 +174,81 @@ class CopterDataModel(QtCore.QAbstractTableModel): return QtGui.QBrush(Qt.red) elif role == Qt.CheckStateRole and col == 0: - return self.data_contents[row].checked + return self.data_contents[row].states.checked if role == QtCore.Qt.TextAlignmentRole and col != 0: return QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter - def update_model(self, index=QtCore.QModelIndex()): - #self.modelReset.emit() - self.selected_ready_signal.emit(set(self.user_selected()).issubset(self.selfchecked_ready())) - self.selected_takeoff_ready_signal.emit(set(self.user_selected()).issubset(self.takeoff_ready())) - self.selected_flip_ready_signal.emit(set(self.user_selected()).issubset(self.flip_ready())) - self.selected_calibrating_signal.emit(set(self.user_selected()).issubset(self.calibrating())) - self.selected_calibration_ready_signal.emit(set(self.user_selected()).issubset(self.calibration_ready())) - self.dataChanged.emit(index, index, (QtCore.Qt.EditRole,)) + 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.dataChanged.emit(index, index, (role,)) @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 + col = index.column() + row = index.row() - elif role == Qt.EditRole: - self.data_contents[index.row()][index.column()] = value - self.update_model(index) + if role == Qt.CheckStateRole: + self.data_contents[row].states.checked = value + elif role == Qt.EditRole: # For user actions with data + if col == 0: # and self.on_id_changed: + #self.data_contents[row][col] = "Awaiting for response" + #self.data_contents[row].states.copter_id = None + + self.data_contents[row].client.send_message("id", {"new_id": value}) + self.data_contents[row].client.remove() + else: + self.data_contents[row][col] = value + + elif role == ModelDataRole: # For inner setting\editing of data + self.data_contents[row][col] = value + elif role == ModelStateRole: + self.data_contents[row].states[col] = value else: return False + self.update_model(index, role) return True def select_all(self): self.first_col_is_checked = not self.first_col_is_checked - for copter in self.data_contents: - copter.checked = int(self.first_col_is_checked)*2 - for row in range(len(self.data_contents)): - self.update_model(self.index(row, 0)) + for row_num, copter in enumerate(self.data_contents): + 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 + roles |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable return roles - @QtCore.pyqtSlot(int, int, QtCore.QVariant) - def update_item(self, row, col, value): - self.setData(self.index(row, col), value) + @QtCore.pyqtSlot(int, int, QtCore.QVariant, QtCore.QVariant) + 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): self.insertRows([client]) + @QtCore.pyqtSlot(int) + def remove_client(self, row): + self.removeRows(row) + def col_check(col): def inner(f): - CopterDataModel.checks[col] = f + Checks.all_checks[col] = f def wrapper(*args, **kwargs): return f(*args, **kwargs) @@ -172,42 +264,49 @@ def check_anim(item): return None return str(item) != 'No animation' + @col_check(2) def check_bat_v(item): if not item: return None return float(item) > 3.2 + @col_check(3) def check_bat_p(item): if not item: return None return float(item) > 30 + @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_selfcheck(item): if not item: return None return item == "OK" + @col_check(7) def check_cal_status(item): if not item: return None return True + @col_check(8) def check_time_delta(item): if not item: @@ -216,35 +315,40 @@ def check_time_delta(item): def all_checks(copter_item): - for col, check in CopterDataModel.checks.items(): + for col, check in Checks.all_checks.items(): if not check(copter_item[col]): return False return True + def takeoff_checks(copter_item): - for i in range(5): - if not CopterDataModel.checks[2+i](copter_item[2+i]): + for col in Checks.takeoff_checklist: + if not Checks.all_checks[col](copter_item[col]): return False return True + def flip_checks(copter_item): - for i in range(5): - if 2+i != 4: - if not CopterDataModel.checks[2+i](copter_item[2+i]): + for col in Checks.takeoff_checklist: + if col != 4: + if not Checks.all_checks[col](copter_item[col]): return False else: if copter_item[4] != "ACTIVE": return False return True + def calibrating_check(copter_item): return copter_item[5] == "CALIBRATING" + def calibration_ready_check(copter_item): - if not CopterDataModel.checks[4](copter_item[4]): + if not Checks.all_checks[4](copter_item[4]): return False return not calibrating_check(copter_item) + class CopterProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, parent=None): super(CopterProxyModel, self).__init__(parent) @@ -265,8 +369,9 @@ class CopterProxyModel(QtCore.QSortFilterProxyModel): class SignalManager(QtCore.QObject): - update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant) + update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant, QtCore.QVariant) add_client_signal = QtCore.pyqtSignal(object) + remove_client_signal = QtCore.pyqtSignal(int) if __name__ == '__main__': @@ -296,9 +401,18 @@ if __name__ == '__main__': 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")) + + msgs = [] + msg = "[{}]: Failure: {}".format("FCU connection", "Angular velocities estimation is not available") + msgs.append(msg) + msg = "[{}]: Failure: {}".format("FCU connection1", "Angular velocities estimation is not available") + msgs.append(msg) + 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.setData(myModel.index(0, 1), "test") diff --git a/Server/server.py b/Server/server.py index e19ba2d..311b924 100644 --- a/Server/server.py +++ b/Server/server.py @@ -28,9 +28,7 @@ logging.basicConfig( # TODO all prints as logs ConfigOption = collections.namedtuple("ConfigOption", ["section", "option", "value"]) -class Server: - BUFFER_SIZE = 1024 - +class Server(messaging.Singleton): def __init__(self, server_id=None, config_path="server_config.ini", on_stop=None): self.id = server_id if server_id else str(random.randint(0, 9999)).zfill(4) self.time_started = 0 @@ -41,8 +39,11 @@ class Server: self.sel = selectors.DefaultSelector() self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + self.server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.host = socket.gethostname() - self.ip = Server.get_ip_address() + self.ip = messaging.get_ip_address() # Init configs self.config_path = config_path @@ -65,7 +66,9 @@ class Server: def load_config(self): self.config.read(self.config_path) self.port = int(self.config['SERVER']['port']) # TODO try, init def - Server.BUFFER_SIZE = int(self.config['SERVER']['buffer_size']) + self.BUFFER_SIZE = int(self.config['SERVER']['buffer_size']) # TODO connect to connection manager + + self.remove_disconnected = self.config.getboolean('SERVER', 'remove_disconnected') self.use_broadcast = self.config.getboolean('BROADCAST', 'use_broadcast') self.broadcast_port = int(self.config['BROADCAST']['broadcast_port']) @@ -112,16 +115,6 @@ class Server: sys.exit("Stopped") - @staticmethod - def get_ip_address(): - try: - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as ip_socket: - ip_socket.connect(("8.8.8.8", 80)) - return ip_socket.getsockname()[0] - except OSError: - logging.warning("No network connection detected, starting on localhost") - return "localhost" - @staticmethod def get_ntp_time(ntp_host, ntp_port): NTP_DELTA = 2208988800 # 1970-01-01 00:00:00 @@ -149,7 +142,7 @@ class Server: while self.client_processor_thread_running.is_set(): events = self.sel.select() - logging.error('tick') + #logging.error('tick') for key, mask in events: # logging.error(mask) # logging.error(str(key.data)) @@ -161,7 +154,7 @@ class Server: client.process_events(mask) except Exception as error: logging.error("Exception {} occurred for {}! Resetting connection!".format(error, client.addr)) - client.close() + client.close(True) else: # Notifier client.process_events(mask) @@ -218,7 +211,7 @@ class Server: try: while self.listener_thread_running.is_set(): - data, addr = broadcast_client.recvfrom(1024) # TODO nonblock + data, addr = broadcast_client.recvfrom(1024) # TODO nonblock message = messaging.MessageManager() message.income_raw = data message.process_message() @@ -301,16 +294,28 @@ class Client(messaging.ConnectionManager): def _got_id(self, value): logging.info("Got copter id: {} for client {}".format(value, self.addr)) self.copter_id = value - if Client.on_first_connect: - Client.on_first_connect(self) + if self.on_first_connect: + self.on_first_connect(self) - def close(self): + def close(self, inner=False): self.connected = False - if Client.on_disconnect: - Client.on_disconnect(self) + if self.on_disconnect: + self.on_disconnect(self) - super(Client, self).close() + if inner: + super(Client, self)._close() + else: + super(Client, self).close() + + logging.info("Connection to {} closed!".format(self.copter_id)) + + def remove(self): + if self.connected: + self.close() + if self.clients: + self.clients.pop(self.addr[0]) + logging.info("Client {} successfully removed!".format(self.copter_id)) @requires_connect def _send(self, data): diff --git a/Server/server_config.ini b/Server/server_config.ini index 0614208..5160e2d 100644 --- a/Server/server_config.ini +++ b/Server/server_config.ini @@ -1,6 +1,7 @@ [SERVER] port = 25000 buffer_size = 1024 +remove_disconnected = True [BROADCAST] use_broadcast = True diff --git a/Server/server_gui.py b/Server/server_gui.py index a26363c..794d524 100644 --- a/Server/server_gui.py +++ b/Server/server_gui.py @@ -167,7 +167,7 @@ class Ui_MainWindow(object): self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1220, 25)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1220, 26)) self.menubar.setObjectName("menubar") self.menuOptions = QtWidgets.QMenu(self.menubar) self.menuOptions.setObjectName("menuOptions") @@ -222,6 +222,8 @@ class Ui_MainWindow(object): self.actionSend_any_command.setObjectName("actionSend_any_command") self.action_stop_music = QtWidgets.QAction(MainWindow) self.action_stop_music.setObjectName("action_stop_music") + self.action_remove_row = QtWidgets.QAction(MainWindow) + self.action_remove_row.setObjectName("action_remove_row") self.action_send_calibrations = QtWidgets.QAction(MainWindow) self.action_send_calibrations.setObjectName("action_send_calibrations") self.menuDeveloper_mode.addAction(self.action_send_any_file) @@ -244,6 +246,7 @@ class Ui_MainWindow(object): self.menuDrone.addAction(self.action_reset_z_offset) self.menuDrone.addSeparator() self.menuDrone.addAction(self.menuDeveloper_mode_2.menuAction()) + self.menuDrone.addAction(self.action_remove_row) self.menuMusic.addAction(self.action_select_music_file) self.menuMusic.addAction(self.action_play_music) self.menuMusic.addAction(self.action_stop_music) @@ -305,4 +308,5 @@ class Ui_MainWindow(object): self.action_send_any_file.setText(_translate("MainWindow", "Send any file")) self.actionSend_any_command.setText(_translate("MainWindow", "Send any command")) self.action_stop_music.setText(_translate("MainWindow", "Stop music")) + self.action_remove_row.setText(_translate("MainWindow", "Remove from table")) self.action_send_calibrations.setText(_translate("MainWindow", "Send camera calibrations")) diff --git a/Server/server_gui.ui b/Server/server_gui.ui index c9e1fc6..7141df1 100644 --- a/Server/server_gui.ui +++ b/Server/server_gui.ui @@ -328,7 +328,7 @@ 0 0 1220 - 25 + 26 @@ -375,6 +375,7 @@ + @@ -492,6 +493,11 @@ Stop music + + + Remove from table + + Send camera calibrations diff --git a/Server/server_qt.py b/Server/server_qt.py index aabcdf8..76343a7 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -3,6 +3,7 @@ import glob import math import time import asyncio +import functools from PyQt5 import QtWidgets, QtMultimedia from PyQt5.QtGui import QStandardItemModel, QStandardItem @@ -36,6 +37,7 @@ 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( args[0], label, @@ -43,11 +45,10 @@ def confirmation_required(text="Are you sure?", label="Confirm operation?"): QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: - print("Dialog accepted") - #print(args) - return f(args[0]) - else: - print("Dialog declined") + logging.debug("Dialog accepted") + return f(*args, **kwargs) + + logging.debug("Dialog declined") return wrapper @@ -66,9 +67,6 @@ class MainWindow(QtWidgets.QMainWindow): self.model = CopterDataModel() self.proxy_model = CopterProxyModel() self.signals = SignalManager() - self.gyro_calibrated = {} - self.level_calibrated = {} - self.first_col_is_checked = False self.player = QtMultimedia.QMediaPlayer() self.init_model() @@ -76,6 +74,8 @@ class MainWindow(QtWidgets.QMainWindow): self.show() def init_model(self): + # self.model.on_id_changed = self.set_copter_id + self.proxy_model.setDynamicSortFilter(True) self.proxy_model.setSourceModel(self.model) @@ -83,9 +83,12 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.tableView.setModel(self.proxy_model) self.ui.tableView.resizeColumnsToContents() + self.ui.tableView.doubleClicked.connect(self.selfcheck_info_dialog) + # 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) # Connect model signals to UI self.model.selected_ready_signal.connect(self.ui.start_button.setEnabled) @@ -106,9 +109,18 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.action_select_all_rows.triggered.connect(self.model.select_all) + def new_client_connected(self, client: Client): + self.signals.add_client_signal.emit(StatedCopterData(copter_id=client.copter_id, client=client)) - def client_connected(self, client: Client): - self.signals.add_client_signal.emit(CopterData(copter_id=client.copter_id, client=client)) + def client_connection_changed(self, client: Client): + 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: + if Server().remove_disconnected and (not client.connected): + client.remove() + self.signals.remove_client_signal.emit(row_num) + else: + self.signals.update_data_signal.emit(row_num, 0, client.connected, ModelStateRole) def init_ui(self): # Connecting @@ -130,6 +142,8 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.calibrate_gyro.clicked.connect(self.calibrate_gyro_selected) self.ui.calibrate_level.clicked.connect(self.calibrate_level_selected) + self.ui.action_remove_row.triggered.connect(self.remove_selected) + self.ui.action_send_animations.triggered.connect(self.send_animations) self.ui.action_send_calibrations.triggered.connect(self.send_calibrations) self.ui.action_send_configurations.triggered.connect(self.send_configurations) @@ -153,21 +167,23 @@ class MainWindow(QtWidgets.QMainWindow): @pyqtSlot() def selfcheck_selected(self): - for copter in self.model.user_selected(): - client = copter.client + for copter_data_row in self.model.user_selected(): + client = copter_data_row.client - client.get_response("anim_id", self._set_copter_data, callback_args=(1, copter.copter_id)) - client.get_response("batt_voltage", self._set_copter_data, callback_args=(2, copter.copter_id)) - client.get_response("cell_voltage", self._set_copter_data, callback_args=(3, copter.copter_id)) - client.get_response("sys_status", self._set_copter_data, callback_args=(4, copter.copter_id)) - client.get_response("cal_status", self._set_copter_data, callback_args=(5, copter.copter_id)) - client.get_response("selfcheck", self._set_copter_data, callback_args=(6, copter.copter_id)) - client.get_response("position", self._set_copter_data, callback_args=(7, copter.copter_id)) - client.get_response("time", self._set_copter_data, callback_args=(8, copter.copter_id)) + client.get_response("anim_id", self.set_copter_data, callback_args=(1, copter_data_row)) + client.get_response("batt_voltage", self.set_copter_data, callback_args=(2, copter_data_row)) + client.get_response("cell_voltage", self.set_copter_data, callback_args=(3, copter_data_row)) + client.get_response("sys_status", self.set_copter_data, callback_args=(4, copter_data_row)) + client.get_response("cal_status", self.set_copter_data, callback_args=(5, copter_data_row)) + client.get_response("selfcheck", self.set_copter_data, callback_args=(6, copter_data_row)) + client.get_response("position", self.set_copter_data, callback_args=(7, copter_data_row)) + client.get_response("time", self.set_copter_data, callback_args=(8, copter_data_row)) - 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, col, copter_data_row): + row = self.model.get_row_index(copter_data_row) + if row is None: + logging.error("No such client!") + return if col == 1: data = value @@ -181,23 +197,63 @@ class MainWindow(QtWidgets.QMainWindow): elif col == 5: data = str(value) elif col == 6: - data = str(value) + data = value elif col == 7: data = str(value) elif col == 8: - #data = time.ctime(int(value)) data = "{}".format(round(float(value) - time.time(), 3)) if abs(float(data)) > 1: - Client.get_by_id(copter_id).send_message("repair_chrony") - #self.signals.update_data_signal.emit(row, col + 1, data2) + copter_data_row.client.send_message("repair_chrony") else: - print("No column matched for response") + logging.error("No column matched for response") return - self.signals.update_data_signal.emit(row, col, data) + self.signals.update_data_signal.emit(row, col, data, ModelDataRole) + + + #def set_copter_id(self, value, copter_data_row): + # col = 0 + # row = self.model.get_row_index(copter_data_row) + # if row is None: + # logging.error("No such client!") + # return + # logging.info("SET COPTER ID TO {}".format(value)) + # + # copter_data_row.client.copter_id = value + # self.signals.update_data_signal.emit(row, col, value, ModelDataRole) + # self.signals.update_data_signal.emit(row, col, True, ModelStateRole) + + @pyqtSlot(QtCore.QModelIndex) + def selfcheck_info_dialog(self, index): + col = index.column() + if col == 6: + data = self.proxy_model.data(index, role=ModelDataRole) + if data and data != "OK": + dialog = QMessageBox() + dialog.setIcon(QMessageBox.NoIcon) + dialog.setStandardButtons(QMessageBox.Ok) + dialog.setWindowTitle("Selfcheck info") + dialog.setText("\n".join(data[:10])) + dialog.setDetailedText("\n".join(data)) + dialog.exec() + + def _selfcheck_shortener(self, data): + shortened = [] + for line in data: + if len(line) > 89: + pass + return shortened - @confirmation_required("This operation will takeoff selected copters with delay and start animation. Proceed?") @pyqtSlot() + def remove_selected(self): + for copter in self.model.user_selected(): + row_num = self.model.data_contents.index(copter) + copter.client.remove() + self.signals.remove_client_signal.emit(row_num) + logging.info("Client removed from table!") + + @pyqtSlot() + @confirmation_required("This operation will takeoff selected copters with delay and start animation. Proceed?") def send_starttime_selected(self, **kwargs): time_now = server.time_now() dt = self.ui.start_delay_spin.value() @@ -244,15 +300,15 @@ class MainWindow(QtWidgets.QMainWindow): def disarm_all(self): Client.broadcast_message("disarm") - @confirmation_required("This operation will takeoff copters immediately. Proceed?") @pyqtSlot() + @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): copter.client.send_message("takeoff") - @confirmation_required("This operation will flip(!!!) copters immediately. Proceed?") @pyqtSlot() + @confirmation_required("This operation will flip(!!!) copters immediately. Proceed?") def flip_selected(self, **kwargs): for copter in self.model.user_selected(): if flip_checks(copter): @@ -270,36 +326,33 @@ class MainWindow(QtWidgets.QMainWindow): @pyqtSlot() def calibrate_gyro_selected(self): - for copter in self.model.user_selected(): - client = copter.client + for copter_data_row in self.model.user_selected(): + client = copter_data_row.client # Update calibration status - row = self.model.data_contents.index(next(filter( - lambda x: x.copter_id == client.copter_id, self.model.data_contents))) + row = self.model.get_row_index(copter_data_row) col = 5 data = 'CALIBRATING' - self.signals.update_data_signal.emit(row, col, data) + self.signals.update_data_signal.emit(row, col, data, ModelDataRole) # Send request - client.get_response("calibrate_gyro", self._get_calibration_info, callback_args=(5, copter.copter_id)) + client.get_response("calibrate_gyro", self._get_calibration_info, callback_args=(copter_data_row, )) @pyqtSlot() def calibrate_level_selected(self): - for copter in self.model.user_selected(): - client = copter.client + for copter_data_row in self.model.user_selected(): + client = copter_data_row.client # Update calibration status - row = self.model.data_contents.index(next(filter( - lambda x: x.copter_id == client.copter_id, self.model.data_contents))) + row = self.model.get_row_index(copter_data_row) col = 5 data = 'CALIBRATING' - self.signals.update_data_signal.emit(row, col, data) + self.signals.update_data_signal.emit(row, col, data, ModelDataRole) # Send request - client.get_response("calibrate_level", self._get_calibration_info, callback_args=(5, copter.copter_id)) + client.get_response("calibrate_level", self._get_calibration_info, callback_args=(copter_data_row, )) - def _get_calibration_info(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 _get_calibration_info(self, value, copter_data_row): + col = 5 + row = self.model.get_row_index(copter_data_row) data = str(value) - self.signals.update_data_signal.emit(row, col, data) - + self.signals.update_data_signal.emit(row, col, data, ModelDataRole) @pyqtSlot() def send_animations(self): @@ -434,20 +487,20 @@ class MainWindow(QtWidgets.QMainWindow): @pyqtSlot() def stop_music(self): if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.InvalidMedia: - logging.info("Can't stop media") + logging.error("Can't stop media") return if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.NoMedia: - logging.info("No media file") + logging.error("No media file") return self.player.stop() @asyncio.coroutine def play_music_at_time(self, t): if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.InvalidMedia: - logging.info("Can't play media") + logging.error("Can't play media") return if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.NoMedia: - logging.info("No media file") + logging.error("No media file") return self.player.stop() yield from asyncio.sleep(t - time.time()) @@ -510,7 +563,11 @@ if __name__ == "__main__": #app.exec_() with loop: window = MainWindow() - Client.on_first_connect = window.client_connected + + Client.on_first_connect = window.new_client_connected + Client.on_connect = window.client_connection_changed + Client.on_disconnect = window.client_connection_changed + server = Server(on_stop=app.quit) server.start() loop.run_forever() diff --git a/builder/assets/clever-show.service b/builder/assets/clever-show.service index e1885e5..3c8466b 100644 --- a/builder/assets/clever-show.service +++ b/builder/assets/clever-show.service @@ -1,6 +1,8 @@ [Unit] Description=Clever Show Client Service -After=clever.service +Requires=clever.service +Requires=network.target +After=network.target [Service] WorkingDirectory=/home/pi/clever-show/Drone diff --git a/messaging_lib.py b/messaging_lib.py index d2444fa..2e9a9b8 100644 --- a/messaging_lib.py +++ b/messaging_lib.py @@ -8,6 +8,8 @@ import logging import threading import collections +from contextlib import closing + try: import selectors except ImportError: @@ -24,6 +26,16 @@ logger = logging.getLogger(__name__) # logger = logging_lib.Logger(_logger, True) +def get_ip_address(): + try: + with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as ip_socket: + ip_socket.connect(("8.8.8.8", 80)) + return ip_socket.getsockname()[0] + except OSError: + logging.warning("No network connection detected, using localhost") + return "localhost" + + class _Singleton(type): """ A metaclass that creates a Singleton base class when called. """ _instances = {} @@ -185,9 +197,7 @@ class ConnectionManager(object): self.socket = None self.addr = None - self.selector = None - self.socket = None - self.addr = None + self._should_close = False self._recv_buffer = b"" self._send_buffer = b"" @@ -198,6 +208,7 @@ class ConnectionManager(object): self._send_lock = threading.Lock() self._request_lock = threading.Lock() + self._close_lock = threading.Lock() self.BUFFER_SIZE = 1024 self.resume_queue = False @@ -225,8 +236,16 @@ class ConnectionManager(object): self._set_selector_events_mask('r') def close(self): + with self._close_lock: + self._should_close = True + + self._set_selector_events_mask('w') + NotifierSock().notify() + + def _close(self): logger.info("Closing connection to {}".format(self.addr)) try: + logger.info("Unregistering selector of {}".format(self.addr)) self.selector.unregister(self.socket) except AttributeError: pass @@ -236,6 +255,7 @@ class ConnectionManager(object): self.selector = None try: + logger.info("Closing socket of of {}".format(self.addr)) self.socket.close() except AttributeError: pass @@ -244,7 +264,18 @@ class ConnectionManager(object): finally: self.socket = None + with self._close_lock: + self._should_close = False + + logger.info("CLOSED connection to {}".format(self.addr)) + def process_events(self, mask): + with self._close_lock: + close = self._should_close + if close: + self._close() + return + if mask & selectors.EVENT_READ: self.read() if mask & selectors.EVENT_WRITE: @@ -304,7 +335,7 @@ class ConnectionManager(object): command = message.content["command"] args = message.content["args"] try: - self.messages_callbacks[command](**args) + self.messages_callbacks[command](self, **args) except KeyError: logger.warning("Command {} does not exist!".format(command)) except Exception as error: @@ -315,7 +346,7 @@ class ConnectionManager(object): request_id = message.content["request_id"] args = message.content["args"] try: - value = self.requests_callbacks[command](**args) + value = self.requests_callbacks[command](self, **args) except KeyError: logger.warning("Request {} does not exist!".format(command)) except Exception as error: # TODO send response error\cancel From 8b3986438bc5d0e4d93176a23d9158b08fb0f12b Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Mon, 21 Oct 2019 10:19:16 +0300 Subject: [PATCH 02/31] Server: Check user input for copter name --- Server/copter_table_models.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Server/copter_table_models.py b/Server/copter_table_models.py index 9b85d6b..0905635 100644 --- a/Server/copter_table_models.py +++ b/Server/copter_table_models.py @@ -202,12 +202,16 @@ 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: # and self.on_id_changed: - #self.data_contents[row][col] = "Awaiting for response" - #self.data_contents[row].states.copter_id = None - - self.data_contents[row].client.send_message("id", {"new_id": value}) - self.data_contents[row].client.remove() + 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}) + 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 From afcf880501df932a69b1b9d7b40440241e264641 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Mon, 21 Oct 2019 08:33:27 +0100 Subject: [PATCH 03/31] Client: add x_ratio, y_ratio and z_ratio settings for animation --- Drone/animation_lib.py | 14 +++++++------- Drone/client_config.ini | 4 +++- Drone/copter_client.py | 21 +++++++++++++++------ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Drone/animation_lib.py b/Drone/animation_lib.py index 28b7374..4c12b92 100644 --- a/Drone/animation_lib.py +++ b/Drone/animation_lib.py @@ -40,7 +40,7 @@ def get_id(filepath="animation.csv"): print("No animation id in file") return anim_id -def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, ratio=1): +def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, x_ratio=1, y_ratio=1, z_ratio=1): imported_frames = [] global anim_id try: @@ -62,9 +62,9 @@ def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, ratio=1): frame_number, x, y, z, yaw, red, green, blue = row_0 imported_frames.append({ 'number': int(frame_number), - 'x': ratio*float(x) + x0, - 'y': ratio*float(y) + y0, - 'z': ratio*float(z) + z0, + 'x': x_ratio*float(x) + x0, + 'y': y_ratio*float(y) + y0, + 'z': z_ratio*float(z) + z0, 'yaw': float(yaw), 'red': int(red), 'green': int(green), @@ -74,9 +74,9 @@ def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, ratio=1): frame_number, x, y, z, yaw, red, green, blue = row imported_frames.append({ 'number': int(frame_number), - 'x': ratio*float(x) + x0, - 'y': ratio*float(y) + y0, - 'z': ratio*float(z) + z0, + 'x': x_ratio*float(x) + x0, + 'y': y_ratio*float(y) + y0, + 'z': z_ratio*float(z) + z0, 'yaw': float(yaw), 'red': int(red), 'green': int(green), diff --git a/Drone/client_config.ini b/Drone/client_config.ini index 4fa87e1..c3c728e 100644 --- a/Drone/client_config.ini +++ b/Drone/client_config.ini @@ -17,7 +17,9 @@ port = 123 takeoff_animation_check = True land_animation_check = True frame_delay = 0.1 -ratio = 1.0 +x_ratio = 1.0 +y_ratio = 1.0 +z_ratio = 1.0 [COPTERS] frame_id = map diff --git a/Drone/copter_client.py b/Drone/copter_client.py index 62d4a73..b093424 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -49,7 +49,9 @@ class CopterClient(client.Client): self.TAKEOFF_CHECK = self.config.getboolean('ANIMATION', 'takeoff_animation_check') self.LAND_CHECK = self.config.getboolean('ANIMATION', 'land_animation_check') self.FRAME_DELAY = self.config.getfloat('ANIMATION', 'frame_delay') - self.RATIO = self.config.getfloat('ANIMATION', 'ratio') + self.X_RATIO = self.config.getfloat('ANIMATION', 'x_ratio') + self.Y_RATIO = self.config.getfloat('ANIMATION', 'y_ratio') + self.Z_RATIO = self.config.getfloat('ANIMATION', 'z_ratio') self.X0 = self.config.getfloat('PRIVATE', 'x0') self.Y0 = self.config.getfloat('PRIVATE', 'y0') self.Z0 = self.config.getfloat('PRIVATE', 'z0') @@ -261,7 +263,9 @@ def _response_animation_id(*args, **kwargs): x0=client.active_client.X0 + client.active_client.X0_COMMON, y0=client.active_client.Y0 + client.active_client.Y0_COMMON, z0=client.active_client.Z0 + client.active_client.Z0_COMMON, - ratio=client.active_client.RATIO, + x_ratio=client.active_client.X_RATIO, + y_ratio=client.active_client.Y_RATIO, + z_ratio=client.active_client.Z_RATIO, ) # Correct start and land frames in animation corrected_frames, start_action, start_delay = animation.correct_animation(frames, @@ -324,9 +328,12 @@ def _command_test(*args, **kwargs): def _command_move_start_to_current_position(*args, **kwargs): # Load animation frames = animation.load_animation(os.path.abspath("animation.csv"), - x0=client.active_client.X0_COMMON, - y0=client.active_client.Y0_COMMON, - ratio=client.active_client.RATIO, + x0=client.active_client.X0 + client.active_client.X0_COMMON, + y0=client.active_client.Y0 + client.active_client.Y0_COMMON, + z0=client.active_client.Z0 + client.active_client.Z0_COMMON, + x_ratio=client.active_client.X_RATIO, + y_ratio=client.active_client.Y_RATIO, + z_ratio=client.active_client.Z_RATIO, ) # Correct start and land frames in animation corrected_frames, start_action, start_delay = animation.correct_animation(frames, @@ -474,7 +481,9 @@ def _play_animation(*args, **kwargs): x0=client.active_client.X0 + client.active_client.X0_COMMON, y0=client.active_client.Y0 + client.active_client.Y0_COMMON, z0=client.active_client.Z0 + client.active_client.Z0_COMMON, - ratio=client.active_client.RATIO, + x_ratio=client.active_client.X_RATIO, + y_ratio=client.active_client.Y_RATIO, + z_ratio=client.active_client.Z_RATIO, ) # Correct start and land frames in animation corrected_frames, start_action, start_delay = animation.correct_animation(frames, From ecbcde16e83b5d7cae38e8afc2942a13ca57fd66 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Mon, 21 Oct 2019 20:07:04 +0300 Subject: [PATCH 04/31] Modify clever-show service --- builder/assets/clever-show.service | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/builder/assets/clever-show.service b/builder/assets/clever-show.service index 3c8466b..33d65a1 100644 --- a/builder/assets/clever-show.service +++ b/builder/assets/clever-show.service @@ -1,13 +1,11 @@ [Unit] Description=Clever Show Client Service -Requires=clever.service -Requires=network.target -After=network.target +After=clever.service [Service] WorkingDirectory=/home/pi/clever-show/Drone ExecStart=/bin/bash -c ". /home/pi/catkin_ws/devel/setup.sh; \ - /usr/bin/python /home/pi/clever-show/Drone/copter_client.py" + ROS_HOSTNAME=`hostname`.local /usr/bin/python /home/pi/clever-show/Drone/copter_client.py" KillSignal=SIGKILL Restart=on-failure RestartSec=3 From 6813d49fd9aabe853c96627939054861272b8223 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 22 Oct 2019 12:50:16 +0300 Subject: [PATCH 05/31] builder: Add camera_info copying from config folder --- builder/image-build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/builder/image-build.sh b/builder/image-build.sh index bb68595..aa802d2 100755 --- a/builder/image-build.sh +++ b/builder/image-build.sh @@ -122,6 +122,7 @@ img-chroot ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/clever-show.service' '/lib/ # Copy config files for clever if [[ -d "${CONFIG_DIR}/launch" ]]; then img-chroot ${IMAGE_PATH} copy ${CONFIG_DIR}'/launch' '/home/pi/catkin_ws/src/clever/clever'; fi if [[ -d "${CONFIG_DIR}/map" ]]; then img-chroot ${IMAGE_PATH} copy ${CONFIG_DIR}'/map' '/home/pi/catkin_ws/src/clever/aruco_pose'; fi +if [[ -d "${CONFIG_DIR}/camera_info" ]]; then img-chroot ${IMAGE_PATH} copy ${CONFIG_DIR}'/camera_info' '/home/pi/catkin_ws/src/clever/clever'; fi # Shrink image img-resize ${IMAGE_PATH} From 0e9597dc266795980406739ec3814614a3b68213 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 22 Oct 2019 12:50:47 +0300 Subject: [PATCH 06/31] builder: Make pi:pi as owner of catkin_ws dir --- builder/image-software.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/builder/image-software.sh b/builder/image-software.sh index 6916c68..cc21aea 100755 --- a/builder/image-software.sh +++ b/builder/image-software.sh @@ -49,9 +49,6 @@ my_travis_retry() { return $result } -echo_stamp "Change repo owner to pi" -chown -Rf pi:pi /home/pi/clever-show/ - echo_stamp "Update apt cache" apt-get update -qq @@ -73,4 +70,8 @@ source devel/setup.bash catkin_make --pkg clever_flight_routines source devel/setup.bash +echo_stamp "Change clever-show and catkin_ws owner to pi" +chown -Rf pi:pi /home/pi/clever-show/ +chown -Rf pi:pi /home/pi/catkin_ws/ + echo_stamp "End of software installation" From c91679964ca26349ae7726c89853a1de026fa04b Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 22 Oct 2019 14:38:52 +0300 Subject: [PATCH 07/31] Server: Add takeoff_z command --- Server/server_gui.py | 29 +++++++++++++++++++--- Server/server_gui.ui | 58 ++++++++++++++++++++++++++++++++++++++++---- Server/server_qt.py | 5 +++- 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/Server/server_gui.py b/Server/server_gui.py index 794d524..94f9018 100644 --- a/Server/server_gui.py +++ b/Server/server_gui.py @@ -52,6 +52,7 @@ class Ui_MainWindow(object): self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.music_text) self.music_delay_spin = QtWidgets.QDoubleSpinBox(self.centralwidget) self.music_delay_spin.setDecimals(1) + self.music_delay_spin.setMaximum(1000.0) self.music_delay_spin.setObjectName("music_delay_spin") self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.music_delay_spin) self.music_checkbox = QtWidgets.QCheckBox(self.centralwidget) @@ -60,8 +61,10 @@ class Ui_MainWindow(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.music_checkbox.sizePolicy().hasHeightForWidth()) self.music_checkbox.setSizePolicy(sizePolicy) + self.music_checkbox.setFocusPolicy(QtCore.Qt.NoFocus) + self.music_checkbox.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) self.music_checkbox.setLayoutDirection(QtCore.Qt.RightToLeft) - self.music_checkbox.setAutoFillBackground(True) + self.music_checkbox.setAutoFillBackground(False) self.music_checkbox.setText("") self.music_checkbox.setChecked(False) self.music_checkbox.setObjectName("music_checkbox") @@ -132,10 +135,10 @@ class Ui_MainWindow(object): self.formLayout_4.setObjectName("formLayout_4") self.land_button = QtWidgets.QPushButton(self.centralwidget) self.land_button.setObjectName("land_button") - self.formLayout_4.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.land_button) + self.formLayout_4.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.land_button) self.flip_button = QtWidgets.QPushButton(self.centralwidget) self.flip_button.setObjectName("flip_button") - self.formLayout_4.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.flip_button) + self.formLayout_4.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.flip_button) self.takeoff_button = QtWidgets.QPushButton(self.centralwidget) self.takeoff_button.setEnabled(True) self.takeoff_button.setObjectName("takeoff_button") @@ -143,6 +146,22 @@ class Ui_MainWindow(object): self.leds_button = QtWidgets.QPushButton(self.centralwidget) self.leds_button.setObjectName("leds_button") self.formLayout_4.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.leds_button) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setContentsMargins(-1, 0, -1, -1) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.z_checkbox = QtWidgets.QCheckBox(self.centralwidget) + self.z_checkbox.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.z_checkbox.setFocusPolicy(QtCore.Qt.NoFocus) + self.z_checkbox.setLayoutDirection(QtCore.Qt.LeftToRight) + self.z_checkbox.setObjectName("z_checkbox") + self.horizontalLayout_2.addWidget(self.z_checkbox) + self.z_spin = QtWidgets.QDoubleSpinBox(self.centralwidget) + self.z_spin.setLayoutDirection(QtCore.Qt.LeftToRight) + self.z_spin.setDecimals(1) + self.z_spin.setProperty("value", 1.0) + self.z_spin.setObjectName("z_spin") + self.horizontalLayout_2.addWidget(self.z_spin) + self.formLayout_4.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_2) self.verticalLayout.addLayout(self.formLayout_4) self.line_4 = QtWidgets.QFrame(self.centralwidget) self.line_4.setFrameShape(QtWidgets.QFrame.HLine) @@ -167,7 +186,7 @@ class Ui_MainWindow(object): self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1220, 26)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1220, 25)) self.menubar.setObjectName("menubar") self.menuOptions = QtWidgets.QMenu(self.menubar) self.menuOptions.setObjectName("menuOptions") @@ -278,6 +297,8 @@ class Ui_MainWindow(object): self.flip_button.setText(_translate("MainWindow", "Flip")) self.takeoff_button.setText(_translate("MainWindow", "Takeoff")) self.leds_button.setText(_translate("MainWindow", "Test leds")) + self.z_checkbox.setText(_translate("MainWindow", " Z =")) + self.z_spin.setSuffix(_translate("MainWindow", " m")) self.reboot_fcu.setText(_translate("MainWindow", "Reboot FCU")) self.calibrate_gyro.setText(_translate("MainWindow", "Calibrate gyro")) self.calibrate_level.setText(_translate("MainWindow", "Calibrate level")) diff --git a/Server/server_gui.ui b/Server/server_gui.ui index 7141df1..60e9d8e 100644 --- a/Server/server_gui.ui +++ b/Server/server_gui.ui @@ -91,6 +91,9 @@ 1 + + 1000.000000000000000 + @@ -101,11 +104,17 @@ 0 + + Qt::NoFocus + + + Qt::DefaultContextMenu + Qt::RightToLeft - true + false @@ -248,14 +257,14 @@ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - + Land - + Flip @@ -279,6 +288,45 @@ + + + + 0 + + + + + ArrowCursor + + + Qt::NoFocus + + + Qt::LeftToRight + + + Z = + + + + + + + Qt::LeftToRight + + + m + + + 1 + + + 1.000000000000000 + + + + + @@ -328,7 +376,7 @@ 0 0 1220 - 26 + 25 @@ -496,7 +544,7 @@ Remove from table - + diff --git a/Server/server_qt.py b/Server/server_qt.py index 76343a7..d08f822 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -305,7 +305,10 @@ class MainWindow(QtWidgets.QMainWindow): def takeoff_selected(self, **kwargs): for copter in self.model.user_selected(): if takeoff_checks(copter): - copter.client.send_message("takeoff") + if self.ui.z_checkbox.isChecked(): + copter.client.send_message("takeoff_z", {"z":str(self.ui.z_spin.value())}) + else: + copter.client.send_message("takeoff") @pyqtSlot() @confirmation_required("This operation will flip(!!!) copters immediately. Proceed?") From a87fab4369854375a609abbbe2f6bb66ee360f42 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 22 Oct 2019 14:36:44 +0100 Subject: [PATCH 08/31] FlightLib: Modify reach_point function, add auto_arm argument --- Drone/FlightLib/FlightLib.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Drone/FlightLib/FlightLib.py b/Drone/FlightLib/FlightLib.py index deae299..d0c304e 100644 --- a/Drone/FlightLib/FlightLib.py +++ b/Drone/FlightLib/FlightLib.py @@ -175,11 +175,11 @@ def navto(x, y, z, yaw=float('nan'), frame_id=FRAME_ID, auto_arm=False, **kwargs return True -def reach_point(x=0.0, y=0.0, z=0.0, yaw=float('nan'), speed=SPEED, tolerance=TOLERANCE, frame_id=FRAME_ID, +def reach_point(x=0.0, y=0.0, z=0.0, yaw=float('nan'), speed=SPEED, tolerance=TOLERANCE, frame_id=FRAME_ID, auto_arm=False, freq=FREQUENCY, timeout=TIMEOUT, interrupter=INTERRUPTER, wait=False): - logger.info('Reaching point: | x: {:.3f} y: {:.3f} z: {:.3f} yaw: {:.3f}'.format(x, y, z, yaw)) + rospy.loginfo('Reaching point: | x: {:.3f} y: {:.3f} z: {:.3f} yaw: {:.3f}'.format(x, y, z, yaw)) #print('Reaching point: | x: {:.3f} y: {:.3f} z: {:.3f} yaw: {:.3f}'.format(x, y, z, yaw)) - navigate(frame_id=frame_id, x=x, y=y, z=z, yaw=yaw, speed=speed) + navigate(frame_id=frame_id, x=x, y=y, z=z, yaw=yaw, speed=speed, auto_arm=auto_arm) # waiting for completion telemetry = get_telemetry(frame_id=frame_id) @@ -188,17 +188,17 @@ def reach_point(x=0.0, y=0.0, z=0.0, yaw=float('nan'), speed=SPEED, tolerance=TO while (get_distance3d(x, y, z, telemetry.x, telemetry.y, telemetry.z) > tolerance) or wait: if interrupter.is_set(): - logger.warning("Reach point function interrupted!") + rospy.logwarn("Reach point function interrupted!") #print("Reach point function interrupted!") interrupter.clear() return False telemetry = get_telemetry(frame_id=frame_id) - logger.info('Telemetry: | x: {:.3f} y: {:.3f} z: {:.3f} yaw: {:.3f}'.format( + rospy.logdebug('Telemetry: | x: {:.3f} y: {:.3f} z: {:.3f} yaw: {:.3f}'.format( telemetry.x, telemetry.y, telemetry.z, telemetry.yaw)) #print('Telemetry: | x: {:.3f} y: {:.3f} z: {:.3f} yaw: {:.3f}'.format( # telemetry.x, telemetry.y, telemetry.z, telemetry.yaw)) - logger.info('Current delta: | {:.3f}'.format( + rospy.logdebug('Current delta: | {:.3f}'.format( get_distance3d(x, y, z, telemetry.x, telemetry.y, telemetry.z))) #print('Current delta: | {:.3f}'.format( # get_distance3d(x, y, z, telemetry.x, telemetry.y, telemetry.z))) @@ -207,12 +207,12 @@ def reach_point(x=0.0, y=0.0, z=0.0, yaw=float('nan'), speed=SPEED, tolerance=TO if timeout is not None: if time_passed >= timeout: - logger.warning('Reaching point timed out! | time: {:3f} seconds'.format(time_passed)) - print('Reaching point timed out! | time: {:3f} seconds'.format(time_passed)) + rospy.logwarn('Reaching point timed out! | time: {:3f} seconds'.format(time_passed)) + # print('Reaching point timed out! | time: {:3f} seconds'.format(time_passed)) return wait rate.sleep() - logger.info("Point reached!") + rospy.loginfo("Point reached!") #print("Point reached!") return True From f4300f5988f8a176fcc612f1cc63327bce790d87 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 22 Oct 2019 14:37:27 +0100 Subject: [PATCH 09/31] Add takeoff_z command callback --- Drone/copter_client.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Drone/copter_client.py b/Drone/copter_client.py index b093424..19e7220 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -2,6 +2,7 @@ import os import time import math import rospy +import datetime import logging from FlightLib import FlightLib @@ -66,7 +67,7 @@ class CopterClient(client.Client): def start(self, task_manager_instance): client.logger.info("Init ROS node") - rospy.init_node('Swarm_client') + rospy.init_node('clever_show_client') if self.USE_LEDS: LedLib.init_led(self.LED_PIN) task_manager_instance.start() @@ -417,7 +418,8 @@ def _copter_flip(*args, **kwargs): @messaging.message_callback("takeoff") def _command_takeoff(*args, **kwargs): - task_manager.add_task(time.time(), 0, animation.takeoff, + print("Takeoff at {}".format(datetime.datetime.now())) + task_manager.add_task(0, 0, animation.takeoff, task_kwargs={ "z": client.active_client.TAKEOFF_HEIGHT, "timeout": client.active_client.TAKEOFF_TIME, @@ -426,6 +428,23 @@ def _command_takeoff(*args, **kwargs): } ) +@messaging.message_callback("takeoff_z") +def _command_takeoff_z(*args, **kwargs): + z_str = kwargs.get("z", None) + if z_str is not None: + telem = FlightLib.get_telemetry(client.active_client.FRAME_ID) + print("Takeoff to z = {} at {}".format(z_str, datetime.datetime.now())) + task_manager.add_task(0, 0, FlightLib.reach_point, + task_kwargs={ + "x": telem.x, + "y": telem.y, + "z": float(z_str), + "frame_id": client.active_client.FRAME_ID, + "timeout": client.active_client.TAKEOFF_TIME, + "auto_arm": True, + } + ) + @messaging.message_callback("land") def _command_land(*args, **kwargs): From cb4616555701eed86e3814a63c9023a4733b872b Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 23 Oct 2019 12:51:51 +0100 Subject: [PATCH 10/31] Update git update function --- Drone/copter_client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Drone/copter_client.py b/Drone/copter_client.py index 19e7220..abd37a8 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -376,10 +376,13 @@ def _command_reset_z(*args, **kwargs): @messaging.message_callback("update_repo") def _command_update_repo(*args, **kwargs): - os.system("git reset --hard origin/master") + os.system("mv /home/pi/clever-show/Drone/client_config.ini /home/pi/clever-show/Drone/client_config_tmp.ini") + os.system("git reset --hard HEAD") + os.system("git checkout master") os.system("git fetch") - os.system("git pull") - os.system("chown -R pi:pi ~/CleverSwarm") + os.system("git pull --rebase") + os.system("mv /home/pi/clever-show/Drone/client_config_tmp.ini /home/pi/clever-show/Drone/client_config.ini") + os.system("chown -R pi:pi /home/pi/clever-show") @messaging.message_callback("reboot_fcu") def _command_reboot(*args, **kwargs): From 29a9fe5cebf6df24842c68e3d03d7ff6eb46c225 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 23 Oct 2019 13:07:56 +0100 Subject: [PATCH 11/31] Return own rights to pi:pi after files manipulation --- Drone/client.py | 2 ++ messaging_lib.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Drone/client.py b/Drone/client.py index aed8dc4..f557081 100644 --- a/Drone/client.py +++ b/Drone/client.py @@ -1,3 +1,4 @@ +import os import time import errno import random @@ -74,6 +75,7 @@ class Client(object): def rewrite_config(self): with open(self.config_path, 'w') as file: self.config.write(file) + os.system("chown -R pi:pi /home/pi/clever-show") def write_config(self, reload_config=True, *config_options): for config_option in config_options: diff --git a/messaging_lib.py b/messaging_lib.py index 2e9a9b8..357e7f1 100644 --- a/messaging_lib.py +++ b/messaging_lib.py @@ -1,4 +1,5 @@ import io +import os import sys import json import socket @@ -382,6 +383,7 @@ class ConnectionManager(object): logger.error("File {} can not be written due error: {}".format(filepath, error)) else: logger.info("File {} successfully received ".format(filepath)) + os.system("chown -R pi:pi /home/pi/clever-show") def write(self): with self._send_lock: From 90af9bb417abb86e23dc0fa6c77c15eebcb5768c Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 23 Oct 2019 13:49:49 +0100 Subject: [PATCH 12/31] Client: Add reboot_all command --- Drone/client_config.ini | 12 ++++++------ Drone/copter_client.py | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Drone/client_config.ini b/Drone/client_config.ini index c3c728e..45585ce 100644 --- a/Drone/client_config.ini +++ b/Drone/client_config.ini @@ -22,11 +22,11 @@ y_ratio = 1.0 z_ratio = 1.0 [COPTERS] -frame_id = map -takeoff_height = 2.0 -takeoff_time = 8.0 +frame_id = floor +takeoff_height = 1.0 +takeoff_time = 5.0 safe_takeoff = False -reach_first_point_time = 8.0 +reach_first_point_time = 5.0 land_time = 3.0 x0_common = 0 y0_common = 0 @@ -36,7 +36,7 @@ z0_common = 0 parent = aruco_map x = 0.0 y = 0.0 -z = 6.5 +z = 3.55 roll = 180 pitch = 0 yaw = -90 @@ -44,7 +44,7 @@ yaw = -90 [PRIVATE] id = /hostname restart_dhcpcd = True -use_leds = True +use_leds = False led_pin = 21 x0 = 0 y0 = 0 diff --git a/Drone/copter_client.py b/Drone/copter_client.py index abd37a8..cd2f113 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -384,6 +384,11 @@ def _command_update_repo(*args, **kwargs): os.system("mv /home/pi/clever-show/Drone/client_config_tmp.ini /home/pi/clever-show/Drone/client_config.ini") os.system("chown -R pi:pi /home/pi/clever-show") +@messaging.message_callback("reboot_all") +def _command_reboot_all(*args, **kwargs): + reboot_fcu() + execute_command("reboot") + @messaging.message_callback("reboot_fcu") def _command_reboot(*args, **kwargs): reboot_fcu() From dd761e7dfbf5c1d1fc914567fe38a1d6fb756eaa Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 23 Oct 2019 15:32:04 +0300 Subject: [PATCH 13/31] Server: Fix copter removing --- Server/server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Server/server.py b/Server/server.py index 311b924..b612711 100644 --- a/Server/server.py +++ b/Server/server.py @@ -314,7 +314,10 @@ class Client(messaging.ConnectionManager): if self.connected: self.close() if self.clients: - self.clients.pop(self.addr[0]) + try: + self.clients.pop(self.addr[0]) + except Exception as e: + logging.error(e) logging.info("Client {} successfully removed!".format(self.copter_id)) @requires_connect From caa14411d02a7ec27e7a7820994af7b067d4181a Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 23 Oct 2019 15:50:48 +0300 Subject: [PATCH 14/31] Server: Add reboot_all command --- Server/server_gui.py | 6 +++++- Server/server_gui.ui | 8 +++++++- Server/server_qt.py | 8 +++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Server/server_gui.py b/Server/server_gui.py index 94f9018..dcc652c 100644 --- a/Server/server_gui.py +++ b/Server/server_gui.py @@ -245,6 +245,8 @@ class Ui_MainWindow(object): self.action_remove_row.setObjectName("action_remove_row") self.action_send_calibrations = QtWidgets.QAction(MainWindow) self.action_send_calibrations.setObjectName("action_send_calibrations") + self.action_reboot_all = QtWidgets.QAction(MainWindow) + self.action_reboot_all.setObjectName("action_reboot_all") self.menuDeveloper_mode.addAction(self.action_send_any_file) self.menuDeveloper_mode.addAction(self.actionSend_any_command) self.menuOptions.addAction(self.action_send_animations) @@ -261,11 +263,12 @@ class Ui_MainWindow(object): self.menuDeveloper_mode_2.addAction(self.action_restart_clever) self.menuDeveloper_mode_2.addAction(self.action_restart_clever_show) self.menuDeveloper_mode_2.addAction(self.action_update_client_repo) + self.menuDeveloper_mode_2.addAction(self.action_reboot_all) self.menuDrone.addAction(self.action_set_z_offset_to_ground) self.menuDrone.addAction(self.action_reset_z_offset) + self.menuDrone.addAction(self.action_remove_row) self.menuDrone.addSeparator() self.menuDrone.addAction(self.menuDeveloper_mode_2.menuAction()) - self.menuDrone.addAction(self.action_remove_row) self.menuMusic.addAction(self.action_select_music_file) self.menuMusic.addAction(self.action_play_music) self.menuMusic.addAction(self.action_stop_music) @@ -331,3 +334,4 @@ class Ui_MainWindow(object): self.action_stop_music.setText(_translate("MainWindow", "Stop music")) self.action_remove_row.setText(_translate("MainWindow", "Remove from table")) self.action_send_calibrations.setText(_translate("MainWindow", "Send camera calibrations")) + self.action_reboot_all.setText(_translate("MainWindow", "Reboot all")) diff --git a/Server/server_gui.ui b/Server/server_gui.ui index 60e9d8e..36569a1 100644 --- a/Server/server_gui.ui +++ b/Server/server_gui.ui @@ -418,12 +418,13 @@ + + - @@ -551,6 +552,11 @@ Send camera calibrations + + + Reboot all + + start_delay_spin diff --git a/Server/server_qt.py b/Server/server_qt.py index d08f822..d690d82 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -152,6 +152,7 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.action_restart_clever.triggered.connect(self.restart_clever) self.ui.action_restart_clever_show.triggered.connect(self.restart_clever_show) self.ui.action_update_client_repo.triggered.connect(self.update_client_repo) + self.ui.action_reboot_all.triggered.connect(self.reboot_all_on_selected) self.ui.action_set_start_to_current_position.triggered.connect(self.update_start_to_current_position) self.ui.action_reset_start.triggered.connect(self.reset_start) self.ui.action_set_z_offset_to_ground.triggered.connect(self.set_z_offset_to_ground) @@ -439,7 +440,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") @pyqtSlot() def update_start_to_current_position(self): From 5d97ca26a8a6b84ad1fa9ed569d66efde0e79d68 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 23 Oct 2019 10:23:38 +0100 Subject: [PATCH 15/31] Client: restore default client_config file --- Drone/client_config.ini | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Drone/client_config.ini b/Drone/client_config.ini index 45585ce..3053175 100644 --- a/Drone/client_config.ini +++ b/Drone/client_config.ini @@ -1,3 +1,4 @@ + [SERVER] port = 25000 broadcast_port = 8181 @@ -22,11 +23,11 @@ y_ratio = 1.0 z_ratio = 1.0 [COPTERS] -frame_id = floor -takeoff_height = 1.0 -takeoff_time = 5.0 +frame_id = map +takeoff_height = 2.0 +takeoff_time = 8.0 safe_takeoff = False -reach_first_point_time = 5.0 +reach_first_point_time = 8.0 land_time = 3.0 x0_common = 0 y0_common = 0 @@ -36,7 +37,7 @@ z0_common = 0 parent = aruco_map x = 0.0 y = 0.0 -z = 3.55 +z = 6.5 roll = 180 pitch = 0 yaw = -90 @@ -44,9 +45,8 @@ yaw = -90 [PRIVATE] id = /hostname restart_dhcpcd = True -use_leds = False +use_leds = True led_pin = 21 x0 = 0 y0 = 0 -z0 = 0 - +z0 = 0 \ No newline at end of file From 79d59f0b28e28e1f9cc06e6ff1d0a9379bff3811 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Fri, 25 Oct 2019 07:06:17 +0100 Subject: [PATCH 16/31] Client: Fix bug in move start callback --- Drone/client_config.ini | 16 ++++++++-------- Drone/copter_client.py | 17 ++++++++--------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Drone/client_config.ini b/Drone/client_config.ini index 3053175..45585ce 100644 --- a/Drone/client_config.ini +++ b/Drone/client_config.ini @@ -1,4 +1,3 @@ - [SERVER] port = 25000 broadcast_port = 8181 @@ -23,11 +22,11 @@ y_ratio = 1.0 z_ratio = 1.0 [COPTERS] -frame_id = map -takeoff_height = 2.0 -takeoff_time = 8.0 +frame_id = floor +takeoff_height = 1.0 +takeoff_time = 5.0 safe_takeoff = False -reach_first_point_time = 8.0 +reach_first_point_time = 5.0 land_time = 3.0 x0_common = 0 y0_common = 0 @@ -37,7 +36,7 @@ z0_common = 0 parent = aruco_map x = 0.0 y = 0.0 -z = 6.5 +z = 3.55 roll = 180 pitch = 0 yaw = -90 @@ -45,8 +44,9 @@ yaw = -90 [PRIVATE] id = /hostname restart_dhcpcd = True -use_leds = True +use_leds = False led_pin = 21 x0 = 0 y0 = 0 -z0 = 0 \ No newline at end of file +z0 = 0 + diff --git a/Drone/copter_client.py b/Drone/copter_client.py index cd2f113..e60a136 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -329,21 +329,20 @@ def _command_test(*args, **kwargs): def _command_move_start_to_current_position(*args, **kwargs): # Load animation frames = animation.load_animation(os.path.abspath("animation.csv"), - x0=client.active_client.X0 + client.active_client.X0_COMMON, - y0=client.active_client.Y0 + client.active_client.Y0_COMMON, - z0=client.active_client.Z0 + client.active_client.Z0_COMMON, x_ratio=client.active_client.X_RATIO, y_ratio=client.active_client.Y_RATIO, z_ratio=client.active_client.Z_RATIO, ) # Correct start and land frames in animation - corrected_frames, start_action, start_delay = animation.correct_animation(frames, - check_takeoff=client.active_client.TAKEOFF_CHECK, - check_land=client.active_client.LAND_CHECK, - ) - x_start = corrected_frames[0]['x'] - y_start = corrected_frames[0]['y'] + # corrected_frames, start_action, start_delay = animation.correct_animation(frames, + # check_takeoff=client.active_client.TAKEOFF_CHECK, + # check_land=client.active_client.LAND_CHECK, + # ) + x_start = frames[0]['x'] + y_start = frames[0]['y'] + print("x_start = {}, y_start = {}".format(x_start, y_start)) telem = FlightLib.get_telemetry(client.active_client.FRAME_ID) + print("x_telem = {}, y_telem = {}".format(telem.x, telem.y)) client.active_client.config.set('PRIVATE', 'x0', telem.x - x_start) client.active_client.config.set('PRIVATE', 'y0', telem.y - y_start) client.active_client.rewrite_config() From eb96ee4a4b42336f18ee2dbd20daa10d8a8105ba Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 29 Oct 2019 14:30:19 +0300 Subject: [PATCH 17/31] Server: don't do selfcheck before takeoff until the connection problem will be solved --- Server/server_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/server_qt.py b/Server/server_qt.py index d690d82..846e012 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -263,7 +263,7 @@ class MainWindow(QtWidgets.QMainWindow): music_dt = self.ui.music_delay_spin.value() 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() + # self.selfcheck_selected() for copter in self.model.user_selected(): if all_checks(copter): server.send_starttime(copter.client, dt+time_now) From 585cb616732c40644970809c9882d39b1ebaa254 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 29 Oct 2019 15:06:55 +0300 Subject: [PATCH 18/31] Server: Change remove_disconnected param to False in server_config --- Server/server_config.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/server_config.ini b/Server/server_config.ini index 5160e2d..8fdfddc 100644 --- a/Server/server_config.ini +++ b/Server/server_config.ini @@ -1,7 +1,7 @@ [SERVER] port = 25000 buffer_size = 1024 -remove_disconnected = True +remove_disconnected = False [BROADCAST] use_broadcast = True @@ -11,4 +11,4 @@ broadcast_delay = 5 [NTP] use_ntp = False host = ntp1.stratum2.ru -port = 123 \ No newline at end of file +port = 123 From 82a434b4caa75f8871609891f78543165d0bb688 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 29 Oct 2019 15:54:08 +0300 Subject: [PATCH 19/31] Add ability to send folder with launch files --- Server/server_gui.ui | 4 ++-- Server/server_qt.py | 10 ++++++---- messaging_lib.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Server/server_gui.ui b/Server/server_gui.ui index 36569a1..bb58ced 100644 --- a/Server/server_gui.ui +++ b/Server/server_gui.ui @@ -7,7 +7,7 @@ 0 0 1220 - 750 + 761 @@ -466,7 +466,7 @@ - Send launch file to clever + Send launch files diff --git a/Server/server_qt.py b/Server/server_qt.py index 846e012..eed0560 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -419,12 +419,14 @@ class MainWindow(QtWidgets.QMainWindow): @pyqtSlot() def send_launch(self): - path = QFileDialog.getOpenFileName(self, "Select launch file for clever", filter="Launch files (*.launch)")[0] + path = str(QFileDialog.getExistingDirectory(self, "Select directory with launch files")) if path: - filename = os.path.basename(path) - print("Selected file:", path, filename) + print("Selected directory:", path) + files = [file for file in glob.glob(path + '/*.launch')] for copter in self.model.user_selected(): - copter.client.send_file(path, "/home/pi/catkin_ws/src/clever/clever/launch/{}".format(filename)) + for file in files: + filename = os.path.basename(file) + copter.client.send_file(file, "/home/pi/catkin_ws/src/clever/clever/launch/{}".format(filename)) # copter.client.send_message("service_restart", {"name": "clever"}) @pyqtSlot() diff --git a/messaging_lib.py b/messaging_lib.py index 357e7f1..1cb0383 100644 --- a/messaging_lib.py +++ b/messaging_lib.py @@ -383,7 +383,7 @@ class ConnectionManager(object): logger.error("File {} can not be written due error: {}".format(filepath, error)) else: logger.info("File {} successfully received ".format(filepath)) - os.system("chown -R pi:pi /home/pi/clever-show") + os.system("chown -R pi:pi /home/pi/clever-show/") def write(self): with self._send_lock: From 34999302f7bfd5069a35f1e2f02be708ac8e11a1 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 29 Oct 2019 15:58:27 +0300 Subject: [PATCH 20/31] Server: Update GUI --- Server/server_gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/server_gui.py b/Server/server_gui.py index dcc652c..4743936 100644 --- a/Server/server_gui.py +++ b/Server/server_gui.py @@ -13,7 +13,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") - MainWindow.resize(1220, 750) + MainWindow.resize(1220, 761) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setEnabled(True) self.centralwidget.setObjectName("centralwidget") @@ -316,7 +316,7 @@ class Ui_MainWindow(object): self.action_send_Aruco_map.setText(_translate("MainWindow", "Send aruco map")) self.action_update_client_repo.setText(_translate("MainWindow", "Update clever-show git")) self.actionSend_launch_file_for_clever.setText(_translate("MainWindow", "Send launch file for clever")) - self.action_send_launch_file.setText(_translate("MainWindow", "Send launch file to clever")) + self.action_send_launch_file.setText(_translate("MainWindow", "Send launch files")) self.action_restart_clever.setText(_translate("MainWindow", "Restart clever service")) self.action_restart_clever_show.setText(_translate("MainWindow", "Restart clever-show service")) self.action_select_all_rows.setText(_translate("MainWindow", "Select all drones")) From 167fafbbb0d5ccda365e4e525381f64f23508697 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 29 Oct 2019 16:03:31 +0300 Subject: [PATCH 21/31] file transfer: return rights to pi:pi after file receive --- messaging_lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messaging_lib.py b/messaging_lib.py index 1cb0383..878c76f 100644 --- a/messaging_lib.py +++ b/messaging_lib.py @@ -383,7 +383,7 @@ class ConnectionManager(object): logger.error("File {} can not be written due error: {}".format(filepath, error)) else: logger.info("File {} successfully received ".format(filepath)) - os.system("chown -R pi:pi /home/pi/clever-show/") + os.system("chown pi:pi {}".format(filepath)) def write(self): with self._send_lock: From 1adade0bdabada884363ab7e5e56dd1c641bf95b Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Tue, 29 Oct 2019 16:38:13 +0300 Subject: [PATCH 22/31] Server: Add check for position col in table --- Server/copter_table_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/copter_table_models.py b/Server/copter_table_models.py index 0905635..62bb977 100644 --- a/Server/copter_table_models.py +++ b/Server/copter_table_models.py @@ -305,10 +305,10 @@ def check_selfcheck(item): @col_check(7) -def check_cal_status(item): +def check_pos_status(item): if not item: return None - return True + return str(item).split(' ')[0] != 'nan' @col_check(8) From d33cc0c32dfa3316d9cb6d4d191189ab0096cf8c Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Fri, 25 Oct 2019 12:28:55 +0100 Subject: [PATCH 23/31] Client: modify calibration response and reduce timeout in service check --- Drone/FlightLib/FlightLib.py | 2 +- Drone/copter_client.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Drone/FlightLib/FlightLib.py b/Drone/FlightLib/FlightLib.py index d0c304e..40d019e 100644 --- a/Drone/FlightLib/FlightLib.py +++ b/Drone/FlightLib/FlightLib.py @@ -91,7 +91,7 @@ def _check_nans(*values): @check("Ros services") def check_ros_services(): - timeout = 5.0 + timeout = 0.1 for service in services_list: try: service.wait_for_service(timeout=timeout) diff --git a/Drone/copter_client.py b/Drone/copter_client.py index e60a136..6566a66 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -301,7 +301,11 @@ def _response_sys_status(*args, **kwargs): @messaging.request_callback("cal_status") def _response_cal_status(*args, **kwargs): - return get_calibration_status() + if check_state_topic(wait_new_status=True): + return get_calibration_status() + else: + stop_subscriber() + return "NOT_CONNECTED_TO_FCU" @messaging.request_callback("position") def _response_position(*args, **kwargs): From dc85d9bd3f65a04f751e0e694034c7cfaaae5762 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Fri, 25 Oct 2019 15:18:56 +0100 Subject: [PATCH 24/31] animation_lib: add get_start_xy function --- Drone/animation_lib.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Drone/animation_lib.py b/Drone/animation_lib.py index 4c12b92..857f7b9 100644 --- a/Drone/animation_lib.py +++ b/Drone/animation_lib.py @@ -37,9 +37,43 @@ def get_id(filepath="animation.csv"): anim_id = row_0[0] print("Got animation_id: {}".format(anim_id)) else: + anim_id = "Empty id" print("No animation id in file") return anim_id +def get_start_xy(filepath="animation.csv"): + try: + animation_file = open(filepath) + except IOError: + logging.error("File {} can't be opened".format(filepath)) + anim_id = "No animation" + return float('nan'), float('nan') + else: + with animation_file: + csv_reader = csv.reader( + animation_file, delimiter=',', quotechar='|' + ) + try: + row_0 = csv_reader.next() + except: + return float('nan'), float('nan') + if len(row_0) == 1: + anim_id = row_0[0] + print("Got animation_id: {}".format(anim_id)) + try: + frame_number, x, y, z, yaw, red, green, blue = csv_reader.next() + except: + return float('nan'), float('nan') + else: + anim_id = "Empty id" + print("No animation id in file") + try: + frame_number, x, y, z, yaw, red, green, blue = row_0 + except: + return float('nan'), float('nan') + return float(x), float(y) + + def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, x_ratio=1, y_ratio=1, z_ratio=1): imported_frames = [] global anim_id From 46802667bde0ef26c1dae9db767df9e273ed3dea Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Fri, 25 Oct 2019 15:32:49 +0100 Subject: [PATCH 25/31] Client: modify move_start function --- Drone/animation_lib.py | 4 ++-- Drone/copter_client.py | 16 ++-------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/Drone/animation_lib.py b/Drone/animation_lib.py index 857f7b9..c9913c7 100644 --- a/Drone/animation_lib.py +++ b/Drone/animation_lib.py @@ -41,7 +41,7 @@ def get_id(filepath="animation.csv"): print("No animation id in file") return anim_id -def get_start_xy(filepath="animation.csv"): +def get_start_xy(filepath="animation.csv", x_ratio=1, y_ratio=1): try: animation_file = open(filepath) except IOError: @@ -71,7 +71,7 @@ def get_start_xy(filepath="animation.csv"): frame_number, x, y, z, yaw, red, green, blue = row_0 except: return float('nan'), float('nan') - return float(x), float(y) + return float(x)*x_ratio, float(y)*y_ratio def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, x_ratio=1, y_ratio=1, z_ratio=1): diff --git a/Drone/copter_client.py b/Drone/copter_client.py index 6566a66..61ee8f3 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -21,7 +21,6 @@ from tf.transformations import quaternion_from_euler, euler_from_quaternion, qua import tf2_ros static_bloadcaster = tf2_ros.StaticTransformBroadcaster() - # logging.basicConfig( # TODO all prints as logs # level=logging.DEBUG, # INFO # format="%(asctime)s [%(name)-7.7s] [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s", @@ -95,7 +94,7 @@ class CopterClient(client.Client): trans.child_frame_id = self.FRAME_ID static_bloadcaster.sendTransform(trans) start_subscriber() - # print(check_state_topic()) + super(CopterClient, self).start() @@ -331,19 +330,10 @@ def _command_test(*args, **kwargs): @messaging.message_callback("move_start") def _command_move_start_to_current_position(*args, **kwargs): - # Load animation - frames = animation.load_animation(os.path.abspath("animation.csv"), + x_start, y_start = animation.get_start_xy(os.path.abspath("animation.csv"), x_ratio=client.active_client.X_RATIO, y_ratio=client.active_client.Y_RATIO, - z_ratio=client.active_client.Z_RATIO, ) - # Correct start and land frames in animation - # corrected_frames, start_action, start_delay = animation.correct_animation(frames, - # check_takeoff=client.active_client.TAKEOFF_CHECK, - # check_land=client.active_client.LAND_CHECK, - # ) - x_start = frames[0]['x'] - y_start = frames[0]['y'] print("x_start = {}, y_start = {}".format(x_start, y_start)) telem = FlightLib.get_telemetry(client.active_client.FRAME_ID) print("x_telem = {}, y_telem = {}".format(telem.x, telem.y)) @@ -596,8 +586,6 @@ def _play_animation(*args, **kwargs): }, ) #print(task_manager.task_queue) - - if __name__ == "__main__": copter_client = CopterClient() From 0f5b5f0346029de2410bae2e81574862e2e16e23 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Fri, 25 Oct 2019 17:22:49 +0100 Subject: [PATCH 26/31] Client: Add telemetry get and send thread --- Drone/copter_client.py | 90 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 8 deletions(-) diff --git a/Drone/copter_client.py b/Drone/copter_client.py index 61ee8f3..44123dc 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -4,6 +4,9 @@ import math import rospy import datetime import logging +import threading +import subprocess +from collections import namedtuple from FlightLib import FlightLib from FlightLib import LedLib @@ -21,6 +24,9 @@ from tf.transformations import quaternion_from_euler, euler_from_quaternion, qua import tf2_ros static_bloadcaster = tf2_ros.StaticTransformBroadcaster() +Telemetry = namedtuple("Telemetry", "git_version animation_id battery_v battery_p system_status calibration_status mode current_position start_position") +telemetry = Telemetry('nan', 'No animation', 'nan', 'nan', 'NO_FCU', 'NO_FCU', 'NO_FCU', 'NO_POS', 'NO_POS') + # logging.basicConfig( # TODO all prints as logs # level=logging.DEBUG, # INFO # format="%(asctime)s [%(name)-7.7s] [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s", @@ -36,6 +42,8 @@ logger = logging.getLogger(__name__) class CopterClient(client.Client): def load_config(self): super(CopterClient, self).load_config() + self.TELEM_FREQ = self.config.getfloat('TELEMETRY', 'frequency') + self.TELEM_TRANSMIT = self.config.getboolean('TELEMETRY', 'transmit') self.FRAME_ID = self.config.get('COPTERS', 'frame_id') self.FRAME_FLIPPED_HEIGHT = 0. self.TAKEOFF_HEIGHT = self.config.getfloat('COPTERS', 'takeoff_height') @@ -94,7 +102,7 @@ class CopterClient(client.Client): trans.child_frame_id = self.FRAME_ID static_bloadcaster.sendTransform(trans) start_subscriber() - + telemetry_thread.start() super(CopterClient, self).start() @@ -335,13 +343,19 @@ def _command_move_start_to_current_position(*args, **kwargs): y_ratio=client.active_client.Y_RATIO, ) print("x_start = {}, y_start = {}".format(x_start, y_start)) - telem = FlightLib.get_telemetry(client.active_client.FRAME_ID) - print("x_telem = {}, y_telem = {}".format(telem.x, telem.y)) - client.active_client.config.set('PRIVATE', 'x0', telem.x - x_start) - client.active_client.config.set('PRIVATE', 'y0', telem.y - y_start) - client.active_client.rewrite_config() - client.active_client.load_config() - print ("Start delta: {:.2f} {:.2f}".format(client.active_client.X0, client.active_client.Y0)) + if not math.isnan(x_start): + telem = FlightLib.get_telemetry(client.active_client.FRAME_ID) + print("x_telem = {}, y_telem = {}".format(telem.x, telem.y)) + if not math.isnan(x_telem): + client.active_client.config.set('PRIVATE', 'x0', telem.x - x_start) + client.active_client.config.set('PRIVATE', 'y0', telem.y - y_start) + client.active_client.rewrite_config() + client.active_client.load_config() + print ("Start delta: {:.2f} {:.2f}".format(client.active_client.X0, client.active_client.Y0)) + else: + print ("Wrong telemetry") + else: + print("Wrong animation file") @messaging.message_callback("reset_start") def _command_reset_start(*args, **kwargs): @@ -587,6 +601,66 @@ def _play_animation(*args, **kwargs): ) #print(task_manager.task_queue) +def telemetry_loop(): + global telemetry + rate = rospy.Rate(client.active_client.TELEM_FREQ) + while not rospy.is_shutdown(): + telemetry = telemetry._replace(animation_id = animation.get_id()) + telemetry = telemetry._replace(git_version = subprocess.check_output("git log --pretty=format:'%h' -n 1", shell=True)) + telemetry = telemetry._replace(battery_v = 'nan') + telemetry = telemetry._replace(battery_p = 'nan') + telemetry = telemetry._replace(calibration_status = 'NO_FCU') + telemetry = telemetry._replace(system_status = 'NO_FCU') + telemetry = telemetry._replace(mode = 'NO_FCU') + telemetry = telemetry._replace(current_position = 'NO_POS in {}'.format(client.active_client.FRAME_ID)) + telemetry = telemetry._replace(start_position = 'NO_POS') + try: + ros_telemetry = FlightLib.get_telemetry(client.active_client.FRAME_ID) + except: + pass + else: + if ros_telemetry.connected == False: + stop_subscriber() + start_subscriber() + else: + telemetry = telemetry._replace(battery_v = '{:.2f}'.format(ros_telemetry.voltage)) + batt_empty_param = get_param('BAT_V_EMPTY') + batt_charged_param = get_param('BAT_V_CHARGED') + if batt_empty_param.success and batt_charged_param.success: + batt_empty = batt_empty_param.value.real + batt_charged = batt_charged_param.value.real + telemetry = telemetry._replace(battery_p = '{:.2f}'.format((ros_telemetry.cell_voltage - batt_empty)/(batt_charged - batt_empty)*100.)) + telemetry = telemetry._replace(calibration_status = get_calibration_status()) + telemetry = telemetry._replace(system_status = get_sys_status()) + telemetry = telemetry._replace(mode = ros_telemetry.mode) + if not math.isnan(ros_telemetry.x): + telemetry = telemetry._replace(current_position = '{:.2f} {:.2f} {:.2f} {:.1f} {}'.format(ros_telemetry.x, ros_telemetry.y, ros_telemetry.z, + math.degrees(ros_telemetry.yaw), client.active_client.FRAME_ID)) + x_start, y_start = animation.get_start_xy(os.path.abspath("animation.csv"), + x_ratio=client.active_client.X_RATIO, + y_ratio=client.active_client.Y_RATIO, + ) + x_delta = client.active_client.X0 + client.active_client.X0_COMMON + y_delta = client.active_client.Y0 + client.active_client.Y0_COMMON + z_delta = client.active_client.Z0 + client.active_client.Z0_COMMON + if not math.isnan(x_start): + telemetry = telemetry._replace(start_position = '{:.2f} {:.2f} {:.2f}'.format(x_start+x_delta, y_start+y_delta, z_delta)) + if client.active_client.TELEM_TRANSMIT: + try: + client.active_client.server_connection.send_message('telem', args={'message':create_telemetry_message(telemetry)}) + except Exception as e: + print e + rate.sleep() + +def create_telemetry_message(telemetry): + msg = "" + for key in telemetry.__dict__: + msg += telemetry.__dict__[key] + ';' + msg += repr(time.time()) + return msg + +telemetry_thread = threading.Thread(target=telemetry_loop, name="Telemetry getting thread") + if __name__ == "__main__": copter_client = CopterClient() task_manager = tasking.TaskManager() From f8ae6c626349d806546e44931d04d48ccda27304 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Fri, 25 Oct 2019 17:23:22 +0100 Subject: [PATCH 27/31] Client: Modify default config file --- Drone/client_config.ini | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Drone/client_config.ini b/Drone/client_config.ini index 45585ce..487f309 100644 --- a/Drone/client_config.ini +++ b/Drone/client_config.ini @@ -13,6 +13,10 @@ use_ntp = False host = ntp1.stratum2.ru port = 123 +[TELEMETRY] +frequency = 1 +transmit = True + [ANIMATION] takeoff_animation_check = True land_animation_check = True @@ -22,7 +26,7 @@ y_ratio = 1.0 z_ratio = 1.0 [COPTERS] -frame_id = floor +frame_id = map takeoff_height = 1.0 takeoff_time = 5.0 safe_takeoff = False @@ -44,7 +48,7 @@ yaw = -90 [PRIVATE] id = /hostname restart_dhcpcd = True -use_leds = False +use_leds = True led_pin = 21 x0 = 0 y0 = 0 From fbff10cc25c9bc529a6648e901a3ef7a274902ec Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Fri, 25 Oct 2019 18:05:59 +0100 Subject: [PATCH 28/31] Client: add selfcheck to telemetry --- Drone/copter_client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Drone/copter_client.py b/Drone/copter_client.py index 44123dc..3ff17c2 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -24,8 +24,8 @@ from tf.transformations import quaternion_from_euler, euler_from_quaternion, qua import tf2_ros static_bloadcaster = tf2_ros.StaticTransformBroadcaster() -Telemetry = namedtuple("Telemetry", "git_version animation_id battery_v battery_p system_status calibration_status mode current_position start_position") -telemetry = Telemetry('nan', 'No animation', 'nan', 'nan', 'NO_FCU', 'NO_FCU', 'NO_FCU', 'NO_POS', 'NO_POS') +Telemetry = namedtuple("Telemetry", "git_version animation_id battery_v battery_p system_status calibration_status mode selfcheck current_position start_position") +telemetry = Telemetry('nan', 'No animation', 'nan', 'nan', 'NO_FCU', 'NO_FCU', 'NO_FCU', 'NO_FCU', 'NO_POS', 'NO_POS') # logging.basicConfig( # TODO all prints as logs # level=logging.DEBUG, # INFO @@ -612,6 +612,7 @@ def telemetry_loop(): telemetry = telemetry._replace(calibration_status = 'NO_FCU') telemetry = telemetry._replace(system_status = 'NO_FCU') telemetry = telemetry._replace(mode = 'NO_FCU') + telemetry = telemetry._replace(selfcheck = 'NO_FCU') telemetry = telemetry._replace(current_position = 'NO_POS in {}'.format(client.active_client.FRAME_ID)) telemetry = telemetry._replace(start_position = 'NO_POS') try: @@ -633,6 +634,10 @@ def telemetry_loop(): telemetry = telemetry._replace(calibration_status = get_calibration_status()) telemetry = telemetry._replace(system_status = get_sys_status()) telemetry = telemetry._replace(mode = ros_telemetry.mode) + check = FlightLib.selfcheck() + if not check: + check = "OK" + telemetry = telemetry._replace(selfcheck = str(check)) if not math.isnan(ros_telemetry.x): telemetry = telemetry._replace(current_position = '{:.2f} {:.2f} {:.2f} {:.1f} {}'.format(ros_telemetry.x, ros_telemetry.y, ros_telemetry.z, math.degrees(ros_telemetry.yaw), client.active_client.FRAME_ID)) @@ -653,7 +658,7 @@ def telemetry_loop(): rate.sleep() def create_telemetry_message(telemetry): - msg = "" + msg = client.active_client.client_id + ';' for key in telemetry.__dict__: msg += telemetry.__dict__[key] + ';' msg += repr(time.time()) From 4619a340e37c8de7af90403fb48b8cc4785da51c Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 30 Oct 2019 06:41:20 +0300 Subject: [PATCH 29/31] Server: Add support for telemetry streaming --- Server/server_qt.py | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Server/server_qt.py b/Server/server_qt.py index eed0560..1db2009 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -16,6 +16,7 @@ from quamash import QEventLoop, QThreadExecutor from server_gui import Ui_MainWindow from server import * +import messaging_lib as messaging from copter_table_models import * from emergency import * @@ -54,6 +55,24 @@ def confirmation_required(text="Are you sure?", label="Confirm operation?"): return inner +class Signal(object): + class Emitter(QObject): + send = pyqtSignal(str) + def __init__(self): + super(Signal.Emitter, self).__init__() + + def __init__(self): + self.emitter = Signal.Emitter() + + def send_message(self, message): + self.emitter.send.emit(message) + + def connect(self, signal, slot): + signal.emitter.send.connect(slot) + +class Sender(object): + def __init__(self): + self.signal = Signal() # noinspection PyArgumentList,PyCallByClass class MainWindow(QtWidgets.QMainWindow): @@ -70,6 +89,7 @@ class MainWindow(QtWidgets.QMainWindow): self.player = QtMultimedia.QMediaPlayer() self.init_model() + self.signal = Signal() self.show() @@ -211,6 +231,33 @@ class MainWindow(QtWidgets.QMainWindow): self.signals.update_data_signal.emit(row, col, data, ModelDataRole) + @pyqtSlot(str) + def update_table_data(self, message): + fields = message.split(';') + logging.info(fields) + # 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] + 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] + row = self.model.get_row_index(self.model.get_row_by_attr('copter_id', copter_id)) + logging.info("Row = {}".format(row)) + self.signals.update_data_signal.emit(row, 1, animation_id, ModelDataRole) + self.signals.update_data_signal.emit(row, 2, battery_v, ModelDataRole) + self.signals.update_data_signal.emit(row, 3, battery_p, 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, selfcheck, ModelDataRole) + self.signals.update_data_signal.emit(row, 7, current_pos, ModelDataRole) + self.signals.update_data_signal.emit(row, 8, "{}".format(round(float(copter_time) - time.time(), 3)), ModelDataRole) #def set_copter_id(self, value, copter_data_row): # col = 0 @@ -565,6 +612,11 @@ 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) + sender.signal.send_message(message) + if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) @@ -573,7 +625,9 @@ if __name__ == "__main__": #app.exec_() with loop: + sender = Sender() window = MainWindow() + sender.signal.connect(sender.signal, window.update_table_data) Client.on_first_connect = window.new_client_connected Client.on_connect = window.client_connection_changed From 9f536356abc75db7056816886740437e168867b9 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 30 Oct 2019 07:05:48 +0300 Subject: [PATCH 30/31] Server: Update request for telemetry --- Server/server_qt.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Server/server_qt.py b/Server/server_qt.py index 1db2009..6f697e0 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -187,7 +187,7 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.flip_button.setEnabled(False) @pyqtSlot() - def selfcheck_selected(self): + def selfcheck_selected_old(self): for copter_data_row in self.model.user_selected(): client = copter_data_row.client @@ -200,6 +200,7 @@ class MainWindow(QtWidgets.QMainWindow): client.get_response("position", self.set_copter_data, callback_args=(7, copter_data_row)) client.get_response("time", self.set_copter_data, callback_args=(8, copter_data_row)) + def set_copter_data(self, value, col, copter_data_row): row = self.model.get_row_index(copter_data_row) if row is None: @@ -231,10 +232,16 @@ class MainWindow(QtWidgets.QMainWindow): self.signals.update_data_signal.emit(row, col, data, ModelDataRole) + @pyqtSlot() + def selfcheck_selected(self): + for copter_data_row in self.model.user_selected(): + client = copter_data_row.client + client.get_response("telemetry", self.update_table_data) + @pyqtSlot(str) def update_table_data(self, message): - fields = message.split(';') - logging.info(fields) + fields = message.split('`') + logging.info(fields[8]) # 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] From 05303cc4b7bd13ae24555342b39ecf360e652bc3 Mon Sep 17 00:00:00 2001 From: Arthur Golubtsov Date: Wed, 30 Oct 2019 04:06:59 +0000 Subject: [PATCH 31/31] Client: Add response for telemetry --- Drone/copter_client.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Drone/copter_client.py b/Drone/copter_client.py index 3ff17c2..666fa36 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -260,6 +260,10 @@ def _response_selfcheck(*args, **kwargs): stop_subscriber() return "NOT_CONNECTED_TO_FCU" +@messaging.request_callback("telemetry") +def _response_telemetry(*args, **kwargs): + return create_telemetry_message(telemetry) + @messaging.request_callback("anim_id") def _response_animation_id(*args, **kwargs): @@ -607,20 +611,20 @@ def telemetry_loop(): while not rospy.is_shutdown(): telemetry = telemetry._replace(animation_id = animation.get_id()) telemetry = telemetry._replace(git_version = subprocess.check_output("git log --pretty=format:'%h' -n 1", shell=True)) - telemetry = telemetry._replace(battery_v = 'nan') - telemetry = telemetry._replace(battery_p = 'nan') - telemetry = telemetry._replace(calibration_status = 'NO_FCU') - telemetry = telemetry._replace(system_status = 'NO_FCU') - telemetry = telemetry._replace(mode = 'NO_FCU') - telemetry = telemetry._replace(selfcheck = 'NO_FCU') - telemetry = telemetry._replace(current_position = 'NO_POS in {}'.format(client.active_client.FRAME_ID)) - telemetry = telemetry._replace(start_position = 'NO_POS') try: ros_telemetry = FlightLib.get_telemetry(client.active_client.FRAME_ID) except: pass else: if ros_telemetry.connected == False: + telemetry = telemetry._replace(battery_v = 'nan') + telemetry = telemetry._replace(battery_p = 'nan') + telemetry = telemetry._replace(calibration_status = 'NO_FCU') + telemetry = telemetry._replace(system_status = 'NO_FCU') + telemetry = telemetry._replace(mode = 'NO_FCU') + telemetry = telemetry._replace(selfcheck = 'NO_FCU') + telemetry = telemetry._replace(current_position = 'NO_POS in {}'.format(client.active_client.FRAME_ID)) + telemetry = telemetry._replace(start_position = 'NO_POS') stop_subscriber() start_subscriber() else: @@ -631,6 +635,8 @@ def telemetry_loop(): batt_empty = batt_empty_param.value.real batt_charged = batt_charged_param.value.real telemetry = telemetry._replace(battery_p = '{:.2f}'.format((ros_telemetry.cell_voltage - batt_empty)/(batt_charged - batt_empty)*100.)) + else: + telemetry = telemetry._replace(battery_p = 'nan') telemetry = telemetry._replace(calibration_status = get_calibration_status()) telemetry = telemetry._replace(system_status = get_sys_status()) telemetry = telemetry._replace(mode = ros_telemetry.mode) @@ -641,6 +647,8 @@ def telemetry_loop(): if not math.isnan(ros_telemetry.x): telemetry = telemetry._replace(current_position = '{:.2f} {:.2f} {:.2f} {:.1f} {}'.format(ros_telemetry.x, ros_telemetry.y, ros_telemetry.z, math.degrees(ros_telemetry.yaw), client.active_client.FRAME_ID)) + else: + telemetry = telemetry._replace(current_position = 'NO_POS in {}'.format(client.active_client.FRAME_ID)) x_start, y_start = animation.get_start_xy(os.path.abspath("animation.csv"), x_ratio=client.active_client.X_RATIO, y_ratio=client.active_client.Y_RATIO, @@ -650,6 +658,8 @@ def telemetry_loop(): z_delta = client.active_client.Z0 + client.active_client.Z0_COMMON if not math.isnan(x_start): telemetry = telemetry._replace(start_position = '{:.2f} {:.2f} {:.2f}'.format(x_start+x_delta, y_start+y_delta, z_delta)) + else: + telemetry = telemetry._replace(start_position = 'NO_POS') if client.active_client.TELEM_TRANSMIT: try: client.active_client.server_connection.send_message('telem', args={'message':create_telemetry_message(telemetry)}) @@ -658,9 +668,9 @@ def telemetry_loop(): rate.sleep() def create_telemetry_message(telemetry): - msg = client.active_client.client_id + ';' + msg = client.active_client.client_id + '`' for key in telemetry.__dict__: - msg += telemetry.__dict__[key] + ';' + msg += telemetry.__dict__[key] + '`' msg += repr(time.time()) return msg