Merge branch 'master' into qt-gui-update

This commit is contained in:
Artem30801
2019-12-30 21:57:21 +03:00
43 changed files with 1378 additions and 619 deletions

120
.gitignore vendored
View File

@@ -1,127 +1,41 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
# Logs
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
# IDE
.mypy_cache/
.vscode/settings.json
.vscode/
\.idea/
# Development
images/
Server/tests.py
Server/convert_ui.sh
Server/config/server.ini
Server/server_logs
Server/testj\.ipynb
Server/tst_client\.py
Server/tst\.py
Drone/test_animation/
Drone/animation.csv
Drone/client_logs
Drone/config/client.ini
Drone/_copter_client_old_\.py
Drone/test_cl\.py
images/
.vscode/
\.idea/
builder/clever-config
Drone/_copter_client_old_\.py
Drone/test_cl\.py
Server/testj\.ipynb
Server/tst_client\.py
Server/tst\.py

View File

@@ -21,6 +21,7 @@ set_mode = rospy.ServiceProxy('/mavros/set_mode', SetMode)
get_telemetry = rospy.ServiceProxy('get_telemetry', srv.GetTelemetry)
arming = rospy.ServiceProxy('/mavros/cmd/arming', CommandBool)
landing = rospy.ServiceProxy('/land', Trigger)
emergency_land = rospy.ServiceProxy('/emergency_land', Trigger)
services_list = ['/navigate', '/set_position', '/set_rates', '/mavros/set_mode',
'/get_telemetry', '/mavros/cmd/arming', '/land', '/mavros/param/get']

View File

@@ -61,8 +61,6 @@ class Client(object):
self.NTP_HOST = self.config.get('NTP', 'host')
self.NTP_PORT = self.config.getint('NTP', 'port')
self.files_directory = self.config.get('FILETRANSFER', 'files_directory') # not used?!
self.client_id = self.config.get('PRIVATE', 'id')
if self.client_id == '/default':
self.client_id = 'copter' + str(random.randrange(9999)).zfill(4)
@@ -199,7 +197,7 @@ class Client(object):
# self._last_ping_time = time.time()
# logging.debug("tick")
for key, mask in events: # TODO add notifier to client!
for key, mask in events:
connection = key.data
if connection is None:
pass
@@ -218,14 +216,16 @@ class Client(object):
if error.errno == errno.EINTR:
raise KeyboardInterrupt
try:
mapping = self.selector.get_map().values()
notifier_key = self.selector.get_key(messaging.NotifierSock().get_sock())
notify_only= len(mapping) == 1 and notifier_key in mapping
if notify_only or not mapping:
mapping_fds = self.selector.get_map().keys() # file descriptors
notifier_fd = messaging.NotifierSock().get_sock().fileno()
except (KeyError, RuntimeError) as e:
logger.error("Exception {} occurred when getting connections map!".format(e))
logger.error("Connections changed during getting connections map, passing")
else:
notify_only= len(mapping_fds) == 1 and notifier_fd in mapping_fds
if notify_only or not mapping_fds:
logger.warning("No active connections left!")
return
except (RuntimeError, KeyError) as e:
logger.error("Exception {} occured when getting net map!".format(e))
@messaging.message_callback("config_write")

View File

@@ -2,7 +2,7 @@
port = 25000
broadcast_port = 8181
host = 192.168.1.19
buffer_size = 1024
buffer_size = 10000
[FILETRANSFER]
files_directory = animation
@@ -15,25 +15,17 @@ port = 123
[VISUAL_POSE_WATCHDOG]
timeout = 1.0
pos_delta_max = 3.0
action = emergency_land
emergency_land_thrust = 0.45
emergency_land_decrease_thrust_after = 5.0
timeout_to_disarm_after_watchdog_action = 10.0
timeout_to_disarm = 10.0
[TELEMETRY]
frequency = 1
transmit = True
land_if_pos_delta_bigger_than = 3.0
log_cpu_and_memory = True
[ANIMATION]
takeoff_animation_check = True
land_animation_check = True
frame_delay = 0.1
x_ratio = 1.0
y_ratio = 1.0
z_ratio = 1.0
[COPTERS]
frame_id = map
takeoff_height = 1.0
@@ -56,12 +48,25 @@ roll = 180
pitch = 0
yaw = -90
[ANIMATION]
takeoff_animation_check = True
land_animation_check = True
frame_delay = 0.1
x_ratio = 1.0
y_ratio = 1.0
z_ratio = 1.0
[PRIVATE]
id = /hostname
restart_dhcpcd = True
restart_after_rename = True
use_leds = True
led_pin = 21
x0 = 0
y0 = 0
z0 = 0
[NTP]
use_ntp = False
host = ntp1.stratum2.ru
port = 123

View File

@@ -112,7 +112,7 @@ class CopterClient(client.Client):
except ConfigParser.Error:
rospy.logerror("No floor frame!")
self.FLOOR_FRAME_EXISTS = False
self.RESTART_DHCPCD = self.config.getboolean('PRIVATE', 'restart_dhcpcd')
self.RESTART_AFTER_RENAME = self.config.getboolean('PRIVATE', 'restart_after_rename')
def on_broadcast_bind(self):
configure_chrony_ip(self.server_host)
@@ -279,7 +279,7 @@ def _response_id(*args, **kwargs):
cfg = client.ConfigOption("PRIVATE", "id", new_id)
client.active_client.write_config(True, cfg)
if new_id != '/hostname':
if client.active_client.RESTART_DHCPCD:
if client.active_client.RESTART_AFTER_RENAME:
hostname = client.active_client.client_id
configure_hostname(hostname)
configure_hosts(hostname)
@@ -307,6 +307,7 @@ def _response_selfcheck(*args, **kwargs):
@messaging.request_callback("telemetry")
def _response_telemetry(*args, **kwargs):
telemetry.update()
return telemetry.create_msg_contents()
@@ -531,6 +532,11 @@ def _command_land(*args, **kwargs):
)
@messaging.message_callback("emergency_land")
def _emergency_land(*args, **kwargs):
logger.info(FlightLib.emergency_land().message)
@messaging.message_callback("disarm")
def _command_disarm(*args, **kwargs):
task_manager.reset()
@@ -682,6 +688,7 @@ class Telemetry:
self._interruption_counter = 0
self._max_interruptions = 2
self._tasks_cleared = False
self.ros_telemetry = None
for key, value in self.params_default_dict.items():
setattr(self, key, value)
@@ -722,6 +729,9 @@ class Telemetry:
@classmethod
def get_battery(cls, ros_telemetry):
if ros_telemetry is None:
return float('nan'), float('nan')
battery_v = ros_telemetry.voltage
batt_empty_param = get_param('BAT_V_EMPTY')
@@ -754,20 +764,15 @@ class Telemetry:
return x, y, z, math.degrees(ros_telemetry.yaw), client.active_client.FRAME_ID
return 'NO_POS'
def update_telemetry(self):
self.animation_id = animation.get_id()
self.git_version = self.get_git_version()
def update_telemetry_fast(self):
self.start_position = self.get_start_position()
try:
ros_telemetry = FlightLib.get_telemetry_locked(client.active_client.FRAME_ID)
if ros_telemetry.connected:
self.battery = self.get_battery(ros_telemetry)
self.armed = ros_telemetry.armed
self.calibration_status = get_calibration_status()
self.system_status = get_sys_status()
self.mode = ros_telemetry.mode
self.ros_telemetry = FlightLib.get_telemetry_locked(client.active_client.FRAME_ID)
if self.ros_telemetry.connected:
self.armed = self.ros_telemetry.armed
self.mode = self.ros_telemetry.mode
self.selfcheck = self.get_selfcheck()
self.current_position = self.get_position(ros_telemetry)
self.current_position = self.get_position(self.ros_telemetry)
else:
self.reset_telemetry_values()
except rospy.ServiceException:
@@ -778,6 +783,26 @@ class Telemetry:
except rospy.TransportException as e:
rospy.logdebug(e)
self.time = time.time()
self.round_telemetry()
def update_telemetry_slow(self):
self.animation_id = animation.get_id()
self.git_version = self.get_git_version()
try:
self.calibration_status = get_calibration_status()
self.system_status = get_sys_status()
except rospy.ServiceException:
rospy.logdebug("Some service is unavailable")
self.selfcheck = ["WAIT_ROS"]
except AttributeError as e:
rospy.logdebug(e)
except rospy.TransportException as e:
rospy.logdebug(e)
self.battery = self.get_battery(self.ros_telemetry)
def update(self):
self.update_telemetry_fast()
self.update_telemetry_slow()
def round_telemetry(self):
round_list = ["battery", "start_position", "current_position"]
@@ -793,7 +818,7 @@ class Telemetry:
self.selfcheck = ['NO_FCU']
self.current_position = 'NO_POS'
def check_failsafe(self):
def check_failsafe_and_interruption(self):
global emergency
# check current state
state = [self.mode, self.armed, task_manager.get_last_task_name()]
@@ -826,12 +851,6 @@ class Telemetry:
else:
self._tasks_cleared = False
self._last_state = state
# check position delta
if not emergency:
delta = FlightLib.get_delta()
if delta > client.active_client.LAND_POS_DELTA:
logger.info("Delta: {}".format(delta))
_command_land()
def transmit_message(self):
try:
@@ -862,25 +881,32 @@ class Telemetry:
rate = rospy.Rate(freq)
while not rospy.is_shutdown():
self.update_telemetry()
self.round_telemetry()
self.check_failsafe()
self.update_telemetry_fast()
self.check_failsafe_and_interruption()
if client.active_client.TELEM_TRANSMIT and client.active_client.connected:
self.transmit_message()
if client.active_client.LOG_CPU_AND_MEMORY:
self.log_cpu_and_memory()
rate.sleep()
def _slow_update_loop(self):
rate = rospy.Rate(1)
while not rospy.is_shutdown():
self.update_telemetry_slow()
rate.sleep()
def start_loop(self):
if client.active_client.TELEM_FREQ > 0:
telemetry_thread = threading.Thread(target=self._update_loop, name="Telemetry getting thread",
args=(client.active_client.TELEM_FREQ,)) # TODO MOVE? Daemon?
slow_telemetry_thread = threading.Thread(target=self._slow_update_loop, name="Slow telemetry getting thread")
slow_telemetry_thread.start()
telemetry_thread.start()
else:
logger.info("Don't create telemetry loop because of zero or negative telemetry frequency")
logger.info("Telemetry loop is not created because of zero or negative telemetry frequency")
def create_msg_contents(self, keys=None): # keys: set or list
if keys is None:

View File

@@ -1,29 +0,0 @@
import logging
import rospy
class RosHandler(logging.Handler):
level_map = {
logging.DEBUG: rospy.logdebug,
logging.INFO: rospy.loginfo,
logging.WARNING: rospy.logwarn,
logging.ERROR: rospy.logerr,
logging.CRITICAL: rospy.logfatal
}
def emit(self, record):
print(record.levelno, record.name, record.msg)
if "rosout" not in record.msg:
try:
pass
#self.level_map[record.levelno]("%s: %s" % (record.name, record.msg))
except KeyError:
rospy.logerr("unknown log level %s LOG: %s: %s" % (record.levelno, record.name, record.msg))
def route_logger_to_ros(logger_name=None):
if logger_name is not None:
logging.getLogger(logger_name).addHandler(RosHandler())
else:
logging.getLogger().addHandler(RosHandler())

View File

@@ -1,23 +1,27 @@
import rospy
import sys
import time
import math
import logging
import threading
import ConfigParser
from clever.srv import SetAttitude
from sensor_msgs.msg import Range
from mavros_msgs.msg import State
from mavros_msgs.msg import State, PositionTarget
from mavros_msgs.srv import SetMode, CommandBool
from std_msgs.msg import Bool
from std_srvs.srv import Trigger, TriggerResponse
from geometry_msgs.msg import PoseStamped
config = ConfigParser.ConfigParser()
config.read("client_config.ini")
visual_pose_timeout = config.getfloat('VISUAL_POSE_WATCHDOG', 'timeout')
pos_delta_max = config.getfloat('VISUAL_POSE_WATCHDOG', 'pos_delta_max')
timeout_action = config.get('VISUAL_POSE_WATCHDOG', 'action')
emergency_land_thrust = config.getfloat('VISUAL_POSE_WATCHDOG', 'emergency_land_thrust')
emergency_land_decrease_thrust_after = config.getfloat('VISUAL_POSE_WATCHDOG', 'emergency_land_decrease_thrust_after')
timeout_to_disarm_after_watchdog_action = config.getfloat('VISUAL_POSE_WATCHDOG', 'timeout_to_disarm_after_watchdog_action')
timeout_to_disarm = config.getfloat('VISUAL_POSE_WATCHDOG', 'timeout_to_disarm')
logging.basicConfig( # TODO all prints as logs
level=logging.DEBUG, # INFO
@@ -33,7 +37,7 @@ formatter = logging.Formatter("%(asctime)s [%(name)-7.7s] [%(threadName)-12.12s]
handler.setFormatter(formatter)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)
set_mode = rospy.ServiceProxy('/mavros/set_mode', SetMode)
@@ -46,19 +50,73 @@ mode = ''
laser_range = 10
emergency = False
local_pose = None
setpoint_raw = None
setpoint_position = None
setpoint_pose = None
offboard_start_time = None
offboard_disarmed_timeout = 3.
emergency_land_called = False
rospy.init_node('visual_pose_watchdog')
logger.info('visual_pose_watchdog inited')
logger.info('timeout = {} | timeout_action = {}'.format(visual_pose_timeout, timeout_action))
logger.info('timeout_to_disarm_after_watchdog_action = {}'.format(timeout_to_disarm_after_watchdog_action))
logger.info('timeout_to_disarm = {}'.format(timeout_to_disarm))
if timeout_action == 'emergency_land':
logger.info('emergency_land_thrust: {}'.format(emergency_land_thrust))
rate = rospy.Rate(10)
def get_distance(x1, y1, z1, x2, y2, z2):
return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2)
def get_pos_delta(PoseStamped1, PoseStamped2):
if PoseStamped1 is None or PoseStamped2 is None:
return float('nan')
pos1 = PoseStamped1.pose.position
pos2 = PoseStamped2.pose.position
return get_distance(pos1.x, pos1.y, pos1.z, pos2.x, pos2.y, pos2.z)
def get_time_delta(PoseStamped1, PoseStamped2):
if PoseStamped1 is None or PoseStamped2 is None:
return float('nan')
time1 = PoseStamped1.header.stamp.to_sec()
time2 = PoseStamped2.header.stamp.to_sec()
return time1 - time2
def visual_pose_callback(data):
global visual_pose_last_timestamp
visual_pose_last_timestamp = data.header.stamp.to_sec()
def local_pose_callback(data):
global local_pose
local_pose = data
def setpoint_raw_callback(data):
global setpoint_raw, setpoint_position, setpoint_pose
setpoint_raw_pose = PoseStamped()
setpoint_raw_pose.header = data.header
setpoint_raw_pose.pose.position = data.position
setpoint_raw = setpoint_raw_pose
setpoint_pose = get_current_setpoint_pose(setpoint_raw, setpoint_position)
def setpoint_position_callback(data):
global setpoint_raw, setpoint_position, setpoint_pose
setpoint_position = data
setpoint_pose = get_current_setpoint_pose(setpoint_raw, setpoint_position)
def get_current_setpoint_pose(_setpoint_raw, _setpoint_position):
if _setpoint_position is None and _setpoint_raw is None:
return None
elif _setpoint_position is not None and _setpoint_raw is None:
return _setpoint_position
elif _setpoint_raw is not None and _setpoint_position is None:
return _setpoint_raw
else:
return _setpoint_raw if _setpoint_raw.header.stamp > _setpoint_position.header.stamp else _setpoint_position
def state_callback(data):
global armed, mode
armed = data.armed
@@ -68,72 +126,130 @@ def laser_callback(data):
global laser_range
laser_range = data.range
def emergency_land(disarm_if_timeout = True):
global emergency_land_thrust, laser_range
current_thrust = emergency_land_thrust
action_timestamp = time.time()
while armed:
logger.debug("Emergency land | range: {:.2f} | thrust: {:.2f}".format(laser_range, current_thrust))
if current_thrust >= 0.03:
try:
set_attitude(thrust = current_thrust, yaw = 0, frame_id = 'body', auto_arm = True)
except rospy.ServiceException as e:
logger.info(e)
delta = time.time() - action_timestamp
if delta > timeout_to_disarm and disarm_if_timeout:
try:
arming(False)
except rospy.ServiceException as e:
logger.info(e)
if (laser_range < 0.1 or delta > emergency_land_decrease_thrust_after) and current_thrust >= 0.:
current_thrust -= 0.02
if current_thrust <= 0.03:
current_thrust = 0
try:
arming(False)
except rospy.ServiceException as e:
logger.info(e)
rate.sleep()
def emergency_land_service(request):
global emergency_land_called, armed
responce = TriggerResponse()
if armed:
responce.success = True
responce.message = "Start emergency landing"
emergency_land_called = True
else:
responce.success = False
responce.message = "Copter is disarmed, no need for emergency landing!"
emergency_land_called = False
return responce
def watchdog_callback(event):
global visual_pose_last_timestamp, armed, mode, timeout_action, laser_range, emergency
logger.debug("armed: {} | mode: {} | delta: {} | action: {} | range: {}".format(armed, mode, abs(time.time() - visual_pose_last_timestamp), timeout_action, laser_range))
if abs(time.time() - visual_pose_last_timestamp) > visual_pose_timeout:
global visual_pose_last_timestamp, armed, mode, timeout_action, laser_range, emergency, local_pose, setpoint_pose, offboard_start_time, emergency_land_called
pos_delta = get_pos_delta(local_pose, setpoint_pose)
pos_dt = get_time_delta(local_pose, setpoint_pose)
logger.debug("armed: {} | mode: {} | viz_dt: {:.2f} | pos_delta: {:.2f} | pos_dt: {:.2f} | action: {} | range: {:.2f}".format(
armed, mode, abs(time.time() - visual_pose_last_timestamp), pos_delta, pos_dt, timeout_action, laser_range))
if mode == 'OFFBOARD':
if offboard_start_time is None:
offboard_start_time = time.time()
if armed:
if timeout_action in ['land', 'emergency_land', 'disarm']:
emergency = True
if timeout_action == 'land':
logger.info('Visual pose data is too old, copter is armed, landing...')
while mode != "AUTO.LAND":
try:
set_mode(custom_mode='AUTO.LAND')
except rospy.ServiceException as e:
logger.info(e)
rate.sleep()
logger.info('Land mode is set')
visual_pose_dt = abs(time.time() - visual_pose_last_timestamp)
if visual_pose_dt > visual_pose_timeout or pos_delta > pos_delta_max:
action_timestamp = time.time()
while armed:
if time.time() - action_timestamp > timeout_to_disarm_after_watchdog_action:
if timeout_action in ['land', 'emergency_land', 'disarm']:
emergency = True
if timeout_action == 'land':
logger.info('Visual pose data is too old, copter is armed, landing...')
while mode != "AUTO.LAND":
try:
set_mode(custom_mode='AUTO.LAND')
except rospy.ServiceException as e:
logger.info(e)
if time.time() - action_timestamp > timeout_to_disarm:
break
rate.sleep()
else:
logger.info('Land mode is set')
while armed:
if time.time() - action_timestamp > timeout_to_disarm:
try:
arming(False)
except rospy.ServiceException as e:
logger.info(e)
rate.sleep()
elif timeout_action == 'disarm':
logger.info('Visual pose data is too old, copter is armed, disarming...')
while armed:
try:
arming(False)
except rospy.ServiceException as e:
logger.info(e)
rate.sleep()
elif timeout_action == 'disarm':
logger.info('Visual pose data is too old, copter is armed, disarming...')
while armed:
try:
arming(False)
except rospy.ServiceException as e:
logger.info(e)
rate.sleep()
elif timeout_action == 'emergency_land':
logger.info('Visual pose data is too old, copter is armed, emergency landing...')
action_timestamp = time.time()
current_thrust = emergency_land_thrust
while armed:
logger.debug("Emergency land | range: {} | thrust: {}".format(laser_range, current_thrust))
try:
set_attitude(thrust = current_thrust, yaw = 0, frame_id = 'body')
except rospy.ServiceException as e:
logger.info(e)
delta = time.time() - action_timestamp
if delta > timeout_to_disarm_after_watchdog_action:
try:
arming(False)
except rospy.ServiceException as e:
logger.info(e)
if (laser_range < 0.1 or delta > emergency_land_decrease_thrust_after) and current_thrust > 0.:
current_thrust -= 0.02
if current_thrust < 0:
current_thrust = 0
rate.sleep()
logger.info('Disarmed')
emergency = False
rate.sleep()
elif timeout_action == 'emergency_land':
if visual_pose_dt > visual_pose_timeout:
logger.info('Visual pose data is too old, copter is armed, emergency landing...')
if pos_delta > pos_delta_max:
logger.info('Position delta is {} m, copter is armed, emergency landing...'.format(pos_delta))
emergency_land()
logger.info('Disarmed')
emergency = False
elif emergency_land_called:
emergency = True
logger.info('/emergency_land service was called, start emergency landing...')
emergency_land()
logger.info('Disarmed')
emergency = False
emergency_land_called = False
else:
if time.time() - offboard_start_time > offboard_disarmed_timeout:
try:
set_mode(custom_mode='AUTO.LAND')
except rospy.ServiceException as e:
logger.info(e)
else:
offboard_start_time = None
if abs(time.time() - visual_pose_last_timestamp) > visual_pose_timeout:
logger.info('Visual pose data is too old')
rospy.Subscriber('/mavros/vision_pose/pose', PoseStamped, visual_pose_callback)
rospy.Subscriber('/mavros/local_position/pose', PoseStamped, local_pose_callback)
rospy.Subscriber('/mavros/setpoint_position/local', PoseStamped, setpoint_position_callback)
rospy.Subscriber('/mavros/setpoint_raw/local', PositionTarget, setpoint_raw_callback)
rospy.Subscriber('/mavros/state', State, state_callback)
rospy.Subscriber('/mavros/distance_sensor/rangefinder', Range, laser_callback)
emergency_pub = rospy.Publisher('/emergency', Bool, queue_size=10)
rospy.Service('emergency_land', Trigger, emergency_land_service)
rospy.Timer(rospy.Duration(0.5), watchdog_callback)
while not rospy.is_shutdown():
@@ -141,4 +257,3 @@ while not rospy.is_shutdown():
emergency_msg.data = emergency
emergency_pub.publish(emergency_msg)
rate.sleep()

View File

@@ -6,6 +6,12 @@ Software for making the drone show controlled by Raspberry Pi and COEX [Clever](
[![Build Status](https://travis-ci.org/CopterExpress/clever-show.svg?branch=master)](https://travis-ci.org/CopterExpress/clever-show)
## Demo video
[![Autonomous drone show in a theater](http://img.youtube.com/vi/HdHbZFz7nR0/0.jpg)](http://www.youtube.com/watch?v=HdHbZFz7nR0)
12 drones perform in a show in Electrotheatre Stanislavsky, Moscow.
## This software includes
* [Drone side](https://github.com/CopterExpress/clever-show/tree/master/Drone) with autonomous flight module, animation player module and client application for remote synchronized control of drones

View File

@@ -101,6 +101,95 @@ def check_start_pos_status(item):
def check_time_delta(item):
return abs(item) < ModelChecks.time_delta_max
battery_min = config.getfloat('CHECKS', 'battery_percentage_min')
start_pos_delta_max = config.getfloat('CHECKS', 'start_pos_delta_max')
time_delta_max = config.getfloat('CHECKS', 'time_delta_max')
class ModelChecks:
checks_dict = {}
takeoff_checklist = (3, 4, 6, 7, 8)
@classmethod
def col_check(cls, col):
def inner(f):
def wrapper(item):
if item is not None:
return f(item)
return None
cls.checks_dict[col] = wrapper
return wrapper
return inner
@classmethod
def all_checks(cls, copter_item):
for col, check in cls.checks_dict.items():
if not check(copter_item[col]):
return False
return True
@classmethod
def takeoff_checks(cls, copter_item):
for col in cls.takeoff_checklist:
if not cls.checks_dict[col](copter_item[col]):
return False
return True
@ModelChecks.col_check(1)
def check_ver(item):
return True # TODO git version!
@ModelChecks.col_check(2)
def check_anim(item):
return str(item) != 'No animation'
@ModelChecks.col_check(3)
def check_bat(item):
if item == "NO_INFO":
return False
return item[1]*100 > battery_min
@ModelChecks.col_check(4)
def check_sys_status(item):
return item == "STANDBY"
@ModelChecks.col_check(5)
def check_cal_status(item):
return item == "OK"
@ModelChecks.col_check(6)
def check_mode(item):
return (item != "NO_FCU") and not ("CMODE" in item)
@ModelChecks.col_check(7)
def check_selfcheck(item):
return item == "OK"
@ModelChecks.col_check(8)
def check_pos_status(item):
if item == 'NO_POS':
return False
return not math.isnan(item[0])
@ModelChecks.col_check(9)
def check_start_pos_status(item):
return item != 'NO_POS'
@ModelChecks.col_check(10)
def check_time_delta(item):
return abs(item) < time_delta_max
class CopterData:
class_basic_attrs = indexed.IndexedOrderedDict([('copter_id', None), ('git_ver', None), ('anim_id', None),

View File

@@ -174,6 +174,7 @@ class Server(messaging.Singleton):
if not any([client_addr == addr[0] for client_addr in Client.clients.keys()]):
client = Client(addr[0])
client.buffer_size = self.BUFFER_SIZE
logging.info("New client")
else:
client = Client.clients[addr[0]]
@@ -332,7 +333,7 @@ class Client(messaging.ConnectionManager):
@requires_connect
def _send(self, data):
super()._send(data)
logging.debug("Queued data to send: {}".format(data))
logging.debug("Queued data to send (first 256 bytes): {}".format(data[:256]))
def send_config_options(self, *options: ConfigOption, reload_config=True):
logging.info("Sending config options: {} to {}".format(options, self.addr))

View File

@@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'server_gui.ui'
#
# Created by: PyQt5 UI code generator 5.13.1
# Created by: PyQt5 UI code generator 5.13.0
#
# WARNING! All changes made in this file will be lost!
@@ -44,9 +44,17 @@ class Ui_MainWindow(object):
self.verticalLayout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize)
self.verticalLayout.setObjectName("verticalLayout")
self.formLayout = QtWidgets.QFormLayout()
self.formLayout.setLabelAlignment(QtCore.Qt.AlignCenter)
self.formLayout.setFormAlignment(QtCore.Qt.AlignCenter)
self.formLayout.setLabelAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop)
self.formLayout.setFormAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop)
self.formLayout.setObjectName("formLayout")
self.start_text = QtWidgets.QLabel(self.centralwidget)
self.start_text.setLayoutDirection(QtCore.Qt.RightToLeft)
self.start_text.setAlignment(QtCore.Qt.AlignCenter)
self.start_text.setObjectName("start_text")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.start_text)
self.start_delay_spin = QtWidgets.QSpinBox(self.centralwidget)
self.start_delay_spin.setObjectName("start_delay_spin")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.start_delay_spin)
self.music_text = QtWidgets.QLabel(self.centralwidget)
self.music_text.setLayoutDirection(QtCore.Qt.RightToLeft)
self.music_text.setObjectName("music_text")
@@ -56,6 +64,10 @@ class Ui_MainWindow(object):
self.music_delay_spin.setMaximum(1000.0)
self.music_delay_spin.setObjectName("music_delay_spin")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.music_delay_spin)
self.music_play_text = QtWidgets.QLabel(self.centralwidget)
self.music_play_text.setLayoutDirection(QtCore.Qt.RightToLeft)
self.music_play_text.setObjectName("music_play_text")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.music_play_text)
self.music_checkbox = QtWidgets.QCheckBox(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@@ -70,18 +82,6 @@ class Ui_MainWindow(object):
self.music_checkbox.setChecked(False)
self.music_checkbox.setObjectName("music_checkbox")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.music_checkbox)
self.music_play_text = QtWidgets.QLabel(self.centralwidget)
self.music_play_text.setLayoutDirection(QtCore.Qt.RightToLeft)
self.music_play_text.setObjectName("music_play_text")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.music_play_text)
self.start_delay_spin = QtWidgets.QSpinBox(self.centralwidget)
self.start_delay_spin.setObjectName("start_delay_spin")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.start_delay_spin)
self.start_text = QtWidgets.QLabel(self.centralwidget)
self.start_text.setLayoutDirection(QtCore.Qt.RightToLeft)
self.start_text.setAlignment(QtCore.Qt.AlignCenter)
self.start_text.setObjectName("start_text")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.start_text)
self.verticalLayout.addLayout(self.formLayout)
self.line = QtWidgets.QFrame(self.centralwidget)
self.line.setFrameShape(QtWidgets.QFrame.HLine)
@@ -89,42 +89,71 @@ class Ui_MainWindow(object):
self.line.setObjectName("line")
self.verticalLayout.addWidget(self.line)
self.formLayout_2 = QtWidgets.QFormLayout()
self.formLayout_2.setFormAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.formLayout_2.setLabelAlignment(QtCore.Qt.AlignCenter)
self.formLayout_2.setFormAlignment(QtCore.Qt.AlignCenter)
self.formLayout_2.setObjectName("formLayout_2")
self.stop_button = QtWidgets.QPushButton(self.centralwidget)
self.stop_button.setObjectName("stop_button")
self.formLayout_2.setWidget(10, QtWidgets.QFormLayout.FieldRole, self.stop_button)
self.pause_button = QtWidgets.QPushButton(self.centralwidget)
self.pause_button.setObjectName("pause_button")
self.formLayout_2.setWidget(9, QtWidgets.QFormLayout.FieldRole, self.pause_button)
self.check_button = QtWidgets.QPushButton(self.centralwidget)
self.check_button.setEnabled(True)
self.check_button.setObjectName("check_button")
self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.check_button)
self.start_button = QtWidgets.QPushButton(self.centralwidget)
self.start_button.setEnabled(True)
self.start_button.setFlat(False)
self.start_button.setObjectName("start_button")
self.formLayout_2.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.start_button)
self.check_button = QtWidgets.QPushButton(self.centralwidget)
self.check_button.setEnabled(True)
self.check_button.setObjectName("check_button")
self.formLayout_2.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.check_button)
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.start_button)
self.pause_button = QtWidgets.QPushButton(self.centralwidget)
self.pause_button.setObjectName("pause_button")
self.formLayout_2.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.pause_button)
self.verticalLayout.addLayout(self.formLayout_2)
self.line_5 = QtWidgets.QFrame(self.centralwidget)
self.line_5.setFrameShape(QtWidgets.QFrame.HLine)
self.line_5.setFrameShadow(QtWidgets.QFrame.Sunken)
self.line_5.setObjectName("line_5")
self.verticalLayout.addWidget(self.line_5)
self.formLayout_5 = QtWidgets.QFormLayout()
self.formLayout_5.setLabelAlignment(QtCore.Qt.AlignCenter)
self.formLayout_5.setFormAlignment(QtCore.Qt.AlignCenter)
self.formLayout_5.setContentsMargins(-1, 0, -1, -1)
self.formLayout_5.setObjectName("formLayout_5")
self.land_selected_button = QtWidgets.QPushButton(self.centralwidget)
self.land_selected_button.setObjectName("land_selected_button")
self.formLayout_5.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.land_selected_button)
self.land_all_button = QtWidgets.QPushButton(self.centralwidget)
self.land_all_button.setObjectName("land_all_button")
self.formLayout_5.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.land_all_button)
self.verticalLayout.addLayout(self.formLayout_5)
self.line_6 = QtWidgets.QFrame(self.centralwidget)
self.line_6.setFrameShape(QtWidgets.QFrame.HLine)
self.line_6.setFrameShadow(QtWidgets.QFrame.Sunken)
self.line_6.setObjectName("line_6")
self.verticalLayout.addWidget(self.line_6)
self.formLayout_7 = QtWidgets.QFormLayout()
self.formLayout_7.setFormAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.formLayout_7.setContentsMargins(-1, 0, -1, -1)
self.formLayout_7.setObjectName("formLayout_7")
self.visual_land_button = QtWidgets.QPushButton(self.centralwidget)
self.visual_land_button.setObjectName("visual_land_button")
self.formLayout_7.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.visual_land_button)
self.emergency_land_button = QtWidgets.QPushButton(self.centralwidget)
self.emergency_land_button.setObjectName("emergency_land_button")
self.formLayout_7.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.emergency_land_button)
self.verticalLayout.addLayout(self.formLayout_7)
self.line_2 = QtWidgets.QFrame(self.centralwidget)
self.line_2.setFrameShape(QtWidgets.QFrame.HLine)
self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
self.line_2.setObjectName("line_2")
self.verticalLayout.addWidget(self.line_2)
self.formLayout_3 = QtWidgets.QFormLayout()
self.formLayout_3.setFormAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.formLayout_3.setLabelAlignment(QtCore.Qt.AlignCenter)
self.formLayout_3.setFormAlignment(QtCore.Qt.AlignCenter)
self.formLayout_3.setVerticalSpacing(6)
self.formLayout_3.setObjectName("formLayout_3")
self.disarm_all_button = QtWidgets.QPushButton(self.centralwidget)
self.disarm_all_button.setObjectName("disarm_all_button")
self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.disarm_all_button)
self.disarm_button = QtWidgets.QPushButton(self.centralwidget)
self.disarm_button.setObjectName("disarm_button")
self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.disarm_button)
self.emergency_button = QtWidgets.QPushButton(self.centralwidget)
self.emergency_button.setObjectName("emergency_button")
self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.emergency_button)
self.disarm_selected_button = QtWidgets.QPushButton(self.centralwidget)
self.disarm_selected_button.setObjectName("disarm_selected_button")
self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.disarm_selected_button)
self.verticalLayout.addLayout(self.formLayout_3)
self.line_3 = QtWidgets.QFrame(self.centralwidget)
self.line_3.setFrameShape(QtWidgets.QFrame.HLine)
@@ -132,11 +161,9 @@ class Ui_MainWindow(object):
self.line_3.setObjectName("line_3")
self.verticalLayout.addWidget(self.line_3)
self.formLayout_4 = QtWidgets.QFormLayout()
self.formLayout_4.setFormAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.formLayout_4.setLabelAlignment(QtCore.Qt.AlignCenter)
self.formLayout_4.setFormAlignment(QtCore.Qt.AlignCenter)
self.formLayout_4.setObjectName("formLayout_4")
self.land_button = QtWidgets.QPushButton(self.centralwidget)
self.land_button.setObjectName("land_button")
self.formLayout_4.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.land_button)
self.flip_button = QtWidgets.QPushButton(self.centralwidget)
self.flip_button.setObjectName("flip_button")
self.formLayout_4.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.flip_button)
@@ -170,7 +197,8 @@ class Ui_MainWindow(object):
self.line_4.setObjectName("line_4")
self.verticalLayout.addWidget(self.line_4)
self.formLayout_6 = QtWidgets.QFormLayout()
self.formLayout_6.setFormAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
self.formLayout_6.setLabelAlignment(QtCore.Qt.AlignCenter)
self.formLayout_6.setFormAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignHCenter)
self.formLayout_6.setObjectName("formLayout_6")
self.reboot_fcu = QtWidgets.QPushButton(self.centralwidget)
self.reboot_fcu.setObjectName("reboot_fcu")
@@ -187,7 +215,7 @@ class Ui_MainWindow(object):
self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1360, 25))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1360, 22))
self.menubar.setObjectName("menubar")
self.menuOptions = QtWidgets.QMenu(self.menubar)
self.menuOptions.setObjectName("menuOptions")
@@ -292,19 +320,20 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Clever Drone Show"))
self.start_text.setText(_translate("MainWindow", " Start after"))
self.start_delay_spin.setSuffix(_translate("MainWindow", " s"))
self.music_text.setText(_translate("MainWindow", " Music after"))
self.music_delay_spin.setSuffix(_translate("MainWindow", " s"))
self.music_play_text.setText(_translate("MainWindow", " Play music"))
self.start_delay_spin.setSuffix(_translate("MainWindow", " s"))
self.start_text.setText(_translate("MainWindow", " Start after"))
self.stop_button.setText(_translate("MainWindow", "Stop and land all"))
self.pause_button.setText(_translate("MainWindow", "Pause"))
self.start_button.setText(_translate("MainWindow", "Start animation"))
self.check_button.setText(_translate("MainWindow", "Preflight check"))
self.start_button.setText(_translate("MainWindow", "Start animation"))
self.pause_button.setText(_translate("MainWindow", "Pause"))
self.land_selected_button.setText(_translate("MainWindow", "Land selected"))
self.land_all_button.setText(_translate("MainWindow", "Land ALL"))
self.visual_land_button.setText(_translate("MainWindow", "Visual land"))
self.emergency_land_button.setText(_translate("MainWindow", "Emergency land"))
self.disarm_all_button.setText(_translate("MainWindow", "Disarm ALL"))
self.disarm_button.setText(_translate("MainWindow", "Disarm selected"))
self.emergency_button.setText(_translate("MainWindow", "Emergency land"))
self.land_button.setText(_translate("MainWindow", "Land"))
self.disarm_selected_button.setText(_translate("MainWindow", "Disarm selected"))
self.flip_button.setText(_translate("MainWindow", "Flip"))
self.takeoff_button.setText(_translate("MainWindow", "Takeoff"))
self.leds_button.setText(_translate("MainWindow", "Test leds"))

View File

@@ -71,11 +71,31 @@
<item>
<layout class="QFormLayout" name="formLayout">
<property name="labelAlignment">
<set>Qt::AlignCenter</set>
<set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
<property name="formAlignment">
<set>Qt::AlignCenter</set>
<set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
<item row="2" column="0">
<widget class="QLabel" name="start_text">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string> Start after</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="start_delay_spin">
<property name="suffix">
<string> s</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="music_text">
<property name="layoutDirection">
@@ -99,6 +119,16 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="music_play_text">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string> Play music</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="music_checkbox">
<property name="sizePolicy">
@@ -127,36 +157,6 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="music_play_text">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string> Play music</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="start_delay_spin">
<property name="suffix">
<string> s</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="start_text">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string> Start after</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
@@ -168,24 +168,23 @@
</item>
<item>
<layout class="QFormLayout" name="formLayout_2">
<property name="formAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
<property name="labelAlignment">
<set>Qt::AlignCenter</set>
</property>
<item row="10" column="1">
<widget class="QPushButton" name="stop_button">
<property name="formAlignment">
<set>Qt::AlignCenter</set>
</property>
<item row="0" column="1">
<widget class="QPushButton" name="check_button">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Stop and land all</string>
<string>Preflight check</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QPushButton" name="pause_button">
<property name="text">
<string>Pause</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="1" column="1">
<widget class="QPushButton" name="start_button">
<property name="enabled">
<bool>true</bool>
@@ -198,13 +197,75 @@
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QPushButton" name="check_button">
<property name="enabled">
<bool>true</bool>
</property>
<item row="2" column="1">
<widget class="QPushButton" name="pause_button">
<property name="text">
<string>Preflight check</string>
<string>Pause</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout_5">
<property name="labelAlignment">
<set>Qt::AlignCenter</set>
</property>
<property name="formAlignment">
<set>Qt::AlignCenter</set>
</property>
<property name="topMargin">
<number>0</number>
</property>
<item row="1" column="1">
<widget class="QPushButton" name="land_selected_button">
<property name="text">
<string>Land selected</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="land_all_button">
<property name="text">
<string>Land ALL</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout_7">
<property name="formAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="topMargin">
<number>0</number>
</property>
<item row="1" column="1">
<widget class="QPushButton" name="visual_land_button">
<property name="text">
<string>Visual land</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="emergency_land_button">
<property name="text">
<string>Emergency land</string>
</property>
</widget>
</item>
@@ -219,8 +280,11 @@
</item>
<item>
<layout class="QFormLayout" name="formLayout_3">
<property name="labelAlignment">
<set>Qt::AlignCenter</set>
</property>
<property name="formAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
<set>Qt::AlignCenter</set>
</property>
<property name="verticalSpacing">
<number>6</number>
@@ -233,19 +297,12 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="disarm_button">
<widget class="QPushButton" name="disarm_selected_button">
<property name="text">
<string>Disarm selected</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="emergency_button">
<property name="text">
<string>Emergency land</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
@@ -257,16 +314,12 @@
</item>
<item>
<layout class="QFormLayout" name="formLayout_4">
<property name="formAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
<property name="labelAlignment">
<set>Qt::AlignCenter</set>
</property>
<property name="formAlignment">
<set>Qt::AlignCenter</set>
</property>
<item row="8" column="1">
<widget class="QPushButton" name="land_button">
<property name="text">
<string>Land</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QPushButton" name="flip_button">
<property name="text">
@@ -341,8 +394,11 @@
</item>
<item>
<layout class="QFormLayout" name="formLayout_6">
<property name="labelAlignment">
<set>Qt::AlignCenter</set>
</property>
<property name="formAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
</property>
<item row="0" column="1">
<widget class="QPushButton" name="reboot_fcu">
@@ -379,7 +435,7 @@
<x>0</x>
<y>0</y>
<width>1360</width>
<height>25</height>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuOptions">

View File

@@ -98,7 +98,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.check_button.clicked.connect(self.selfcheck_selected)
self.ui.start_button.clicked.connect(self.send_start_time_selected)
self.ui.pause_button.clicked.connect(self.pause_resume_selected)
self.ui.stop_button.clicked.connect(self.land_all)
self.ui.emergency_button.clicked.connect(self.emergency)
self.ui.disarm_button.clicked.connect(partial(self.send_to_selected, "disarm"))
@@ -273,6 +272,12 @@ class MainWindow(QtWidgets.QMainWindow):
self.send_to_selected("resume", {"time": server.time_now() + time_gap})
self.ui.pause_button.setText('Pause')
@pyqtSlot()
def land_selected(self):
for copter in self.model.user_selected():
copter.client.send_message("land")
@pyqtSlot()
def land_all(self):
Client.broadcast_message("land")
@@ -281,6 +286,12 @@ class MainWindow(QtWidgets.QMainWindow):
def disarm_all(self):
Client.broadcast_message("disarm")
@pyqtSlot()
def test_leds_selected(self):
for copter in self.model.user_selected():
copter.client.send_message("led_test")
@pyqtSlot()
@confirmation_required("This operation will takeoff copters immediately. Proceed?")
def takeoff_selected(self):

View File

@@ -1,36 +1,20 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'emergency.ui'
# Form implementation generated from reading ui file 'visual_land.ui'
#
# Created by: PyQt5 UI code generator 5.11.3
# Created by: PyQt5 UI code generator 5.13.0
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
import os
import glob
from PyQt5 import QtWidgets
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QObject
from PyQt5.QtWidgets import QDialog
# Importing gui form
from server_qt import *
from server import *
class Ui_Dialog(object):
def __init__(self):
self.Dialog = None
def setupUi(self, Dialog):
self.Dialog = Dialog
Dialog.setObjectName("Dialog")
Dialog.resize(746, 620)
Dialog.setStyleSheet("QDialog{\n"
"background-color: #fffdd0;\n"
"}")
Dialog.setStyleSheet("")
self.two_button = QtWidgets.QPushButton(Dialog)
self.two_button.setGeometry(QtCore.QRect(420, 120, 231, 171))
self.two_button.setSizeIncrement(QtCore.QSize(16, 16))
@@ -42,10 +26,12 @@ class Ui_Dialog(object):
"}")
self.two_button.setObjectName("two_button")
self.label = QtWidgets.QLabel(Dialog)
self.label.setGeometry(QtCore.QRect(90, 30, 561, 51))
self.label.setGeometry(QtCore.QRect(60, 30, 631, 51))
font = QtGui.QFont()
font.setPointSize(16)
font.setPointSize(20)
self.label.setFont(font)
self.label.setLayoutDirection(QtCore.Qt.LeftToRight)
self.label.setAlignment(QtCore.Qt.AlignCenter)
self.label.setObjectName("label")
self.one_button = QtWidgets.QPushButton(Dialog)
self.one_button.setGeometry(QtCore.QRect(90, 120, 231, 171))
@@ -54,7 +40,7 @@ class Ui_Dialog(object):
"color: white;\n"
"font-weight: 600;\n"
"font-size: 25pt;\n"
"background-color: RGB(118, 255, 122);\n"
"background-color: green;\n"
"}")
self.one_button.setObjectName("one_button")
self.land_emergency_button = QtWidgets.QPushButton(Dialog)
@@ -73,39 +59,15 @@ class Ui_Dialog(object):
"background-color: white;\n"
"}")
self.disarm_emergency_button.setObjectName("disarm_emergency_button")
self.one_button.clicked.connect(self.one_button_click)
self.two_button.clicked.connect(self.two_button_click)
self.land_emergency_button.clicked.connect(self.land_emergency_click)
self.disarm_emergency_button.clicked.connect(self.disarm_emergency_click)
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
Dialog.setWindowTitle(_translate("Dialog", "Visual land"))
self.two_button.setText(_translate("Dialog", "2"))
self.label.setText(_translate("Dialog", "\n"
"Select a group in which the drone does not work correctly"))
self.label.setText(_translate("Dialog", "Select the group with the defective copter"))
self.one_button.setText(_translate("Dialog", "1"))
self.land_emergency_button.setText(_translate("Dialog", "Land"))
self.disarm_emergency_button.setText(_translate("Dialog", "Disarm"))
def one_button_click(self):
self.Dialog.done(1)
def two_button_click(self):
self.Dialog.done(2)
def land_emergency_click(self):
self.Dialog.done(3)
def disarm_emergency_click(self):
self.Dialog.done(4)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())
self.disarm_emergency_button.setText(_translate("Dialog", "Disarm"))

View File

@@ -11,12 +11,10 @@
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
<string>Visual land</string>
</property>
<property name="styleSheet">
<string notr="true">QDialog{
background-color: #fffdd0;
}</string>
<string notr="true"/>
</property>
<widget class="QPushButton" name="two_button">
<property name="geometry">
@@ -48,20 +46,25 @@ background-color: red;
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>90</x>
<x>60</x>
<y>30</y>
<width>561</width>
<width>631</width>
<height>51</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>16</pointsize>
<pointsize>20</pointsize>
</font>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>
Select a group in which the drone does not work correctly</string>
<string>Select the group with the defective copter</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QPushButton" name="one_button">
@@ -84,7 +87,7 @@ Select a group in which the drone does not work correctly</string>
color: white;
font-weight: 600;
font-size: 25pt;
background-color: RGB(118, 255, 122);
background-color: green;
}</string>
</property>
<property name="text">

View File

@@ -1,25 +1,13 @@
# blender-csv-animation
A Blender extension that export paths of objects in blender animation to a csv files
# Blender animation export add-on
## 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.
The add-on for Blender is designed to convert the flight animation of copters drawn in Blender into flight paths for each copter, taking into account the color of objects at each given time.
## 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
Export result is a folder with .csv files where each line in file represents a sequence with comma delimiter:
Use [official docs](https://docs.blender.org/manual/en/latest/preferences/addons.html) for getting additional information
* `x, y, z` coordinates of an object in meters
* `yaw` of an object in radians
* `red, green, blue` values of the color of an object, each is integer from 0 to 255
Documentation is located here:
* English
* [Russian](../docs/ru/blender-addon.md)

View File

@@ -16,8 +16,8 @@ bl_info = {
"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",
"wiki_url": "https://github.com/CopterExpress/clever-show/blob/master/blender-addon/README.md",
"tracker_url": "https://github.com/CopterExpress/clever-show/issues",
"category": "Import-Export"
}

28
builder/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Scripts for building Raspberry Pi image
This directory contains scripts for automated image building via travis-ci.org.
You can place the folders with the `clever` settings files (`launch`,`map` and `camera_info`) to the folder `clever-config` located in this directory. Then you can build your image with custom drone settings locally.
* All files from the `launch` folder will be copied to the `/home/pi/catkin_ws/src/clever/clever/launch` directory in the assembled image.
* All files from the `map` folder will be copied to the `/home/pi/catkin_ws/src/clever/aruco_pose/map` directory in the assembled image.
* All files from the `camera_info` folder will be copied to the `/home/pi/catkin_ws/src/clever/clever/camera_info` directory in the assembled image.
Install docker if needed:
```bash
sudo apt install docker.io
```
Build your custom image with docker:
```bash
cd source-dir
sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-tool:v0.5
```
The image will be located in `images` directory in the clever-show source code directory.
Article about building custom image is located here:
* English
* [Russian](../docs/ru/image-building.md)

View File

@@ -0,0 +1,45 @@
image_width: 320
image_height: 240
distortion_model: plumb_bob
camera_name: raspicam
camera_matrix:
rows: 3
cols: 3
data:
- 166.23942373073172
- 0.
- 162.19011246829268
- 0.
- 166.5880923974026
- 109.82227735714285
- 0.
- 0.
- 1.
distortion_coefficients:
rows: 1
cols: 8
data: [ 2.15356885e-01, -1.17472846e-01, -3.06197672e-04,
-1.09444025e-04, -4.53657258e-03, 5.73090623e-01,
-1.27574577e-01, -2.86125589e-02, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00]
rectification_matrix:
rows: 3
cols: 3
data: [1, 0, 0, 0, 1, 0, 0, 0, 1]
projection_matrix:
rows: 3
cols: 4
data:
- 166.23942373073172
- 0.
- 162.19011246829268
- 0.
- 0.
- 166.5880923974026
- 109.82227735714285
- 0.
- 0.
- 0.
- 1.
- 0.

View File

@@ -0,0 +1,41 @@
<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.coex.tech/aruco -->
<!-- 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"/>
<remap from="map_markers" to="aruco_map/markers" if="$(arg aruco_map)"/>
<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="image_axis" value="true"/>
<param name="frame_id" value="aruco_map_detected" if="$(arg aruco_vpe)"/>
<param name="frame_id" value="aruco_map" unless="$(arg aruco_vpe)"/>
<param name="markers/frame_id" value="aruco_map"/>
<param name="markers/child_frame_id_prefix" value="aruco_"/>
</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,73 @@
<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="false"/>
<arg name="rangefinder_vl53l1x" default="true"/>
<arg name="led" default="false"/>
<arg name="rc" default="true"/>
<!-- log formatting -->
<env name="ROSCONSOLE_FORMAT" value="[${severity}] [${time}]: ${logger}: ${message}"/>
<!-- 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"/>
<param name="reference_frames/navigate_target" value="map"/>
</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="rangefinder" pkg="vl53l1x" type="vl53l1x_node" output="screen" if="$(arg rangefinder_vl53l1x)">
<param name="frame_id" value="rangefinder"/>
</node>
<!-- led strip -->
<include file="$(find clever)/launch/led.launch" if="$(arg led)"/>
<!-- rc backend -->
<node name="rc" pkg="clever" type="rc" output="screen" if="$(arg rc)"/>
</launch>

View File

@@ -0,0 +1,38 @@
<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.coex.tech/camera_frame -->
<!-- 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/calibration.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 -->
<param name="rescale_camera_info" value="true"/> <!-- automatically rescale camera calibration info -->
<!-- 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,100 @@
0 0.33 0.0 9.0 0 0 0 0
1 0.33 1.0 9.0 0 0 0 0
2 0.33 2.0 9.0 0 0 0 0
3 0.33 3.0 9.0 0 0 0 0
4 0.33 4.0 9.0 0 0 0 0
5 0.33 5.0 9.0 0 0 0 0
6 0.33 6.0 9.0 0 0 0 0
7 0.33 7.0 9.0 0 0 0 0
8 0.33 8.0 9.0 0 0 0 0
9 0.33 9.0 9.0 0 0 0 0
10 0.33 0.0 8.0 0 0 0 0
11 0.33 1.0 8.0 0 0 0 0
12 0.33 2.0 8.0 0 0 0 0
13 0.33 3.0 8.0 0 0 0 0
14 0.33 4.0 8.0 0 0 0 0
15 0.33 5.0 8.0 0 0 0 0
16 0.33 6.0 8.0 0 0 0 0
#17 0.33 7.0 8.0 0 0 0 0
18 0.33 8.0 8.0 0 0 0 0
19 0.33 9.0 8.0 0 0 0 0
20 0.33 0.0 7.0 0 0 0 0
21 0.33 1.0 7.0 0 0 0 0
22 0.33 2.0 7.0 0 0 0 0
23 0.33 3.0 7.0 0 0 0 0
24 0.33 4.0 7.0 0 0 0 0
25 0.33 5.0 7.0 0 0 0 0
26 0.33 6.0 7.0 0 0 0 0
27 0.33 7.0 7.0 0 0 0 0
28 0.33 8.0 7.0 0 0 0 0
29 0.33 9.0 7.0 0 0 0 0
30 0.33 0.0 6.0 0 0 0 0
31 0.33 1.0 6.0 0 0 0 0
32 0.33 2.0 6.0 0 0 0 0
33 0.33 3.0 6.0 0 0 0 0
34 0.33 4.0 6.0 0 0 0 0
35 0.33 5.0 6.0 0 0 0 0
36 0.33 6.0 6.0 0 0 0 0
37 0.33 7.0 6.0 0 0 0 0
38 0.33 8.0 6.0 0 0 0 0
39 0.33 9.0 6.0 0 0 0 0
40 0.33 0.0 5.0 0 0 0 0
41 0.33 1.0 5.0 0 0 0 0
42 0.33 2.0 5.0 0 0 0 0
43 0.33 3.0 5.0 0 0 0 0
44 0.33 4.0 5.0 0 0 0 0
45 0.33 5.0 5.0 0 0 0 0
46 0.33 6.0 5.0 0 0 0 0
47 0.33 7.0 5.0 0 0 0 0
48 0.33 8.0 5.0 0 0 0 0
49 0.33 9.0 5.0 0 0 0 0
50 0.33 0.0 4.0 0 0 0 0
51 0.33 1.0 4.0 0 0 0 0
52 0.33 2.0 4.0 0 0 0 0
53 0.33 3.0 4.0 0 0 0 0
54 0.33 4.0 4.0 0 0 0 0
55 0.33 5.0 4.0 0 0 0 0
56 0.33 6.0 4.0 0 0 0 0
57 0.33 7.0 4.0 0 0 0 0
58 0.33 8.0 4.0 0 0 0 0
59 0.33 9.0 4.0 0 0 0 0
60 0.33 0.0 3.0 0 0 0 0
61 0.33 1.0 3.0 0 0 0 0
62 0.33 2.0 3.0 0 0 0 0
63 0.33 3.0 3.0 0 0 0 0
64 0.33 4.0 3.0 0 0 0 0
65 0.33 5.0 3.0 0 0 0 0
66 0.33 6.0 3.0 0 0 0 0
67 0.33 7.0 3.0 0 0 0 0
68 0.33 8.0 3.0 0 0 0 0
69 0.33 9.0 3.0 0 0 0 0
70 0.33 0.0 2.0 0 0 0 0
71 0.33 1.0 2.0 0 0 0 0
72 0.33 2.0 2.0 0 0 0 0
73 0.33 3.0 2.0 0 0 0 0
74 0.33 4.0 2.0 0 0 0 0
75 0.33 5.0 2.0 0 0 0 0
76 0.33 6.0 2.0 0 0 0 0
77 0.33 7.0 2.0 0 0 0 0
78 0.33 8.0 2.0 0 0 0 0
79 0.33 9.0 2.0 0 0 0 0
80 0.33 0.0 1.0 0 0 0 0
81 0.33 1.0 1.0 0 0 0 0
82 0.33 2.0 1.0 0 0 0 0
83 0.33 3.0 1.0 0 0 0 0
84 0.33 4.0 1.0 0 0 0 0
85 0.33 5.0 1.0 0 0 0 0
86 0.33 6.0 1.0 0 0 0 0
87 0.33 7.0 1.0 0 0 0 0
88 0.33 8.0 1.0 0 0 0 0
89 0.33 9.0 1.0 0 0 0 0
90 0.33 0.0 0.0 0 0 0 0
91 0.33 1.0 0.0 0 0 0 0
92 0.33 2.0 0.0 0 0 0 0
93 0.33 3.0 0.0 0 0 0 0
94 0.33 4.0 0.0 0 0 0 0
95 0.33 5.0 0.0 0 0 0 0
96 0.33 6.0 0.0 0 0 0 0
97 0.33 7.0 0.0 0 0 0 0
98 0.33 8.0 0.0 0 0 0 0
99 0.33 9.0 0.0 0 0 0 0

View File

@@ -116,8 +116,9 @@ img-chroot ${IMAGE_PATH} exec ${SCRIPTS_DIR}'/image-software.sh'
# Configure image
img-chroot ${IMAGE_PATH} exec ${SCRIPTS_DIR}'/image-configure.sh'
# Copy service file for clever show client
# Copy service files for clever show client and visual_pose_watchdog
img-chroot ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/clever-show.service' '/lib/systemd/system/'
img-chroot ${IMAGE_PATH} copy ${SCRIPTS_DIR}'/assets/visual_pose_watchdog.service' '/lib/systemd/system/'
# 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

View File

@@ -61,6 +61,7 @@ chrony \
echo_stamp "Install python libs"
my_travis_retry pip install selectors2
my_travis_retry pip install psutil
echo_stamp "Install catkin packages"
cd /home/pi/catkin_ws/src

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/assets/server-gui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -1,30 +1,33 @@
# Установка и настройка аддона
## Установка
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` - удалят (деинсталлирует) аддон (перед установокой новой версии рекомендуется удалить старую).
# Подготовка и создание анимации дронов
## Взлёт
...
## Посадка
**Внимание!** В анимации НЕ нужно отображать процесс посадки коптера до момента касания с полом. Снижение (с любой высоты) и посадка будут совершены автоматически из последней точки анимации данного дрона.
# Аддон для экспорта анимации из Blender
Аддон для Blender предназначен для преобразования анимации полёта коптеров, нарисованной в Blender, в полётные пути для каждого коптера анимации, с учётом цвета объектов в каждый момент времени.
## Установка и настройка
* Скачайте и установите согласно инструкциям последнюю версию Blender 2.81 с [оффициального сайта](https://www.blender.org/download/).
* Откройте Blender, в верхнем меню выберите `Edit > Preferences`. В открывшемся окне настроек в боковой панели выберите пункт `Add-ons`. Нажмите на кнопку `Install...` в верхнем правом углу окна. В диалоговом окне откройте путь к папке с аддоном [clever-show/blender-addon](../../blender-addon/) и выберите файл `addon.py`. Нажмите `Install Add-on from file...`. Аддон установлен.
* После установки аддона поставьте "галочку" напротив аддона `Import-Export: Export > CSV Drone Swarm Animation Exporter` для активации аддона.
Аддон активирован и готов к работе. Выполнение этих операций не понадобится при дальнейших запусках Blender.
## Экпорт при помощи аддона
[Пример](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`
<!--stackedit_data:
eyJoaXN0b3J5IjpbLTE5OTU5ODU3ODJdfQ==
-->
* `Use name filter for objects` - чекбокс, определяет, будет ли использован фильтр объектов при сохранении их путей. При отключении этого параметра будут экспортированы пути всех видимых объектов.
* `Name identifier` - фильтр имён объектов. Если активен чекбокс `Use name filter for objects`, будут сохранены только пути объектов, содержащих данное значение в названии.
* `Show detailed animation warnings` - чекбокс, определяет, будут ли показаны предупреждения по превышению скоростей и расстояний в анимации.
* `Speed limit` - при нарушении указанного ограничения по скорости передвижения дронов будут выведены предупреждения.
* `Distance limit` - при нарушении указанной минимальной дистанции между дронами будут выведены предупреждения.
После настройки нужных параметров нажмите кнопку `Export Drone Swarm animation`. В указанную папку экспортируются анимации указанных объектов из проекта Blender в формате `.csv`.
## Деактивация и удаление
Для деактивации аддона уберите "галочку" напротив имени аддона, как описано [выше](#установка-и-настройка).
Для получения дополнительных сведений нажмите знак стрелочки слева от поля активации. В развернувшемся блоке так же есть кнопки:
* `Documentation` - ведет на страницу документации аддона
* `Report a bug` - ведет на страницу issues репозитория clever-show
* `Remove` - удаляет аддон (перед установкой новой версии рекомендуется удалить старую)

View File

@@ -0,0 +1,171 @@
# Клиент clever-show
Приложение для удаленного синхронизированного управления дронами в шоу и модуль экстренной защиты дронов.
* [Установка и запуск](start-tutorial.md#установка-и-запуск-клиента)
* [Настройка клиента](#настройка-клиента)
## Настройка клиента
### Файл конфигурации
Конфигурация клиента задаётся в файле [client_config.ini](../../Drone/client_config.ini), имеющем следующий вид по умолчанию:
```ini
[SERVER]
port = 25000
broadcast_port = 8181
host = 192.168.1.101
buffer_size = 10000
[VISUAL_POSE_WATCHDOG]
timeout = 1.0
pos_delta_max = 3.0
action = emergency_land
emergency_land_thrust = 0.45
emergency_land_decrease_thrust_after = 5.0
timeout_to_disarm = 10.0
[TELEMETRY]
transmit = True
frequency = 1
log_cpu_and_memory = True
[COPTERS]
frame_id = map
takeoff_height = 1.0
takeoff_time = 5.0
safe_takeoff = False
reach_first_point_time = 5.0
land_time = 1.0
x0_common = 0
y0_common = 0
z0_common = 0
yaw = 180
land_timeout = 10.0
[FLOOR FRAME]
parent = aruco_map
x = 2.4
y = 12.4
z = 6.4
roll = 180
pitch = 0
yaw = -90
[ANIMATION]
takeoff_animation_check = True
land_animation_check = True
frame_delay = 0.1
x_ratio = 1.0
y_ratio = 1.0
z_ratio = 1.0
[PRIVATE]
id = /hostname
restart_after_rename = True
use_leds = True
led_pin = 21
x0 = 0
y0 = 0
z0 = 0
[NTP]
use_ntp = False
host = ntp1.stratum2.ru
port = 123
```
Конфигурация по умолчанию является полностью работоспособной и не требует изменений для быстрого старта клиента.
Для централизованной загрузки конфигурации на все коптеры нужно использовать пункт меню `Send configurations` на [сервере](server.md#раздел-server). Допускается загрузка неполного файла параметров конфигурации, с отсутствующими разделами или параметрами относительно конфигурации по умолчанию.
#### Раздел SERVER
В этом разделе задаются параметры сетевого взаимодействия клиента с сервером. Доступны следующие параметры:
* `port` - TCP порт, на который будут приниматься входящие соединения от сервера. При использовании настройки [use_broadcast](server.md#раздел-broadcast) на сервере, данный порт будет сконфигурирован у клиента автоматически. *Рекомендуется изменить значение по умолчанию в целях безопасности* (любое пятизначное и более число, если другое ПО не использует выбранный порт).
* `broadcast_port` - UDP порт, на который по широковещательному каналу сервер передаёт свои настройки. С помощью данного механизма возможно автоматическое подключение клиента к серверу.
* `host` - IP адрес сервера.
* `buffer_size` - размер буфера при приёме и передаче данных. *Не рекомендуется изменять. Рекомендуется использовать единое значение у сервера и клиентов.*
#### Раздел VISUAL_POSE_WATCHDOG
В данном разделе настраивается программа экстренной защиты коптера от потери позиции или столкновения с объектом.
* `timeout` - время срабатывания экстренной защиты после потери визуальной позиции, в секундах.
* `pos_delta_max` - максимальная разница между текущим положением и точкой, в которой сейчас должен находиться коптер, в метрах. Требуется для проверки на столкновение коптера с объектом. Если расстояние между текущим положением коптера и положением, в котором он должен сейчас находиться, больше этого числа (в метрах), срабатывает экстренная защита.
* `action` - действие при срабатывании экстренной защиты. Доступные варианты: `land` - посадка коптера в режиме полётного контроллера AUTO.LAND, `emergency_land` - посадка коптера с постепенным уменьшением мощности моторов, `disarm`- выключение моторов. **Внимание!** Не рекомендуется использовать режим AUTO.LAND с выключенным барометром - при потере источника высоты в полёте, например показаний лазера или визуальной позиции, режим AUTO.LAND не гарантирует посадку коптера, так как ориентируется на показания высоты. Для посадки коптера при его позиционировании с использованием визуальной позиции или лазера и возможности потери данных с этих систем рекомендуется использовать режим `emergency_land`.
* `emergency_land_thrust` - начальная мощность, подаваемая на моторы в случае выбора действия `emergency_land` при срабатывании экстренной защиты. Безразмерная величина, от 0 (отсутствие мошности) до 1 (полная мощность). Для гарантированной посадки рекомендуется устанавливать в значение, меньшее по величине на 5-10 процентов, чем газ висения (параметр `MPC_THR_HOVER` в px4).
* `emergency_land_decrease_thrust_after` - время, через которое мощность на моторах плавно начинает уменьшаться в случае выбора действия `emergency_land` при срабатывании экстренной защиты, в секундах.
* `timeout_to_disarm` - время, через которое коптер безусловно выключает моторы после срабатывания экстренной защиты, в секундах.
#### Раздел TELEMETRY
В данном разделе настраивается поток передачи телеметрии на сервер.
* `transmit` - логическое значение, определяет, нужно ли передавать данные на сервер.
* `frequency` - частота передачи данных на сервер, целочисленное значение, количество раз в секунду.
* `log_cpu_and_memory` - логическое значение, определяет, будет ли записываться в лог сервиса клиента clever-show состояние процессора и памяти.
#### Раздел COPTERS
В данном разделе находятся настройки, влияющие на процесс полёта коптера.
* `frame_id` - название системы координат, относительно которой будут публиковаться координаты точек для воспроизведения анимации. Если значение `floor` - клиент публикует статическую систему координат с названием `floor` и настройками из раздела [FLOOR_FRAME](#раздел-floor_frame). **Внимание!** Убедитесь, что коптер удерживает позицию в данной системе координат. Для этого вы можете воспользоваться командой [Takeoff](server.md#тестовые-команды) из серверного приложения. Коптер взлетит на высоту `takeoff_height` относительно текущей.
* `takeoff_height` - высота взлёта коптера, в метрах. Используется в начале воспроизведения анимации или при тестировании коптера с сервера.
* `takeoff_time` - максимальное время взлёта коптера, в секундах.
* `safe_takeoff` - логическое значение, определяет, нужно ли производить посадку в безопасном режиме.
* `reach_first_point_time` - максимальное время полёта к первой точке анимации, в секундах.
* `land_time` - время зависания в конечной точке анимации перед посадкой, в секундах.
* `x0_common` - смещение по оси x, общее для всех коптеров, в метрах.
* `y0_common` - смещение по оси y, общее для всех коптеров, в метрах.
* `z0_common` - смещение по оси z, общее для всех коптеров, в метрах.
* `yaw` - поворот коптера при полёте по точкам, в градусах. Если значение `nan` - коптер сохраняет изначальную ориентацию в полёте.
* `land_timeout` - время таймаута посадки, после которого происходит выключение моторов коптера, в секундах.
#### Раздел FLOOR_FRAME
Данный раздел описывает смещение системы координат с названием `floor` и используется только при указании параметра `frame_id` как `floor` в разделе [COPTERS](#раздел-copters).
* `parent` - название опорной системы координат, относительно которой будет располагаться система координат `floor`.
* `x` - смещение системы координат `floor` по оси x относительно системы координат `parent`, в метрах.
* `y` - смещение системы координат `floor` по оси y относительно системы координат `parent`, в метрах.
* `z` - смещение системы координат `floor` по оси z относительно системы координат `parent`, в метрах.
* `roll` - поворот системы координат `floor` вокруг оси x относительно системы координат `parent`, в градусах.
* `pitch` - поворот системы координат `floor` вокруг оси y относительно системы координат `parent`, в градусах.
* `yaw` - поворот системы координат `floor` вокруг оси z относительно системы координат `parent`, в градусах.
**Внимание!** Повороты `roll`, `pitch`, `yaw` производятся последовательно в указанном порядке.
#### Раздел ANIMATION
В данном разделе настраивается обработка анимации.
* `takeoff_animation_check` - логическое значение, определяет, будет ли производиться автоматическая обработка старта анимации. **Если значение True**, при загрузке анимации проверяется взлёт коптеров. Если в файле анимации коптер взлетает с земли, при старте анимации будет применена *логика немедленного воспроизведения*: коптер сразу начинает следовать точкам, указанным в анимации. Если в файле анимации коптер начинает полёт в воздухе, при старте анимации будет применена *логика полёта к первой точке*: коптер в начале взлетает на высоту `takeoff_height` за время `takeoff_time`, затем перемещается к первой точке за время `reach_first_point_time`, и затем начинает следовать точкам, указанным в анимации. **Если значение False**, при загрузке анимации не проверяется взлёт коптеров, а при старте анимации действует *логика полёта к первой точке*.
* `land_animation_check` - логическое значение, определяет, будет ли производиться автоматическая обработка завершения анимации. **Если значение True**, при загрузке анимации проверяется посадка коптеров. Если в файле анимации коптер садится на землю и стоит до завершения анимации, проверка удалит все точки в анимации после начала посадки коптера. Таким образом, коптер в конце анимации зависнет над точкой посадки на время `land_time`, сядет автоматически и выключит моторы. **Если значение False**, при загрузке анимации не проверяется посадка коптеров и точкой посадки считается последняя точка в анимации. Например, если анимация посадки нарисована полностью и коптер стоит после посадки на земле некоторое время, а значение данного параметра **False**, всё это время у коптера будут включены моторы и он будет пытаться удержать указанную позицию посадки вплоть до завершении файла анимации, затем через время `land_time` перейдёт в редим посадки.
* `frame_delay` - время воспроизведения одного кадра в секундах.
* `x_ratio` - масштаб анимации по оси x
* `y_ratio` - масштаб анимации по оси y
* `z_ratio` - масштаб анимации по оси z
#### Раздел PRIVATE
В данном разделе находятся параметры, специфичные для конкретного коптера.
* `id` - имя коптера, отображаемое в таблице. Если значение `/hostname` - имя определяется из файла `/etc/hostname`.
* `restart_dhcpcd` - логический параметр, определяет, требуется ли перезагрузка коптера при переименовании его `id` удалённо с сервера.
* `use_leds` - логический параметр, определяет, использует ли коптер светодиодную ленту.
* `led_pin` - номер пина GPIO на Raspberry Pi, к которому подключена светодиодная лента.
* `x0` - смещение по оси x, только для данного коптера.
* `y0` - смещение по оси y, только для данного коптера.
* `z0` - смещение по оси z, только для данного коптера.
#### Раздел NTP
Помимо синхронизации времени (с миллисекундной точностью) с помощью пакета chrony, предоставляется альтернатива - возможность использования внешних (при наличии соединения локальной сети с интернетом) или внутрисетевых NTP-серверов. **Внимание!** Для корректной работы системы, **и сервер, и клиенты** должны использовать единый способ синхронизации времени (набор параметров в этом разделе). Данный раздел полностью унифицирован и для сервера, и для клиентов.
* `use_ntp` - определяет, будет ли использоваться синхронизация времени с помощью NTP. (при значении `False` будет использовано локальное время ОС (синхронизируется автоматически при использовании chrony). *Рекомендуется использование chrony, а не NTP*
* `host` - имя хоста или IP адрес NTP сервера (локального или удаленного)
* `port` - порт, используемый NTP сервером

View File

@@ -3,15 +3,21 @@
Иногда возникает необходимость собрать образ с настройками коптера, отличными от релизной версии образа. Есть несколько способов это сделать.
## Подготовка к сборке
Установите [docker](https://www.docker.com):
```bash
sudo apt install docker.io
```
## Локальная сборка с изменением настроек Клевера
* Замените файлы настроек Клевера (launch файлы и карту) в [папке](../builder/clever-config) `builder/clever-config` в директории с исходным кодом CleverSwarm.
* Поместите папки с файлами настроек Клевера (`launch`, `map` и `camera_info`) в [папку](../../builder/clever-config) `builder/clever-config` в директории с исходным кодом clever-show.
* Все файлы из папки `launch` будут скопированы в директорию `/home/pi/catkin_ws/src/clever/clever/launch` в собранном образе.
* Все файлы из папки `map` будут скопированы в директорию `/home/pi/catkin_ws/src/clever/aruco_pose/map` в собранном образе.
* Все файлы из папки `camera_info` будут скопированы в директорию `/home/pi/catkin_ws/src/clever/clever/camera_info` в собранном образе.
* Соберите свой образ с помощью docker:
```bash
cd source-dir
sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-tool:v0.5
@@ -20,26 +26,36 @@ sudo docker run --privileged -it --rm -v /dev:/dev -v $(pwd):/mnt goldarte/img-t
## Ручная настройка образа
* Разархивируйте файл со скачанным образом, перейдите в директорию с этим образом, и войдите в консоль сборщика образа с помощью команды:
```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)
Статья по изменению скриптов сборки образа и создания кастомной сборки написана [здесь](https://clever.copterexpress.com/ru/image_building.html)

View File

@@ -1,103 +1,214 @@
# Настройка сервера
## Файл конфигурации
Конфигурация сервера задаётся в файле Server/server_config.ini, имеющем вид (по умолчанию):
# Сервер
Приложение для создания и запуска шоу, настройки дронов, анимации и музыки.
* [Установка и запуск](start-tutorial.md#установка-и-запуск-сервера)
* [Интерфейс](#интерфейс-сервера)
* [Настройка](#настройка-сервера)
* [Дополнительные операции](#дополнительные-операции)
## Интерфейс сервера
Сервер имеет визуальный графический интерфейс для удобства взаимодействия.
![Интерфейс сервера](../assets/server-gui.png)
### Таблица состояния коптеров
При первом подключении клиента к серверу в таблицу добавляется строка для отображения состояния клиента, содержащая только имя клиента (`copter ID`). Если на клиентах настроена автоматическая передача телеметрии, данные в таблице будут обновляться автоматически. Так же возможно запросить телеметрию выбранных клиентов с помощью кнопки [`Preflight check`](#управление) Строки можно сортировать по возрастанию или убыванию значений любого из столбцов, кликнув по его заголовку.
Ячейки таблицы подсвечиваются:
* жёлтым, если необходимое значение отсутствует
* красным, если данные в ячейке не прошли проверку
* зелёным, если данные в ячейке прошли проверку
Коптер считается **готовым к воспроизведению анимации**, если все ячейки в строке прошли проверку и подсвечены зелёным.
Коптер считается **готовым к полёту**, если все ячейки в строке, кроме `animation ID` и `dt`, прошли проверку и подсвечены зелёным.
#### Столбцы таблицы
* `copter ID` - имя клиента. Может быть сконфигурирован на стороне клиента. Отображается сразу при подключении клиента. Рядом с каждым ID коптера расположен чекбокс - коптеры, чей ID отмечен чекбоксом положительно (галочка), считаются *выбранными*. Ячейки в этом столбце всегда проходят проверку.
* `version` - хеш-код текущей git версии клиента. Ячейки в этом столбце всегда проходят проверку.
* `animation ID` - внутреннее название файла анимации, подгруженного клиентом. Ячейка в данном столбце не проходит проверку, если анимация отсутствует (значение `No animation`). В остальных случаях, если ячейка не пустая, она проходит проверку. **Внимание!** Проверьте соответствие названий файлов анимаций у коптеров перед запуском.
* `battery` - значение напряжения на аккумуляторе коптера в вольтах и заряд в процентах по данным полётного контроллера. Ячейка в данном столбце проходит проверку, если значение заряда батареи выше значения [battery_percentage_min](#раздел-checks), задаваемого в настройках сервера. В остальных случаях, если ячейка не пустая, она не проходит проверку.
* `system` - состояние полётного контроллера. Ячейка в данном столбце проходит проверку, если её значение `STANDBY`. В остальных случаях, если ячейка не пустая, она не проходит проверку.
* `sensors` - состояние калибровки компаса, акселлерометра и гироскопа полётного контроллера. Ячейка в данном столбце проходит проверку, если её значение `OK`. В остальных случаях, если ячейка не пустая, она не проходит проверку.
* `mode` - режим полётного контроллера. Ячейка в данном столбце не проходит проверку, если её значение `NO_FCU` или содержит `CMODE`. В остальных случаях, если ячейка не пустая, она проходит проверку.
* `checks` - состояние самодиагностики коптера. Ячейка в данном столбце проходит проверку, если её значение `OK`. В остальных случаях, если ячейка не пустая, она не проходит проверку.
* `current x y z yaw frame_id` - текущее положение коптера с указанием названия системы координат. Ячейка в данном столбце не проходит проверку, если её значение `NO_POS` или содержит `nan`. В остальных случаях, если ячейка не пустая, она проходит проверку.
* `start x y z` - стартовое положение коптера для воспроизведения анимации. Ячейка в данном столбце не проходит проверку, если её значение `NO_POS` или разница между текущим и стартовым положением коптера больше значения [start_pos_delta_max](#раздел-checks). В остальных случаях, если ячейка не пустая, она проходит проверку.
* `dt` - разница между временем на сервере и клиенте в секундах, включая сетевую задержку. Ячейка в данном столбце проходит проверку, если её значение меньше значения [time_delta_max](#раздел-checks), задаваемого в настройках сервера. В остальных случаях, если ячейка не пустая, она не проходит проверку. При слишком больших значениях сигнализирует об отсутствии синхронизации времени между коптером и клиентом.
### Меню
#### Раздел Server
![Скриншот раздела Server](../assets/server-server.png)
Данный раздел содержит несколько утилит по отправке различных данных на *выбранные* клиенты. **Внимание!** Не используйте данные команды во время полёта коптеров!
* `Send animations` - отправка файлов анимации, экспортированных аддоном к Blender, на выбранные коптеры. В диалоговом окне необходимо выбрать *папку*, содержащую файлы анимации. Каждый файл анимации будет отправлен на клиент с именем, соответствующим имени файла без расширения.
* `Send configurations` - отправка *единого* файла конфигурации клиента на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл конфигурации в установленном формате. Файл конфигурации может быть неполным, в таком случае будут перезаписаны лишь указанные в файле параметры. **Внимание!** Не рекомендуется использовать данное действие для массовой перезаписи `copter ID`, кроме значения `/hostname`. **Внимание!** НЕ отправляйте на клиенты файл конфигурации сервера.
* `Send launch files` - отправка launch-файлов конфигурации сервиса `clever`. В диалоговом окне необходимо выбрать *папку*, содержащую файлы конфигурации с сширением `.launch`. Все файлы с таким расширением будут отправлены *на каждый* из клиентов. **Внимание!** Существующие файлы конфигурации на коптерах будут перезаписаны, однако файлы, не отправленные сервером, не будут удалены или модифицированы.
* `Send aruco map` - отправка *единого* файла карты aruco маркеров на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл карты в установленном формате. Файл на клиенте будет перезаписан. После получения и записи файла клиент автоматически перезапустит сервис `clever`. Для возобновления работоспособности полётных функций и получения некоторых значений телеметрии *необходимо подождать* некоторое время до полного запуска сервиса.
* `Send camera calibrations` - отправка yaml-файлов калибровки камеры для сервиса `clever`. В диалоговом окне необходимо выбрать *папку*, содержащую файлы конфигурации с расширением `.yaml`. Каждый файл калибровки будет отправлен на клиент с именем (copter ID), соответствующим имени файла без расширения. **Внимание!** Существующий файл калибровки на коптере будет перезаписан.
* `Send FCU parameters` - отправка и запись *единого* файла конфигураций полётного контроллера (FCU) на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл параметров в установленном формате. Параметры на полётном контроллере будут перезаписаны.
* `Developer mode`: **Внимание!** Используйте данные действия с большой осторожностью.
* `Send any file` - отправка *одного* любого файла на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл. Далее, необходимо указать путь, по которому данный файл будет записан на клиенты (не включая имя файла).
* `Send any command` - отправка и выполнение любой команды терминала на все выбранные клиенты. В диалоговом окне необходимо ввести требуемую команду. Команды *могут* использовать `sudo`-права.
* `Select all drones` (`Ctrl+A`) - выделяет все коптеры в таблице. При следующем вызове команды, выделение всех коптеров будет отменено.
#### Раздел Drone
![Скриншот раздела Drone](../assets/server-drone.png)
* `Set Z offfset to ground` - устанавливает собственный отступ по Z каждого из выбранных клиентов в значение, равное текущему положению по координате Z. Можно применять для выравнимания общей высоты полёта коптеров.
* `Reset Z offfset` - устанавливает собственный отступ по Z каждого из выбранных клиентов в значение `0`.
* `Restart chrony` - перезапускает сервис синхронизации времени `chrony` на выбранных клиентах. Используйте для ручной синхронизации в случаях, если время между сервером и клиентами не синхронихированно.
* `Remove from table` - удаляет выбранные коптеры из таблицы. **Внимание!** В случае, если клиент был подключен, будет произведено отключение. В случае если удалённый таким образом клиент исправно функционировал, он переподключится в кратчайшие сроки.
* `Developer mode`: **Внимание!** Используйте данные действия с большой осторожностью.
* `Restart clever service` - перезапускает сервис `clever` на выбранных клиентах. Для возобновления работоспособности полётных функций и получения некоторых значений телеметрии *необходимо подождать* некоторое время до полного запуска сервиса.
* `Restart clever-show service` - перезапускает сервис шоу коптеров `clever-show` на выбранных клиентах. Во время перезапуска клиенты будут отключены.
* `Update clever-show git` - обновляет папку репозитория `clever-show` на выбранных клиентах. Файлы конфигурации клиента *не будут* перезаписаны. **Внимание!** Для того, чтобы изменения вступили в силу, *необходимо* перезапустить сервис `clever-show`.
* `Reboot all` - полностью перезагружает полётный контроллер и компьютер на выбранных коптерах. Во время перезапуска клиенты будут отключены.
#### Раздел Animation
![Скриншот раздела Animation](../assets/server-animation.png)
* `Set start X Y to current position` - устанавливает точку старта анимации у выбранных клиентов в значения текущей позиции по X Y.
* `Reset start position` - устанавливает точку старта анимации у выбранных клиентов в значения `0.0`, `0.0`.
#### Раздел Music
![Скриншот раздела Music](../assets/server-music.png)
* `Select music file` - загружает выбранный музыкальный файл для дальнейшего воспроизведения вручную или через определённое время после старта анимации. Поддерживаемые расширения: `.mp3` или `.wav`.
* `Play music` - воспроизводит загруженную музыку.
* `Stop music` - останавливает воспроизведение проигрываемой музыки.
### Боковая панель команд
![Скриншот боковой панели](../assets/server-sidemenu.png)
#### Управление
Данный раздел команд предназначен для выскоуровневого управления роем дронов.
* Спинбокс `Start after` - задаёт время задержки синхронного запуска выполнения анимаций коптерами после нажатия на кнопку `Start animation`. Для загруженных, подверженных помехам или имеющих большой пинг сетей рекомендуется использовать значения больше нуля.
* Спинбокс `Music after` - задаёт время задержки запуска музыки после нажатия на кнопку `Start animation`.
* Чекбокс `Play music` - определяет, будет ли воспроизведена музыка при запуске анимации.
* Кнопка `Preflight check` - все выбранные клиенты выполняют самодиагностику и предполётную проверку. Результаты, вместе с другими параметрами клиента, будут отображены в таблице по мере поступления данных. Необходима в том случае, если на клиенте не настроена автоматическая передача телеметрии.
* Кнопка `Start animation` - посылает время старта анимации на все выбранные коптеры с учётом заданного в спинбоксе `Start after` времени. Все выбранные коптеры начинают синхронное воспроизведение анимации после нажатия на данную кнопку и через время, заданное в спинбоксе `Start after`. По окончанию анимации все коптеры выполнят посадку на месте окончания своей анимации. Кнопка активна только в том случае, если все коптеры готовы к воспроизведению анимации. При нажатии запрашивается дополнительное предупреждение.
* Кнопка `Pause/Resume` - ставит на паузу и возобновляет выполнение полётных задач. После каждого нажатия кнопка меняет состояние на обратное.
* Состояние`Pause` - ставит на паузу очередь заданий всех выбранных коптеров: приостанавливается выполнение любого полётного задания. Рекомендуется использовать в чрезвычайных ситуациях для определения неисправного коптера. **Внимание!** Данная команда НЕ прерывает полёт коптера в уже указанную точку (например: элементы взлёта, посадки; следование до начальной точки анимации и т.д.)
* Состояние `Resume` - все выбранные коптеры *синхронизированно* продолжат выполнение своих очередей заданий (например исполнение анимации)
#### Средства перехвата в экстренных ситуациях
* Кнопка `Land selected` - все выбранные коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно переходят в режим посадки. **Используйте в экстренных случаях как одно из средств перехвата.**
* Кнопка `Land ALL` - ВСЕ коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно переходят в режим посадки. **Используйте в экстренных случаях как одно из средств перехвата.**
* Кнопка `Emergency land` - прерывает выполнение полётных заданий *ВСЕХ* подключенных коптеров. Сбрасывает очередь заданий - *действие необратимо*. Выполняет полную остановку и немедленную посадку коптеров. **Используйте в экстренных случаях как одно из средств перехвата.**
* Кнопка `Visual land` - открывает диалоговое окно модуля визуальной посадки неисправного коптера. Полное описание находится в [конце статьи](#visual-land).
* Кнопка `Disarm selected` - все выбранные коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно отключают моторы. Это может привести к падению и повреждению коптеров.
* Кнопка `Disarm ALL` - ВСЕ коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно отключают моторы. Это может привести к падению и повреждению коптеров **Используйте в крайних случаях как последнее из средств перехвата.**
#### Тестовые команды
В данном разделе находятся команды, позволяющие напрямую управлять коптерами для их проверки.
* Кнопка `Test leds` - все выбранные коптеры выполняют двухсекундную анимацию (бегущие точки) светодиодной лентой (белым цветом). Команда *безопасна* и может быть использована для проверки работы светодиодных лент, качества и задержки подключения к серверу или определения соответствия коптера и его `copter ID` в таблице.
* Кнопка `Takeoff` - все выбранные коптеры совершают вертикальный взлёт, после чего зависают над точкой взлёта. Кнопка активна, *только* если все выбранные коптеры готовы к полёту. **Внимание!** Используйте осторожно, соблюдайте технику безопасности. Не применяйте во время выполнения других полётных функций!
* Чекбокс `Z` - если чекбокс активен, коптер взлетит в указанное значение по `z`. Иначе коптерами будут использоваться значения по умолчанию, указанные в их конфигурациях, а взлёт будет производиться относительно текущей высоты.
* Спинбокс `Z` - задаёт значение координаты `z` взлёта коптеров в метрах.
* Кнопка `Flip` - все выбранные коптеры **совершают флип (flip)** - переворот на 360 градусов вокруг одной из *горизонтальных* осей. **Внимание!** Используйте осторожно, соблюдайте технику безопасности. **Внимание!** Для исполнения флипа коптер должен иметь минимальную высоту больше 2м. **Внимание!** Не применяйте во время выполнения других полётных функций!
#### Системные команды
В данном разделе находятся команды, исполняемые непосредственно на полётном контроллере коптера.
* Кнопка `Reboot FCU` - перезагружает полётные контроллеры всех выбранных коптеров. Можно использовать для обновления поворота коптера при его определении только с помощью инерциальной системы коптера, например при полёте по системе позиционирования Pozyx или с помощью Optical Flow. Для возобновления работоспособности полётных функций и получения некоторых значений телеметрии *необходимо подождать* некоторое время до перезагрузки полётного контроллера.
* Кнопка `Calibrate gyro` - переводит полётные контроллеры всех выбранных коптеров в режим калибровки гироскопа. **Внимание!** Коптеры должны быть неподвижны в течение калибровки.
* Кнопка `Calibrate level` - переводит полётные контроллеры всех выбранных коптеров в режим калибровки уровня горизонта. **Внимание!** Коптеры должны быть неподвижны в течение калибровки.
## Настройка сервера
### Файл конфигурации
Конфигурация сервера задаётся в файле [server_config.ini](../../Server/server_config.ini), имеющем следующий вид по умолчанию:
```ini
[SERVER]
port = 25000
buffer_size = 1024
buffer_size = 1024
remove_disconnected = False
[CHECKS]
battery_percentage_min = 50.0
start_pos_delta_max = 1.0
time_delta_max = 1.0
[BROADCAST]
use_broadcast = True
broadcast_port = 8181
broadcast_delay = 5
broadcast_delay = 5.0
[NTP]
use_ntp = False
host = ntp1.stratum2.ru
port = 123
```
```
Конфигурация по умолчанию является полностью работоспособной и не требует изменений для быстрого начала работы системы.
### Раздел 'Server'
В этом разделе задаются параметры сетевого взаимодействия сервера, доступны следующие параметры:
* `port` - TCP порт, на котором будут приниматься входящие соединения от клиентов (коптеров). При использовании broadcast данный порт будет сконфигурирован у клиента автоматически. *Рекомендуется изменить значение по умолчанию в целях безопасности* (любое пятизначное и более число, если другое ПО не использует выбранный порт).
* `buffer_size` - размер буфера при приёме и передаче данных. *Не рекомендуется изменять. Рекомендуется использовать единое значение у сервера и клиентов.*
#### Раздел SERVER
### Раздел 'Broadcast'
Сервер использует UDP broadcast (на адрес 255.255.255.255 с выбранным портом), чтобы передавать клиентам (коптерам) актуальную информацию о конфигурации сервера и собственном адресе сервера для подключения (IP адрес и порт сервера). Таким образом, обеспечивается автоматическое подключение клиентов к серверу без необходимости дополнительной ручной конфигурации. В данном разделе задаются параметры этого механизма.
* `use_broadcast` - будут ли использованы broadcast'ы для передачи данных (при значении `False` broadcast'ы НЕ будут отправляться). Используйте `False` в случае повышенных требований безопасности, перегруженности сети или невозможности передачи по широковещательному каналу (из-за конфигурации брандмауэра или сети)
* `broadcast_port` - UDP порт, по которому будет осуществляться отправка сообщений. *Рекомендуется изменить значение по умолчанию в целях безопасности.* **Внимание!** При изменении этого параметра клиенты НЕ смогут принимать сообщения автоконфигурации до изменения (вручную) соответствующего параметра в конфигурации клиента на равное значение.
* `broadcast_delay` - Периодичность (в секундах, целочисленное значение), с которой будет происходить отправка broadcast сообщений. Увеличьте задержку для уменьшения нагрузки на сеть. *ИЛИ* Уменьшите задержку для уменьшения времени отклика и подключения при первом запуске клиентов.
### Раздел 'NTP'
Помимо синхронизации времени (с миллисекундной точностью) с помощью пакета chrony, предоставляется альтернатива - возможность использования внешних (при наличии соединения локальной сети с интернетом) или внутрисетевых NTP-серверов. **Внимание!** Для корректной работы системы, и сервер, *и* клиенты должны использовать единый способ синхронизации времени (набор параметров в этом разделе). Данный раздел полностью унифицирован и для сервера, и для клиентов.
* `use_ntp` - Определяет, будет ли использоваться синхронизация времени с помощью NTP. (при значении `False` будет использовано локальное время ОС (синхронизируется автоматически при использовании chrony). *Рекомендуется использование crhony, а не NTP*
* `host` - имя хоста или IP адрес NTP сервера (локального или удаленного)
* `port` - порт, используемый NTP сервером
В этом разделе задаются параметры сетевого взаимодействия сервера. Доступны следующие параметры:
# Интерфейс сервера
Сервер имеет визуальный графический интерфейс для удобства взаимодействия.
## Глоссарий
Некоторые термины, используемые для краткости записи:
* Готовый [к полёту] коптер:
Прошедший предполётную проверку (`Preflight check`) и имеющий удовлетворительные результаты по всем необходимым столбцам (зелёные ячейки в таблице). Учитываются проверки аккумулятора и сообщения selfcheck.
* Не готовый [к полёту] коптер:
НЕ прошедший предполётную проверку (`Preflight check`) (не имеющий её результатов в таблице - жёлтые ячейки) или же имеющий НЕудовлетворительные результаты (красные ячейки в таблице). Учитываются проверки аккумулятора и сообщения selfcheck.
* `port` - TCP порт, на который будут приниматься входящие соединения от клиентов. При использовании broadcast данный порт будет сконфигурирован у клиента автоматически. *Рекомендуется изменить значение по умолчанию в целях безопасности* (любое пятизначное и более число, если другое ПО не использует выбранный порт).
* `buffer_size` - размер буфера при приёме и передаче данных. *Не рекомендуется изменять. Рекомендуется использовать единое значение у сервера и клиентов.*
* `remove_disconnected` - Определяет поведение при разрыве связи с клиентом. При значении `True` вся информация о клиенте *будет удалена* как из внутренней памяти, так и *из таблицы*. *Это может привести к 'скачкам' таблицы при отключении клиентов.* При значении `False` отключённые клиенты *не будут* удалены из таблицы, но будут отображены с подсвечиванием ячейки в столбце `copter ID` красным цветом. Все данные будут сохранены. При переподключении клиента, он будет ассоциирован с той же строкой таблицы, а ячейка со значением `copter ID` вновь станет зелёного цвета.
## Меню
### Раздел 'Actions'
Данный раздел содержит несколько утилит по отправке различных данных на *выбранные* клиенты. **Внимание!** Не пытайтесь использовать данные команды во время полёта коптеров!
* `Send Animations` - отправка файлов анимации (экспортированных аддоном к Blender) на выбранные клиенты (коптеры). В диалоговом окне необходимо выбрать *папку*, содержащую файлы анимации (автоматически создается аддоном). Каждый файл анимации будет отправлен на клиент с именем (copter ID), соответствующим имени файла без расширения.
* `Send Configurations` - отправка *единого* файла конфигурации клиента на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл конфигурации в установленном формате. Файл конфигурации может быть неполным, в таком случае будут перезаписаны лишь указанные в файле параметры. *Не рекомендуется использовать данное действие для массовой перезаписи `Copter ID`, кроме значения `/hostname`.* **Внимание!** НЕ отправляйте на клиенты файл конфигурации сервера.
* `Send Aruco map` - отправка *единого* файла карты aruco маркеров на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл карты в установленном формате. Файл на клиенте будет перезаписан. После получения и записи файла клиент автоматически перезапустит сервис `clever`. Для работоспособности полётных функция *необходимо подождать* некоторое время до полного запуска сервиса.
#### Раздел CHECKS
## Боковая панель инструментов (команд)
### Управление
Данный раздел команд предназначен для выскоуровневого управления роем дронов.
* Кнопка `Preflight check` - Все выбранные клиенты выполняют самодиагностику и предполётную проверку. Результаты, вместе с другими параметрами клиента, будут отображены в таблице по мере поступления данных.
* Спинбокс `Start after N seconds` - Задаёт время задержки до синхронного запуска выполнения анимаций коптерами. *Не рекомендуется использовать `0` (нулевую задержку). Для загруженных\подверженных помехам\имеющих большой пинг сетей рекомендуется использовать бо́льшие значения (>5 секунд).*
* Кнопка `Start animation` - По истечению заданного в `Start after` времени, все выбранные коптеры совершат **взлёт**ные процедуры, перелетают на стартовые точки своих анимаций и *синхронно* выполнят полётное задание (анимацию). По окончанию анимации все коптеры выполнят посадку *на месте окончания своей анимации*. Кнопка деактивирована по умолчанию и если среди выбранных коптеров есть *не готовые коптеры* (все выбранные коптеры должны быть готовыми). *При нажатии запрашивается дополнительное предупреждение!*
* Кнопка `Pause` - Ставит на 'паузу' все выбранные коптеры (их очередь заданий): приостанавливается выполнение любого полётного задания. *Используйте в чрезвычайных случаях.* **Внимание!** Данная команда НЕ прерывает полёт коптера в уже указанную точку (например: элементы взлёта, посадки; следование до начальной точки анимации и т.д.)
* Кнопка `Resume` - Все выбранные коптеры *синхронизированно* продолжат выполнение своих очередей заданий (например: исполнение анимации)
* Кнопка `Stop` - Прерывает выполнение полётных заданий *ВСЕХ* подключенных коптеров. Сбрасывает очередь заданий - *действие необратимо*. **Используйте в экстренных случаях как одно из средств перехвата.** **Внимание!** Данная команда НЕ прерывает полёт коптера в уже указанную точку (например: элементы взлёта, посадки; следование до начальной точки анимации и т.д.)
* Кнопка `Emergency land` - Открывает диалоговое окно дополнительного модуля быстрого выбора коптера и его последующей экстренной посадки \ дизарма. *Смотреть далее.*
### Полётные функции (команды)
В данном разделе находятся команды, позволяющие напрямую управлять коптером(ами).
* Кнопка `Test leds` - Все выбранные коптеры выполняют двухсекундную анимацию (бегущие точки) светодиодной лентой (белым цветом). Команда *безопасна* и может быть использована для проверки работы светодиодных лент \ качества и задержки подключения к серверу \ определения соответствия коптера и его `Copter ID` в таблице.
* Кнопка `Takeoff` - Все выбранные коптеры **совершают взлёт**, после чего зависают над точкой взлёта. Аналогично `Start animation`, кнопка активна, *только* если все выбранные коптеры готовы. **Внимание!** Используйте осторожно, соблюдайте технику безопасности. Не применяйте во время выполнения других полётных функций!
* Кнопка `Flip` - Все выбранные коптеры **совершают флип (flip)** - переворот на 360 градусов вокруг одной из *горизонтальных* осей. **Внимание!** Используйте осторожно, соблюдайте технику безопасности. *Для исполнения флипа коптер должен иметь минимальную высоту >2м.* Не применяйте во время выполнения других полётных функций!
* Кнопка `Land` - ВСЕ коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно *переходят в режим посадки.* **Используйте в экстренных случаях как одно из средств перехвата.**
* Кнопка `Diarm` - ВСЕ коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно *отключают моторы (disarm).* ==Это может привести к падению и повреждению коптеров== **Используйте в крайних случаях как последнее из средств перехвата.**
В этом разделе задаются параметры проверок коптера, которые регулируются на стороне сервера. Доступны следующие параметры:
## Таблица состояния коптеров (клиентов)
При первом подключении клиента к серверу в таблицу добавляется строка для отображения состояния клиента, содержащая начальные данные, переданные клиентом при подключении (`Copter ID`). Строки НЕ удаляются после зарегистрированного отключения клиента. Строки можно сортировать по возрастанию \ убыванию значений любого из столбцов (кликнув по заголовку столбца).
* `battery_percentage_min` - Минимальный заряд батарии коптера, допустимый для взлёта. Указывается *в процентах* (дробное значение от 0 до 100). Значение меньше указанного будет отмечено в столбце `battery` как неудовлетворительное.
* `start_pos_delta_max` - Максимальное расстояние от текущего положения коптера до его точки взлёта в файле анимации, допустимое для взлёта. Указывается *в метрах* (дробное значение от 0 до 'inf'). Значение больше указанного будет отмечено в столбце `start x y z` как неудовлетворительное. Допустимо использование строки 'inf' для любого допустимого расстояния.
* `time_delta_max` - Максимальная разница (абсолютное значение) между временем сервера и клиента (включая сетевую задержку), допустимая для взлёта. Указывается *в секундах* (дробное значение от 0 до 'inf'). Значение больше указанного будет отмечено в столбце `dt` как неудовлетворительное.
Ячейки таблицы подсвечиваются:
* жёлтым, если необходимое значение отсутствует
* красным, если значение (состояние) ячейки неудовлетворительно (согласно внутренним проверкам)
* зелёным, если значение (состояние) ячейки удовлетворительно (согласно внутренним проверкам)
### Столбцы таблицы
* `copter ID` - имя (идентификатор) клиента. Может быт сконфигурирован на стороне клиента. Отображается сразу при подключении клиента. Рядом с каждым ID коптера расположен чекбокс - коптеры, чей ID отмечен чекбоксом положительно (галочка), считаются *выбранными*.
* `animation ID` - внутреннее название файла анимации, подгруженного клиентом. Отображается после выполнения `selfcheck`. *Проверьте соответствие названий файлов анимаций у коптеров*
* `battery V` - абсолютное значение напряжения на аккумуляторе коптера (в Вольтах, по данным полётного контроллера). *Убедитесь, что напряжение не ниже порогового для вашего аккумулятора.* **При критически низком значении коптер считается не готовым** - блокируется возможность взлёта и старта анимации.
* `battery %` - относительное значение напряжения на аккумуляторе коптера. Значение рассчитывается по среднему напряжению (по данным полётного контроллера) на ячейку аккумулятора (банку). *Убедитесь, что уровень заряда перед вылетом не менее 30%* **При критически низком значении коптер считается не готовым** - блокируется возможность взлёта и старта анимации.
* `selfcheck` - Все дополнительные сообщения и ошибки при самодиагностике (*Смотреть далее.*). При успешном прохождении самодиагностики без ошибок выводится значение `OK`, ячейка подсвечивается зелёным цветом. **При наличии ошибок коптер считается не готовым** - блокируется возможность взлёта и старта анимации.
* `time delta` - Разница между временем на сервере и клиенте (в секундах). *При слишком больших значениях сигнализирует об отсутствии синхронизации времени между коптером и клиентом!* В это значение так же входит сетевая задержка.
#### Раздел BROADCAST
# Дополнительные операции
## Selfcheck
..
Сервер может использовать UDP broadcast, чтобы передавать клиентам актуальную информацию о конфигурации сервера. Таким образом становится возможным автоматическое подключение клиентов к серверу без необходимости дополнительной ручной конфигурации. В данном разделе задаются параметры этого механизма:
## Emergency land
Модуль экстренной посадки/дизарма, предназначенный для быстрого поиска оператором визуально неисправного коптера методом бинарного поиска
### Интерфейс
* Зелёная кнопка `1` - ...
* Красная кнопка `2` - ...
* Кнопка `Land` - все коптеры в выбранной ('зелёной') группе совершат процедуру экстренной посадки (аналогично кнопке `Land` в панели инструментов).
* Кнопка `Disarm` - все коптеры в выбранной ('зелёной') группе *немедленно отключают моторы (disarm).* (аналогично кнопке `Disarm` в панели инструментов) ==Это может привести к падению и повреждению коптеров==.
### Алгоритм использования
* ...
<!--stackedit_data:
eyJoaXN0b3J5IjpbLTE1MDIyMDAwMjgsOTI1MDAyMDkwLC0xMz
IzMTk2MzMzLC01MDIzODYyNjMsLTI5NDk3MDcyOCw5MzY1NzEz
ODgsODcyNjgwNjE4XX0=
-->
* `use_broadcast` - будут ли использованы broadcast'ы для передачи данных (при значении `False` broadcast'ы НЕ будут отправляться). Используйте `False` в случае повышенных требований безопасности, перегруженности сети или невозможности передачи по широковещательному каналу (из-за конфигурации брандмауэра или сети)
* `broadcast_port` - UDP порт, по которому будет осуществляться отправка сообщений. *Рекомендуется изменить значение по умолчанию в целях безопасности.* **Внимание!** При изменении этого параметра клиенты НЕ смогут принимать сообщения автоконфигурации до изменения (вручную) соответствующего параметра в конфигурации клиента на равное значение.
* `broadcast_delay` - периодичность (в секундах, целочисленное значение), с которой будет происходить отправка broadcast сообщений. Увеличьте задержку для уменьшения нагрузки на сеть. Уменьшите задержку для уменьшения времени отклика и подключения при первом запуске клиентов.
#### Раздел NTP
Помимо синхронизации времени (с миллисекундной точностью) с помощью пакета chrony, предоставляется альтернатива - возможность использования внешних (при наличии соединения локальной сети с интернетом) или внутрисетевых NTP-серверов. **Внимание!** Для корректной работы системы, **и сервер, и клиенты** должны использовать единый способ синхронизации времени (набор параметров в этом разделе). Данный раздел полностью унифицирован и для сервера, и для клиентов.
* `use_ntp` - определяет, будет ли использоваться синхронизация времени с помощью NTP. (при значении `False` будет использовано локальное время ОС (синхронизируется автоматически при использовании chrony). *Рекомендуется использование chrony, а не NTP*
* `host` - имя хоста или IP адрес NTP сервера (локального или удаленного)
* `port` - порт, используемый NTP сервером
## Дополнительные операции
### Visual land
Модуль визуальной экстренной посадки, предназначенный для быстрого поиска оператором визуально неисправного коптера методом бинарного поиска. Для успешного применения на всех коптерах должна быть установлена светодиодная лента.
#### Интерфейс
![LED Visual Land](../assets/server-led-emergency-land.png)
При нажатии на кнопку `Visual land` все коптеры делятся на 2 равные группы по порядку расположения в таблице. Первая половина коптеров зажигает светодиодную ленту зелёным цветом, вторая - красным. При нажатии на зелёную или красную кнопку происходит выбор группы, соответствующей цвету нажатой кнопки. Коптеры выбранного цвета снова делятся на две половины и каждая половина зажигает светодиодную ленту зелёным и красным цветом соответственно. Остальные коптеры выключают светодиодную ленту.
Нажимая на кнопки, соответствующие цвету группы, в которой находится неисправный коптер, можно определить его номер и выполнить экстренную посадку за логорифмическое количество шагов от количества коптеров, т.е. гораздо быстрее, чем перебирая коптеры по одному.
На любом шаге можно произвести посадку или выключение моторов всех коптеров, на которых включена светодиодная лента, нажав кнопку `Land` или `Disarm`.

View File

@@ -1,20 +1,25 @@
# Инструкция по настройке и запуску клиента и сервера
## Список оборудования
Данное ПО предназначено для управления несколькими квадракоптерами с компьютера-сервера. Для полноценной работы необходимо следующее оборудование:
* Один или несколько квадрокоптеров, работающих на базе ПО [Клевер](https://github.com/copterexpress/clever).
* Компьютер с операционной системой Linux.
* Wifi роутер, работающий на частоте 2.4 ГГц, либо 5.8 ГГц, если эту частоту поддерживают wifi модули коптеров и компьютера.
## Подготовка ПО
Скачайте на компьютер последний образ (clever-show_XXX.img.zip) и исходный код (Source code) из последнего [релиза](https://github.com/copterexpress/clever-show/releases/latest). Разархивируйте исходный код в удобную директорию.
## Настройка роутера
Для управления одним или несколькими коптерами требуется подключение коптеров и сервера к одной сети. Для этого требуется отдельный wifi роутер с известным SSID и паролем.
Для управления одним или несколькими коптерами требуется подключение коптеров и сервера к одной сети. Для этого требуется отдельный wifi роутер с известным SSID и паролем.
Подключите компьютер, который будет использоваться в качестве сервера, к сети роутера и узнайте его ip адрес - он понадобится для дальнейшей настройки.
## Настройка и запуск клиента
## Установка и запуск клиента
* Запишите образ на microSD карту, используя [Etcher](https://www.balena.io/etcher/).
* Вставьте флешку в Raspberry Pi, включите коптер. Дождитесь появления сети `CLEVERSHOW-XXXX`.
* Подключитесь к сети коптера, используя пароль `cleverwifi`.
@@ -31,11 +36,12 @@ cd ~/clever-show/Drone
sudo ./client_setup.sh <SSID> <password> <copter name> <server ip>
```
* Теперь при запуске серверного приложения настроенные коптеры будут отображаться в виде таблицы. Также можно подключаться к Raspberry Pi на коптере по его имени через `ssh` в указанной при настройке wifi сети, например `ssh pi@clever-1`, пароль `cleverwifi`.
* Теперь при запуске серверного приложения настроенные коптеры будут отображаться в виде таблицы. Также можно подключаться к Raspberry Pi на коптере по его имени через `ssh` в указанной при настройке wifi сети, например `ssh pi@clever-1`, пароль `raspberry`.
Документация по клиентской части находится [здесь](client.md).
## Настройка и запуск сервера
## Установка и запуск сервера
* Установите [chrony](https://chrony.tuxfamily.org/index.html), [samba](https://help.ubuntu.ru/wiki/samba) и Python 3 на ваш компьютер:
```bash

View File

@@ -1,2 +0,0 @@
#!/usr/bin/env bash
pip3 install -r requirements.txt

View File

@@ -1,2 +0,0 @@
pip3 install -r requirements.txt
pause

View File

@@ -1,44 +0,0 @@
import logging
try:
import rospy
except ImportError:
ros = False
else:
ros = True
class Logger:
def __init__(self, logger=logging.getLogger(), use_ros=False):
self.ros = True if use_ros and ros else False
self.logger = logger
def info(self, msg):
self.logger.info(msg)
if self.ros:
rospy.loginfo(msg)
def debug(self, msg):
self.logger.debug(msg)
if self.ros:
rospy.logdebug(msg)
def warning(self, msg):
self.logger.warning(msg)
if self.ros:
rospy.logwarn(msg)
def error(self, msg):
self.logger.error(msg)
if self.ros:
rospy.logerr(msg)
def critical(self, msg):
self.logger.critical(msg)
if self.ros:
rospy.logfatal(msg)

View File

@@ -204,7 +204,7 @@ class ConnectionManager(object):
messages_callbacks = {}
requests_callbacks = {}
def __init__(self, whoami = "computer"):
def __init__(self, whoami="computer"):
self.selector = None
self.socket = None
self.addr = None
@@ -224,7 +224,7 @@ class ConnectionManager(object):
self._request_lock = threading.Lock()
self._close_lock = threading.Lock()
self.BUFFER_SIZE = 1024
self.buffer_size = 1024
self.resume_queue = False
self.resend_requests = True
@@ -330,14 +330,14 @@ class ConnectionManager(object):
def _read(self):
try:
data = self.socket.recv(self.BUFFER_SIZE)
data = self.socket.recv(self.buffer_size)
except io.BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
if data:
self._recv_buffer += data
logger.debug("Received {} from {}".format(data, self.addr))
logger.debug("Received {} bytes from {}".format(len(data), self.addr))
else:
logger.warning("Connection to {} lost!".format(self.addr))
@@ -423,7 +423,7 @@ class ConnectionManager(object):
def _write(self):
try:
sent = self.socket.send(self._send_buffer)
sent = self.socket.send(self._send_buffer[:self.buffer_size])
except io.BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
@@ -433,8 +433,10 @@ class ConnectionManager(object):
raise error
else:
logger.debug("Sent {} to {}".format(self._send_buffer[:sent], self.addr))
self._send_buffer = self._send_buffer[sent:]
left = len(self._send_buffer)
logger.debug("Sent message to {}: sent {} bytes, {} bytes left.".format(self.addr, sent, left))#, self._send_buffer[:sent],))
def _send(self, data):
with self._send_lock:
@@ -512,17 +514,10 @@ class NotifierSock(Singleton):
self._server_socket.listen(1)
self._sending_sock.connect(('127.0.0.1', port))
self._receiving_sock, _ = self._server_socket.accept()
logger.info("Notify socket connected")
logger.info("Notify socket: connected")
selector.register(self._receiving_sock, selectors.EVENT_READ, data=self)
logger.info("Notify socket registered in selector")
def close(self):
if self._server_socket is not None:
self._server_socket.close()
if self._receiving_sock is not None:
self._receiving_sock.close()
logger.info("Notify socket closed")
logger.info("Notify socket: selector registered")
def get_sock(self):
return self._receiving_sock
@@ -531,13 +526,13 @@ class NotifierSock(Singleton):
with self._send_lock:
if self._receiving_sock is not None:
self._sending_sock.sendall(bytes(1))
logger.debug("Notify socket notified")
logger.debug("Notify socket: notified")
def process_events(self, mask):
if mask & selectors.EVENT_READ and self._receiving_sock is not None:
try:
self._receiving_sock.recv(1024)
logger.debug("Notify socket received")
logger.debug("Notify socket: received")
except io.BlockingIOError:
pass
except Exception as e:

View File

@@ -5,3 +5,4 @@ numpy==1.17.4
PyQt5==5.13.2
PyQt5-sip==12.7.0
selectors2==2.0.1
Quamash==0.6.1

View File

@@ -1,2 +1,2 @@
2 6 1 1
-0.6 0 1
2 6 0.8 1
-0.7 0 1