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
[](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