Merge remote-tracking branch 'origin/master'

This commit is contained in:
Artem30801
2019-07-26 11:45:27 +03:00
27 changed files with 705 additions and 88 deletions

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -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.

View File

@@ -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.

2
.gitignore vendored
View File

@@ -109,7 +109,7 @@ Drone/test_animation/
Drone/animation.csv
Drone/client_logs
images/
.vscode/
\.idea/
Drone/_copter_client_old_\.py

View File

@@ -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:

View File

@@ -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):

View File

@@ -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)

View File

@@ -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,

View File

@@ -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).

5
Server/chrony.conf Normal file
View File

@@ -0,0 +1,5 @@
server master iburst
driftfile /var/lib/chrony/drift
allow 192.168.0.0/16
makestep 1.0 3
rtcsync

View File

@@ -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):

View File

@@ -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:

View File

@@ -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()

25
blender-addon/README.md Normal file
View File

@@ -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

213
blender-addon/addon.py Normal file
View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,37 @@
<launch>
<arg name="aruco_detect" default="true"/>
<arg name="aruco_map" default="true"/>
<arg name="aruco_vpe" default="true"/>
<!-- For additional help go to https://clever.copterexpress.com/aruco.html -->
<!-- aruco_detect: detect aruco markers, estimate poses -->
<node name="aruco_detect" pkg="nodelet" if="$(arg aruco_detect)" type="nodelet" args="load aruco_pose/aruco_detect nodelet_manager" output="screen" clear_params="true">
<remap from="image_raw" to="main_camera/image_raw"/>
<remap from="camera_info" to="main_camera/camera_info"/>
<param name="estimate_poses" value="true"/>
<param name="send_tf" value="true"/>
<param name="known_tilt" value="map"/>
<param name="length" value="0.33"/>
</node>
<!-- aruco_map: estimate aruco map pose -->
<node name="aruco_map" pkg="nodelet" type="nodelet" if="$(arg aruco_map)" args="load aruco_pose/aruco_map nodelet_manager" output="screen" clear_params="true">
<remap from="image_raw" to="main_camera/image_raw"/>
<remap from="camera_info" to="main_camera/camera_info"/>
<remap from="markers" to="aruco_detect/markers"/>
<param name="map" value="$(find aruco_pose)/map/animation_map.txt"/>
<param name="known_tilt" value="map"/>
<param name="frame_id" value="aruco_map_detected" if="$(arg aruco_vpe)"/>
<param name="frame_id" value="aruco_map" unless="$(arg aruco_vpe)"/>
</node>
<!-- vpe publisher from aruco markers -->
<node name="vpe_publisher" pkg="clever" type="vpe_publisher" if="$(arg aruco_vpe)" output="screen" clear_params="true">
<remap from="~pose_cov" to="aruco_map/pose"/>
<remap from="~vpe" to="mavros/vision_pose/pose"/>
<param name="frame_id" value="aruco_map_detected"/>
<param name="publish_zero" value="true"/>
<param name="offset_frame_id" value="aruco_map"/>
</node>
</launch>

View File

@@ -0,0 +1,71 @@
<launch>
<arg name="fcu_conn" default="usb"/>
<arg name="fcu_ip" default="127.0.0.1"/>
<arg name="gcs_bridge" default="tcp"/>
<arg name="web_video_server" default="true"/>
<arg name="rosbridge" default="true"/>
<arg name="main_camera" default="true"/>
<arg name="optical_flow" default="true"/>
<arg name="aruco" default="true"/>
<arg name="rc" default="true"/>
<arg name="rangefinder_vl53l1x" default="true"/>
<!-- mavros -->
<include file="$(find clever)/launch/mavros.launch">
<arg name="fcu_conn" value="$(arg fcu_conn)"/>
<arg name="fcu_ip" value="$(arg fcu_ip)"/>
<arg name="gcs_bridge" value="$(arg gcs_bridge)"/>
</include>
<!-- web video server -->
<node name="web_video_server" pkg="web_video_server" type="web_video_server" if="$(arg web_video_server)" required="false" respawn="true" respawn_delay="5">
<param name="default_stream_type" value="ros_compressed"/>
<param name="publish_rate" value="1.0"/>
</node>
<!-- aruco markers -->
<include file="$(find clever)/launch/aruco.launch" if="$(arg aruco)"/>
<!-- optical flow -->
<node pkg="nodelet" type="nodelet" name="optical_flow" args="load clever/optical_flow nodelet_manager" if="$(arg optical_flow)" clear_params="true" output="screen">
<remap from="image_raw" to="main_camera/image_raw"/>
<remap from="camera_info" to="main_camera/camera_info"/>
<param name="calc_flow_gyro" value="true"/>
</node>
<!-- main nodelet manager -->
<node pkg="nodelet" type="nodelet" name="nodelet_manager" args="manager" output="screen" clear_params="true">
<param name="num_worker_threads" value="2"/>
</node>
<node pkg="tf2_ros" type="static_transform_publisher" name="map_flipped_frame" args="0 0 0 3.1415926 3.1415926 0 map map_flipped"/>
<!-- simplified offboard control -->
<node name="simple_offboard" pkg="clever" type="simple_offboard" output="screen" clear_params="true">
<param name="reference_frames/body" value="map"/>
<param name="reference_frames/base_link" value="map"/>
</node>
<!-- Auxiliary frames -->
<node name="frames" pkg="clever" type="frames" output="screen">
<param name="body/frame_id" value="body"/>
</node>
<!-- main camera -->
<include file="$(find clever)/launch/main_camera.launch" if="$(arg main_camera)"/>
<!-- rosbridge -->
<include file="$(find rosbridge_server)/launch/rosbridge_websocket.launch" if="$(eval rosbridge or rc)"/>
<!-- tf2 republisher for web visualization -->
<node name="tf2_web_republisher" pkg="tf2_web_republisher" type="tf2_web_republisher" output="screen" if="$(arg rosbridge)"/>
<!-- vl53l1x ToF rangefinder -->
<node name="vl53l1x" pkg="vl53l1x" type="vl53l1x_node" output="screen" if="$(arg rangefinder_vl53l1x)">
<param name="frame_id" value="rangefinder"/>
<remap from="~range" to="mavros/distance_sensor/rangefinder_sub"/> <!-- redirect data to FCU -->
</node>
<!-- rc backend -->
<node name="rc" pkg="clever" type="rc" output="screen" if="$(arg rc)"/>
</launch>

View File

@@ -0,0 +1,37 @@
<launch>
<!-- Camera position and orientation are represented by base_link -> main_camera_optical transform -->
<!-- static_transform_publisher arguments: x y z yaw pitch roll frame_id child_frame_id -->
<!-- article about camera setup: https://clever.copterexpress.com/camera_frame.html -->
<!-- camera is oriented downward, camera cable goes backward [option 1] -->
<node pkg="tf2_ros" type="static_transform_publisher" name="main_camera_frame" args="0.05 0 -0.07 -1.5707963 0 3.1415926 base_link main_camera_optical"/>
<!-- camera is oriented downward, camera cable goes forward [option 2] -->
<!--<node pkg="tf2_ros" type="static_transform_publisher" name="main_camera_frame" args="0.05 0 -0.07 1.5707963 0 3.1415926 base_link main_camera_optical"/>-->
<!-- camera is oriented upward, camera cable goes backward [option 3] -->
<!--<node pkg="tf2_ros" type="static_transform_publisher" name="main_camera_frame" args="0.05 0 0.07 1.5707963 0 0 base_link main_camera_optical"/>-->
<!-- camera is oriented upward, camera cable goes forward [option 4] -->
<!--<node pkg="tf2_ros" type="static_transform_publisher" name="main_camera_frame" args="0.05 0 0.07 -1.5707963 0 0 base_link main_camera_optical"/>-->
<!-- camera node -->
<node pkg="nodelet" type="nodelet" name="main_camera" args="load cv_camera/CvCameraNodelet nodelet_manager" clear_params="true">
<param name="frame_id" value="main_camera_optical"/>
<param name="camera_info_url" value="file://$(find clever)/camera_info/fisheye_cam_320.yaml"/>
<param name="rate" value="100"/> <!-- poll rate -->
<param name="cv_cap_prop_fps" value="40"/> <!-- camera FPS -->
<param name="capture_delay" value="0.02"/> <!-- approximate delay on frame retrieving -->
<!-- camera resolution, NOTE: camera_info file should match it -->
<param name="image_width" value="320"/>
<param name="image_height" value="240"/>
</node>
<!-- camera visualization markers -->
<node pkg="clever" type="camera_markers" ns="main_camera" name="main_camera_markers">
<param name="scale" value="3.0"/>
</node>
</launch>

View File

@@ -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

View File

@@ -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}

View File

@@ -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 '/<arg name="aruco_map"/c \ <arg name="aruco_map" default="true"/>' /home/pi/catkin_ws/src/clever/clever/launch/aruco.launch
sed -i '/<arg name="aruco_vpe"/c \ <arg name="aruco_vpe" default="true"/>' /home/pi/catkin_ws/src/clever/clever/launch/aruco.launch
sed -i '/<param name="map"/c \ <param name="map" value="\$\(find aruco_pose\)/map/animation_map.txt"/>' /home/pi/catkin_ws/src/clever/clever/launch/aruco.launch
sed -i '/<arg name="aruco"/c \ <arg name="aruco" default="true"/>' /home/pi/catkin_ws/src/clever/clever/launch/clever.launch
sed -i '/<arg name="rangefinder_vl53l1x"/c \ <arg name="rangefinder_vl53l1x" default="true"/>' /home/pi/catkin_ws/src/clever/clever/launch/clever.launch
#sed -i '/<arg name="optical_flow"/c \ <arg name="optical_flow" default="true"/>' /home/pi/catkin_ws/src/clever/clever/launch/clever.launch
echo_stamp "Image was configured!" "SUCCESS"

23
docs/blender-addon.md Normal file
View File

@@ -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`

0
docs/client.md Normal file
View File

45
docs/image-building.md Normal file
View File

@@ -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/<IMAGE>
```
где `<IMAGE>` - имя файла образа. В открывшемся терминале с помощью стандартных программ (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/<IMAGE> copy /mnt/<MOVE_FILE> <MOVE_TO>
```
где `<MOVE_FILE>` - файл, который нужно перенести в образ (расположение относительно папки с образом, например `../builder/assets/clever-show.service`), а `<MOVE_TO>` - путь в образе, куда нужно переместить файл.
* Если в образе не хватает места для всех необходимых файлов, можно расширить образ с помощью команды:
```bash
sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-tool:v0.5 img-resize /mnt/<IMAGE> max <SIZE>
```
где `<SIZE>` - размер в байтах. Например 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/<IMAGE> min
```
## Изменение скриптов сборки
Статья по изменению скриптов сборки образа и создания кастомной сборки написана [здесь](https://clever.copterexpress.com/ru/image_building.html)

4
docs/server.md Normal file
View File

@@ -0,0 +1,4 @@
#Установка и настройка серв
<!--stackedit_data:
eyJoaXN0b3J5IjpbODM1MjYyNTQzXX0=
-->

50
docs/start-tutorial.md Normal file
View File

@@ -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).