Merge branch 'master' into qt-gui-update
120
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@ Software for making the drone show controlled by Raspberry Pi and COEX [Clever](
|
||||
|
||||
[](https://travis-ci.org/CopterExpress/clever-show)
|
||||
|
||||
## Demo video
|
||||
|
||||
[](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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
@@ -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">
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
45
builder/clever-config/camera_info/calibration.yaml
Normal 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.
|
||||
41
builder/clever-config/launch/aruco.launch
Normal 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>
|
||||
73
builder/clever-config/launch/clever.launch
Normal 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>
|
||||
38
builder/clever-config/launch/main_camera.launch
Normal 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>
|
||||
100
builder/clever-config/map/animation_map.txt
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
docs/assets/server-animation.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
docs/assets/server-drone.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/assets/server-gui.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
docs/assets/server-led-emergency-land.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/assets/server-music.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
docs/assets/server-server.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/assets/server-sidemenu.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
@@ -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` - удаляет аддон (перед установкой новой версии рекомендуется удалить старую)
|
||||
|
||||
@@ -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 сервером
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,103 +1,214 @@
|
||||
# Настройка сервера
|
||||
## Файл конфигурации
|
||||
Конфигурация сервера задаётся в файле Server/server_config.ini, имеющем вид (по умолчанию):
|
||||
# Сервер
|
||||
|
||||
Приложение для создания и запуска шоу, настройки дронов, анимации и музыки.
|
||||
|
||||
* [Установка и запуск](start-tutorial.md#установка-и-запуск-сервера)
|
||||
* [Интерфейс](#интерфейс-сервера)
|
||||
* [Настройка](#настройка-сервера)
|
||||
* [Дополнительные операции](#дополнительные-операции)
|
||||
|
||||
## Интерфейс сервера
|
||||
|
||||
Сервер имеет визуальный графический интерфейс для удобства взаимодействия.
|
||||
|
||||

|
||||
|
||||
### Таблица состояния коптеров
|
||||
|
||||
При первом подключении клиента к серверу в таблицу добавляется строка для отображения состояния клиента, содержащая только имя клиента (`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
|
||||
|
||||

|
||||
|
||||
Данный раздел содержит несколько утилит по отправке различных данных на *выбранные* клиенты. **Внимание!** Не используйте данные команды во время полёта коптеров!
|
||||
|
||||
* `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
|
||||
|
||||

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

|
||||
|
||||
* `Set start X Y to current position` - устанавливает точку старта анимации у выбранных клиентов в значения текущей позиции по X Y.
|
||||
* `Reset start position` - устанавливает точку старта анимации у выбранных клиентов в значения `0.0`, `0.0`.
|
||||
|
||||
#### Раздел Music
|
||||
|
||||

|
||||
|
||||
* `Select music file` - загружает выбранный музыкальный файл для дальнейшего воспроизведения вручную или через определённое время после старта анимации. Поддерживаемые расширения: `.mp3` или `.wav`.
|
||||
* `Play music` - воспроизводит загруженную музыку.
|
||||
* `Stop music` - останавливает воспроизведение проигрываемой музыки.
|
||||
|
||||
### Боковая панель команд
|
||||
|
||||

|
||||
|
||||
#### Управление
|
||||
|
||||
Данный раздел команд предназначен для выскоуровневого управления роем дронов.
|
||||
|
||||
* Спинбокс `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
|
||||
|
||||
Модуль визуальной экстренной посадки, предназначенный для быстрого поиска оператором визуально неисправного коптера методом бинарного поиска. Для успешного применения на всех коптерах должна быть установлена светодиодная лента.
|
||||
|
||||
#### Интерфейс
|
||||
|
||||

|
||||
|
||||
При нажатии на кнопку `Visual land` все коптеры делятся на 2 равные группы по порядку расположения в таблице. Первая половина коптеров зажигает светодиодную ленту зелёным цветом, вторая - красным. При нажатии на зелёную или красную кнопку происходит выбор группы, соответствующей цвету нажатой кнопки. Коптеры выбранного цвета снова делятся на две половины и каждая половина зажигает светодиодную ленту зелёным и красным цветом соответственно. Остальные коптеры выключают светодиодную ленту.
|
||||
|
||||
Нажимая на кнопки, соответствующие цвету группы, в которой находится неисправный коптер, можно определить его номер и выполнить экстренную посадку за логорифмическое количество шагов от количества коптеров, т.е. гораздо быстрее, чем перебирая коптеры по одному.
|
||||
|
||||
На любом шаге можно произвести посадку или выключение моторов всех коптеров, на которых включена светодиодная лента, нажав кнопку `Land` или `Disarm`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
pip3 install -r requirements.txt
|
||||
@@ -1,2 +0,0 @@
|
||||
pip3 install -r requirements.txt
|
||||
pause
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -1,2 +1,2 @@
|
||||
2 6 1 1
|
||||
-0.6 0 1
|
||||
2 6 0.8 1
|
||||
-0.7 0 1
|
||||
|
||||