mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-31 01:09:33 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
2
.gitignore
vendored
@@ -109,7 +109,7 @@ Drone/test_animation/
|
||||
Drone/animation.csv
|
||||
Drone/client_logs
|
||||
images/
|
||||
|
||||
.vscode/
|
||||
\.idea/
|
||||
|
||||
Drone/_copter_client_old_\.py
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
24
README.md
24
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).
|
||||
|
||||
5
Server/chrony.conf
Normal file
5
Server/chrony.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
server master iburst
|
||||
driftfile /var/lib/chrony/drift
|
||||
allow 192.168.0.0/16
|
||||
makestep 1.0 3
|
||||
rtcsync
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
25
blender-addon/README.md
Normal 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
213
blender-addon/addon.py
Normal 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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
37
builder/clever-config/launch/aruco.launch
Normal file
37
builder/clever-config/launch/aruco.launch
Normal 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>
|
||||
71
builder/clever-config/launch/clever.launch
Normal file
71
builder/clever-config/launch/clever.launch
Normal 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>
|
||||
37
builder/clever-config/launch/main_camera.launch
Normal file
37
builder/clever-config/launch/main_camera.launch
Normal 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>
|
||||
8
builder/clever-config/map/animation_map.txt
Normal file
8
builder/clever-config/map/animation_map.txt
Normal 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
|
||||
@@ -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}
|
||||
|
||||
@@ -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
23
docs/blender-addon.md
Normal 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
0
docs/client.md
Normal file
45
docs/image-building.md
Normal file
45
docs/image-building.md
Normal 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
4
docs/server.md
Normal file
@@ -0,0 +1,4 @@
|
||||
#Установка и настройка серв
|
||||
<!--stackedit_data:
|
||||
eyJoaXN0b3J5IjpbODM1MjYyNTQzXX0=
|
||||
-->
|
||||
50
docs/start-tutorial.md
Normal file
50
docs/start-tutorial.md
Normal 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).
|
||||
Reference in New Issue
Block a user