diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..377da27 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**The bug description** +A clear and concise description of what the bug is. + +**Steps to reproduce the behavior** +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Version** +v0.3-alpha.2 (for example) + +**Additional information** +Add any other information about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6f8ddbc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Request description** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index 88cd199..f9b589a 100644 --- a/.gitignore +++ b/.gitignore @@ -109,7 +109,7 @@ Drone/test_animation/ Drone/animation.csv Drone/client_logs images/ - +.vscode/ \.idea/ Drone/_copter_client_old_\.py diff --git a/.travis.yml b/.travis.yml index 9fe38dd..5603a77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ env: - if [[ -z ${TRAVIS_TAG} ]]; then IMAGE_VERSION="${TRAVIS_COMMIT}}"; else IMAGE_VERSION="${TRAVIS_TAG}"; fi - IMAGE_NAME="$(basename -s '.git' ${TARGET_REPO})_${IMAGE_VERSION}.img" git: - depth: 50 + depth: false jobs: fast_finish: true include: diff --git a/Drone/FlightLib/FlightLib.py b/Drone/FlightLib/FlightLib.py index ac37f91..cb653a1 100644 --- a/Drone/FlightLib/FlightLib.py +++ b/Drone/FlightLib/FlightLib.py @@ -107,7 +107,7 @@ def check_connection(): @check("Linear velocity estimation") -def check_linear_speeds(speed_limit=0.1): +def check_linear_speeds(speed_limit=0.15): telemetry = get_telemetry(frame_id='body') if _check_nans(telemetry.vx, telemetry.vy, telemetry.vz): diff --git a/Drone/client.py b/Drone/client.py index 8b5d6fe..f824e5e 100644 --- a/Drone/client.py +++ b/Drone/client.py @@ -175,10 +175,14 @@ class Client(object): ConfigOption("SERVER", "port", self.server_port), ConfigOption("SERVER", "host", self.server_host)) logger.info("Binding to new IP: {}:{}".format(self.server_host, self.server_port)) + self.on_broadcast_bind() break finally: broadcast_client.close() + def on_broadcast_bind(self): + pass + def _process_connections(self): while True: events = self.selector.select(timeout=1) diff --git a/Drone/copter_client.py b/Drone/copter_client.py index ac22c1f..065906c 100644 --- a/Drone/copter_client.py +++ b/Drone/copter_client.py @@ -41,6 +41,10 @@ class CopterClient(client.Client): self.USE_LEDS = self.config.getboolean('PRIVATE', 'use_leds') self.LED_PIN = self.config.getint('PRIVATE', 'led_pin') + def on_broadcast_bind(self): + configure_chrony_ip(self.server_host) + restart_service("chrony") + def start(self, task_manager_instance): client.logger.info("Init ROS node") rospy.init_node('Swarm_client', anonymous=True) @@ -52,6 +56,43 @@ class CopterClient(client.Client): super(CopterClient, self).start() +def restart_service(name): + os.system("systemctl restart {}".format(name)) + + +def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1): + try: + with open(path, 'r') as f: + raw_content = f.read() + except IOError as e: + print("Reading error {}".format(e)) + return False + + content = raw_content.split(" ") + + try: + current_ip = content[ip_index] + except IndexError: + print("Something wrong with config") + return False + + if "." not in current_ip: + print("That's not ip!") + return False + + if current_ip != ip: + content[ip_index] = ip + + try: + with open(path, 'w') as f: + f.write(" ".join(content)) + except IOError: + print("Error writing") + return False + + return True + + @messaging.request_callback("selfcheck") def _response_selfcheck(): check = FlightLib.selfcheck() @@ -75,12 +116,18 @@ def _response_cell(): @messaging.message_callback("test") def _command_test(**kwargs): - print("test") + logger.info("logging info test") + print("stdout test") @messaging.message_callback("service_restart") def _command_service_restart(**kwargs): - os.system("systemctl restart {}".format(kwargs["name"])) + restart_service(kwargs["name"]) + +@messaging.message_callback("repair_chrony") +def _command_chrony_repair(): + configure_chrony_ip(client.active_client.server_host) + restart_service("chrony") @messaging.message_callback("led_test") @@ -103,7 +150,6 @@ def _command_led_fill(**kwargs): def _copter_flip(): FlightLib.flip(frame_id=client.active_client.FRAME_ID) - @messaging.message_callback("takeoff") def _command_takeoff(**kwargs): task_manager.add_task(time.time(), 0, animation.takeoff, @@ -193,7 +239,7 @@ def _play_animation(**kwargs): frame_time = rfp_time + client.active_client.RFP_TIME frame_delay = 0.125 # TODO from animation file for frame in frames: - point, color = animation.convert_frame(frame) # TODO add param to calculate delta + point, color, yaw = animation.convert_frame(frame) # TODO add param to calculate delta task_manager.add_task(frame_time, 0, animation.execute_frame, task_kwargs={ "point": point, diff --git a/README.md b/README.md index 68673b6..ff3c0a0 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# CleverSwarm -Програмное обеспечение для запуска шоу дронов под управлением Raspberry Pi с пакетом COEX Clever. - +# clever-show [![Build Status](https://travis-ci.org/artem30801/CleverSwarm.svg?branch=master)](https://travis-ci.org/artem30801/CleverSwarm) -### Пакет включает в себя: -* Набор ПО для дрона, включащее в себя библиотеку для автономного полёта, модуль для воспроизведения анимаций и клиентское приложение для удаленного синхронизированного управления -* Серверное приложение для удаленного синхронизированного управления дронами и удобной передачи анимации +Програмное обеспечение для запуска шоу дронов под управлением Raspberry Pi с пакетом COEX [Clever](https://github.com/copterexpress/clever). -## Установка -Скачайте или склонируйте этот репозиторий на компьютер и дроны: -```bash -git clone https://github.com/artem30801/CleverSwarm.git -``` -Для дальнейших инструкций перейдите на Wiki +### Пакет включает в себя: +* [Набор ПО для дрона](https://github.com/artem30801/CleverSwarm/tree/master/Drone), включащее в себя библиотеку для автономного полёта, модуль для воспроизведения анимаций и клиентское приложение для удаленного синхронизированного управления +* [Серверное приложение](https://github.com/artem30801/CleverSwarm/tree/master/Server) для удаленного синхронизированного управления дронами и удобной настройки системы для воспроизведения анимации +* [Аддон для Blender 2.8](https://github.com/artem30801/CleverSwarm/tree/master/blender-addon) для преобразования анимации полёта коптеров, созданной в Blender, в файлы полётов для каждого коптера +* [Образ для Raspberry Pi](https://github.com/artem30801/CleverSwarm/releases/latest) для быстрого запуска ПО на коптере + +## Документация +Инструкция по запуску ПО находится [здесь](docs/start-tutorial.md). + +Подробная документация расположена в папке [docs](https://github.com/artem30801/CleverSwarm/tree/master/docs). diff --git a/Server/chrony.conf b/Server/chrony.conf new file mode 100644 index 0000000..bccbf9e --- /dev/null +++ b/Server/chrony.conf @@ -0,0 +1,5 @@ +server master iburst +driftfile /var/lib/chrony/drift +allow 192.168.0.0/16 +makestep 1.0 3 +rtcsync \ No newline at end of file diff --git a/Server/copter_table_models.py b/Server/copter_table_models.py index 527c8ae..57fe84f 100644 --- a/Server/copter_table_models.py +++ b/Server/copter_table_models.py @@ -1,14 +1,15 @@ import sys import re -from operator import itemgetter +import collections from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import Qt as Qt class CopterData: - class_attrs = {'copter_id': None, 'anim_id': None, 'batt_v': None, 'batt_p': None, 'selfcheck': None, - 'time_utc': None, "time_delta": None, "client": None, "checked": 0} + class_attrs = collections.OrderedDict([('copter_id', None), ('anim_id', None), ('batt_v', None), ('batt_p', None), + ('selfcheck', None), ('time_utc', None), ("time_delta", None), + ("client", None), ("checked", 0)], ) def __init__(self, **kwargs): self.attrs = self.class_attrs.copy() @@ -27,10 +28,11 @@ class CopterData: class CopterDataModel(QtCore.QAbstractTableModel): checks = {} selected_ready_signal = QtCore.pyqtSignal(bool) + selected_takeoff_ready_signal = QtCore.pyqtSignal(bool) def __init__(self, parent=None): super(CopterDataModel, self).__init__(parent) - self.headers = ('copter ID', 'animation ID', 'battery (V.)', 'battery (%)', 'selfcheck', 'time UTC', "time delta") + self.headers = ('copter ID', 'animation ID', 'battery V', 'battery %', 'selfcheck', 'time delta') self.data_contents = [] def insertRows(self, contents, position='last', parent=QtCore.QModelIndex()): @@ -49,6 +51,10 @@ class CopterDataModel(QtCore.QAbstractTableModel): contents = contents or self.data_contents return filter(lambda x: all_checks(x), contents) + def takeoff_ready(self, contents=()): + contents = contents or self.data_contents + return filter(lambda x: takeoff_checks(x), contents) + def rowCount(self, n=None): return len(self.data_contents) @@ -84,6 +90,8 @@ class CopterDataModel(QtCore.QAbstractTableModel): 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.dataChanged.emit(index, index, (QtCore.Qt.EditRole,)) @QtCore.pyqtSlot() @@ -93,8 +101,6 @@ class CopterDataModel(QtCore.QAbstractTableModel): if role == Qt.CheckStateRole: self.data_contents[index.row()].checked = value - # check if all selected are selfcheck and ok (ready) - self.selected_ready_signal.emit(set(self.user_selected()).issubset(self.selfchecked_ready())) elif role == Qt.EditRole: self.data_contents[index.row()][index.column()] = value @@ -135,6 +141,8 @@ def col_check(col): def check_anim(item): if not item: return None + if str(item) == 'No animation': + return False else: return True @@ -150,13 +158,14 @@ def check_bat_v(item): @col_check(3) -def check_bat_v(item): +def check_bat_p(item): if not item: return None - if float(item) > 15: # todo config + if float(item) > 30: # todo config return True else: return False + #return True #For testing @col_check(4) @@ -168,6 +177,15 @@ def check_selfcheck(item): else: return False +@col_check(5) +def check_time_delta(item): + if not item: + return None + if abs(float(item)) < 1: + return True + else: + return False + def all_checks(copter_item): for col, check in CopterDataModel.checks.items(): @@ -175,6 +193,11 @@ def all_checks(copter_item): return False return True +def takeoff_checks(copter_item): + for i in range(3): + if not CopterDataModel.checks[2+i](copter_item[2+i]): + return False + return True class CopterProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, parent=None): diff --git a/Server/server.py b/Server/server.py index 897d2b3..51f6e27 100644 --- a/Server/server.py +++ b/Server/server.py @@ -140,7 +140,7 @@ class Server: self.sel.register(self.server_socket, selectors.EVENT_READ | selectors.EVENT_WRITE, data=None) while self.client_processor_thread_running.is_set(): - events = self.sel.select(timeout=0) + events = self.sel.select() for key, mask in events: if key.data is None: self._connect_client(key.fileobj) @@ -159,7 +159,7 @@ class Server: logging.info("Got connection from: {}".format(str(addr))) conn.setblocking(False) - if not any(client_addr == addr[0] for client_addr in Client.clients.keys()): + if not any([client_addr == addr[0] for client_addr in Client.clients.keys()]): client = Client(addr[0]) logging.info("New client") else: diff --git a/Server/server_qt.py b/Server/server_qt.py index 02060d5..1d3247d 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -27,7 +27,8 @@ def confirmation_required(text="Are you sure?", label="Confirm operation?"): ) if reply == QMessageBox.Yes: print("Dialog accepted") - return f(*args, **kwargs) + #print(args) + return f(args[0]) else: print("Dialog declined") @@ -68,7 +69,7 @@ class MainWindow(QtWidgets.QMainWindow): # Connect model signals to UI self.model.selected_ready_signal.connect(self.ui.start_button.setEnabled) - self.model.selected_ready_signal.connect(self.ui.takeoff_button.setEnabled) + self.model.selected_takeoff_ready_signal.connect(self.ui.takeoff_button.setEnabled) def client_connected(self, client: Client): self.signals.add_client_signal.emit(CopterData(copter_id=client.copter_id, client=client)) @@ -77,7 +78,7 @@ class MainWindow(QtWidgets.QMainWindow): # Connecting self.ui.check_button.clicked.connect(self.selfcheck_selected) self.ui.start_button.clicked.connect(self.send_starttime) - self.ui.pause_button.clicked.connect(self.pause_resume_all) + self.ui.pause_button.clicked.connect(self.pause_resume_selected) self.ui.stop_button.clicked.connect(self.stop_all) self.ui.emergency_button.clicked.connect(self.emergency) @@ -112,42 +113,44 @@ class MainWindow(QtWidgets.QMainWindow): if col == 1: data = value elif col == 2: - data = "{} V.".format(round(float(value), 3)) + data = "{}".format(round(float(value), 3)) elif col == 3: batt_percent = ((float(value) - 3.2) / (4.2 - 3.2)) * 100 # TODO config - data = "{} %".format(round(batt_percent, 3)) + data = "{}".format(round(batt_percent, 3)) elif col == 4: data = str(value) elif col == 5: - data = time.ctime(int(value)) - data2 = "{} sec.".format(round(int(value) - time.time(), 3)) - self.signals.update_data_signal.emit(row, col + 1, data2) + #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) else: print("No column matched for response") return self.signals.update_data_signal.emit(row, col, data) - @pyqtSlot() @confirmation_required("This operation will takeoff selected copters with delay and start animation. Proceed?") - def send_starttime(self): + @pyqtSlot() + def send_starttime(self, **kwargs): dt = self.ui.start_delay_spin.value() for copter in self.model.user_selected(): if all_checks(copter): server.send_starttime(copter.client, dt) - @pyqtSlot() @confirmation_required("This operation will takeoff copters immediately. Proceed?") - def takeoff_selected(self): + @pyqtSlot() + def takeoff_selected(self, **kwargs): for copter in self.model.user_selected(): - if all_checks(copter): + if takeoff_checks(copter): copter.client.send_message("takeoff") - @pyqtSlot() @confirmation_required("This operation will flip(!!!) copters immediately. Proceed?") - def flip(self): + @pyqtSlot() + def flip(self, **kwargs): for copter in self.model.user_selected(): - if all_checks(copter): + if takeoff_checks(copter): copter.client.send_message("flip") @pyqtSlot() @@ -160,17 +163,19 @@ class MainWindow(QtWidgets.QMainWindow): Client.broadcast_message("stop") @pyqtSlot() - def pause_resume_all(self): + def pause_resume_selected(self): if self.ui.pause_button.text() == 'Pause': - Client.broadcast_message('pause') + for copter in self.model.user_selected(): + copter.client.send_message("pause") self.ui.pause_button.setText('Resume') else: - self._resume_all() + self._resume_selected() - @confirmation_required("This operation will resume ALL copter tasks with given delay. Proceed?") - def _resume_all(self): - dt = self.ui.start_delay_spin.value() - Client.broadcast_message('resume', {"time": 0 if dt == 0 else server.time_now()}) + #@confirmation_required("This operation will resume ALL copter tasks with given delay. Proceed?") + def _resume_selected(self, **kwargs): + time_gap = 0.1 + for copter in self.model.user_selected(): + copter.client.send_message('resume', {"time": server.time_now() + time_gap}) self.ui.pause_button.setText('Pause') @pyqtSlot() diff --git a/blender-addon/README.md b/blender-addon/README.md new file mode 100644 index 0000000..50a5992 --- /dev/null +++ b/blender-addon/README.md @@ -0,0 +1,25 @@ +# blender-csv-animation +A Blender extension that export paths of objects in blender animation to a csv files + +## CSV file format +First row is the animation filename. +Every next row of the file contains following information about an object: +- frame number, +- x coordinate, +- y coordinate, +- z coordinate, +- rotaion around z-axis angle (yaw for copter), +- rgb. + +## How to use it +Clone or download this repository +```bash +git clone https://github.com/artem30801/CleverSwarm.git +``` +Open Blender and install the addon: +1) Open User Prerences windows using main menu or shortcut (Ctrl + Alt + U): Files - User Preferences +2) Under Add-ons tab click Install Add-on from File... +3) Choose addon.py file from the directory of this repository +4) Enable the Add-on + +Use [official docs](https://docs.blender.org/manual/en/latest/preferences/addons.html) for getting additional information diff --git a/blender-addon/addon.py b/blender-addon/addon.py new file mode 100644 index 0000000..1a9d527 --- /dev/null +++ b/blender-addon/addon.py @@ -0,0 +1,213 @@ +import os +import csv +import math + +import bpy +from bpy_extras.io_utils import ExportHelper +from bpy.types import Operator +from bpy.props import StringProperty, BoolProperty, FloatProperty, IntProperty + +bl_info = { + "name": "Export > CSV Drone Swarm Animation Exporter (.csv)", + "author": "Artem Vasiunik", + "version": (0, 4, 0), + "blender": (2, 80, 0), + #"api": 36079, + "location": "File > Export > CSV Drone Swarm Animation Exporter (.csv)", + "description": "Export > CSV Drone Swarm Animation Exporter (.csv)", + "warning": "", + "wiki_url": "https://github.com/artem30801/blender-csv-animation/blob/master/README.md", + "tracker_url": "https://github.com/artem30801/blender-csv-animation/issues", + "category": "Import-Export" +} + + +class ExportCsv(Operator, ExportHelper): + bl_idname = "export_swarm_anim.folder" + bl_label = "Export Drone Swarm animation" + filename_ext = '' + use_filter_folder = True + + use_namefilter: bpy.props.BoolProperty( + name="Use name filter for objects", + default=True, + ) + + drones_name: bpy.props.StringProperty( + name="Name identifier", + description="Name identifier for all drone objects", + default="copter" + ) + + show_warnings: bpy.props.BoolProperty( + name="Show detailed animation warnings", + default=False, + ) + + speed_warning_limit: bpy.props.FloatProperty( + name="Speed limit", + description="Limit of drone movement speed (m/s)", + unit='VELOCITY', + default=3, + min=0, + ) + drone_distance_limit: bpy.props.FloatProperty( + name="Distance limit", + description="Closest possible distance between drones (m)", + unit='LENGTH', + default=1.5, + min=0, + ) + + filepath: StringProperty( + name="File Path", + description="File path used for exporting CSV files", + maxlen=1024, + subtype='DIR_PATH', + default="" + ) + + def execute(self, context): + + create_folder_if_does_not_exist(self.filepath) + scene = context.scene + objects = context.visible_objects + + drone_objects = [] + if self.use_namefilter: + for drone_obj in objects: + if self.drones_name.lower() in drone_obj.name.lower(): + drone_objects.append(drone_obj) + else: + drone_objects = objects + + frame_start = scene.frame_start + frame_end = scene.frame_end + + for drone_obj in drone_objects: + with open(os.path.join(self.filepath, '{}.csv'.format(drone_obj.name.lower())), 'w') as csv_file: + animation_file_writer = csv.writer( + csv_file, + delimiter=',', + quotechar='|', + quoting=csv.QUOTE_MINIMAL + ) + speed_exeeded = False + distance_exeeded = False + + prev_x, prev_y, prev_z = 0, 0, 0 + + animation_file_writer.writerow([ + os.path.splitext(bpy.path.basename(bpy.data.filepath))[0] + ]) + + for frame_number in range(frame_start, frame_end + 1): + scene.frame_set(frame_number) + rgb = get_rgb_from_object(drone_obj) + x, y, z = drone_obj.matrix_world.to_translation() + rot_z = drone_obj.matrix_world.to_euler('XYZ')[2] + + speed = calc_speed((x, y, z), (prev_x, prev_y, prev_z)) if frame_number != frame_start else 1 + prev_x, prev_y, prev_z = x, y, z + + if speed > self.speed_warning_limit: + speed_exeeded = True + if self.show_warnings: + self.report({'WARNING'}, + "Speed of drone '%s' is greater than %s m/s (%s m/s) on frame %s" % + (drone_obj.name, round(self.speed_warning_limit, 5), round(speed, 5), frame_number)) + + for second_drone_obj in drone_objects: + if second_drone_obj is not drone_obj: + x2, y2, z2 = second_drone_obj.matrix_world.to_translation() + distance = calc_distance((x, y, z), (x2, y2, z2)) + if distance < self.drone_distance_limit: + distance_exeeded = True + if self.show_warnings: + self.report({'WARNING'}, + "Distance beteween drones '%s' and '%s' is less than %s m (%s m) on frame %s" % + (drone_obj.name, second_drone_obj.name, + round(self.drone_distance_limit, 5), round(distance, 5), frame_number)) + + animation_file_writer.writerow([ + str(frame_number), + round(x, 5), round(y, 5), round(z, 5), + round(rot_z, 5), + *rgb, + ]) + + + + if speed_exeeded: + self.report({'WARNING'}, "Drone '%s' speed limits exeeded" % drone_obj.name) + if distance_exeeded: + self.report({'WARNING'}, "Drone '%s' distance limits exeeded" % drone_obj.name) + self.report({'WARNING'}, "Animation file exported for drone '%s'" % drone_obj.name) + return {'FINISHED'} + + +def create_folder_if_does_not_exist(folder_path): + if os.path.isdir(folder_path): + return + os.mkdir(folder_path) + + +def get_rgb_from_object(obj): + rgb = [0, 0, 0] + try: + if len(obj.material_slots) > 0: + print('material slots true') + for slot in obj.material_slots: + if "led_color" in slot.name.lower(): + print('led color') + if slot.material.use_nodes: + for node in slot.material.node_tree.nodes: + if node.type in ('EMISSION', 'BSDF_DIFFUSE'): + alpha = node.inputs[0].default_value[3] + for component in range(3): + rgb[component] = int(node.inputs[0].default_value[component] * alpha * 255) + else: + print('no led color') + for component in range(3): + rgb[component] = int(slot.material.diffuse_color[component] * 255) + + except AttributeError: + pass + finally: + return rgb + + +def calc_speed(start_point, end_point): + time_delta = 0.1 + distance = calc_distance(start_point, end_point) + return distance / time_delta + + +def calc_distance(start_point, end_point): + distance = math.sqrt( + (start_point[0] - end_point[0]) ** 2 + + (start_point[1] - end_point[1]) ** 2 + + (start_point[2] - end_point[2]) ** 2 + ) + return distance + + +def menu_func(self, context): + self.layout.operator( + ExportCsv.bl_idname, + text="CSV Drone Swarm Animation Exporter (.csv)" + ) + + +def register(): + bpy.utils.register_class(ExportCsv) + bpy.types.TOPBAR_MT_file_export.append(menu_func) + + +def unregister(): + bpy.utils.unregister_class(ExportCsv) + bpy.types.TOPBAR_MT_file_export.remove(menu_func) + + +if __name__ == "__main__": + register() diff --git a/builder/assets/animation_map.txt b/builder/assets/animation_map.txt deleted file mode 100644 index 8e04a74..0000000 --- a/builder/assets/animation_map.txt +++ /dev/null @@ -1,25 +0,0 @@ -0 0.3375 0.0 4.6 0 0 0 0 -1 0.3375 1.15 4.6 0 0 0 0 -2 0.3375 2.3 4.6 0 0 0 0 -3 0.3375 3.45 4.6 0 0 0 0 -4 0.3375 4.6 4.6 0 0 0 0 -5 0.3375 0.0 3.45 0 0 0 0 -6 0.3375 1.15 3.45 0 0 0 0 -7 0.3375 2.3 3.45 0 0 0 0 -8 0.3375 3.45 3.45 0 0 0 0 -9 0.3375 4.6 3.45 0 0 0 0 -10 0.3375 0.0 2.3 0 0 0 0 -11 0.3375 1.15 2.3 0 0 0 0 -12 0.3375 2.3 2.3 0 0 0 0 -13 0.3375 3.45 2.3 0 0 0 0 -14 0.3375 4.6 2.3 0 0 0 0 -15 0.3375 0.0 1.15 0 0 0 0 -16 0.3375 1.15 1.15 0 0 0 0 -17 0.3375 2.3 1.15 0 0 0 0 -18 0.3375 3.45 1.15 0 0 0 0 -19 0.3375 4.6 1.15 0 0 0 0 -20 0.3375 0.0 0.0 0 0 0 0 -21 0.3375 1.15 0.0 0 0 0 0 -22 0.3375 2.3 0.0 0 0 0 0 -23 0.3375 3.45 0.0 0 0 0 0 -24 0.3375 4.6 0.0 0 0 0 0 diff --git a/builder/assets/clever-show.service b/builder/assets/clever-show.service index 0c57193..c93c431 100644 --- a/builder/assets/clever-show.service +++ b/builder/assets/clever-show.service @@ -1,12 +1,12 @@ [Unit] Description=Clever Show Client Service -Requires=clever.service After=clever.service [Service] WorkingDirectory=/home/pi/CleverSwarm/Drone EnvironmentFile=/lib/systemd/system/roscore.env ExecStart=/usr/bin/python /home/pi/CleverSwarm/Drone/copter_client.py +KillSignal=SIGKILL Restart=on-failure RestartSec=3 diff --git a/builder/clever-config/launch/aruco.launch b/builder/clever-config/launch/aruco.launch new file mode 100644 index 0000000..d7754d9 --- /dev/null +++ b/builder/clever-config/launch/aruco.launch @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/builder/clever-config/launch/clever.launch b/builder/clever-config/launch/clever.launch new file mode 100644 index 0000000..1020ea2 --- /dev/null +++ b/builder/clever-config/launch/clever.launch @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/builder/clever-config/launch/main_camera.launch b/builder/clever-config/launch/main_camera.launch new file mode 100644 index 0000000..92bec22 --- /dev/null +++ b/builder/clever-config/launch/main_camera.launch @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/builder/clever-config/map/animation_map.txt b/builder/clever-config/map/animation_map.txt new file mode 100644 index 0000000..9a54274 --- /dev/null +++ b/builder/clever-config/map/animation_map.txt @@ -0,0 +1,8 @@ +107 0.33 0 0 0 0 0 0 +106 0.33 0.77 0 0 0 0 0 +105 0.33 0 0.77 0 0 0 0 +104 0.33 0.77 0.77 0 0 0 0 +103 0.33 0 1.54 0 0 0 0 +102 0.33 0.77 1.54 0 0 0 0 +101 0.33 0 2.31 0 0 0 0 +100 0.33 0.77 2.31 0 0 0 0 diff --git a/builder/image-build.sh b/builder/image-build.sh index 5432a5f..e2f70d5 100755 --- a/builder/image-build.sh +++ b/builder/image-build.sh @@ -30,10 +30,12 @@ echo_stamp() { REPO_DIR="/mnt" SCRIPTS_DIR="${REPO_DIR}/builder" +CONFIG_DIR="${SCRIPTS_DIR}/clever-config" IMAGES_DIR="${REPO_DIR}/images" [[ ! -d ${SCRIPTS_DIR} ]] && (echo_stamp "Directory ${SCRIPTS_DIR} doesn't exist" "ERROR"; exit 1) [[ ! -d ${IMAGES_DIR} ]] && mkdir ${IMAGES_DIR} && echo_stamp "Directory ${IMAGES_DIR} was created successful" "SUCCESS" +[[ ! -d ${CONFIG_DIR} ]] && mkdir ${CONFIG_DIR} && echo_stamp "Directory ${CONFIG_DIR} was created successful" "SUCCESS" if [[ -z ${TRAVIS_TAG} ]]; then IMAGE_VERSION="$(cd ${REPO_DIR}; git log --format=%h -1)"; else IMAGE_VERSION="${TRAVIS_TAG}"; fi # IMAGE_VERSION="${TRAVIS_TAG:=$(cd ${REPO_DIR}; git log --format=%h -1)}" @@ -105,7 +107,10 @@ git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" # Copy service file for clever show client img-chroot ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/clever-show.service' '/lib/systemd/system/' -img-chroot ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/animation_map.txt' '/home/pi/catkin_ws/src/clever/aruco_pose/map/' + +# 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 # Shrink image img-resize ${IMAGE_PATH} diff --git a/builder/image-configure.sh b/builder/image-configure.sh index 8f96540..f7e0885 100755 --- a/builder/image-configure.sh +++ b/builder/image-configure.sh @@ -23,7 +23,7 @@ echo_stamp() { } # rename wifi ssid -sed -i "s/NEW_SSID='CLEVER/NEW_SSID='CleverShow/" /root/init_rpi.sh +sed -i "s/NEW_SSID='CLEVER/NEW_SSID='CLEVERSHOW/" /root/init_rpi.sh # add sudoers variables to make sudo works with ros (for led strip) grep -qxF 'Defaults env_keep += "ROS_LOG_DIR"' /etc/sudoers || cat << EOT >> /etc/sudoers @@ -38,13 +38,5 @@ Defaults env_keep += "ROS_HOME" Defaults env_keep += "ROS_LOG_DIR" EOT -# configure aruco.launch and clever.launch (for positioning with aruco map) -sed -i '/' /home/pi/catkin_ws/src/clever/clever/launch/aruco.launch -sed -i '/' /home/pi/catkin_ws/src/clever/clever/launch/aruco.launch -sed -i '/' /home/pi/catkin_ws/src/clever/clever/launch/aruco.launch -sed -i '/' /home/pi/catkin_ws/src/clever/clever/launch/clever.launch -sed -i '/' /home/pi/catkin_ws/src/clever/clever/launch/clever.launch -#sed -i '/' /home/pi/catkin_ws/src/clever/clever/launch/clever.launch - echo_stamp "Image was configured!" "SUCCESS" diff --git a/docs/blender-addon.md b/docs/blender-addon.md new file mode 100644 index 0000000..8c8b009 --- /dev/null +++ b/docs/blender-addon.md @@ -0,0 +1,23 @@ +# Установка и настройка аддона +## Установка +1. Скачайте [аддон](https://github.com/artem30801/blender-csv-animation) для экспорта анимации из Blender в полётные пути для коптеров. +2. Скачайте и установите согласно инструкциям последнюю версию Blender 2.8 (beta) с [оффициального сайта](https://builder.blender.org/download/) или при использовании OS Linux через команду терминала: +```bash +snap install blender --channel=beta --classic +``` +3. Откройте Blender, в верхнем меню выберите `Edit > Preferences`. В открывшемся окне настроек в боковой панели выберите пункт `Add-ons`. Нажмите на кнопку `Install...` в верхнем правом углу окна. В диалоговом окне откройте путь к папке со склонированным репозиторием проекта и выберите файл `addon.py` по пути [`blender-csv-animation/addon.py`](https://github.com/artem30801/blender-csv-animation/blob/master/addon.py). Нажмите `Install Add-on from file...`. Аддон установлен. +## Активация +В выпадающем списке `All` выберите пункт `User`. Поставьте "галочку" напротив аддона `Import-Export: Export > CSV Drone Swarm Animation Exporter` для активации аддона. Аддон активирован и готов к работе. Выполнение этих операций не понадобится при дальнейших запусках Blender. +## Дополнительно +Для деактивации аддона уберите "галочку" напротив имени аддона, как описано в предыдущем пункте. Для получения дополнительных сведений (версия, путь к файлу...) нажмите знак стрелочки слева от поля активации. В развернувшемся блоке так же есть кнопки: `Documentation` - ведет на страницу документации аддона (вы тут); `Report a bug` - ведет на страницу багтрекера на репозитории аддона; `Remove` - удалят (деинсталлирует) аддон (перед установокой новой версии рекомендуется удалить старую). +# Подготовка и создание анимации дронов +... +[Пример](https://github.com/artem30801/blender-csv-animation/blob/master/Examples/copter_base_animation.blend) можно использовать в качестве шаблона. +# Экпорт при помощи аддона +Для вызова диалогового окна экспорта нажмите в верхнем меню `File > Export > CSV Drone Swarm Animation Exporter`. В открывшемся окне экспорта необходимо выбрать целевой путь экспорта и название папки, которую создаст аддон в процессе экспорта. В боковом меню доступна панель параметров экспорта: +* `Use name filter for objects` - при отключении этого параметра будут экспортированы _все видимые объекты_ +* `Name identifier` +* `Show detailed animation warnings` - +* `Speed limit` - при нарушении указанного ограничения по скорости передвижения дронов будут выведены предупреждения +* `Distance limit` - при нарушении указанной минимальной дистанции между дронами будут выведены предупреждения +После настройки (при необходимости) нужных параметров нажмите кнопку `Export Drone Swarm animation` \ No newline at end of file diff --git a/docs/client.md b/docs/client.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/image-building.md b/docs/image-building.md new file mode 100644 index 0000000..855f3a9 --- /dev/null +++ b/docs/image-building.md @@ -0,0 +1,45 @@ +# Сборка модифицированного образа + +Иногда возникает необходимость собрать образ с настройками коптера, отличными от релизной версии образа. Есть несколько способов это сделать. + +## Подготовка к сборке +Установите [docker](https://www.docker.com): +```bash +sudo apt install docker.io +``` + +## Локальная сборка с изменением настроек Клевера + +* Замените файлы настроек Клевера (launch файлы и карту) в [папке](../builder/clever-config) `builder/clever-config` в директории с исходным кодом CleverSwarm. +* Соберите свой образ с помощью docker: +```bash +cd source-dir +sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-tool:v0.5 +``` + +## Ручная настройка образа + +* Разархивируйте файл со скачанным образом, перейдите в директорию с этим образом, и войдите в консоль сборщика образа с помощью команды: +```bash +cd image-dir +sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-tool:v0.5 img-chroot /mnt/ +``` +где `` - имя файла образа. В открывшемся терминале с помощью стандартных программ (nano, git, cp, apt-get) вы можете донастроить образ. +* Внешние файлы вы можете перенести в образ с помощью команды: +```bash +sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-tool:v0.5 img-chroot /mnt/ copy /mnt/ +``` +где `` - файл, который нужно перенести в образ (расположение относительно папки с образом, например `../builder/assets/clever-show.service`), а `` - путь в образе, куда нужно переместить файл. +* Если в образе не хватает места для всех необходимых файлов, можно расширить образ с помощью команды: +```bash +sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-tool:v0.5 img-resize /mnt/ max +``` +где `` - размер в байтах. Например 5G будет означать 5GB, а 5M - 5MB. +* После расширения образа его можно сжать до минимального размера + 10МB командой +```bash +sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-tool:v0.5 img-resize /mnt/ min +``` + +## Изменение скриптов сборки + +Статья по изменению скриптов сборки образа и создания кастомной сборки написана [здесь](https://clever.copterexpress.com/ru/image_building.html) \ No newline at end of file diff --git a/docs/server.md b/docs/server.md new file mode 100644 index 0000000..cbf66cb --- /dev/null +++ b/docs/server.md @@ -0,0 +1,4 @@ +#Установка и настройка серв + \ No newline at end of file diff --git a/docs/start-tutorial.md b/docs/start-tutorial.md new file mode 100644 index 0000000..104243c --- /dev/null +++ b/docs/start-tutorial.md @@ -0,0 +1,50 @@ +# Инструкция по настройке и запуску клиента и сервера + +## Список оборудования +Данное ПО предназначено для управления несколькими квадракоптерами с компьютера-сервера. Для полноценной работы необходимо следующее оборудование: +* Один или несколько квадрокоптеров, работающих на базе ПО [Клевер](https://github.com/copterexpress/clever). +* Компьютер с операционной системой Linux. +* Wifi роутер, работающий на частоте 2.4 ГГц, либо 5.8 ГГц, если эту частоту поддерживают wifi модули коптеров и компьютера. + +## Подготовка ПО +Скачайте на компьютер последний образ (CleverSwarm-XXX.img.zip) и исходный код (Source code) из последнего [релиза](https://github.com/artem30801/CleverSwarm/releases/latest). Разархивируйте исходный код в удобную директорию. + +## Настройка роутера +Для управления одним или несколькими коптерами требуется подключение коптеров и сервера к одной сети. Для этого требуется отдельный wifi роутер с известным SSID и паролем. Подключите компьютер, который будет использоваться в качестве сервера, к сети роутера и узнайте его ip адрес - он понадобится для дальнейшей настройки. + +## Настройка и запуск клиента + +* Запишите образ на microSD карту, используя [Etcher](https://www.balena.io/etcher/). +* Вставьте флешку в Raspberry Pi, включите коптер. Дождитесь появления сети `CLEVERSHOW-XXXX`. +* Подключитесь к сети коптера, используя пароль `cleverwifi`. +* Настройте коптер, чтобы корректно работал режим позиции. По-умолчанию образ сконфигурирован для получения позиции с камеры с помощью aruco-маркеров и optical flow. Камера направлена вниз и вперёд, загружена тестовая карта меток. Если ваш способ позиционирования отличается - можно либо настроить данный образ, либо [собрать образ](image-building.md) со своими настройками. +* Перейдите в директорию клиента и запустите скрипт настройки клиента +```bash +cd ~/CleverSwarm/Drone +sudo ./client_setup.sh +``` +* Выполните скрипт настройки клиента с указанными параметрами - SSID, пароль точки доступа, имя коптера, ip сервера. +* Коптер переключится в режим клиента указанной точки доступа и настроит автозапуск клиента copter_client.py + +Документация по клиентской части находится [здесь](client.md). + +## Настройка и запуск сервера + +* Установите [chrony](https://chrony.tuxfamily.org/index.html), Python 3 и PyQt5 на ваш компьютер +```bash +sudo apt install chrony python3 python3-pip +pip3 install PyQt5 +``` +* Подключитесь к wifi сети роутера, к которому подключены коптеры. +* Скопируйте [файл настроек chrony](../Server/chrony.conf) в `/etc/chrony/chrony.conf`. Если ip адрес сети начинается не с `192.168.`, то исправьте адрес после слова allow в скопированном файле настроек. +* Перезапустите сервис chrony +```bash +sudo systemctl restart chrony +``` +* Перейдите в директорию сервера из директории с исходным кодом и запустите сервер +```bash +cd source-code-dir/Server +python3 server_qt.py +``` + +Документация по серверной части находится [здесь](server.md). \ No newline at end of file