8
.gitignore
vendored
@@ -1,9 +1,6 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Logs
|
||||
@@ -18,14 +15,19 @@ __pycache__/
|
||||
# Development
|
||||
images/
|
||||
show-env/
|
||||
builder/clever-config
|
||||
|
||||
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
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
1 1 COM_OBL_ACT 0 6
|
||||
1 1 COM_OBL_RC_ACT 4 6
|
||||
1 1 BAT_V_CHARGED 4.050000190734863281 9
|
||||
1 1 BAT_V_EMPTY 3.500000000000000000 9
|
||||
1 1 BAT_V_EMPTY 3.400000000000000000 9
|
||||
1 1 NAV_RCL_ACT 0 6
|
||||
|
||||
@@ -6,7 +6,13 @@ import time
|
||||
import logging
|
||||
import threading
|
||||
import rospy
|
||||
from clever import srv
|
||||
|
||||
# for backward compatibility with clever
|
||||
try:
|
||||
from clever import srv
|
||||
except ImportError:
|
||||
from clover import srv
|
||||
|
||||
from mavros_msgs.srv import SetMode
|
||||
from mavros_msgs.srv import CommandBool
|
||||
from std_srvs.srv import Trigger
|
||||
@@ -337,7 +343,7 @@ def takeoff(height=TAKEOFF_HEIGHT, speed=TAKEOFF_SPEED, tolerance=TOLERANCE, fra
|
||||
return 'interrupted'
|
||||
|
||||
climb = abs(get_telemetry_locked(frame_id=frame_id).z - start.z)
|
||||
rospy.logdebug("Takeoff to {:.2f} of {:.2f} meters".format(climb, height))
|
||||
rospy.loginfo("Takeoff to {:.2f} of {:.2f} meters".format(climb, height))
|
||||
|
||||
time_passed = time.time() - time_start
|
||||
|
||||
|
||||
@@ -21,11 +21,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
interrupt_event = threading.Event()
|
||||
|
||||
config = ConfigParser.ConfigParser()
|
||||
config.read("client_config.ini")
|
||||
|
||||
default_delay = config.getfloat('ANIMATION', 'frame_delay')
|
||||
|
||||
anim_id = "Empty id"
|
||||
|
||||
# TODO refactor as class
|
||||
@@ -36,7 +31,7 @@ def get_id(filepath="animation.csv"):
|
||||
try:
|
||||
animation_file = open(filepath)
|
||||
except IOError:
|
||||
logger.error("File {} can't be opened".format(filepath))
|
||||
logger.debug("File {} can't be opened".format(filepath))
|
||||
anim_id = "No animation"
|
||||
return anim_id
|
||||
else:
|
||||
@@ -53,11 +48,11 @@ def get_id(filepath="animation.csv"):
|
||||
logger.debug("No animation id in file")
|
||||
return anim_id
|
||||
|
||||
def get_start_xy(filepath="animation.csv", x_ratio=1, y_ratio=1):
|
||||
def get_start_xy(filepath="animation.csv", x_ratio=1, y_ratio=1, z_ratio=1):
|
||||
try:
|
||||
animation_file = open(filepath)
|
||||
except IOError:
|
||||
logger.error("File {} can't be opened".format(filepath))
|
||||
logger.debug("File {} can't be opened".format(filepath))
|
||||
anim_id = "No animation"
|
||||
return float('nan'), float('nan')
|
||||
else:
|
||||
@@ -83,13 +78,13 @@ def get_start_xy(filepath="animation.csv", x_ratio=1, y_ratio=1):
|
||||
return float(x)*x_ratio, float(y)*y_ratio
|
||||
|
||||
|
||||
def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, x_ratio=1, y_ratio=1, z_ratio=1):
|
||||
def load_animation(filepath="animation.csv", default_delay = 0.1, x0=0, y0=0, z0=0, x_ratio=1, y_ratio=1, z_ratio=1):
|
||||
imported_frames = []
|
||||
global anim_id
|
||||
try:
|
||||
animation_file = open(filepath)
|
||||
except IOError:
|
||||
logging.error("File {} can't be opened".format(filepath))
|
||||
logger.debug("File {} can't be opened".format(filepath))
|
||||
anim_id = "No animation"
|
||||
else:
|
||||
with animation_file:
|
||||
|
||||
173
Drone/client.py
@@ -1,86 +1,60 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import errno
|
||||
import random
|
||||
import socket
|
||||
import struct
|
||||
import logging
|
||||
import collections
|
||||
import ConfigParser
|
||||
import selectors2 as selectors
|
||||
import threading
|
||||
|
||||
from contextlib import closing
|
||||
|
||||
import os,sys,inspect # Add parent dir to PATH to import messaging_lib
|
||||
import inspect # Add parent dir to PATH to import messaging_lib
|
||||
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
sys.path.insert(0, parent_dir)
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import messaging_lib as messaging
|
||||
from config import ConfigManager
|
||||
|
||||
ConfigOption = collections.namedtuple("ConfigOption", ["section", "option", "value"])
|
||||
active_client = None # needs to be refactored: Singleton \ factory callbacks
|
||||
|
||||
active_client = None # maybe needs to be refactored
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, config_path="client_config.ini"):
|
||||
def __init__(self, config_path="config/client.ini"):
|
||||
self.selector = selectors.DefaultSelector()
|
||||
self.client_socket = None
|
||||
|
||||
self.server_connection = messaging.ConnectionManager("pi")
|
||||
|
||||
self.server_host = None
|
||||
self.server_port = None
|
||||
self.broadcast_port = None
|
||||
|
||||
self.connected = False
|
||||
self.client_id = None
|
||||
|
||||
# Init configs
|
||||
self.config = ConfigManager()
|
||||
self.config_path = config_path
|
||||
self.config = ConfigParser.ConfigParser()
|
||||
self.load_config()
|
||||
|
||||
global active_client
|
||||
active_client = self
|
||||
|
||||
# self._last_ping_time = 0
|
||||
|
||||
def load_config(self):
|
||||
self.config.read(self.config_path)
|
||||
self.config.load_config_and_spec(self.config_path)
|
||||
|
||||
self.broadcast_port = self.config.getint('SERVER', 'broadcast_port')
|
||||
self.server_port = self.config.getint('SERVER', 'port')
|
||||
self.server_host = self.config.get('SERVER', 'host')
|
||||
self.BUFFER_SIZE = self.config.getint('SERVER', 'buffer_size')
|
||||
self.USE_NTP = self.config.getboolean('NTP', 'use_ntp')
|
||||
self.NTP_HOST = self.config.get('NTP', 'host')
|
||||
self.NTP_PORT = self.config.getint('NTP', 'port')
|
||||
|
||||
self.client_id = self.config.get('PRIVATE', 'id')
|
||||
if self.client_id == '/default':
|
||||
config_id = self.config.private_id.lower()
|
||||
if config_id == '/default':
|
||||
self.client_id = 'copter' + str(random.randrange(9999)).zfill(4)
|
||||
self.write_config(False, ConfigOption('PRIVATE', 'id', self.client_id))
|
||||
elif self.client_id == '/hostname':
|
||||
self.config.set('PRIVATE', 'id', self.client_id, write=True) # set and write
|
||||
elif config_id == '/hostname':
|
||||
self.client_id = socket.gethostname()
|
||||
elif self.client_id == '/ip':
|
||||
elif config_id == '/ip':
|
||||
self.client_id = messaging.get_ip_address()
|
||||
else:
|
||||
self.client_id = config_id
|
||||
|
||||
def rewrite_config(self):
|
||||
with open(self.config_path, 'w') as file:
|
||||
self.config.write(file)
|
||||
os.system("chown -R pi:pi /home/pi/clever-show")
|
||||
|
||||
def write_config(self, reload_config=True, *config_options):
|
||||
for config_option in config_options:
|
||||
self.config.set(config_option.section, config_option.option, config_option.value)
|
||||
self.rewrite_config()
|
||||
|
||||
if reload_config:
|
||||
self.load_config()
|
||||
logger.info("Config loaded")
|
||||
|
||||
@staticmethod
|
||||
def get_ntp_time(ntp_host, ntp_port):
|
||||
@@ -95,13 +69,15 @@ class Client(object):
|
||||
return unpacked[10] + float(unpacked[11]) / 2 ** 32 - NTP_DELTA
|
||||
|
||||
def time_now(self):
|
||||
if self.USE_NTP:
|
||||
timenow = self.get_ntp_time(self.NTP_HOST, self.NTP_PORT)
|
||||
if self.config.ntp_use:
|
||||
timenow = self.get_ntp_time(self.config.ntp_host, self.config.ntp_port)
|
||||
else:
|
||||
timenow = time.time()
|
||||
return timenow
|
||||
|
||||
def start(self):
|
||||
self.load_config()
|
||||
|
||||
logger.info("Starting client")
|
||||
messaging.NotifierSock().init(self.selector)
|
||||
|
||||
@@ -114,17 +90,17 @@ class Client(object):
|
||||
logger.critical("Caught interrupt, exiting!")
|
||||
self.selector.close()
|
||||
|
||||
def _reconnect(self, timeout=2.0, attempt_limit=3):
|
||||
logger.info("Trying to connect to {}:{} ...".format(self.server_host, self.server_port))
|
||||
def _reconnect(self, timeout=2.0, attempt_limit=3): # TODO reconnecting broadcast listener in another thread
|
||||
logger.info("Trying to connect to {}:{} ...".format(self.config.server_host, self.config.server_port))
|
||||
attempt_count = 0
|
||||
while not self.connected:
|
||||
logger.info("Waiting for connection, attempt {}".format(attempt_count))
|
||||
try:
|
||||
self.client_socket = socket.socket()
|
||||
self.client_socket.settimeout(timeout)
|
||||
self.client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
messaging.set_keepalive(self.client_socket)
|
||||
self.client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.client_socket.connect((self.server_host, self.server_port))
|
||||
self.client_socket.connect((self.config.server_host, self.config.server_port))
|
||||
except socket.error as error:
|
||||
if isinstance(error, OSError):
|
||||
if error.errno == errno.EINTR:
|
||||
@@ -148,21 +124,25 @@ class Client(object):
|
||||
def _connect(self):
|
||||
self.connected = True
|
||||
self.client_socket.setblocking(False)
|
||||
events = selectors.EVENT_READ # | selectors.EVENT_WRITE
|
||||
self.selector.register(self.client_socket, events, data=self.server_connection)
|
||||
self.server_connection.connect(self.selector, self.client_socket, (self.server_host, self.server_port))
|
||||
self.selector.register(self.client_socket, selectors.EVENT_READ, data=self.server_connection)
|
||||
self.server_connection.connect(self.selector, self.client_socket,
|
||||
(self.config.server_host, self.config.server_port))
|
||||
|
||||
def broadcast_bind(self, timeout=2.0, attempt_limit=3):
|
||||
broadcast_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
broadcast_client.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
broadcast_client.bind(("", self.broadcast_port))
|
||||
broadcast_client.settimeout(timeout)
|
||||
try:
|
||||
broadcast_client.bind(("", self.config.broadcast_port))
|
||||
except socket.error as error:
|
||||
logger.error("Error during broadcast listening binding: {}".format(error))
|
||||
return
|
||||
|
||||
attempt_count = 0
|
||||
try:
|
||||
while attempt_count <= attempt_limit:
|
||||
try:
|
||||
data, addr = broadcast_client.recvfrom(self.BUFFER_SIZE)
|
||||
data, addr = broadcast_client.recvfrom(self.config.server_buffer_size)
|
||||
except socket.error as error:
|
||||
logger.warning("Could not receive broadcast due error: {}".format(error))
|
||||
attempt_count += 1
|
||||
@@ -170,37 +150,30 @@ class Client(object):
|
||||
message = messaging.MessageManager()
|
||||
message.income_raw = data
|
||||
message.process_message()
|
||||
if message.content:
|
||||
if message.content and message.jsonheader["action"] == "server_ip":
|
||||
logger.info("Received broadcast message {} from {}".format(message.content, addr))
|
||||
if message.content["command"] == "server_ip":
|
||||
args = message.content["args"]
|
||||
self.server_port = int(args["port"])
|
||||
self.server_host = args["host"]
|
||||
self.write_config(False,
|
||||
ConfigOption("SERVER", "port", self.server_port),
|
||||
ConfigOption("SERVER", "host", self.server_host))
|
||||
logger.info("Binding to new IP: {}:{}".format(self.server_host, self.server_port))
|
||||
self.on_broadcast_bind()
|
||||
break
|
||||
|
||||
kwargs = message.content["kwargs"]
|
||||
self.config.set("SERVER", "port", int(kwargs["port"]))
|
||||
self.config.set("SERVER", "host", kwargs["host"])
|
||||
self.config.write()
|
||||
|
||||
logger.info("Binding to new IP: {}:{}".format(
|
||||
self.config.server_host, self.config.server_port))
|
||||
self.on_broadcast_bind()
|
||||
break
|
||||
finally:
|
||||
broadcast_client.close()
|
||||
|
||||
def on_broadcast_bind(self):
|
||||
def on_broadcast_bind(self): # TODO move ALL binding code here
|
||||
pass
|
||||
|
||||
def _process_connections(self):
|
||||
while True:
|
||||
events = self.selector.select(timeout=1)
|
||||
# if time.time() - self._last_ping_time > 5:
|
||||
# self.server_connection.send_message("ping")
|
||||
# self._last_ping_time = time.time()
|
||||
# logging.debug("tick")
|
||||
|
||||
for key, mask in events:
|
||||
connection = key.data
|
||||
if connection is None:
|
||||
pass
|
||||
else:
|
||||
if connection is not None:
|
||||
try:
|
||||
connection.process_events(mask)
|
||||
|
||||
@@ -227,19 +200,36 @@ class Client(object):
|
||||
return
|
||||
|
||||
|
||||
@messaging.message_callback("config_write")
|
||||
@messaging.message_callback("config")
|
||||
def _command_config_write(*args, **kwargs):
|
||||
options = [ConfigOption(**raw_option) for raw_option in kwargs["options"]]
|
||||
logger.info("Writing config options: {}".format(options))
|
||||
active_client.write_config(kwargs["reload"], *options)
|
||||
mode = kwargs.get("mode", "modify")
|
||||
# exceptions would be risen in case of incorrect config
|
||||
if mode == "rewrite":
|
||||
active_client.config.load_from_dict(kwargs["config"], configspec=active_client.config_path) # with validation
|
||||
elif mode == "modify":
|
||||
new_config = ConfigManager()
|
||||
new_config.load_from_dict(kwargs["config"])
|
||||
active_client.config.merge(new_config, validate=True)
|
||||
|
||||
active_client.config.write()
|
||||
logger.info("Config successfully updated from command")
|
||||
active_client.load_config()
|
||||
|
||||
@messaging.request_callback("config")
|
||||
def _response_config(*args, **kwargs):
|
||||
send_configspec = kwargs.get("send_configspec", False)
|
||||
response = {"config": active_client.config.full_dict()}
|
||||
if send_configspec:
|
||||
response.update({"configspec": dict(active_client.config.config.configspec)})
|
||||
return response
|
||||
|
||||
@messaging.request_callback("id")
|
||||
def _response_id(*args, **kwargs):
|
||||
new_id = kwargs.get("new_id", None)
|
||||
if new_id is not None:
|
||||
cfg = ConfigOption("PRIVATE", "id", new_id)
|
||||
active_client.write_config(True, cfg)
|
||||
active_client.config.set("PRIVATE", "id", new_id, True)
|
||||
active_client.load_config()
|
||||
# TODO renaming here
|
||||
|
||||
return active_client.client_id
|
||||
|
||||
@@ -250,6 +240,31 @@ def _response_time(*args, **kwargs):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
startup_cwd = os.getcwd()
|
||||
|
||||
import threading
|
||||
|
||||
|
||||
def restart(): # move to core
|
||||
args = sys.argv[:]
|
||||
logging.info('Restarting {}'.format(args))
|
||||
args.insert(0, sys.executable)
|
||||
if sys.platform == 'win32':
|
||||
args = ['"%s"' % arg for arg in args]
|
||||
os.chdir(startup_cwd)
|
||||
os.execv(sys.executable, args)
|
||||
|
||||
def mock_telem():
|
||||
while True:
|
||||
time.sleep(5)
|
||||
#t = dict([('fcu_status', None), ('current_position', [-2.89, 2.12, 3.64, 15.22, 'aruco_map']), ('animation_id', 'two_drones_test'), ('selfcheck', 'OK'), ('battery', None), ('git_version', '01bf95e'), ('calibration_status', None), ('start_position', [0.2, 0.2, 0.0]), ('mode', 'MANUAL'), ('time_delta', 1581338473.438682), ('armed', False), ('config_version', None), ('last_task', 'No task')])
|
||||
t = dict([('fcu_status', 'STANDBY'), ('current_position', [-1.17, 2.04, 3.45, 0, "11"]), ('animation_id', 'two_drones_test'), ('selfcheck', 'OK'), ('battery', [12.2, 1.0]), ('git_version', '42aee96'), ('calibration_status', None), ('start_position', [0.2, 0.2, 0.0]), ('mode', 'MANUAL'), ('time_delta', 1581342970.889573), ('armed', False), ('config_version', 'Copter config V0.0'), ('last_task', 'No task')])
|
||||
if active_client.connected:
|
||||
active_client.server_connection.send_message("telemetry", kwargs={"value": t})
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
client = Client()
|
||||
tr = threading.Thread(target=mock_telem)
|
||||
tr.start()
|
||||
client.start()
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
[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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# $1 - ssid, $2 - password of wifi router
|
||||
# $3 - hostname of rpi
|
||||
# $4 - server ip
|
||||
# $4 - server ip
|
||||
|
||||
if [ $(whoami) != "root" ]; then
|
||||
echo -e "\nThis should be run as root!\n"
|
||||
@@ -38,7 +38,7 @@ country=GB
|
||||
network={
|
||||
ssid="$1"
|
||||
psk="$2"
|
||||
scan_ssid=1
|
||||
scan_ssid=1
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
90
Drone/config/spec/configspec_client.ini
Normal file
@@ -0,0 +1,90 @@
|
||||
config_name = string(default='client')
|
||||
config_version = float(default=1.0)
|
||||
|
||||
[SERVER]
|
||||
port = integer(default=25000, min=1)
|
||||
host = ip_addr(default=192.168.1.101) # string?
|
||||
buffer_size = integer(default=1024)
|
||||
|
||||
[BROADCAST]
|
||||
use = boolean(default=True)
|
||||
port = integer(default=8181, min=1)
|
||||
|
||||
[TELEMETRY]
|
||||
transmit = boolean(default=True)
|
||||
frequency = float(default=1.0, min=0)
|
||||
log_resources = boolean(default=False)
|
||||
|
||||
[POSITION WATCHDOG]
|
||||
enabled = boolean(default=True)
|
||||
log_state = boolean(default=True)
|
||||
# Available options: emergency_land, land, disarm
|
||||
action = string(default=emergency_land)
|
||||
# Time to get vision position after arm
|
||||
# No visual position will be checked
|
||||
# during this time after arming
|
||||
vision_pose_delay_after_arm = float(default=3.0, min=0)
|
||||
# Timeout for the last vision pose in /mavros/vision_pose/pose
|
||||
# Set 0 to disable vision pose check
|
||||
vision_pose_timeout = float(default=0.0, min=0)
|
||||
# Max delta between current position and setpoint
|
||||
# Set 0 to disable position delta check
|
||||
position_delta_max = float(default=3.0, min=0)
|
||||
# Time to disarm after action is triggered
|
||||
disarm_timeout = float(default=10.0, min=0)
|
||||
|
||||
[EMERGENCY LAND]
|
||||
thrust = float(default=0.45, min=0, max=1)
|
||||
decrease_thrust_after = float(default=5.0, min=0)
|
||||
|
||||
[COPTER]
|
||||
frame_id = string(default=map)
|
||||
takeoff_height = float(default=1.0)
|
||||
takeoff_time = float(default=5.0, min=0)
|
||||
safe_takeoff = boolean(default=False)
|
||||
reach_first_point_time = float(default=5.0, min=0)
|
||||
land_time = float(default=1.0, min=0)
|
||||
land_timeout = float(default=10.0, min=0)
|
||||
# __list__ x y z
|
||||
common_offset = float_list(default=list(0, 0, 0), min=3, max=3)
|
||||
|
||||
[FLOOR FRAME]
|
||||
enabled = boolean(default=False)
|
||||
parent = string(default=map)
|
||||
# Frame translation (x, y, z)
|
||||
# __list__ x y z
|
||||
translation = float_list(default=list(0.0, 0.0, 0.0), min=3, max=3)
|
||||
# Frame rotation (roll, pitch, yaw) in degrees
|
||||
# __list__ roll pitch yaw
|
||||
rotation = float_list(default=list(0.0, 0.0, 0.0), min=3, max=3)
|
||||
|
||||
[ANIMATION]
|
||||
takeoff_detection = boolean(default=True)
|
||||
land_detection = boolean(default=True)
|
||||
frame_delay = float(default=0.1, min=0.01)
|
||||
# Animation ratio (x, y, z)
|
||||
# __list__ x y z
|
||||
ratio = float_list(default=list(1.0, 1.0, 1.0), min=3, max=3)
|
||||
# Available options: 'animation', 'nan' or a number in degrees
|
||||
yaw = string(default=180.0)
|
||||
|
||||
[LED]
|
||||
use = boolean(default=False)
|
||||
pin = integer(default=21, min=0, max=100)
|
||||
count = integer(default=60, min=1)
|
||||
|
||||
[PRIVATE]
|
||||
# Available options: /hostname ; /default ; /ip ; any string 63 characters length
|
||||
id = string(default=/hostname, max=63) #TODO our re check
|
||||
# Drone's individual offset (x, y, z)
|
||||
# __list__ x y z
|
||||
offset = float_list(default=list(0, 0, 0), min=3, max=3)
|
||||
|
||||
[SYSTEM]
|
||||
change_hostname = boolean(default=True)
|
||||
restart_after_rename = boolean(default=True)
|
||||
|
||||
[NTP]
|
||||
use = boolean(default=False)
|
||||
host = string(default=ntp1.stratum2.ru)
|
||||
port = integer(default=123, min=1)
|
||||
@@ -3,17 +3,22 @@ import sys
|
||||
import time
|
||||
import math
|
||||
import rospy
|
||||
from clever import srv
|
||||
import numpy
|
||||
|
||||
# for backward compatibility with clever
|
||||
try:
|
||||
from clever import srv
|
||||
except ImportError:
|
||||
from clover import srv
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
import psutil
|
||||
import subprocess
|
||||
import ConfigParser
|
||||
from collections import namedtuple
|
||||
|
||||
from FlightLib import FlightLib
|
||||
from FlightLib import LedLib
|
||||
|
||||
import client
|
||||
|
||||
@@ -33,12 +38,12 @@ static_bloadcaster = tf2_ros.StaticTransformBroadcaster()
|
||||
emergency = False
|
||||
|
||||
logging.basicConfig( # TODO all prints as logs
|
||||
level=logging.DEBUG, # INFO
|
||||
stream=sys.stdout,
|
||||
format="%(asctime)s [%(name)-7.7s] [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
])
|
||||
level=logging.DEBUG, # INFO
|
||||
stream=sys.stdout,
|
||||
format="%(asctime)s [%(name)-7.7s] [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
])
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(logging.DEBUG)
|
||||
@@ -69,65 +74,35 @@ flightlib_logger = logging.getLogger('FlightLib')
|
||||
flightlib_logger.setLevel(logging.INFO)
|
||||
flightlib_logger.addHandler(handler)
|
||||
|
||||
mavros_mavlink_logger = logging.getLogger('mavros_mavlink')
|
||||
mavros_mavlink_logger.setLevel(logging.INFO)
|
||||
mavros_mavlink_logger.addHandler(handler)
|
||||
|
||||
|
||||
class CopterClient(client.Client):
|
||||
def __init__(self, config_path="config/client.ini"):
|
||||
super(CopterClient, self).__init__(config_path)
|
||||
self.load_config()
|
||||
self.frames = {}
|
||||
|
||||
def load_config(self):
|
||||
self.FLOOR_FRAME_EXISTS = False
|
||||
super(CopterClient, self).load_config()
|
||||
self.TELEM_FREQ = self.config.getfloat('TELEMETRY', 'frequency')
|
||||
self.TELEM_TRANSMIT = self.config.getboolean('TELEMETRY', 'transmit')
|
||||
self.LOG_CPU_AND_MEMORY = self.config.getboolean('TELEMETRY', 'log_cpu_and_memory')
|
||||
self.FRAME_ID = self.config.get('COPTERS', 'frame_id')
|
||||
self.FRAME_FLIPPED_HEIGHT = 0.
|
||||
self.TAKEOFF_HEIGHT = self.config.getfloat('COPTERS', 'takeoff_height')
|
||||
self.TAKEOFF_TIME = self.config.getfloat('COPTERS', 'takeoff_time')
|
||||
self.SAFE_TAKEOFF = self.config.getboolean('COPTERS', 'safe_takeoff')
|
||||
self.RFP_TIME = self.config.getfloat('COPTERS', 'reach_first_point_time')
|
||||
self.LAND_TIME = self.config.getfloat('COPTERS', 'land_time')
|
||||
self.LAND_TIMEOUT = self.config.getfloat('COPTERS', 'land_timeout')
|
||||
self.X0_COMMON = self.config.getfloat('COPTERS', 'x0_common')
|
||||
self.Y0_COMMON = self.config.getfloat('COPTERS', 'y0_common')
|
||||
self.Z0_COMMON = self.config.getfloat('COPTERS', 'z0_common')
|
||||
self.YAW = self.config.get('COPTERS', 'yaw')
|
||||
self.TAKEOFF_CHECK = self.config.getboolean('ANIMATION', 'takeoff_animation_check')
|
||||
self.LAND_CHECK = self.config.getboolean('ANIMATION', 'land_animation_check')
|
||||
self.FRAME_DELAY = self.config.getfloat('ANIMATION', 'frame_delay')
|
||||
self.X_RATIO = self.config.getfloat('ANIMATION', 'x_ratio')
|
||||
self.Y_RATIO = self.config.getfloat('ANIMATION', 'y_ratio')
|
||||
self.Z_RATIO = self.config.getfloat('ANIMATION', 'z_ratio')
|
||||
self.X0 = self.config.getfloat('PRIVATE', 'x0')
|
||||
self.Y0 = self.config.getfloat('PRIVATE', 'y0')
|
||||
self.Z0 = self.config.getfloat('PRIVATE', 'z0')
|
||||
self.USE_LEDS = self.config.getboolean('PRIVATE', 'use_leds')
|
||||
self.LED_PIN = self.config.getint('PRIVATE', 'led_pin')
|
||||
try:
|
||||
self.FLOOR_DX = self.config.getfloat('FLOOR FRAME', 'x')
|
||||
self.FLOOR_DY = self.config.getfloat('FLOOR FRAME', 'y')
|
||||
self.FLOOR_DZ = self.config.getfloat('FLOOR FRAME', 'z')
|
||||
self.FLOOR_ROLL = self.config.getfloat('FLOOR FRAME', 'roll')
|
||||
self.FLOOR_PITCH = self.config.getfloat('FLOOR FRAME', 'pitch')
|
||||
self.FLOOR_YAW = self.config.getfloat('FLOOR FRAME', 'yaw')
|
||||
self.FLOOR_PARENT = self.config.get('FLOOR FRAME', 'parent')
|
||||
self.FLOOR_FRAME_EXISTS = True
|
||||
except ConfigParser.Error:
|
||||
rospy.logerror("No floor frame!")
|
||||
self.FLOOR_FRAME_EXISTS = False
|
||||
self.RESTART_AFTER_RENAME = self.config.getboolean('PRIVATE', 'restart_after_rename')
|
||||
|
||||
def on_broadcast_bind(self):
|
||||
configure_chrony_ip(self.server_host)
|
||||
restart_service("chrony")
|
||||
repair_chrony(self.config.server_host)
|
||||
|
||||
def start(self, task_manager_instance):
|
||||
rospy.loginfo("Init ROS node")
|
||||
rospy.init_node('clever_show_client')
|
||||
if self.USE_LEDS:
|
||||
LedLib.init_led(self.LED_PIN)
|
||||
task_manager_instance.start()
|
||||
if self.FRAME_ID == "floor":
|
||||
if self.FLOOR_FRAME_EXISTS:
|
||||
rospy.init_node('clever_show_client', anonymous=True)
|
||||
if self.config.led_use:
|
||||
from FlightLib import LedLib
|
||||
LedLib.init_led(self.config.led_pin)
|
||||
task_manager_instance.start() # TODO move to self
|
||||
if self.config.copter_frame_id == "floor":
|
||||
if self.config.floor_frame_enabled:
|
||||
self.start_floor_frame_broadcast()
|
||||
else:
|
||||
rospy.logerror("Can't make floor frame!")
|
||||
rospy.logerr("Can't make floor frame!")
|
||||
start_subscriber()
|
||||
|
||||
telemetry.start_loop()
|
||||
@@ -135,23 +110,30 @@ class CopterClient(client.Client):
|
||||
|
||||
def start_floor_frame_broadcast(self):
|
||||
trans = TransformStamped()
|
||||
trans.transform.translation.x = self.FLOOR_DX
|
||||
trans.transform.translation.y = self.FLOOR_DY
|
||||
trans.transform.translation.z = self.FLOOR_DZ
|
||||
trans.transform.rotation = Quaternion(*quaternion_from_euler(math.radians(self.FLOOR_ROLL),
|
||||
math.radians(self.FLOOR_PITCH),
|
||||
math.radians(self.FLOOR_YAW)))
|
||||
trans.header.frame_id = self.FLOOR_PARENT
|
||||
trans.child_frame_id = self.FRAME_ID
|
||||
trans.transform.translation.x = self.config.floor_frame_translation[0]
|
||||
trans.transform.translation.y = self.config.floor_frame_translation[1]
|
||||
trans.transform.translation.z = self.config.floor_frame_translation[2]
|
||||
trans.transform.rotation = Quaternion(*quaternion_from_euler(math.radians(self.config.floor_frame_rotation[0]),
|
||||
math.radians(self.config.floor_frame_rotation[1]),
|
||||
math.radians(self.config.floor_frame_rotation[2])))
|
||||
trans.header.frame_id = self.config.floor_frame_parent
|
||||
trans.child_frame_id = self.config.copter_frame_id
|
||||
static_bloadcaster.sendTransform(trans)
|
||||
|
||||
|
||||
def restart_service(name):
|
||||
os.system("systemctl restart {}".format(name))
|
||||
|
||||
def repair_chrony(ip):
|
||||
logger.info("Configure chrony ip to {}".format(ip))
|
||||
configure_chrony_ip(ip)
|
||||
restart_service("chrony")
|
||||
|
||||
def execute_command(command):
|
||||
os.system(command)
|
||||
|
||||
def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1):
|
||||
|
||||
def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1): # TODO simplify
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
raw_content = f.read()
|
||||
@@ -169,7 +151,6 @@ def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1):
|
||||
|
||||
if "." not in current_ip:
|
||||
logger.debug("That's not ip!")
|
||||
return False
|
||||
|
||||
if current_ip != ip:
|
||||
content[ip_index] = ip
|
||||
@@ -223,7 +204,8 @@ def configure_hosts(hostname):
|
||||
_ip = hosts_array[0]
|
||||
current_hostname = hosts_array[1]
|
||||
if current_hostname != hostname:
|
||||
content = raw_content[:index_start] + "{} {} {}.local".format(_ip, hostname, hostname) + raw_content[index_stop:]
|
||||
content = raw_content[:index_start] + "{} {} {}.local".format(_ip, hostname, hostname) + raw_content[
|
||||
index_stop:]
|
||||
try:
|
||||
with open(path, 'w') as f:
|
||||
f.write(content)
|
||||
@@ -233,10 +215,12 @@ def configure_hosts(hostname):
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def configure_motd(hostname):
|
||||
with open("/etc/motd", "w") as f:
|
||||
f.write("\r\n{}\r\n\r\n".format(hostname))
|
||||
|
||||
|
||||
def configure_bashrc(hostname):
|
||||
path = "/home/pi/.bashrc"
|
||||
try:
|
||||
@@ -261,6 +245,7 @@ def configure_bashrc(hostname):
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@messaging.message_callback("execute")
|
||||
def _execute(*args, **kwargs):
|
||||
command = kwargs.get("command", None)
|
||||
@@ -269,28 +254,29 @@ def _execute(*args, **kwargs):
|
||||
execute_command(command)
|
||||
logger.info("Executing done")
|
||||
|
||||
@messaging.message_callback("id")
|
||||
|
||||
@messaging.message_callback("id") # TODO redo
|
||||
def _response_id(*args, **kwargs):
|
||||
new_id = kwargs.get("new_id", None)
|
||||
if new_id is not None:
|
||||
old_id = client.active_client.client_id
|
||||
if new_id != old_id:
|
||||
cfg = client.ConfigOption("PRIVATE", "id", new_id)
|
||||
client.active_client.write_config(True, cfg)
|
||||
client.active_client.config.set('PRIVATE', 'id', new_id, write=True)
|
||||
client.active_client.client_id = new_id
|
||||
if new_id != '/hostname':
|
||||
if client.active_client.RESTART_AFTER_RENAME:
|
||||
if client.active_client.config.system_restart_after_rename:
|
||||
hostname = client.active_client.client_id
|
||||
configure_hostname(hostname)
|
||||
configure_hosts(hostname)
|
||||
configure_bashrc(hostname)
|
||||
configure_motd(hostname)
|
||||
execute_command("reboot")
|
||||
#execute_command("hostname {}".format(hostname))
|
||||
#restart_service("dhcpcd")
|
||||
#restart_service("avahi-daemon")
|
||||
#restart_service("smbd")
|
||||
#restart_service("roscore")
|
||||
#restart_service("clever")
|
||||
execute_command("systemctl stop clever-show & reboot")
|
||||
# execute_command("hostname {}".format(hostname))
|
||||
# restart_service("dhcpcd")
|
||||
# restart_service("avahi-daemon")
|
||||
# restart_service("smbd")
|
||||
# restart_service("roscore")
|
||||
# restart_service("clever")
|
||||
restart_service("clever-show")
|
||||
|
||||
|
||||
@@ -315,25 +301,21 @@ def _response_animation_id(*args, **kwargs):
|
||||
# Load animation
|
||||
result = animation.get_id()
|
||||
if result != 'No animation':
|
||||
logger.debug ("Saving corrected animation")
|
||||
frames = animation.load_animation(os.path.abspath("animation.csv"),
|
||||
x0=client.active_client.X0 + client.active_client.X0_COMMON,
|
||||
y0=client.active_client.Y0 + client.active_client.Y0_COMMON,
|
||||
z0=client.active_client.Z0 + client.active_client.Z0_COMMON,
|
||||
x_ratio=client.active_client.X_RATIO,
|
||||
y_ratio=client.active_client.Y_RATIO,
|
||||
z_ratio=client.active_client.Z_RATIO,
|
||||
)
|
||||
logger.debug("Saving corrected animation")
|
||||
offset = numpy.array(client.active_client.config.private_offset) + numpy.array(client.active_client.config.copter_common_offset)
|
||||
frames = animation.load_animation(os.path.abspath("animation.csv"), client.active_client.config.animation_frame_delay,
|
||||
offset[0], offset[1], offset[2], *client.active_client.config.animation_ratio)
|
||||
# Correct start and land frames in animation
|
||||
corrected_frames, start_action, start_delay = animation.correct_animation(frames,
|
||||
check_takeoff=client.active_client.TAKEOFF_CHECK,
|
||||
check_land=client.active_client.LAND_CHECK,
|
||||
)
|
||||
check_takeoff=client.active_client.config.animation_takeoff_detection,
|
||||
check_land=client.active_client.config.animation_land_detection,
|
||||
)
|
||||
logger.debug("Start action: {}".format(start_action))
|
||||
# Save corrected animation
|
||||
animation.save_corrected_animation(corrected_frames)
|
||||
return result
|
||||
|
||||
|
||||
@messaging.request_callback("batt_voltage")
|
||||
def _response_batt(*args, **kwargs):
|
||||
if check_state_topic(wait_new_status=True):
|
||||
@@ -351,10 +333,12 @@ def _response_cell(*args, **kwargs):
|
||||
stop_subscriber()
|
||||
return float('nan')
|
||||
|
||||
|
||||
@messaging.request_callback("sys_status")
|
||||
def _response_sys_status(*args, **kwargs):
|
||||
return get_sys_status()
|
||||
|
||||
|
||||
@messaging.request_callback("cal_status")
|
||||
def _response_cal_status(*args, **kwargs):
|
||||
if check_state_topic(wait_new_status=True):
|
||||
@@ -363,77 +347,84 @@ def _response_cal_status(*args, **kwargs):
|
||||
stop_subscriber()
|
||||
return "NOT_CONNECTED_TO_FCU"
|
||||
|
||||
|
||||
@messaging.request_callback("position")
|
||||
def _response_position(*args, **kwargs):
|
||||
telem = FlightLib.get_telemetry_locked(client.active_client.FRAME_ID)
|
||||
telem = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id)
|
||||
return "{:.2f} {:.2f} {:.2f} {:.1f} {}".format(
|
||||
telem.x, telem.y, telem.z, math.degrees(telem.yaw), client.active_client.FRAME_ID)
|
||||
telem.x, telem.y, telem.z, math.degrees(telem.yaw), client.active_client.config.copter_frame_id)
|
||||
|
||||
|
||||
@messaging.request_callback("calibrate_gyro")
|
||||
def _calibrate_gyro(*args, **kwargs):
|
||||
calibrate('gyro')
|
||||
return get_calibration_status()
|
||||
|
||||
|
||||
@messaging.request_callback("calibrate_level")
|
||||
def _calibrate_level(*args, **kwargs):
|
||||
calibrate('level')
|
||||
return get_calibration_status()
|
||||
|
||||
|
||||
@messaging.request_callback("load_params")
|
||||
def _load_params(*args, **kwargs):
|
||||
result = load_param_file('temp.params')
|
||||
logger.info("Load parameters to FCU success: {}".format(result))
|
||||
return result
|
||||
|
||||
|
||||
@messaging.message_callback("test")
|
||||
def _command_test(*args, **kwargs):
|
||||
logger.info("logging info test")
|
||||
rospy.logdebug("ros logdebug test")
|
||||
print("stdout test")
|
||||
|
||||
|
||||
@messaging.message_callback("move_start")
|
||||
def _command_move_start_to_current_position(*args, **kwargs):
|
||||
x_start, y_start = animation.get_start_xy(os.path.abspath("animation.csv"),
|
||||
x_ratio=client.active_client.X_RATIO,
|
||||
y_ratio=client.active_client.Y_RATIO,
|
||||
)
|
||||
*client.active_client.config.animation_ratio)
|
||||
logger.debug("x_start = {}, y_start = {}".format(x_start, y_start))
|
||||
if not math.isnan(x_start):
|
||||
telem = FlightLib.get_telemetry_locked(client.active_client.FRAME_ID)
|
||||
telem = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id)
|
||||
logger.debug("x_telem = {}, y_telem = {}".format(telem.x, telem.y))
|
||||
if not math.isnan(telem.x):
|
||||
client.active_client.config.set('PRIVATE', 'x0', telem.x - x_start)
|
||||
client.active_client.config.set('PRIVATE', 'y0', telem.y - y_start)
|
||||
client.active_client.rewrite_config()
|
||||
client.active_client.load_config()
|
||||
logger.info ("Set start delta: {:.2f} {:.2f}".format(client.active_client.X0, client.active_client.Y0))
|
||||
client.active_client.config.set('PRIVATE', 'offset',
|
||||
[telem.x - x_start, telem.y - y_start, client.active_client.config.private_offset[2]],
|
||||
write=True)
|
||||
logger.info("Set start delta: {:.2f} {:.2f}".format(client.active_client.config.private_offset[0],
|
||||
client.active_client.config.private_offset[1]))
|
||||
else:
|
||||
logger.debug ("Wrong telemetry")
|
||||
logger.debug("Wrong telemetry")
|
||||
else:
|
||||
logger.debug("Wrong animation file")
|
||||
|
||||
|
||||
@messaging.message_callback("reset_start")
|
||||
def _command_reset_start(*args, **kwargs):
|
||||
client.active_client.config.set('PRIVATE', 'x0', 0)
|
||||
client.active_client.config.set('PRIVATE', 'y0', 0)
|
||||
client.active_client.rewrite_config()
|
||||
client.active_client.load_config()
|
||||
logger.info ("Reset start to {:.2f} {:.2f}".format(client.active_client.X0, client.active_client.Y0))
|
||||
client.active_client.config.set('PRIVATE', 'offset',
|
||||
[0, 0, client.active_client.config.private_offset[2]],
|
||||
write=True)
|
||||
logger.info("Reset start to {:.2f} {:.2f}".format(client.active_client.config.private_offset[0],
|
||||
client.active_client.config.private_offset[1]))
|
||||
|
||||
|
||||
@messaging.message_callback("set_z_to_ground")
|
||||
def _command_set_z(*args, **kwargs):
|
||||
telem = FlightLib.get_telemetry_locked(client.active_client.FRAME_ID)
|
||||
client.active_client.config.set('PRIVATE', 'z0', telem.z)
|
||||
client.active_client.rewrite_config()
|
||||
client.active_client.load_config()
|
||||
logger.info ("Set z offset to {:.2f}".format(client.active_client.Z0))
|
||||
telem = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id)
|
||||
client.active_client.config.set('PRIVATE', 'offset',
|
||||
[client.active_client.config.private_offset[0], client.active_client.config.private_offset[1], telem.z],
|
||||
write=True)
|
||||
logger.info("Set z offset to {:.2f}".format(client.active_client.config.private_offset[2]))
|
||||
|
||||
|
||||
@messaging.message_callback("reset_z_offset")
|
||||
def _command_reset_z(*args, **kwargs):
|
||||
client.active_client.config.set('PRIVATE', 'z0', 0)
|
||||
client.active_client.rewrite_config()
|
||||
client.active_client.load_config()
|
||||
logger.info ("Reset z offset to {:.2f}".format(client.active_client.Z0))
|
||||
client.active_client.config.set('PRIVATE', 'offset',
|
||||
[client.active_client.config.private_offset[0], client.active_client.config.private_offset[1], 0],
|
||||
write=True)
|
||||
logger.info("Reset z offset to {:.2f}".format(client.active_client.config.private_offset[2]))
|
||||
|
||||
|
||||
@messaging.message_callback("update_repo")
|
||||
@@ -446,11 +437,13 @@ def _command_update_repo(*args, **kwargs):
|
||||
os.system("mv /home/pi/clever-show/Drone/client_config_tmp.ini /home/pi/clever-show/Drone/client_config.ini")
|
||||
os.system("chown -R pi:pi /home/pi/clever-show")
|
||||
|
||||
|
||||
@messaging.message_callback("reboot_all")
|
||||
def _command_reboot_all(*args, **kwargs):
|
||||
reboot_fcu()
|
||||
execute_command("reboot")
|
||||
|
||||
|
||||
@messaging.message_callback("reboot_fcu")
|
||||
def _command_reboot(*args, **kwargs):
|
||||
reboot_fcu()
|
||||
@@ -464,8 +457,7 @@ def _command_service_restart(*args, **kwargs):
|
||||
|
||||
@messaging.message_callback("repair_chrony")
|
||||
def _command_chrony_repair(*args, **kwargs):
|
||||
configure_chrony_ip(client.active_client.server_host)
|
||||
restart_service("chrony")
|
||||
repair_chrony(client.active_client.config.server_host)
|
||||
|
||||
|
||||
@messaging.message_callback("led_test")
|
||||
@@ -486,36 +478,38 @@ def _command_led_fill(*args, **kwargs):
|
||||
|
||||
@messaging.message_callback("flip")
|
||||
def _copter_flip(*args, **kwargs):
|
||||
FlightLib.flip(frame_id=client.active_client.FRAME_ID)
|
||||
FlightLib.flip(frame_id=client.active_client.config.copter_frame_id)
|
||||
|
||||
|
||||
@messaging.message_callback("takeoff")
|
||||
def _command_takeoff(*args, **kwargs):
|
||||
logger.info("Takeoff at {}".format(datetime.datetime.now()))
|
||||
task_manager.add_task(0, 0, animation.takeoff,
|
||||
task_kwargs={
|
||||
"z": client.active_client.TAKEOFF_HEIGHT,
|
||||
"timeout": client.active_client.TAKEOFF_TIME,
|
||||
"safe_takeoff": client.active_client.SAFE_TAKEOFF,
|
||||
"use_leds": client.active_client.USE_LEDS,
|
||||
"z": client.active_client.config.copter_takeoff_height,
|
||||
"timeout": client.active_client.config.copter_takeoff_time,
|
||||
"safe_takeoff": client.active_client.config.copter_safe_takeoff,
|
||||
"use_leds": client.active_client.config.led_use,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@messaging.message_callback("takeoff_z")
|
||||
def _command_takeoff_z(*args, **kwargs):
|
||||
z_str = kwargs.get("z", None)
|
||||
if z_str is not None:
|
||||
telem = FlightLib.get_telemetry_locked(client.active_client.FRAME_ID)
|
||||
telem = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id)
|
||||
logger.info("Takeoff to z = {} at {}".format(z_str, datetime.datetime.now()))
|
||||
task_manager.add_task(0, 0, FlightLib.reach_point,
|
||||
task_kwargs={
|
||||
"x": telem.x,
|
||||
"y": telem.y,
|
||||
"z": float(z_str),
|
||||
"frame_id": client.active_client.FRAME_ID,
|
||||
"timeout": client.active_client.TAKEOFF_TIME,
|
||||
"auto_arm": True,
|
||||
}
|
||||
)
|
||||
task_kwargs={
|
||||
"x": telem.x,
|
||||
"y": telem.y,
|
||||
"z": float(z_str),
|
||||
"frame_id": client.active_client.config.copter_frame_id,
|
||||
"timeout": client.active_client.config.copter_takeoff_time,
|
||||
"auto_arm": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@messaging.message_callback("land")
|
||||
@@ -523,10 +517,10 @@ def _command_land(*args, **kwargs):
|
||||
task_manager.reset()
|
||||
task_manager.add_task(0, 0, animation.land,
|
||||
task_kwargs={
|
||||
"z": client.active_client.TAKEOFF_HEIGHT,
|
||||
"timeout": client.active_client.TAKEOFF_TIME,
|
||||
"frame_id": client.active_client.FRAME_ID,
|
||||
"use_leds": client.active_client.USE_LEDS,
|
||||
"z": client.active_client.config.copter_takeoff_height,
|
||||
"timeout": client.active_client.config.copter_takeoff_time,
|
||||
"frame_id": client.active_client.config.copter_frame_id,
|
||||
"use_leds": client.active_client.config.led_use,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -570,115 +564,114 @@ def _play_animation(*args, **kwargs):
|
||||
return
|
||||
|
||||
task_manager.reset(interrupt_next_task=False)
|
||||
|
||||
logger.info("Start time = {}, wait for {} seconds".format(start_time, start_time-time.time()))
|
||||
|
||||
logger.info("Start time = {}, wait for {} seconds".format(start_time, start_time - time.time()))
|
||||
# Load animation
|
||||
frames = animation.load_animation(os.path.abspath("animation.csv"),
|
||||
x0=client.active_client.X0 + client.active_client.X0_COMMON,
|
||||
y0=client.active_client.Y0 + client.active_client.Y0_COMMON,
|
||||
z0=client.active_client.Z0 + client.active_client.Z0_COMMON,
|
||||
x_ratio=client.active_client.X_RATIO,
|
||||
y_ratio=client.active_client.Y_RATIO,
|
||||
z_ratio=client.active_client.Z_RATIO,
|
||||
)
|
||||
offset = numpy.array(client.active_client.config.private_offset) + numpy.array(client.active_client.config.copter_common_offset)
|
||||
frames = animation.load_animation(os.path.abspath("animation.csv"), client.active_client.config.animation_frame_delay,
|
||||
offset[0], offset[1], offset[2], *client.active_client.config.animation_ratio)
|
||||
# Correct start and land frames in animation
|
||||
corrected_frames, start_action, start_delay = animation.correct_animation(frames,
|
||||
check_takeoff=client.active_client.TAKEOFF_CHECK,
|
||||
check_land=client.active_client.LAND_CHECK,
|
||||
)
|
||||
check_takeoff=client.active_client.config.animation_takeoff_detection,
|
||||
check_land=client.active_client.config.animation_land_detection,
|
||||
)
|
||||
# Choose start action
|
||||
if start_action == 'takeoff':
|
||||
# Takeoff first
|
||||
task_manager.add_task(start_time, 0, animation.takeoff,
|
||||
task_kwargs={
|
||||
"z": client.active_client.TAKEOFF_HEIGHT,
|
||||
"timeout": client.active_client.TAKEOFF_TIME,
|
||||
"safe_takeoff": client.active_client.SAFE_TAKEOFF,
|
||||
# "frame_id": client.active_client.FRAME_ID,
|
||||
"use_leds": client.active_client.USE_LEDS,
|
||||
}
|
||||
)
|
||||
task_kwargs={
|
||||
"z": client.active_client.config.copter_takeoff_height,
|
||||
"timeout": client.active_client.config.copter_takeoff_time,
|
||||
"safe_takeoff": client.active_client.config.copter_safe_takeoff,
|
||||
# "frame_id": client.active_client.config.copter_frame_id,
|
||||
"use_leds": client.active_client.config.led_use,
|
||||
}
|
||||
)
|
||||
# Fly to first point
|
||||
rfp_time = start_time + client.active_client.TAKEOFF_TIME
|
||||
rfp_time = start_time + client.active_client.config.copter_takeoff_time
|
||||
task_manager.add_task(rfp_time, 0, animation.execute_frame,
|
||||
task_kwargs={
|
||||
"point": animation.convert_frame(corrected_frames[0])[0],
|
||||
"color": animation.convert_frame(corrected_frames[0])[1],
|
||||
"frame_id": client.active_client.FRAME_ID,
|
||||
"use_leds": client.active_client.USE_LEDS,
|
||||
"flight_func": FlightLib.reach_point,
|
||||
}
|
||||
)
|
||||
task_kwargs={
|
||||
"point": animation.convert_frame(corrected_frames[0])[0],
|
||||
"color": animation.convert_frame(corrected_frames[0])[1],
|
||||
"frame_id": client.active_client.config.copter_frame_id,
|
||||
"use_leds": client.active_client.config.led_use,
|
||||
"flight_func": FlightLib.reach_point,
|
||||
}
|
||||
)
|
||||
# Calculate first frame start time
|
||||
frame_time = rfp_time + client.active_client.RFP_TIME
|
||||
frame_time = rfp_time + client.active_client.config.copter_reach_first_point_time
|
||||
|
||||
elif start_action == 'arm':
|
||||
# Calculate start time
|
||||
start_time += start_delay
|
||||
# Arm
|
||||
#task_manager.add_task(start_time, 0, FlightLib.arming_wrapper,
|
||||
# task_manager.add_task(start_time, 0, FlightLib.arming_wrapper,
|
||||
# task_kwargs={
|
||||
# "state": True
|
||||
# }
|
||||
# )
|
||||
frame_time = start_time # + 1.0
|
||||
frame_time = start_time # + 1.0
|
||||
point, color, yaw = animation.convert_frame(corrected_frames[0])
|
||||
task_manager.add_task(frame_time, 0, animation.execute_frame,
|
||||
task_kwargs={
|
||||
"point": point,
|
||||
"color": color,
|
||||
"frame_id": client.active_client.FRAME_ID,
|
||||
"use_leds": client.active_client.USE_LEDS,
|
||||
"flight_func": FlightLib.navto,
|
||||
"auto_arm": True,
|
||||
}
|
||||
)
|
||||
task_kwargs={
|
||||
"point": point,
|
||||
"color": color,
|
||||
"frame_id": client.active_client.config.copter_frame_id,
|
||||
"use_leds": client.active_client.config.led_use,
|
||||
"flight_func": FlightLib.navto,
|
||||
"auto_arm": True,
|
||||
}
|
||||
)
|
||||
# Calculate first frame start time
|
||||
frame_time += client.active_client.FRAME_DELAY # TODO Think about arming time
|
||||
frame_time += corrected_frames[0]["delay"] # TODO Think about arming time
|
||||
logger.debug(task_manager.task_queue)
|
||||
# Play animation file
|
||||
for frame in corrected_frames:
|
||||
point, color, yaw = animation.convert_frame(frame)
|
||||
if client.active_client.YAW == "animation":
|
||||
if client.active_client.config.animation_yaw == "animation":
|
||||
yaw = frame["yaw"]
|
||||
else:
|
||||
yaw = math.radians(float(client.active_client.YAW))
|
||||
yaw = math.radians(float(client.active_client.config.animation_yaw))
|
||||
task_manager.add_task(frame_time, 0, animation.execute_frame,
|
||||
task_kwargs={
|
||||
"point": point,
|
||||
"color": color,
|
||||
"yaw": yaw,
|
||||
"frame_id": client.active_client.FRAME_ID,
|
||||
"use_leds": client.active_client.USE_LEDS,
|
||||
"flight_func": FlightLib.navto,
|
||||
}
|
||||
)
|
||||
task_kwargs={
|
||||
"point": point,
|
||||
"color": color,
|
||||
"yaw": yaw,
|
||||
"frame_id": client.active_client.config.copter_frame_id,
|
||||
"use_leds": client.active_client.config.led_use,
|
||||
"flight_func": FlightLib.navto,
|
||||
}
|
||||
)
|
||||
frame_time += frame["delay"]
|
||||
|
||||
# Calculate land_time
|
||||
land_time = frame_time + client.active_client.LAND_TIME
|
||||
land_time = frame_time + client.active_client.config.copter_land_time
|
||||
# Land
|
||||
task_manager.add_task(land_time, 0, animation.land,
|
||||
task_kwargs={
|
||||
"timeout": client.active_client.LAND_TIMEOUT,
|
||||
"frame_id": client.active_client.FRAME_ID,
|
||||
"use_leds": client.active_client.USE_LEDS,
|
||||
},
|
||||
)
|
||||
task_kwargs={
|
||||
"timeout": client.active_client.config.copter_land_timeout,
|
||||
"frame_id": client.active_client.config.copter_frame_id,
|
||||
"use_leds": client.active_client.config.led_use,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
class Telemetry:
|
||||
params_default_dict = {
|
||||
"git_version": None,
|
||||
"animation_id": None,
|
||||
"battery": None,
|
||||
"armed": False,
|
||||
"system_status": None,
|
||||
"fcu_status": None,
|
||||
"calibration_status": None,
|
||||
"mode": None,
|
||||
"selfcheck": None,
|
||||
"current_position": None,
|
||||
"start_position": None,
|
||||
"time": None,
|
||||
"last_task": None,
|
||||
"time_delta": None,
|
||||
"config_version": None,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
@@ -710,20 +703,20 @@ class Telemetry:
|
||||
def get_git_version(cls):
|
||||
return subprocess.check_output("git log --pretty=format:'%h' -n 1", shell=True)
|
||||
|
||||
@classmethod
|
||||
def get_config_version(cls):
|
||||
return "{} V{}".format(client.active_client.config.config_name, client.active_client.config.config_version)
|
||||
|
||||
@classmethod
|
||||
def get_start_position(cls):
|
||||
x_start, y_start = animation.get_start_xy(os.path.abspath("animation.csv"),
|
||||
x_ratio=client.active_client.X_RATIO,
|
||||
y_ratio=client.active_client.Y_RATIO,
|
||||
)
|
||||
x_delta = client.active_client.X0 + client.active_client.X0_COMMON
|
||||
y_delta = client.active_client.Y0 + client.active_client.Y0_COMMON
|
||||
z_delta = client.active_client.Z0 + client.active_client.Z0_COMMON
|
||||
|
||||
x = x_start + x_delta
|
||||
y = y_start + y_delta
|
||||
if not FlightLib._check_nans(x, y, z_delta):
|
||||
return x, y, z_delta
|
||||
*client.active_client.config.animation_ratio)
|
||||
offset = numpy.array(client.active_client.config.private_offset) + numpy.array(client.active_client.config.copter_common_offset)
|
||||
x = x_start + offset[0]
|
||||
y = y_start + offset[1]
|
||||
z = offset[2]
|
||||
if not FlightLib._check_nans(x, y, z):
|
||||
return x, y, z
|
||||
return 'NO_POS'
|
||||
|
||||
@classmethod
|
||||
@@ -760,13 +753,14 @@ class Telemetry:
|
||||
def get_position(cls, ros_telemetry):
|
||||
x, y, z = ros_telemetry.x, ros_telemetry.y, ros_telemetry.z
|
||||
if not math.isnan(x):
|
||||
return x, y, z, math.degrees(ros_telemetry.yaw), client.active_client.FRAME_ID
|
||||
return x, y, z, math.degrees(ros_telemetry.yaw), client.active_client.config.copter_frame_id
|
||||
return 'NO_POS'
|
||||
|
||||
def update_telemetry_fast(self):
|
||||
self.start_position = self.get_start_position()
|
||||
self.start_position = self.get_start_position()
|
||||
self.last_task = task_manager.get_current_task()
|
||||
try:
|
||||
self.ros_telemetry = FlightLib.get_telemetry_locked(client.active_client.FRAME_ID)
|
||||
self.ros_telemetry = FlightLib.get_telemetry_locked(client.active_client.config.copter_frame_id)
|
||||
if self.ros_telemetry.connected:
|
||||
self.armed = self.ros_telemetry.armed
|
||||
self.mode = self.ros_telemetry.mode
|
||||
@@ -781,15 +775,16 @@ class Telemetry:
|
||||
rospy.logdebug(e)
|
||||
except rospy.TransportException as e:
|
||||
rospy.logdebug(e)
|
||||
self.time = time.time()
|
||||
self.time_delta = time.time()
|
||||
self.round_telemetry()
|
||||
|
||||
def update_telemetry_slow(self):
|
||||
self.animation_id = animation.get_id()
|
||||
self.git_version = self.get_git_version()
|
||||
self.config_version = self.get_config_version()
|
||||
try:
|
||||
self.calibration_status = get_calibration_status()
|
||||
self.system_status = get_sys_status()
|
||||
self.fcu_status = get_sys_status()
|
||||
self.battery = self.get_battery(self.ros_telemetry)
|
||||
except rospy.ServiceException:
|
||||
rospy.logdebug("Some service is unavailable")
|
||||
@@ -807,12 +802,12 @@ class Telemetry:
|
||||
round_list = ["battery", "start_position", "current_position"]
|
||||
for key in round_list:
|
||||
if self.__dict__[key] not in [None, 'NO_POS', 'NO_FCU']:
|
||||
self.__dict__[key] = [round(v,2) if type(v) == float else v for v in self.__dict__[key]]
|
||||
self.__dict__[key] = [round(v, 2) if type(v) == float else v for v in self.__dict__[key]]
|
||||
|
||||
def reset_telemetry_values(self):
|
||||
self.battery = float('nan'), float('nan')
|
||||
self.calibration_status = 'NO_FCU'
|
||||
self.system_status = 'NO_FCU'
|
||||
self.fcu_status = 'NO_FCU'
|
||||
self.mode = 'NO_FCU'
|
||||
self.selfcheck = ['NO_FCU']
|
||||
self.current_position = 'NO_POS'
|
||||
@@ -851,9 +846,9 @@ class Telemetry:
|
||||
self._tasks_cleared = False
|
||||
self._last_state = state
|
||||
|
||||
def transmit_message(self):
|
||||
def transmit_message(self): # todo if connected
|
||||
try:
|
||||
client.active_client.server_connection.send_message('telemetry', args={'value': self.create_msg_contents()})
|
||||
client.active_client.server_connection.send_message('telemetry', kwargs={'value': self.create_msg_contents()})
|
||||
except AttributeError as e:
|
||||
logger.debug(e)
|
||||
|
||||
@@ -865,7 +860,7 @@ class Telemetry:
|
||||
cpu_temp = cpu_temp_info.current
|
||||
# https://github.com/raspberrypi/documentation/blob/JamesH65-patch-vcgencmd-vcdbg-docs/raspbian/applications/vcgencmd.md
|
||||
throttled_hex = subprocess.check_output("vcgencmd get_throttled", shell=True).split('=')[1]
|
||||
under_voltage = bool(int(bin(int(throttled_hex,16))[2:][-1]))
|
||||
under_voltage = bool(int(bin(int(throttled_hex, 16))[2:][-1]))
|
||||
power_state = 'normal' if not under_voltage else 'under voltage!'
|
||||
if cpu_temp_info.critical:
|
||||
cpu_temp_state = 'critical'
|
||||
@@ -874,7 +869,7 @@ class Telemetry:
|
||||
else:
|
||||
cpu_temp_state = 'normal'
|
||||
logger.info("CPU usage: {} | Memory: {} % | T: {} ({}) | Power: {}".format(
|
||||
cpu_usage, mem_usage, cpu_temp, cpu_temp_state, power_state))
|
||||
cpu_usage, mem_usage, cpu_temp, cpu_temp_state, power_state))
|
||||
|
||||
def _update_loop(self, freq): # TODO extract?
|
||||
rate = rospy.Rate(freq)
|
||||
@@ -883,11 +878,8 @@ class Telemetry:
|
||||
self.update_telemetry_fast()
|
||||
self.check_failsafe_and_interruption()
|
||||
|
||||
if client.active_client.TELEM_TRANSMIT and client.active_client.connected:
|
||||
if client.active_client.config.telemetry_transmit and client.active_client.connected:
|
||||
self.transmit_message()
|
||||
|
||||
if client.active_client.LOG_CPU_AND_MEMORY:
|
||||
self.log_cpu_and_memory()
|
||||
|
||||
rate.sleep()
|
||||
|
||||
@@ -895,13 +887,16 @@ class Telemetry:
|
||||
rate = rospy.Rate(1)
|
||||
while not rospy.is_shutdown():
|
||||
self.update_telemetry_slow()
|
||||
if client.active_client.config.telemetry_log_resources:
|
||||
self.log_cpu_and_memory()
|
||||
rate.sleep()
|
||||
|
||||
def start_loop(self):
|
||||
if client.active_client.TELEM_FREQ > 0:
|
||||
if client.active_client.config.telemetry_frequency > 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")
|
||||
args=(client.active_client.config.telemetry_frequency,)) # 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:
|
||||
@@ -911,12 +906,14 @@ class Telemetry:
|
||||
if keys is None:
|
||||
keys = self.params_default_dict.keys()
|
||||
# return only existing keys from 'keys'
|
||||
return {k: self.__dict__[k] for k in keys if k in self.params_default_dict}
|
||||
return {k: self.__dict__[k] for k in keys if k in self.params_default_dict}
|
||||
|
||||
|
||||
def emergency_callback(data):
|
||||
global emergency
|
||||
emergency = data.data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
telemetry = Telemetry()
|
||||
copter_client = CopterClient()
|
||||
|
||||
@@ -6,6 +6,8 @@ from mavros_msgs.srv import ParamGet, ParamSet
|
||||
from mavros_msgs.msg import State, ParamValue
|
||||
from pymavlink.dialects.v20 import common as mavlink
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
send_command_long = rospy.ServiceProxy('/mavros/cmd/command', CommandLong)
|
||||
get_param = rospy.ServiceProxy('/mavros/param/get', ParamGet)
|
||||
set_param = rospy.ServiceProxy('/mavros/param/set', ParamSet)
|
||||
@@ -61,7 +63,7 @@ def calibrate(sensor):
|
||||
return False
|
||||
# Make calibration message
|
||||
calibration_message = calibration_msg(sensor)
|
||||
# Send mavlink calibration command
|
||||
# Send mavlink calibration command
|
||||
send_command_long(False, mavlink.MAV_CMD_PREFLIGHT_CALIBRATION, 0, *calibration_message)
|
||||
rospy.loginfo('Send {} calibration message'.format(sensor))
|
||||
# Wait until system status to uninit (during calibration on px4)
|
||||
@@ -85,7 +87,7 @@ def get_calibration_status():
|
||||
if mag_status.value.integer == 0 and mag_status.success:
|
||||
status_text += "mag: uncalibrated; "
|
||||
if acc_status.value.integer == 0 and acc_status.success:
|
||||
status_text += "acc: uncalibrated; "
|
||||
status_text += "acc: uncalibrated; "
|
||||
if status_text == "":
|
||||
if not gyro_status.success or not mag_status.success or not acc_status.success:
|
||||
status_text = "NO_INFO"
|
||||
@@ -127,23 +129,45 @@ def stop_subscriber():
|
||||
|
||||
def load_param_file(px4_file):
|
||||
result = True
|
||||
err_lines = ""
|
||||
err_params = ""
|
||||
lines_commented = ""
|
||||
params_loaded = ""
|
||||
try:
|
||||
px4_params = open(px4_file)
|
||||
except IOError:
|
||||
logging.error("File {} can't be opened".format(filepath))
|
||||
logger.error("File {} can't be opened".format(filepath))
|
||||
result = False
|
||||
else:
|
||||
else:
|
||||
with open(px4_file) as px4_params:
|
||||
row = 0
|
||||
for line in px4_params:
|
||||
param_str_array = line[:-1].split('\t')
|
||||
param_name = param_str_array[2]
|
||||
param_value_str = param_str_array[3]
|
||||
param_type = param_str_array[4]
|
||||
if param_type == '6':
|
||||
param_value = ParamValue(integer=int(param_value_str))
|
||||
row += 1
|
||||
param_str_array = line.split('\t')
|
||||
if len(param_str_array) == 5 and '#' not in param_str_array[0]:
|
||||
param_name = param_str_array[2]
|
||||
param_value_str = param_str_array[3]
|
||||
param_type = int(param_str_array[4])
|
||||
if param_type == 6:
|
||||
param_value = ParamValue(integer=int(param_value_str))
|
||||
else:
|
||||
param_value = ParamValue(real=float(param_value_str))
|
||||
if not set_param(param_name, param_value):
|
||||
err_params += "{} ,".format(row)
|
||||
result = False
|
||||
else:
|
||||
params_loaded += "{} ,".format(row)
|
||||
elif '#' in param_str_array[0]:
|
||||
lines_commented += "{} ,".format(row)
|
||||
else:
|
||||
param_value = ParamValue(real=float(param_value_str))
|
||||
if not set_param(param_name, param_value):
|
||||
result = False
|
||||
err_lines += "{} ,".format(row)
|
||||
if err_lines:
|
||||
logger.info("Can't parse lines: {}".format(err_lines[:-1]))
|
||||
if err_params:
|
||||
logger.info("Can't set params from lines: {}".format(err_params[:-1]))
|
||||
if lines_commented:
|
||||
logger.info("Lines commented: {}".format(lines_commented[:-1]))
|
||||
if params_loaded:
|
||||
logger.info("Params are successfully loaded from lines: {}".format(params_loaded[:-1]))
|
||||
return result
|
||||
|
||||
|
||||
3
Drone/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
selectors2
|
||||
psutil
|
||||
configobj
|
||||
@@ -54,7 +54,7 @@ class TaskManager(object):
|
||||
self._wait_interrupt_event.set()
|
||||
self._running_event.clear()
|
||||
|
||||
task = Task(task_function, task_args, task_kwargs, task_delayable)
|
||||
task = Task(task_function, task_args, task_kwargs, task_delayable)
|
||||
|
||||
count = next(self._counter)
|
||||
entry = (timestamp, priority, count, task)
|
||||
@@ -64,21 +64,21 @@ class TaskManager(object):
|
||||
entry_old = self.task_queue[0]
|
||||
else:
|
||||
entry_old = entry
|
||||
|
||||
|
||||
heapq.heappush(self.task_queue, entry)
|
||||
|
||||
if self.task_queue[0] != entry_old:
|
||||
self._task_interrupt_event.set()
|
||||
#print("Task queue updated with more priority task")
|
||||
|
||||
|
||||
if self._reset_event.is_set():
|
||||
self._task_interrupt_event.set()
|
||||
self._reset_event.clear()
|
||||
#print("Task queue updated after reset")
|
||||
|
||||
|
||||
self._wait_interrupt_event.clear()
|
||||
self._running_event.set()
|
||||
|
||||
|
||||
# #print(self.task_queue)
|
||||
|
||||
def pop_task(self):
|
||||
@@ -90,6 +90,21 @@ class TaskManager(object):
|
||||
def get_last_task_name(self):
|
||||
return self._last_task
|
||||
|
||||
def get_current_task(self):
|
||||
try:
|
||||
start_time, priority, count, task = self.task_queue[0]
|
||||
except IndexError as e:
|
||||
logger.debug("Task queue checking exception: {}".format(e))
|
||||
return "No task"
|
||||
else:
|
||||
if self._running_event.is_set():
|
||||
time_to_start = start_time - time.time()
|
||||
if time_to_start > 0:
|
||||
return "{} in {:.1f} s".format(task.func.__name__,time_to_start)
|
||||
return task.func.__name__
|
||||
else:
|
||||
return "paused"
|
||||
|
||||
def start(self):
|
||||
#print("Task manager is started")
|
||||
logger.info("Task manager is started")
|
||||
@@ -119,7 +134,7 @@ class TaskManager(object):
|
||||
if self.task_queue:
|
||||
next_task_time = self.task_queue[0][0]
|
||||
if time_to_start_next_task > next_task_time:
|
||||
self._timeshift = time_to_start_next_task - next_task_time
|
||||
self._timeshift = time_to_start_next_task - next_task_time
|
||||
self._wait_interrupt_event.clear()
|
||||
self._task_interrupt_event.clear()
|
||||
self._running_event.set()
|
||||
@@ -138,7 +153,7 @@ class TaskManager(object):
|
||||
with self._task_queue_lock:
|
||||
try:
|
||||
start_time, priority, count, task = self.task_queue[0]
|
||||
except Exception as e:
|
||||
except IndexError as e:
|
||||
logger.debug("Task queue checking exception: {}".format(e))
|
||||
self._timeshift = 0.0
|
||||
self._wait_interrupt_event.clear()
|
||||
@@ -163,7 +178,7 @@ class TaskManager(object):
|
||||
#print("Interrupter is set: {}".format(self._task_interrupt_event.is_set()))
|
||||
try:
|
||||
task.func(*task.args, interrupter=self._task_interrupt_event, **task.kwargs)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error '{}' occurred in task {}".format(e, task))
|
||||
#print("Error '{}' occurred in task {}".format(e, task))
|
||||
@@ -180,7 +195,7 @@ class TaskManager(object):
|
||||
if time.time() > start_time:
|
||||
try:
|
||||
start_time_n, priority_n, count_n, task_n = self.task_queue[0]
|
||||
except Exception as e:
|
||||
except IndexError as e:
|
||||
logger.warning("Timeout checking exception: {}".format(e))
|
||||
self._timeshift = 0.0
|
||||
self._wait_interrupt_event.clear()
|
||||
@@ -196,10 +211,10 @@ class TaskManager(object):
|
||||
#try:
|
||||
#print("Pop {} function!".format(task.func.__name__))
|
||||
#except Exception as e:
|
||||
#print("Pop something!")
|
||||
#print("Pop something!")
|
||||
|
||||
if self._task_interrupt_event.is_set():
|
||||
self._task_interrupt_event.clear()
|
||||
self._task_interrupt_event.clear()
|
||||
|
||||
logger.info("Execution done")
|
||||
#print("Execution done")
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import rospy
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import math
|
||||
import logging
|
||||
import threading
|
||||
import ConfigParser
|
||||
from clever.srv import SetAttitude
|
||||
|
||||
# for backward compatibility with clever
|
||||
try:
|
||||
from clever import SetAttitude
|
||||
except ImportError:
|
||||
from clover import SetAttitude
|
||||
|
||||
from sensor_msgs.msg import Range
|
||||
from mavros_msgs.msg import State, PositionTarget
|
||||
from mavros_msgs.srv import SetMode, CommandBool
|
||||
@@ -13,15 +19,25 @@ 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")
|
||||
import inspect # Add parent dir to PATH to import messaging_lib
|
||||
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
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 = config.getfloat('VISUAL_POSE_WATCHDOG', 'timeout_to_disarm')
|
||||
from config import ConfigManager
|
||||
|
||||
config = ConfigManager()
|
||||
config.load_config_and_spec("config/client.ini")
|
||||
|
||||
watchdog_is_enabled = config.position_watchdog_enabled
|
||||
log_state = config.position_watchdog_log_state
|
||||
vision_pose_delay_after_arm = config.position_watchdog_vision_pose_delay_after_arm
|
||||
visual_pose_timeout = config.position_watchdog_vision_pose_timeout
|
||||
pos_delta_max = config.position_watchdog_position_delta_max
|
||||
watchdog_action = config.position_watchdog_action
|
||||
timeout_to_disarm = config.position_watchdog_disarm_timeout
|
||||
emergency_land_thrust = config.emergency_land_thrust
|
||||
emergency_land_decrease_thrust_after = config.emergency_land_decrease_thrust_after
|
||||
|
||||
logging.basicConfig( # TODO all prints as logs
|
||||
level=logging.DEBUG, # INFO
|
||||
@@ -55,6 +71,7 @@ setpoint_raw = None
|
||||
setpoint_position = None
|
||||
setpoint_pose = None
|
||||
|
||||
arm_start_time = None
|
||||
offboard_start_time = None
|
||||
offboard_disarmed_timeout = 3.
|
||||
|
||||
@@ -62,9 +79,9 @@ 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('visual_pose_timeout = {} | position_delta_max = {} | watchdog_action = {}'.format(visual_pose_timeout, pos_delta_max, watchdog_action))
|
||||
logger.info('timeout_to_disarm = {}'.format(timeout_to_disarm))
|
||||
if timeout_action == 'emergency_land':
|
||||
if watchdog_action == 'emergency_land':
|
||||
logger.info('emergency_land_thrust: {}'.format(emergency_land_thrust))
|
||||
|
||||
rate = rospy.Rate(10)
|
||||
@@ -127,7 +144,7 @@ def laser_callback(data):
|
||||
laser_range = data.range
|
||||
|
||||
def emergency_land(disarm_if_timeout = True):
|
||||
global emergency_land_thrust, laser_range
|
||||
global emergency_land_thrust, laser_range
|
||||
current_thrust = emergency_land_thrust
|
||||
action_timestamp = time.time()
|
||||
while armed:
|
||||
@@ -136,13 +153,13 @@ def emergency_land(disarm_if_timeout = True):
|
||||
try:
|
||||
set_attitude(thrust = current_thrust, yaw = 0, frame_id = 'body', auto_arm = True)
|
||||
except rospy.ServiceException as e:
|
||||
logger.info(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)
|
||||
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:
|
||||
@@ -150,7 +167,7 @@ def emergency_land(disarm_if_timeout = True):
|
||||
try:
|
||||
arming(False)
|
||||
except rospy.ServiceException as e:
|
||||
logger.info(e)
|
||||
logger.info(e)
|
||||
rate.sleep()
|
||||
|
||||
def emergency_land_service(request):
|
||||
@@ -164,59 +181,68 @@ def emergency_land_service(request):
|
||||
responce.success = False
|
||||
responce.message = "Copter is disarmed, no need for emergency landing!"
|
||||
emergency_land_called = False
|
||||
return responce
|
||||
return responce
|
||||
|
||||
def watchdog_callback(event):
|
||||
global visual_pose_last_timestamp, armed, mode, timeout_action, laser_range, emergency, local_pose, setpoint_pose, offboard_start_time, emergency_land_called
|
||||
global visual_pose_last_timestamp, armed, mode, watchdog_action, laser_range
|
||||
global emergency, local_pose, setpoint_pose, emergency_land_called, log_state
|
||||
global offboard_start_time, arm_start_time, vision_pose_delay_after_arm
|
||||
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))
|
||||
visual_pose_dt = abs(time.time() - visual_pose_last_timestamp)
|
||||
if log_state:
|
||||
logger.info("armed: {} | mode: {} | vis_dt: {:.2f} | pos_delta: {:.2f} | pos_dt: {:.2f} | range: {:.2f} | watchdog_action: {}".format(
|
||||
armed, mode, visual_pose_dt, pos_delta, pos_dt, laser_range, watchdog_action))
|
||||
if mode == 'OFFBOARD':
|
||||
if offboard_start_time is None:
|
||||
offboard_start_time = time.time()
|
||||
if armed:
|
||||
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()
|
||||
if timeout_action in ['land', 'emergency_land', 'disarm']:
|
||||
if arm_start_time is None:
|
||||
arm_start_time = time.time()
|
||||
arm_time = time.time() - arm_start_time
|
||||
logger.debug('arm time: {}'.format(arm_time))
|
||||
if arm_time > vision_pose_delay_after_arm and watchdog_is_enabled:
|
||||
if (visual_pose_dt > visual_pose_timeout and visual_pose_timeout != 0.) or (pos_delta > pos_delta_max and pos_delta_max != 0.):
|
||||
action_timestamp = time.time()
|
||||
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:
|
||||
if watchdog_action not in ['land', 'emergency_land', 'disarm']:
|
||||
watchdog_action = 'land'
|
||||
if watchdog_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 watchdog_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':
|
||||
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:
|
||||
logger.info(e)
|
||||
rate.sleep()
|
||||
elif watchdog_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
|
||||
if emergency_land_called:
|
||||
emergency = True
|
||||
logger.info('/emergency_land service was called, start emergency landing...')
|
||||
emergency_land()
|
||||
@@ -224,6 +250,7 @@ def watchdog_callback(event):
|
||||
emergency = False
|
||||
emergency_land_called = False
|
||||
else:
|
||||
arm_start_time = None
|
||||
if time.time() - offboard_start_time > offboard_disarmed_timeout:
|
||||
try:
|
||||
set_mode(custom_mode='AUTO.LAND')
|
||||
@@ -231,7 +258,7 @@ def watchdog_callback(event):
|
||||
logger.info(e)
|
||||
else:
|
||||
offboard_start_time = None
|
||||
if abs(time.time() - visual_pose_last_timestamp) > visual_pose_timeout:
|
||||
if (abs(time.time() - visual_pose_last_timestamp) > visual_pose_timeout and visual_pose_timeout != 0.0):
|
||||
logger.info('Visual pose data is too old')
|
||||
|
||||
rospy.Subscriber('/mavros/vision_pose/pose', PoseStamped, visual_pose_callback)
|
||||
|
||||
55
Server/config/spec/configspec_server.ini
Normal file
@@ -0,0 +1,55 @@
|
||||
config_name = string(default='server')
|
||||
config_version = float(default='1.0')
|
||||
|
||||
[SERVER]
|
||||
port = integer(default=25000)
|
||||
buffer_size = integer(default=1024)
|
||||
|
||||
[CLIENT]
|
||||
clever_dir = string(default=/home/pi/catkin_ws/src/clever/clover)
|
||||
|
||||
[TABLE]
|
||||
# True -> clients are removed on disconnection
|
||||
# False -> disconnected clients indicated
|
||||
remove_disconnected = boolean(default=False)
|
||||
[[PRESETS]]
|
||||
current = string(default="DEFAULT")
|
||||
[[[DEFAULT]]]
|
||||
copter_id = preset_param(default=list(True, 100))
|
||||
git_version = preset_param(default=list(True, 75))
|
||||
config_version = preset_param(default=list(True, 140))
|
||||
animation_id = preset_param(default=list(True, 100))
|
||||
battery = preset_param(default=list(True, 100))
|
||||
fcu_status = preset_param(default=list(True, 100))
|
||||
calibration_status = preset_param(default=list(True, 65))
|
||||
mode = preset_param(default=list(True, 100))
|
||||
selfcheck = preset_param(default=list(True, 65))
|
||||
current_position = preset_param(default=list(True, 250))
|
||||
start_position = preset_param(default=list(True, 150))
|
||||
last_task = preset_param(default=list(True, 250))
|
||||
time_delta = preset_param(default=list(True, 100))
|
||||
[[[__many__]]]
|
||||
__many__ = preset_param
|
||||
|
||||
[CHECKS]
|
||||
check_git_version = boolean(default=True)
|
||||
check_current_position = boolean(default=True)
|
||||
# in percents; set 0 to disable this check
|
||||
battery_min = float(default=50.0, min=0, max=100)
|
||||
# in meters; set 0 to disable this check
|
||||
start_pos_delta_max = float(default=1.0, min=0)
|
||||
# in seconds
|
||||
time_delta_max = float(default=1.0, min=0)
|
||||
|
||||
[BROADCAST]
|
||||
send = boolean(default=True)
|
||||
listen = boolean(default=True)
|
||||
port = integer(default=8181)
|
||||
send_ip = string(default=255.255.255.255)
|
||||
# delay for message sending in seconds
|
||||
delay = float(default=5.0, min=0)
|
||||
|
||||
[NTP]
|
||||
use = boolean(default=False)
|
||||
host = string(default=ntp1.stratum2.ru)
|
||||
port = integer(default=123)
|
||||
72
Server/config_editor.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file 'config_editor.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.14.0
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_config_dialog(object):
|
||||
def setupUi(self, config_dialog):
|
||||
config_dialog.setObjectName("config_dialog")
|
||||
config_dialog.resize(600, 700)
|
||||
config_dialog.setModal(False)
|
||||
self.gridLayout = QtWidgets.QGridLayout(config_dialog)
|
||||
self.gridLayout.setObjectName("gridLayout")
|
||||
self.config_view = QtWidgets.QTreeView(config_dialog)
|
||||
self.config_view.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed|QtWidgets.QAbstractItemView.SelectedClicked)
|
||||
self.config_view.setObjectName("config_view")
|
||||
self.config_view.header().setCascadingSectionResizes(False)
|
||||
self.config_view.header().setDefaultSectionSize(250)
|
||||
self.gridLayout.addWidget(self.config_view, 0, 0, 1, 1)
|
||||
self.gridLayout_2 = QtWidgets.QGridLayout()
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.gridLayout_2.addItem(spacerItem, 0, 2, 1, 1)
|
||||
self.do_restart = QtWidgets.QCheckBox(config_dialog)
|
||||
self.do_restart.setObjectName("do_restart")
|
||||
self.gridLayout_2.addWidget(self.do_restart, 0, 1, 1, 1)
|
||||
self.buttonBox = QtWidgets.QDialogButtonBox(config_dialog)
|
||||
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Save)
|
||||
self.buttonBox.setObjectName("buttonBox")
|
||||
self.gridLayout_2.addWidget(self.buttonBox, 0, 4, 1, 1)
|
||||
self.do_coloring = QtWidgets.QCheckBox(config_dialog)
|
||||
self.do_coloring.setChecked(True)
|
||||
self.do_coloring.setObjectName("do_coloring")
|
||||
self.gridLayout_2.addWidget(self.do_coloring, 0, 0, 1, 1)
|
||||
self.save_as_button = QtWidgets.QPushButton(config_dialog)
|
||||
self.save_as_button.setObjectName("save_as_button")
|
||||
self.gridLayout_2.addWidget(self.save_as_button, 0, 3, 1, 1)
|
||||
self.gridLayout.addLayout(self.gridLayout_2, 2, 0, 1, 1)
|
||||
self.line = QtWidgets.QFrame(config_dialog)
|
||||
self.line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
self.line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
self.line.setObjectName("line")
|
||||
self.gridLayout.addWidget(self.line, 1, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(config_dialog)
|
||||
self.buttonBox.accepted.connect(config_dialog.accept)
|
||||
self.buttonBox.rejected.connect(config_dialog.reject)
|
||||
QtCore.QMetaObject.connectSlotsByName(config_dialog)
|
||||
|
||||
def retranslateUi(self, config_dialog):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
config_dialog.setWindowTitle(_translate("config_dialog", "Config Editor"))
|
||||
self.do_restart.setText(_translate("config_dialog", "Restart"))
|
||||
self.do_restart.setShortcut(_translate("config_dialog", "R"))
|
||||
self.do_coloring.setText(_translate("config_dialog", "Color Indication"))
|
||||
self.save_as_button.setText(_translate("config_dialog", "Save as"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
config_dialog = QtWidgets.QDialog()
|
||||
ui = Ui_config_dialog()
|
||||
ui.setupUi(config_dialog)
|
||||
config_dialog.show()
|
||||
sys.exit(app.exec_())
|
||||
128
Server/config_editor.ui
Normal file
@@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>config_dialog</class>
|
||||
<widget class="QDialog" name="config_dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>700</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Config Editor</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTreeView" name="config_view">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
|
||||
</property>
|
||||
<attribute name="headerCascadingSectionResizes">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="headerDefaultSectionSize">
|
||||
<number>250</number>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="2">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="do_restart">
|
||||
<property name="text">
|
||||
<string>Restart</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>R</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="do_coloring">
|
||||
<property name="text">
|
||||
<string>Color Indication</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QPushButton" name="save_as_button">
|
||||
<property name="text">
|
||||
<string>Save as</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>config_dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>260</x>
|
||||
<y>237</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>246</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>config_dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>260</x>
|
||||
<y>239</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>267</x>
|
||||
<y>246</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
954
Server/config_editor_models.py
Normal file
@@ -0,0 +1,954 @@
|
||||
import pickle
|
||||
import logging
|
||||
from ast import literal_eval
|
||||
from functools import partial
|
||||
from copy import deepcopy
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5.QtCore import Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QCursor, QKeySequence
|
||||
from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QMenu, QAction, QMessageBox, QInputDialog, QFileDialog, \
|
||||
QShortcut
|
||||
|
||||
import config_editor
|
||||
|
||||
import sys
|
||||
import os, inspect # Add parent dir to PATH to import messaging_lib
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import config
|
||||
|
||||
|
||||
states_colors = {
|
||||
'normal': Qt.white,
|
||||
'unchanged': Qt.blue,
|
||||
'default': Qt.cyan,
|
||||
'edited': Qt.yellow,
|
||||
'added': Qt.green,
|
||||
'deleted': Qt.red,
|
||||
}
|
||||
|
||||
StateRole = 999
|
||||
TypeRole = 998
|
||||
|
||||
|
||||
def convert_type(data):
|
||||
try:
|
||||
data = literal_eval(data) if data else None
|
||||
except (SyntaxError, ValueError):
|
||||
data = str(data)
|
||||
return data
|
||||
|
||||
|
||||
class ConfigModelItem:
|
||||
def __init__(self, values=(None, None, None, None), item_type='option',
|
||||
state='normal', default=None, parent=None):
|
||||
self.spec_default = default
|
||||
self.itemData = list(values)
|
||||
self.state = state
|
||||
self.type = item_type
|
||||
|
||||
if isinstance(self.data(1), (list, tuple)):
|
||||
self.type = 'list'
|
||||
|
||||
self.default_values = deepcopy(self.itemData)
|
||||
self.default_state = state
|
||||
|
||||
self.childItems = []
|
||||
self.parentItem = parent
|
||||
|
||||
self.setup_type()
|
||||
|
||||
if self.parentItem is not None:
|
||||
self.parentItem.appendChild(self)
|
||||
|
||||
def setup_type(self):
|
||||
if self.type == 'section':
|
||||
self.itemData[1:1] = ('<section>',)
|
||||
self.spec_default = self.data(1)
|
||||
|
||||
elif self.type == 'list':
|
||||
self._setup_list(self.get_list_items())
|
||||
|
||||
def _get_list_spec(self):
|
||||
data = self.data(1)
|
||||
comments = self.data(2)
|
||||
if comments:
|
||||
try:
|
||||
raw_spec = comments.split('\n')[-1].split()[1:]
|
||||
if raw_spec[0] == '__list__': # and len(raw_spec[1:]) == len(data):
|
||||
return raw_spec[1:]
|
||||
except IndexError:
|
||||
pass
|
||||
return list(map(str, range(len(data))))
|
||||
|
||||
def get_list_items(self):
|
||||
spec = self._get_list_spec()
|
||||
values = self.data(1)
|
||||
if isinstance(self.spec_default, list):
|
||||
defaults = self.spec_default
|
||||
else:
|
||||
defaults = (None, )*len(spec)
|
||||
|
||||
self.itemData[1] = '<list: {}>'.format(' '.join(spec))
|
||||
# self.spec_default = self.itemData[1]
|
||||
|
||||
for key, value, default in zip(spec, values, defaults):
|
||||
yield ConfigModelItem((key, value, None, None), item_type='list_item',
|
||||
state=self.state, default=default)
|
||||
|
||||
def _setup_list(self, items): # use only at initialization
|
||||
for child in items:
|
||||
self.appendChild(child)
|
||||
|
||||
@property
|
||||
def is_section(self): # probably deprecated
|
||||
return self.type == 'section'
|
||||
|
||||
def appendChild(self, item):
|
||||
self.childItems.append(item)
|
||||
item.parentItem = self
|
||||
|
||||
def addChildren(self, items, row):
|
||||
if row == -1:
|
||||
row = 0
|
||||
self.childItems[row:row] = items
|
||||
|
||||
for item in items:
|
||||
item.parentItem = self
|
||||
|
||||
def child(self, row):
|
||||
return self.childItems[row]
|
||||
|
||||
def childCount(self):
|
||||
return len(self.childItems)
|
||||
|
||||
def columnCount(self):
|
||||
return len(self.itemData)
|
||||
|
||||
def data(self, column):
|
||||
try:
|
||||
return self.itemData[column]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def set_data(self, data, column):
|
||||
old_data = self.data(column)
|
||||
if old_data is None:
|
||||
data = convert_type(data)
|
||||
|
||||
if data == '<list>':
|
||||
data = []
|
||||
|
||||
try:
|
||||
self.itemData[column] = data
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
if old_data != data:
|
||||
self.set_state('edited')
|
||||
self.check_state()
|
||||
|
||||
return True
|
||||
|
||||
def check_state(self):
|
||||
if self.spec_default is not None and self.data(1) == self.spec_default \
|
||||
and self.data(0) == self.default_values[0] and self.type != 'section':
|
||||
self.set_state('default')
|
||||
# print('def', self.data(1), self.data(0), self.spec_default)
|
||||
|
||||
child_states = [child.state for child in self.childItems]
|
||||
if any(state in child_states for state in ['edited', 'added', 'deleted']):
|
||||
self.state = 'edited'
|
||||
if len(set(child_states)) == 1: # if all states equal
|
||||
self.set_state(child_states[0], set_children=False)
|
||||
# print(child_states)
|
||||
|
||||
if self.parentItem is not None:
|
||||
self.parentItem.check_state()
|
||||
|
||||
def set_state(self, state, set_children=True):
|
||||
if self.state == 'unchanged' and state == 'default':
|
||||
return
|
||||
|
||||
if self.state == 'added' and state in ('edited', 'unchanged', 'default', 'normal'):
|
||||
return
|
||||
|
||||
self.state = state
|
||||
|
||||
if set_children: # to prevent cycle state set
|
||||
for child in self.childItems:
|
||||
child.set_state(state)
|
||||
|
||||
# if state == 'edited':
|
||||
# self.parentItem.state = state
|
||||
|
||||
def set_type(self, item_type):
|
||||
self.type = item_type
|
||||
|
||||
def parent(self):
|
||||
return self.parentItem
|
||||
|
||||
def row(self):
|
||||
if self.parentItem is not None:
|
||||
return self.parentItem.childItems.index(self)
|
||||
return 0
|
||||
|
||||
def removeChild(self, position):
|
||||
if position < 0 or position > len(self.childItems):
|
||||
return False
|
||||
child = self.childItems.pop(position)
|
||||
child.parentItem = None
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.itemData)
|
||||
|
||||
|
||||
def ensure_unique_names(item, include_self=True):
|
||||
name = item.data(0)
|
||||
siblings_names = [child.data(0) for child in item.parent().childItems]
|
||||
if not include_self:
|
||||
siblings_names.remove(name)
|
||||
|
||||
while name in siblings_names:
|
||||
if '_copy' in name:
|
||||
spl = name.split('_copy')
|
||||
num = int(spl[1]) if spl[1] else 0
|
||||
num += 1
|
||||
name = spl[0] + '_copy' + str(num)
|
||||
else:
|
||||
name = name + '_copy'
|
||||
|
||||
item.set_data(name, 0)
|
||||
|
||||
|
||||
class ConfigModel(QtCore.QAbstractItemModel):
|
||||
def __init__(self, parent=None, widget=None,
|
||||
headers=("Option", "Value", 'Comment', 'Inline Comment')):
|
||||
self.rootItem = ConfigModelItem(headers)
|
||||
super(ConfigModel, self).__init__(parent)
|
||||
self.widget = widget
|
||||
|
||||
self.do_color = True
|
||||
|
||||
self.initial_comment = ''
|
||||
self.final_comment = ''
|
||||
|
||||
@QtCore.pyqtSlot(int)
|
||||
def enable_color(self, value):
|
||||
self.do_color = value
|
||||
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex(), (Qt.BackgroundRole, ))
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||
return self.rootItem.data(section)
|
||||
|
||||
def columnCount(self, parent):
|
||||
return self.rootItem.columnCount()
|
||||
|
||||
def rowCount(self, parent):
|
||||
if parent.column() > 0:
|
||||
return 0
|
||||
|
||||
if not parent.isValid():
|
||||
parentItem = self.rootItem
|
||||
else:
|
||||
parentItem = parent.internalPointer()
|
||||
|
||||
return parentItem.childCount()
|
||||
|
||||
def childrenIndexes(self, parent):
|
||||
column = parent.column()
|
||||
parent = self.index(parent.row(), 0, parent.parent())
|
||||
for i in range(self.rowCount(parent)):
|
||||
yield self.index(i, column, parent)
|
||||
|
||||
def index(self, row, column, parent):
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
parentItem = self.nodeFromIndex(parent)
|
||||
childItem = parentItem.child(row)
|
||||
|
||||
if childItem:
|
||||
return self.createIndex(row, column, childItem)
|
||||
else:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid():
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
childItem = index.internalPointer()
|
||||
if not isinstance(childItem, ConfigModelItem):
|
||||
# print(childItem, index.column()), # index.row(), index.parent().internalPointer())
|
||||
return QtCore.QModelIndex()
|
||||
parentItem = childItem.parent()
|
||||
|
||||
if parentItem == self.rootItem: #or parentItem is None:
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
return self.createIndex(parentItem.row(), 0, parentItem)
|
||||
|
||||
def modifyCol(self, index, col):
|
||||
return self.index(index.row(), col, index.parent())
|
||||
|
||||
def nodeFromIndex(self, index):
|
||||
if index.isValid():
|
||||
return index.internalPointer()
|
||||
return self.rootItem
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
item = index.internalPointer()
|
||||
|
||||
if role == Qt.DisplayRole or role == Qt.EditRole:
|
||||
return item.data(index.column())
|
||||
|
||||
if role == Qt.BackgroundRole and self.do_color:
|
||||
return QtGui.QBrush(states_colors[item.state])
|
||||
|
||||
if role == StateRole:
|
||||
return item.state
|
||||
if role == TypeRole:
|
||||
return item.type
|
||||
|
||||
return None
|
||||
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
if not index.isValid():
|
||||
return False
|
||||
|
||||
item = index.internalPointer()
|
||||
|
||||
if role == Qt.EditRole:
|
||||
column = index.column()
|
||||
|
||||
if column == 0 and value != item.data(column):
|
||||
if not self.widget.edit_caution():
|
||||
return False
|
||||
|
||||
item.set_data(value, column)
|
||||
|
||||
if column == 0:
|
||||
ensure_unique_names(item, include_self=False)
|
||||
|
||||
elif column == 1 and isinstance(item.data(1), (list, tuple)) \
|
||||
and item.type not in ('list', 'list_item'):
|
||||
|
||||
item.set_type('list')
|
||||
self.insertItems(0, list(item.get_list_items()), index)
|
||||
self.widget.ui.config_view.expandAll()
|
||||
|
||||
elif role == StateRole:
|
||||
item.set_state(value)
|
||||
|
||||
elif role == TypeRole:
|
||||
# if value != item.type and value == 'list': # when list is created:
|
||||
# pass
|
||||
item.set_type(value)
|
||||
|
||||
self.dataChanged.emit(index, index, (role,))
|
||||
|
||||
return True
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled # Qt.NoItemFlags
|
||||
item = index.internalPointer()
|
||||
|
||||
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
if index.column() == 0:
|
||||
if item.type != 'list_item':
|
||||
flags |= int(Qt.ItemIsDragEnabled)
|
||||
|
||||
if item.type == 'section':
|
||||
flags |= int(Qt.ItemIsDropEnabled)
|
||||
|
||||
not_section = not (index.column() > 0 and item.type == 'section')
|
||||
not_list_item = not (index.column() > 1 and item.type == 'list_item')
|
||||
not_list_val = not (index.column() == 1 and item.type == 'list')
|
||||
|
||||
if not_section and not_list_item and not_list_val:
|
||||
flags |= Qt.ItemIsEditable
|
||||
|
||||
return flags
|
||||
|
||||
def supportedDropActions(self):
|
||||
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
|
||||
|
||||
def mimeTypes(self):
|
||||
return ['app/configitem']
|
||||
|
||||
def mimeData(self, indexes):
|
||||
mimedata = QtCore.QMimeData()
|
||||
index = indexes[0]
|
||||
mimedata.setData('app/configitem', pickle.dumps(self.nodeFromIndex(index)))
|
||||
return mimedata
|
||||
|
||||
def dropMimeData(self, mimedata, action, row, column, parentIndex):
|
||||
if action == Qt.IgnoreAction:
|
||||
return True
|
||||
|
||||
droppedNode = deepcopy(pickle.loads(mimedata.data('app/configitem')))
|
||||
|
||||
self.insertItems(row, [droppedNode], parentIndex)
|
||||
self.dataChanged.emit(parentIndex, parentIndex)
|
||||
|
||||
self.widget.ui.config_view.expandAll()
|
||||
|
||||
if action & Qt.CopyAction:
|
||||
return False # to not delete original item
|
||||
return True
|
||||
|
||||
def removeRows(self, row, count, parent):
|
||||
self.beginRemoveRows(parent, row, row + count - 1)
|
||||
parentItem = self.nodeFromIndex(parent)
|
||||
for _ in range(count):
|
||||
parentItem.removeChild(row)
|
||||
|
||||
self.endRemoveRows()
|
||||
return True
|
||||
|
||||
def removeRow(self, index):
|
||||
parent = index.parent()
|
||||
self.beginRemoveRows(parent, index.row(), index.row())
|
||||
|
||||
parentItem = self.nodeFromIndex(parent)
|
||||
parentItem.removeChild(index.row())
|
||||
|
||||
self.endRemoveRows()
|
||||
return True
|
||||
|
||||
def insertItems(self, row, items, parentIndex):
|
||||
parent = self.nodeFromIndex(parentIndex)
|
||||
self.beginInsertRows(parentIndex, row, row + len(items) - 1) # parentIndex or QtCore.QModelIndex()
|
||||
|
||||
parent.addChildren(items, row)
|
||||
|
||||
self.endInsertRows()
|
||||
self.update_all()
|
||||
|
||||
return True
|
||||
|
||||
def update_all(self):
|
||||
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
|
||||
|
||||
def dict_setup(self, data: dict, parent=None, convert_types=False):
|
||||
if parent is None:
|
||||
parent = self.rootItem
|
||||
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
item = ConfigModelItem((key,), parent=parent, item_type='section')
|
||||
self.dict_setup(value, parent=item)
|
||||
else:
|
||||
if convert_types:
|
||||
value = convert_type(value)
|
||||
parent.appendChild(ConfigModelItem((key, value, '', '')))
|
||||
|
||||
def config_dict_setup(self, data: dict, convert_types=False, parent=None):
|
||||
if parent is None:
|
||||
parent = self.rootItem
|
||||
|
||||
self.initial_comment = '\n'.join(data.pop('initial_comment', ['']))
|
||||
self.final_comment = '\n'.join(data.pop('final_comment', ['']))
|
||||
|
||||
for key, item in data.items():
|
||||
if '__value__' in item:
|
||||
value = item.get('__value__')
|
||||
if convert_types:
|
||||
value = convert_type(value)
|
||||
|
||||
default = item['default']
|
||||
comments = '\n'.join(item.get('comments', '')) or ''
|
||||
inline_comment = item.get('inline_comment', '') or ''
|
||||
|
||||
if item['unchanged']:
|
||||
state = 'unchanged'
|
||||
elif value == default:
|
||||
state = 'default'
|
||||
else:
|
||||
state = 'normal'
|
||||
|
||||
parent.appendChild(ConfigModelItem((key, value, comments, inline_comment),
|
||||
state=state, default=default))
|
||||
|
||||
else:
|
||||
section = ConfigModelItem((key,), parent=parent, item_type='section')
|
||||
self.config_dict_setup(item, convert_types=convert_types, parent=section)
|
||||
section.check_state()
|
||||
|
||||
def to_dict(self, parent=None) -> dict:
|
||||
if parent is None:
|
||||
parent = self.rootItem
|
||||
|
||||
data = {}
|
||||
for item in parent.childItems:
|
||||
item_name, item_data = item.data(0), item.data(1)
|
||||
if item.is_section:
|
||||
data[item_name] = self.to_dict(item)
|
||||
else:
|
||||
data[item_name] = item_data
|
||||
|
||||
return data
|
||||
|
||||
def to_config_dict(self, parent=None) -> dict:
|
||||
data = {}
|
||||
|
||||
if parent is None:
|
||||
parent = self.rootItem
|
||||
data['initial_comment'] = self.initial_comment.split('\n')
|
||||
data['final_comment'] = self.final_comment.split('\n')
|
||||
|
||||
for item in parent.childItems:
|
||||
key = item.data(0)
|
||||
|
||||
if item.is_section:
|
||||
d = self.to_config_dict(item)
|
||||
if d: # to prevent empty sections
|
||||
data[key] = d
|
||||
|
||||
elif item.state not in ('unchanged', 'deleted'):
|
||||
if item.type == 'list':
|
||||
value = [child.data(1) for child in item.childItems]
|
||||
else:
|
||||
value = item.data(1)
|
||||
|
||||
d = {'__value__': value}
|
||||
|
||||
comment = item.data(2)
|
||||
if comment:
|
||||
d.update({'comments': comment.split('\n')})
|
||||
|
||||
inline_comment = item.data(3)
|
||||
if inline_comment:
|
||||
d.update({'inline_comment': inline_comment})
|
||||
|
||||
data[key] = d
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def dict(self):
|
||||
return self.to_dict()
|
||||
|
||||
|
||||
class ConfigTreeWidget(QTreeView):
|
||||
def __init__(self):
|
||||
QTreeView.__init__(self)
|
||||
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.open_menu)
|
||||
|
||||
self.setSelectionMode(self.SingleSelection)
|
||||
# self.setSelectionBehavior(self.SelectItems)
|
||||
|
||||
self.setDragDropMode(QAbstractItemView.DragDrop)
|
||||
self.setDefaultDropAction(Qt.MoveAction)
|
||||
self.setDragEnabled(True)
|
||||
self.setAcceptDrops(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
self.setAnimated(True)
|
||||
|
||||
self.duplicate_shortcut = QShortcut(QKeySequence('Shift+D'), self)
|
||||
self.duplicate_shortcut.activated.connect(self.with_selected(self.duplicate))
|
||||
self.exclude_shortcut = QShortcut(QKeySequence('Alt+Del'), self)
|
||||
self.exclude_shortcut.activated.connect(self.with_selected(self.exclude))
|
||||
self.remove_shortcut = QShortcut(QKeySequence('Del'), self)
|
||||
self.remove_shortcut.activated.connect(self.with_selected(self.remove))
|
||||
self.clear_shortcut = QShortcut(QKeySequence('Shift+R'), self)
|
||||
self.clear_shortcut.activated.connect(self.with_selected(self.reset_item, 'clear_value'))
|
||||
self.default_shortcut = QShortcut(QKeySequence('Ctrl+R'), self)
|
||||
self.default_shortcut.activated.connect(self.with_selected(self.reset_item, 'default'))
|
||||
self.reset_shortcut = QShortcut(QKeySequence('Alt+R'), self)
|
||||
self.reset_shortcut.activated.connect(self.with_selected(self.reset_item, 'all'))
|
||||
self.item_shortcut = QShortcut(QKeySequence('Shift+A'), self)
|
||||
self.item_shortcut.activated.connect(self.with_selected(self.add_item, False))
|
||||
self.section_shortcut = QShortcut(QKeySequence('Ctrl+A'), self)
|
||||
self.section_shortcut.activated.connect(self.with_selected(self.add_item, True))
|
||||
|
||||
def with_selected(self, f, *args, **kwargs):
|
||||
def decorated():
|
||||
index = self.selectedIndexes()[0]
|
||||
return f(index, *args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
def open_menu(self, point):
|
||||
index = self.indexAt(point)
|
||||
item = index.internalPointer()
|
||||
|
||||
menu = QMenu()
|
||||
|
||||
duplicate = QAction("Duplicate")
|
||||
duplicate.setShortcut(self.duplicate_shortcut.key())
|
||||
duplicate.triggered.connect(partial(self.duplicate, index))
|
||||
menu.addAction(duplicate)
|
||||
|
||||
exclude = QAction("Toggle exclude")
|
||||
exclude.setShortcut(self.exclude_shortcut.key())
|
||||
exclude.triggered.connect(partial(self.exclude, index))
|
||||
menu.addAction(exclude)
|
||||
|
||||
remove = QAction("Remove from config")
|
||||
remove.setShortcut(self.remove_shortcut.key())
|
||||
remove.triggered.connect(partial(self.remove, index))
|
||||
menu.addAction(remove)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
clear = QAction("Clear item value")
|
||||
clear.setShortcut(self.clear_shortcut.key())
|
||||
clear.triggered.connect(partial(self.reset_item, index, 'clear_value'))
|
||||
menu.addAction(clear)
|
||||
|
||||
reset_default = QAction("Reset value to default")
|
||||
reset_default.setShortcut(self.default_shortcut.key())
|
||||
reset_default.triggered.connect(partial(self.reset_item, index, 'default'))
|
||||
menu.addAction(reset_default)
|
||||
|
||||
reset_all = QAction("Reset all changes")
|
||||
reset_all.setShortcut(self.reset_shortcut.key())
|
||||
reset_all.triggered.connect(partial(self.reset_item, index, 'all'))
|
||||
menu.addAction(reset_all)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
add_option = QAction("Add option")
|
||||
add_option.setShortcut(self.item_shortcut.key())
|
||||
add_option.triggered.connect(partial(self.add_item, index, False))
|
||||
menu.addAction(add_option)
|
||||
|
||||
add_section = QAction("Add section")
|
||||
add_section.setShortcut(self.section_shortcut.key())
|
||||
add_section.triggered.connect(partial(self.add_item, index, True))
|
||||
menu.addAction(add_section)
|
||||
|
||||
if item is None:
|
||||
clear.setDisabled(True)
|
||||
reset_all.setDisabled(True)
|
||||
reset_default.setDisabled(True)
|
||||
|
||||
duplicate.setDisabled(True)
|
||||
remove.setDisabled(True)
|
||||
exclude.setDisabled(True)
|
||||
else:
|
||||
if item.type in ('list', 'list_item'):
|
||||
add_section.setDisabled(True)
|
||||
|
||||
if item.type == 'list':
|
||||
clear.setDisabled(True) # Temporary, cuz buggg
|
||||
|
||||
# if item.type == 'section':
|
||||
# clear.setDisabled(True)
|
||||
|
||||
menu.exec_(QCursor.pos())
|
||||
|
||||
def duplicate(self, index):
|
||||
item = deepcopy(index.internalPointer())
|
||||
item.set_state('added')
|
||||
ensure_unique_names(item)
|
||||
self.model().insertItems(index.row() + 1, [item], index.parent())
|
||||
self.expandAll() # fixes not expanded duplicated section
|
||||
|
||||
def remove(self, index):
|
||||
self.model().removeRow(index)
|
||||
|
||||
def exclude(self, index):
|
||||
item = self.model().nodeFromIndex(index)
|
||||
if item.state == 'deleted':
|
||||
self.model().setData(index, item.previous_state, StateRole)
|
||||
else:
|
||||
self.model().setData(index, 'deleted', StateRole)
|
||||
|
||||
def add_item(self, index, is_section):
|
||||
parentItem = self.model().nodeFromIndex(index)
|
||||
|
||||
if parentItem.type in ('list', 'list_item'):
|
||||
if is_section:
|
||||
return
|
||||
item_type = 'list_item'
|
||||
else:
|
||||
item_type = 'section' if is_section else 'option'
|
||||
|
||||
prompt = 'Enter {} name'.format(item_type.replace('_', ' '))
|
||||
text, ok = QInputDialog.getText(self, prompt, prompt)
|
||||
if not ok:
|
||||
return
|
||||
|
||||
if parentItem.type in ('list', 'section'): # to append at first index in section or list
|
||||
row = 0
|
||||
parent = index
|
||||
else:
|
||||
row = index.row()
|
||||
parent = index.parent()
|
||||
if row == -1: # to append at last position e.g. at root
|
||||
row = parentItem.childCount()
|
||||
else:
|
||||
row += 1 # to append under current position
|
||||
|
||||
item = ConfigModelItem((text, None, '', ''), item_type=item_type, state='added')
|
||||
self.model().insertItems(row, [item], parent)
|
||||
|
||||
ensure_unique_names(item, include_self=False)
|
||||
# parent.internalPointer().set_state('edited')
|
||||
self.expandAll()
|
||||
|
||||
def reset_item(self, index, reset_type):
|
||||
item = index.internalPointer()
|
||||
model = self.model()
|
||||
itemdataindex = model.modifyCol(index, 1)
|
||||
|
||||
if reset_type == 'all':
|
||||
for i, default in enumerate(item.default_values):
|
||||
model.setData(model.modifyCol(index, i), default)
|
||||
|
||||
model.setData(index, item.default_state, role=StateRole)
|
||||
|
||||
elif reset_type == 'default':
|
||||
# if item.type == 'list' and \
|
||||
# not isinstance(item.spec_default, (list, tuple)):
|
||||
# self.reset_item(item, 'clear_value')
|
||||
|
||||
model.setData(itemdataindex, item.spec_default)
|
||||
|
||||
if item.default_state == 'unchanged':
|
||||
model.setData(index, 'unchanged', role=StateRole)
|
||||
|
||||
elif reset_type == 'clear_value':
|
||||
item_type = model.data(itemdataindex, TypeRole)
|
||||
if item_type == 'list':
|
||||
return
|
||||
if item_type != 'section':
|
||||
model.setData(itemdataindex, None)
|
||||
|
||||
# if model.data(itemdataindex, TypeRole) == 'list': # TODO
|
||||
# model.removeRows(0, item.childCount(), index)
|
||||
# model.setData(index, 'option', role=TypeRole)
|
||||
# return
|
||||
|
||||
for child in model.childrenIndexes(index):
|
||||
self.reset_item(child, reset_type)
|
||||
|
||||
|
||||
class ConfigDialog(QtWidgets.QDialog):
|
||||
copter_editor_signal = QtCore.pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ConfigDialog, self).__init__(parent)
|
||||
self.ui = config_editor.Ui_config_dialog()
|
||||
self.model = ConfigModel(widget=self)
|
||||
self._filename = None
|
||||
self.unsaved = False
|
||||
self.setupUi()
|
||||
self.copter_editor_signal.connect(self._call_copter_dialog)
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return self._filename or 'Untitled.ini'
|
||||
|
||||
def setupModel(self, data, pure_dict=False, convert_types=False):
|
||||
if pure_dict:
|
||||
self.model.dict_setup(data, convert_types=convert_types)
|
||||
else:
|
||||
self.model.config_dict_setup(data, convert_types=convert_types)
|
||||
|
||||
self.ui.config_view.expandAll()
|
||||
self.ui.config_view.resizeColumnToContents(0)
|
||||
self.ui.config_view.resizeColumnToContents(1)
|
||||
|
||||
self.model.dataChanged.connect(self.unsaved_call) # connect after setup
|
||||
|
||||
def setupUi(self):
|
||||
self.ui.setupUi(self)
|
||||
|
||||
self.ui.config_view = ConfigTreeWidget()
|
||||
self.ui.config_view.setObjectName("config_view")
|
||||
self.ui.config_view.setModel(self.model)
|
||||
self.ui.gridLayout.addWidget(self.ui.config_view, 0, 0, 1, 1)
|
||||
self.ui.config_view.expandAll()
|
||||
|
||||
self.ui.do_coloring.stateChanged.connect(self.model.enable_color)
|
||||
self.ui.save_as_button.clicked.connect(self.save_as)
|
||||
|
||||
# self.ui.delete_button.pressed.connect(self.remove_selected)
|
||||
|
||||
def update_title(self):
|
||||
self.setWindowTitle(f"Config editor - {self.filename}" + "*"*self.unsaved)
|
||||
|
||||
def unsaved_call(self):
|
||||
self.unsaved = True
|
||||
self.update_title()
|
||||
self.model.dataChanged.disconnect(self.unsaved_call)
|
||||
|
||||
def closeEvent(self, event):
|
||||
if not self.unsaved or self.result():
|
||||
event.accept()
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(self, "Confirm exit", "There are unsaved changes in config file. "
|
||||
"Are you sure you want to exit?",
|
||||
QMessageBox.No | QMessageBox.Yes, QMessageBox.No)
|
||||
|
||||
if reply != QMessageBox.Yes:
|
||||
event.ignore()
|
||||
else:
|
||||
event.accept()
|
||||
|
||||
def edit_caution(self):
|
||||
reply = QMessageBox().warning(self, "Editing caution",
|
||||
"Are you sure you want to edit section/option name? "
|
||||
"Proceed with caution!",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
||||
)
|
||||
return reply == QMessageBox.Yes
|
||||
|
||||
def save_as(self):
|
||||
save_path = QFileDialog.getSaveFileName(self, "Save as configuration file (.ini)",
|
||||
directory=self.filename+'.ini',
|
||||
options=QFileDialog.DontConfirmOverwrite,
|
||||
filter="Config files (*.ini);;All files (*.*)")[0]
|
||||
if not save_path:
|
||||
return
|
||||
|
||||
split_path = save_path.split('.')
|
||||
|
||||
if not (len(split_path) > 1 and split_path[-1] == 'ini'):
|
||||
save_path += '.ini'
|
||||
|
||||
cfg = config.ConfigManager()
|
||||
cfg.load_from_dict(self.model.to_config_dict())
|
||||
cfg.config.filename = save_path
|
||||
cfg.write()
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self):
|
||||
self.show()
|
||||
self.exec()
|
||||
return self.result()
|
||||
|
||||
def validation_loop(self, cfg, configspec=None): # modifies cfg object
|
||||
filename = cfg.config.filename
|
||||
while True:
|
||||
if not self.run():
|
||||
return False
|
||||
|
||||
try:
|
||||
cfg.load_from_dict(self.model.to_config_dict(), configspec=configspec)
|
||||
except config.ValidationError as error:
|
||||
msg = "Can not validate. Proceed with editing? Errors: \n" + "\n".join(error.flatten_errors())
|
||||
reply = QMessageBox.warning(self, "Validation error!", msg, QMessageBox.Yes | QMessageBox.Cancel)
|
||||
|
||||
if reply == QMessageBox.Cancel:
|
||||
return False
|
||||
|
||||
else:
|
||||
return True
|
||||
|
||||
finally:
|
||||
if filename is not None:
|
||||
cfg.config.filename = filename
|
||||
|
||||
def call_copter_dialog(self, client, value):
|
||||
self.copter_editor_signal.emit(client, value)
|
||||
|
||||
@pyqtSlot(object, object)
|
||||
def _call_copter_dialog(self, client, value):
|
||||
logging.info("Opening copter config dialog")
|
||||
config_dict, spec_dict = value["config"], value["configspec"]
|
||||
cfg = config.ConfigManager()
|
||||
cfg.load_from_dict(config_dict, spec_dict)
|
||||
|
||||
def save_callback():
|
||||
edited_dict = cfg.full_dict(include_defaults=False)
|
||||
client.send_message("config", kwargs={"config": edited_dict, "mode": "rewrite"})
|
||||
|
||||
def restart_callback():
|
||||
client.send_message("service_restart", kwargs={"name": "clever-show"})
|
||||
|
||||
if not self.call_config_dialog(cfg, save_callback, restart_callback, f"{client.copter_id}"):
|
||||
return False
|
||||
return True
|
||||
|
||||
def call_config_dialog(self, cfg: config.ConfigManager, on_save=None, on_restart=None, name="Untitled.ini"):
|
||||
self.setupModel(cfg.full_dict(include_defaults=True), convert_types=(not cfg.validated))
|
||||
self.ui.do_restart.setEnabled(on_restart is not None)
|
||||
self._filename = name
|
||||
self.update_title()
|
||||
|
||||
if not self.validation_loop(cfg, cfg.config.configspec):
|
||||
return False
|
||||
|
||||
if on_save is not None:
|
||||
on_save()
|
||||
|
||||
if on_restart is not None and self.ui.do_restart.isChecked():
|
||||
on_restart()
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def call_standalone_dialog(cls):
|
||||
dialog = cls()
|
||||
dialog._call_standalone_dialog()
|
||||
|
||||
def _call_standalone_dialog(self):
|
||||
path = QFileDialog.getOpenFileName(self, "Select configuration or specification file",
|
||||
filter="Config and spec files (*.ini)")[0]
|
||||
if not path:
|
||||
return False
|
||||
|
||||
cfg = config.ConfigManager()
|
||||
try:
|
||||
cfg.load_from_file(path)
|
||||
except ValueError as error: # When file do not exist or not validated properly
|
||||
QMessageBox.warning(self, "Error while opening file!",
|
||||
"Config cannot be opened or validated: {}".format(error))
|
||||
return False
|
||||
|
||||
def save_callback():
|
||||
if cfg.config.filename is None:
|
||||
save_path = QFileDialog.getSaveFileName(self, "Save configuration file",
|
||||
directory=self.filename,
|
||||
filter="Config files (*.ini)")[0]
|
||||
if not save_path:
|
||||
return False
|
||||
else:
|
||||
save_path = cfg.config.filename
|
||||
|
||||
cfg.config.filename = save_path
|
||||
cfg.write()
|
||||
|
||||
if cfg.config.filename is not None:
|
||||
name = os.path.split(cfg.config.filename)[1]
|
||||
else: # when editing only configspec-based file
|
||||
name = os.path.split(path)[1]
|
||||
|
||||
if not self.call_config_dialog(cfg, on_save=save_callback, name=name):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def except_hook(cls, exception, traceback):
|
||||
print(cls, exception, traceback)
|
||||
sys.__excepthook__(cls, exception, traceback)
|
||||
|
||||
sys.excepthook = except_hook
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
ui = ConfigDialog()
|
||||
ui.call_standalone_dialog()
|
||||
# d = {'section': {'opt': 1, "opt222": 'text'}}
|
||||
# ui.setupModel(d, pure_dict=True)
|
||||
# ui.show()
|
||||
# app.exec()
|
||||
# print(ui.model.to_config_dict())
|
||||
557
Server/copter_table.py
Normal file
@@ -0,0 +1,557 @@
|
||||
from functools import partial
|
||||
from copy import deepcopy
|
||||
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
from PyQt5.QtCore import Qt as Qt, QObject, QEvent, QModelIndex
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtGui import QCursor
|
||||
from PyQt5.QtWidgets import QTableView, QMessageBox, QMenu, QAction, QWidgetAction, QListWidget, \
|
||||
QAbstractItemView, QListWidgetItem, QVBoxLayout, QHBoxLayout, QPushButton, QInputDialog, QLineEdit, QApplication
|
||||
|
||||
from config_editor_models import ConfigDialog
|
||||
import copter_table_models as table
|
||||
|
||||
|
||||
def save_preset(config, current, header_dict):
|
||||
presets = config.table_presets
|
||||
|
||||
for key in presets[HeaderEditWidget.default]:
|
||||
if key not in presets[current] and not header_dict[key][0]:
|
||||
header_dict.pop(key)
|
||||
|
||||
presets[current] = header_dict
|
||||
# config.write()
|
||||
|
||||
|
||||
class HeaderViewFilter(QObject):
|
||||
def __init__(self, parent, header, *args):
|
||||
super().__init__(parent, *args)
|
||||
self.header = header
|
||||
self._parent = parent
|
||||
|
||||
def eventFilter(self, object, event):
|
||||
if event.type() == QEvent.Enter:
|
||||
# logicalIndex = self.header.logicalIndexAt(event.pos())
|
||||
self.parent().cellHover.emit(QModelIndex())
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class CopterTableWidget(QTableView):
|
||||
override_cursors = {
|
||||
"copter_id": Qt.IBeamCursor,
|
||||
"config_version": Qt.OpenHandCursor,
|
||||
"selfcheck": Qt.PointingHandCursor,
|
||||
}
|
||||
|
||||
cellHover = QtCore.pyqtSignal(QModelIndex)
|
||||
cellEntered = QtCore.pyqtSignal(int, int)
|
||||
cellExited = QtCore.pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, model: table.CopterDataModel, config):
|
||||
QTableView.__init__(self)
|
||||
|
||||
self.config = config
|
||||
self.model = model
|
||||
|
||||
self.proxy_model = table.CopterProxyModel()
|
||||
self.proxy_model.setSourceModel(self.model)
|
||||
self.proxy_model.setDynamicSortFilter(True)
|
||||
|
||||
# Initiate table and table self.model
|
||||
self.setModel(self.proxy_model)
|
||||
|
||||
self.columns = self.model.columns # [header.strip() for header in self.model.headers] # header keys
|
||||
self.current_columns = self.columns[:]
|
||||
|
||||
self._last_hover_index = QtCore.QModelIndex()
|
||||
self._previous_cursor = None
|
||||
|
||||
self.cellHover.connect(self.cell_hover)
|
||||
self.cellExited.connect(self.cell_exited)
|
||||
self.cellEntered.connect(self.cell_entered)
|
||||
|
||||
header = self.horizontalHeader()
|
||||
self.filter = HeaderViewFilter(self, header)
|
||||
header.installEventFilter(self.filter)
|
||||
header.setCascadingSectionResizes(False)
|
||||
header.setStretchLastSection(True)
|
||||
header.setSectionsMovable(True)
|
||||
header.sectionMoved.connect(self.moved)
|
||||
header.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
header.customContextMenuRequested.connect(self.showHeaderMenu)
|
||||
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.open_menu)
|
||||
|
||||
# Adjust properties
|
||||
self.setTextElideMode(QtCore.Qt.ElideMiddle)
|
||||
self.setWordWrap(True)
|
||||
self.setSortingEnabled(True)
|
||||
self.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
|
||||
self.resizeColumnsToContents()
|
||||
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
||||
self.doubleClicked.connect(self.on_double_click)
|
||||
self.setDragDropMode(QAbstractItemView.DragDrop)
|
||||
self.setMouseTracking(True)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
super().mousePressEvent(event)
|
||||
index = self.indexAt(event.pos())
|
||||
if index.column() == -1 and index.row() == -1:
|
||||
self.clearSelection()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
self.cell_hover(self.indexAt(event.pos()))
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self.cell_hover(QtCore.QModelIndex())
|
||||
|
||||
def dragEnterEvent(self, *args, **kwargs):
|
||||
self.cell_hover(QtCore.QModelIndex())
|
||||
super().dragEnterEvent(*args, **kwargs)
|
||||
|
||||
def cell_hover(self, index):
|
||||
if index != self._last_hover_index:
|
||||
self.cellExited.emit(self._last_hover_index.row(), self._last_hover_index.column())
|
||||
self.cellEntered.emit(index.row(), index.column())
|
||||
|
||||
self._last_hover_index = QtCore.QPersistentModelIndex(index)
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def cell_entered(self, row, column):
|
||||
if column != -1 and self.columns[column] in self.override_cursors:
|
||||
self._previous_cursor = QApplication.overrideCursor()
|
||||
if self._previous_cursor is None:
|
||||
QApplication.setOverrideCursor(self.override_cursors[self.columns[column]])
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def cell_exited(self, row, column):
|
||||
# if self._previous_cursor is not None:
|
||||
# QApplication.setOverrideCursor(self._previous_cursor)
|
||||
if self._previous_cursor is None:
|
||||
QApplication.restoreOverrideCursor()
|
||||
|
||||
def moved(self, logical_index, old_index, new_index):
|
||||
name = self.current_columns.pop(old_index)
|
||||
self.current_columns.insert(new_index, name)
|
||||
|
||||
def set_column_order(self, order):
|
||||
if set(order) != set(self.current_columns):
|
||||
raise ValueError
|
||||
|
||||
for index_to, item in enumerate(order):
|
||||
index_from = self.current_columns.index(item)
|
||||
if index_to != index_from:
|
||||
self.horizontalHeader().moveSection(index_from, index_to)
|
||||
|
||||
def load_columns(self, item_dict: dict = None):
|
||||
presets = self.config.table_presets
|
||||
if item_dict is None:
|
||||
item_dict = presets[self.config.table_presets_current]
|
||||
|
||||
item_dict.update({key: (False, presets[HeaderEditWidget.default][key][1])
|
||||
for key in presets[HeaderEditWidget.default] if key not in item_dict})
|
||||
|
||||
self.set_column_order(item_dict.keys())
|
||||
# self.set_column_widths({key: val[1] for key, val in item_dict.items()})
|
||||
|
||||
for name, value in item_dict.items(): # for index, name in enumerate(self.columns):
|
||||
index = self.columns.index(name)
|
||||
show, width = value
|
||||
self.setColumnHidden(index, not show) # self.setColumnHidden(index, not item_dict.get(name, False))
|
||||
self.setColumnWidth(index, width)
|
||||
|
||||
def _get_column_item(self, column):
|
||||
index = self.columns.index(column)
|
||||
presets = self.config.table_presets
|
||||
show = not self.isColumnHidden(index)
|
||||
# columnWidth is 0 when hidden, trying to get previous width from config or default
|
||||
width = self.columnWidth(index) or \
|
||||
presets[self.config.table_presets_current].get(column, 0)[1] or \
|
||||
presets[HeaderEditWidget.default][column][1]
|
||||
return show, width
|
||||
|
||||
@property
|
||||
def item_dict(self):
|
||||
return {column: self._get_column_item(column) for column in self.current_columns}
|
||||
|
||||
def save_columns(self):
|
||||
current = self.config.table_presets_current
|
||||
header_dict = self.item_dict
|
||||
save_preset(self.config, current, header_dict)
|
||||
|
||||
def select_all(self, state):
|
||||
for i in range(self.model.rowCount()):
|
||||
self.model.update_data(i, 0, state, Qt.CheckStateRole)
|
||||
|
||||
def toggle_select(self):
|
||||
if len(list(self.model.user_selected())) == self.model.rowCount(): # if all items are selected
|
||||
state = Qt.Unchecked
|
||||
else:
|
||||
state = Qt.Checked
|
||||
self.select_all(state)
|
||||
|
||||
@pyqtSlot(QtCore.QModelIndex)
|
||||
def on_double_click(self, index):
|
||||
if self.model.is_column(index, "selfcheck"):
|
||||
data = self.proxy_model.data(index, role=table.ModelDataRole)
|
||||
if data and data != "OK":
|
||||
self._show_info("Selfcheck info", data)
|
||||
|
||||
def _show_info(self, title, data):
|
||||
dialog = QMessageBox()
|
||||
dialog.setIcon(QMessageBox.NoIcon)
|
||||
dialog.setStandardButtons(QMessageBox.Ok)
|
||||
dialog.setWindowTitle(title)
|
||||
dialog.setText("\n".join(data[:10]))
|
||||
dialog.setDetailedText("\n".join(data))
|
||||
dialog.exec()
|
||||
|
||||
def showHeaderMenu(self, event):
|
||||
self.save_columns()
|
||||
menu = QMenu(self)
|
||||
header_view = HeaderEditWidget(self, self.config, menu_mode=True, parent=menu)
|
||||
# header_view.setFixedHeight((header_view.geometry().height()-2) * len(header_view.columns))
|
||||
# box.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
action = QWidgetAction(menu)
|
||||
action.setDefaultWidget(header_view)
|
||||
menu.addAction(action)
|
||||
menu.exec_(QCursor.pos())
|
||||
header_view.save_preset()
|
||||
|
||||
@pyqtSlot(QtCore.QPoint)
|
||||
def open_menu(self, point):
|
||||
menu = QMenu(self)
|
||||
index = self.indexAt(point)
|
||||
item = self.model.get_row_data(index)
|
||||
|
||||
edit_config = QAction("Edit config")
|
||||
edit_config.triggered.connect(partial(self.edit_copter_config, item))
|
||||
menu.addAction(edit_config)
|
||||
|
||||
copy_config = QAction("Copy config to selected")
|
||||
copy_config.triggered.connect(partial(self.copy_config, item))
|
||||
menu.addAction(copy_config)
|
||||
|
||||
if item is None:
|
||||
edit_config.setDisabled(True)
|
||||
copy_config.setDisabled(True)
|
||||
|
||||
menu.exec_(QCursor.pos())
|
||||
|
||||
@pyqtSlot()
|
||||
def edit_copter_config(self, copter):
|
||||
dialog = ConfigDialog()
|
||||
copter.client.get_response("config", dialog.call_copter_dialog, request_kwargs={'send_configspec': True})
|
||||
|
||||
@pyqtSlot()
|
||||
def copy_config(self, copter):
|
||||
def send_callback(client, value):
|
||||
config = value["config"]
|
||||
config.pop("PRIVATE", None) # delete private section
|
||||
|
||||
for _copter in self.model.user_selected():
|
||||
if _copter.client is client:
|
||||
continue # don't send config back to the same copter
|
||||
_copter.client.send_message("config", kwargs={"config": config, "mode": "modify"})
|
||||
|
||||
copter.client.get_response("config", send_callback, request_kwargs={'send_configspec': False})
|
||||
|
||||
# def _selfcheck_shortener(self, data): # TODO!!!
|
||||
# shortened = []
|
||||
# for line in data:
|
||||
# if len(line) > 89:
|
||||
# pass
|
||||
# return shortened
|
||||
|
||||
|
||||
class HeaderListWidget(QListWidget):
|
||||
ColumnKeyRole = Qt.UserRole + 1000
|
||||
ColumnWidthRole = Qt.UserRole + 1001
|
||||
|
||||
dropped = QtCore.pyqtSignal(bool)
|
||||
|
||||
def __init__(self, parent=None, default_items=None):
|
||||
super().__init__(parent)
|
||||
if default_items is not None:
|
||||
self.populate_items(default_items)
|
||||
|
||||
self.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
self.setDefaultDropAction(Qt.MoveAction)
|
||||
|
||||
def populate_items(self, item_dict: dict):
|
||||
self.clear()
|
||||
for name, value in item_dict.items():
|
||||
visible, width = value
|
||||
flags = Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsEnabled
|
||||
state = Qt.Checked if visible else Qt.Unchecked
|
||||
|
||||
item = QListWidgetItem(table.CopterDataModel.columns_dict.get(name, "").strip() or name, self)
|
||||
item.setFlags(flags)
|
||||
item.setCheckState(state)
|
||||
item.setData(self.ColumnKeyRole, name)
|
||||
item.setData(self.ColumnWidthRole, width)
|
||||
|
||||
@property
|
||||
def item_dict(self):
|
||||
return {self.item(i).data(self.ColumnKeyRole):
|
||||
(bool(self.item(i).checkState()), self.item(i).data(self.ColumnWidthRole))
|
||||
for i in range(self.count())}
|
||||
|
||||
def dropEvent(self, event: QtGui.QDropEvent):
|
||||
super().dropEvent(event)
|
||||
self.dropped.emit(True)
|
||||
|
||||
|
||||
class ActiveHeaderListWidget(HeaderListWidget):
|
||||
def __init__(self, source: CopterTableWidget, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.source_widget = source
|
||||
|
||||
self.current_columns = source.current_columns
|
||||
self.columns = source.columns
|
||||
|
||||
self._populate_from_widget()
|
||||
|
||||
self.itemChanged.connect(self.on_itemChanged)
|
||||
|
||||
def _populate_from_widget(self):
|
||||
self.populate_items(self.source_widget.item_dict)
|
||||
|
||||
@pyqtSlot(QListWidgetItem)
|
||||
def on_itemChanged(self, item):
|
||||
key = item.data(HeaderListWidget.ColumnKeyRole)
|
||||
if key is None:
|
||||
return
|
||||
self.source_widget.setColumnHidden(self.columns.index(key), not bool(item.checkState()))
|
||||
|
||||
def dropEvent(self, event: QtGui.QDropEvent):
|
||||
super().dropEvent(event)
|
||||
column_order = [self.item(i).data(HeaderListWidget.ColumnKeyRole) for i in range(self.count())]
|
||||
self.source_widget.set_column_order(column_order)
|
||||
|
||||
|
||||
class HeaderEditWidget(QtWidgets.QWidget):
|
||||
add_new_text = "< add new >"
|
||||
default = "DEFAULT"
|
||||
|
||||
saved_signal = QtCore.pyqtSignal(bool)
|
||||
|
||||
def __init__(self, source, config, menu_mode=False, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# self.auto_apply = auto_apply
|
||||
self.source = source # source = copter table
|
||||
self.config = config
|
||||
self.menu_mode = menu_mode
|
||||
|
||||
self.preset_widget = QtWidgets.QComboBox()
|
||||
self.header_widget = ActiveHeaderListWidget(self.source) \
|
||||
if self.menu_mode else HeaderListWidget()
|
||||
# self.header_widget.itemChanged.connect(partial(self.saved_signal.emit, False))
|
||||
self.header_widget.model().dataChanged.connect(partial(self.saved_signal.emit, False))
|
||||
self.header_widget.dropped.connect(partial(self.saved_signal.emit, False))
|
||||
|
||||
self.previous = self.config.table_presets_current
|
||||
self.save = True
|
||||
|
||||
self.setupUi()
|
||||
|
||||
@pyqtSlot()
|
||||
def call_dialog(self):
|
||||
self.save_preset()
|
||||
self.save = False
|
||||
HeaderEditDialog(self.source, self.config).exec()
|
||||
|
||||
def setupUi(self):
|
||||
self.header_widget.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
|
||||
self.update_preset_list()
|
||||
self.preset_widget.currentTextChanged.connect(self.on_preset_changed)
|
||||
self.on_preset_changed(self.previous) # to init
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(self.header_widget)
|
||||
vbox.addWidget(self.preset_widget)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
if not self.menu_mode:
|
||||
add_button = QPushButton("Add")
|
||||
add_button.clicked.connect(self.add_preset)
|
||||
remove_button = QPushButton("Remove")
|
||||
remove_button.setToolTip("Permanently remove preset from config")
|
||||
remove_button.clicked.connect(self.remove_preset)
|
||||
save_button = QPushButton("Save")
|
||||
save_button.clicked.connect(self.save_preset)
|
||||
apply_button = QPushButton("Apply")
|
||||
apply_button.clicked.connect(self.apply_preset)
|
||||
apply_button.setDefault(True)
|
||||
apply_button.setFocus()
|
||||
|
||||
hbox.addWidget(add_button)
|
||||
hbox.addWidget(remove_button)
|
||||
hbox.addStretch()
|
||||
hbox.addWidget(save_button)
|
||||
hbox.addWidget(apply_button)
|
||||
else:
|
||||
dialog_button = QPushButton("Manage presets")
|
||||
dialog_button.clicked.connect(self.call_dialog)
|
||||
hbox.addWidget(dialog_button)
|
||||
|
||||
vbox.addLayout(hbox)
|
||||
self.setLayout(vbox)
|
||||
|
||||
def update_preset_list(self):
|
||||
self.preset_widget.clear()
|
||||
for name, preset in self.config.table_presets.items():
|
||||
if isinstance(preset, dict): # looking only for preset sections
|
||||
self.preset_widget.addItem(name)
|
||||
|
||||
self.preset_widget.addItem(self.add_new_text)
|
||||
self.preset_widget.setCurrentText(self.previous)
|
||||
|
||||
def on_preset_changed(self, index):
|
||||
if not index:
|
||||
return
|
||||
|
||||
if index == self.add_new_text:
|
||||
self.add_preset()
|
||||
return
|
||||
|
||||
self.previous = index
|
||||
presets = self.config.table_presets
|
||||
item_dict = {key: value for key, value in presets[index].items()}
|
||||
item_dict.update({key: (False, presets[self.default][key][1])
|
||||
for key in presets[self.default] if key not in item_dict})
|
||||
|
||||
if self.menu_mode:
|
||||
self.source.set_column_order(list(item_dict.keys())) # hidden\shown is hold by header widget's itemChanged
|
||||
for name, value in item_dict.items():
|
||||
self.source.setColumnWidth(self.source.columns.index(name), value[1])
|
||||
|
||||
self.config.table_presets_current = index
|
||||
self.header_widget.populate_items(item_dict)
|
||||
self.saved_signal.emit(True)
|
||||
|
||||
def add_preset(self):
|
||||
name, ok = QInputDialog.getText(None, "Enter new preset name", "Name:",
|
||||
QLineEdit.Normal, "")
|
||||
if not ok or not name:
|
||||
self.preset_widget.setCurrentText(self.previous)
|
||||
return
|
||||
|
||||
name = name.strip()
|
||||
if name in self.config.table_presets or name == self.default or name == self.add_new_text:
|
||||
QMessageBox.warning(None, "Preset already exists!", "Preset already exists!")
|
||||
self.preset_widget.setCurrentText(self.previous)
|
||||
return
|
||||
|
||||
self.config.table_presets[name] = deepcopy(dict(self.config.table_presets[self.default]))
|
||||
# self.config.write()
|
||||
|
||||
self.update_preset_list()
|
||||
self.preset_widget.setCurrentText(name)
|
||||
|
||||
def remove_preset(self):
|
||||
if self.preset_widget.currentText() == self.default:
|
||||
QMessageBox.warning(None, "Can't delete default preset!", "Can't delete default preset!")
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(None, "Action can't be undone", "Remove anyway?",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
|
||||
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
|
||||
self.config.table_presets.pop(self.preset_widget.currentText())
|
||||
# self.config.write()
|
||||
|
||||
self.previous = self.default
|
||||
self.update_preset_list()
|
||||
|
||||
@pyqtSlot()
|
||||
def save_preset(self):
|
||||
if not self.save: # don't save after calling dialog to avoid overrides
|
||||
return
|
||||
|
||||
current = self.preset_widget.currentText()
|
||||
header_dict = self.header_widget.item_dict
|
||||
save_preset(self.config, current, header_dict)
|
||||
self.saved_signal.emit(True)
|
||||
|
||||
@pyqtSlot()
|
||||
def apply_preset(self):
|
||||
self.config.table_presets_current = self.preset_widget.currentText()
|
||||
self.save_preset()
|
||||
self.source.load_columns()
|
||||
|
||||
|
||||
class HeaderEditDialog(QtWidgets.QDialog):
|
||||
def __init__(self, source, config, parent=None):
|
||||
super(HeaderEditDialog, self).__init__(parent=None)
|
||||
self.widget = HeaderEditWidget(source, config, menu_mode=False)
|
||||
self.setupUI()
|
||||
self.unsaved = False
|
||||
|
||||
self.widget.saved_signal.connect(self.update_title)
|
||||
self.update_title(True)
|
||||
|
||||
def setupUI(self):
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.widget)
|
||||
self.setLayout(layout)
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def update_title(self, saved):
|
||||
unsaved = not saved
|
||||
self.unsaved = unsaved
|
||||
self.setWindowTitle(f"Column preset editor - {self.widget.preset_widget.currentText()}"
|
||||
+ "*" * unsaved)
|
||||
|
||||
def closeEvent(self, event):
|
||||
if not self.unsaved:
|
||||
event.accept()
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(self, "Confirm exit", "There are unsaved changes in current preset. "
|
||||
"Are you sure you want to exit?",
|
||||
QMessageBox.No | QMessageBox.Yes, QMessageBox.No)
|
||||
|
||||
if reply != QMessageBox.Yes:
|
||||
event.ignore()
|
||||
else:
|
||||
event.accept()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
|
||||
def except_hook(cls, exception, traceback):
|
||||
sys.__excepthook__(cls, exception, traceback)
|
||||
|
||||
|
||||
sys.excepthook = except_hook # for debugging (exceptions traceback)
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
import copter_table_models
|
||||
|
||||
model = copter_table_models.CopterDataModel()
|
||||
# for i in range(10):
|
||||
# model.add_client(copter_table_models.StatedCopterData())
|
||||
|
||||
import config
|
||||
|
||||
c = config.ConfigManager()
|
||||
c.load_config_and_spec("config\server.ini")
|
||||
# print(c.config)
|
||||
# print(c._name_dict)
|
||||
w1 = CopterTableWidget(model, c)
|
||||
w = HeaderEditWidget(w1, c)
|
||||
print(w1.item_dict)
|
||||
# print(*w1.current_columns, sep='\n')
|
||||
# w.show()
|
||||
app.exec()
|
||||
@@ -1,222 +1,265 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import math
|
||||
import configparser
|
||||
import collections
|
||||
import indexed
|
||||
import time
|
||||
import subprocess
|
||||
from contextlib import suppress
|
||||
from functools import partialmethod
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5.QtCore import Qt as Qt
|
||||
from PyQt5.QtCore import Qt as Qt, QUrl, QDir
|
||||
|
||||
from config import ConfigManager
|
||||
|
||||
# Additional custom roles to interact with various table data
|
||||
ModelDataRole = 998
|
||||
ModelStateRole = 999
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read("server_config.ini")
|
||||
|
||||
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')
|
||||
def get_git_version(): # TODO import from animation
|
||||
try:
|
||||
return subprocess.check_output("git log --pretty=format:'%h' -n 1", shell=True).decode('UTF-8')
|
||||
except subprocess.CalledProcessError: # when no git repository info present
|
||||
return None # todo probably add special file
|
||||
|
||||
|
||||
class CheckState:
|
||||
def __init__(self, bool_state, color):
|
||||
self._bool = bool_state
|
||||
self.color = color
|
||||
self.brush = QtGui.QBrush(self.color)
|
||||
|
||||
def __bool__(self):
|
||||
return self._bool
|
||||
|
||||
|
||||
# State objects providing both boolean and color information for table
|
||||
# Add more if required
|
||||
true_state = CheckState(True, Qt.green)
|
||||
false_state = CheckState(False, Qt.red)
|
||||
missing_state = CheckState(False, Qt.yellow)
|
||||
outdated_state = CheckState(False, Qt.magenta)
|
||||
|
||||
|
||||
class ModelChecks:
|
||||
checks_dict = {}
|
||||
takeoff_checklist = (3, 4, 6, 7, 8)
|
||||
|
||||
battery_min = 50.0
|
||||
start_pos_delta_max = 1.0
|
||||
time_delta_max = 1.0
|
||||
check_current_pos = True
|
||||
check_git = True
|
||||
|
||||
@classmethod
|
||||
def col_check(cls, col):
|
||||
def column_check(cls, column, pass_context=False):
|
||||
def inner(f):
|
||||
def wrapper(item):
|
||||
if item is not None:
|
||||
return f(item)
|
||||
return None
|
||||
def wrapper(item, context=None):
|
||||
if item is None:
|
||||
return None
|
||||
if pass_context:
|
||||
return f(item, context)
|
||||
return f(item)
|
||||
|
||||
cls.checks_dict[col] = wrapper
|
||||
cls.checks_dict[column] = 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
|
||||
def check(cls, column, context):
|
||||
if isinstance(column, int):
|
||||
column = context.columns[column]
|
||||
item = context[column]
|
||||
try:
|
||||
return cls.checks_dict[column](item, context)
|
||||
except KeyError: # When there is no check
|
||||
return None if item is None else true_state # item is not None
|
||||
|
||||
|
||||
@ModelChecks.col_check(1)
|
||||
@ModelChecks.column_check("git_version")
|
||||
def check_ver(item):
|
||||
return True # TODO git version!
|
||||
if not ModelChecks.check_git:
|
||||
return True
|
||||
|
||||
version = get_git_version()
|
||||
if version is not None:
|
||||
return version == item
|
||||
return True
|
||||
|
||||
|
||||
@ModelChecks.col_check(2)
|
||||
@ModelChecks.column_check("animation_id")
|
||||
def check_anim(item):
|
||||
return str(item) != 'No animation'
|
||||
|
||||
|
||||
@ModelChecks.col_check(3)
|
||||
@ModelChecks.column_check("battery")
|
||||
def check_bat(item):
|
||||
if item == "NO_INFO":
|
||||
return False
|
||||
return item[1]*100 > battery_min
|
||||
return item[1] * 100 > ModelChecks.battery_min
|
||||
|
||||
|
||||
@ModelChecks.col_check(4)
|
||||
@ModelChecks.column_check("fcu_status")
|
||||
def check_sys_status(item):
|
||||
return item == "STANDBY"
|
||||
|
||||
|
||||
@ModelChecks.col_check(5)
|
||||
@ModelChecks.column_check("calibration_status")
|
||||
def check_cal_status(item):
|
||||
return item == "OK"
|
||||
|
||||
|
||||
@ModelChecks.col_check(6)
|
||||
@ModelChecks.column_check("mode")
|
||||
def check_mode(item):
|
||||
return (item != "NO_FCU") and not ("CMODE" in item)
|
||||
|
||||
|
||||
@ModelChecks.col_check(7)
|
||||
@ModelChecks.column_check("selfcheck")
|
||||
def check_selfcheck(item):
|
||||
return item == "OK"
|
||||
|
||||
|
||||
@ModelChecks.col_check(8)
|
||||
def check_pos_status(item):
|
||||
@ModelChecks.column_check("current_position")
|
||||
def check_pos(item):
|
||||
if not ModelChecks.check_current_pos:
|
||||
return True
|
||||
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.column_check("last_task")
|
||||
# def check_task(item):
|
||||
# return True
|
||||
|
||||
|
||||
@ModelChecks.col_check(10)
|
||||
@ModelChecks.column_check('time_delta')
|
||||
def check_time_delta(item):
|
||||
return abs(item) < time_delta_max
|
||||
return abs(item) < ModelChecks.time_delta_max
|
||||
|
||||
|
||||
@ModelChecks.column_check("start_position", pass_context=True)
|
||||
def check_start_pos(item, context):
|
||||
|
||||
if ModelChecks.start_pos_delta_max == 0:
|
||||
return True
|
||||
|
||||
if context.current_position is None:
|
||||
return item != 'NO_POS' # maybe should return true
|
||||
|
||||
delta = get_distance(get_position(context.current_position),
|
||||
get_position(context.start_position))
|
||||
if math.isnan(delta):
|
||||
return False
|
||||
|
||||
return delta < ModelChecks.start_pos_delta_max
|
||||
|
||||
|
||||
def get_position(position):
|
||||
if position != 'NO_POS' and position[0] != 'nan': # float('nan')?
|
||||
return position[:3]
|
||||
return [float('nan')] * 3
|
||||
|
||||
|
||||
def get_distance(pos1, pos2): # todo as common function
|
||||
if any(math.isnan(x) for x in pos1 + pos2):
|
||||
return float('nan')
|
||||
return math.sqrt(sum(map(lambda p: p[0] - p[1], zip(pos1, pos2))) ** 2) # point distance formula
|
||||
|
||||
|
||||
class CopterData:
|
||||
class_basic_attrs = indexed.IndexedOrderedDict([('copter_id', None), ('git_ver', None), ('anim_id', None),
|
||||
('battery', None), ('sys_status', None), ('cal_status', None),
|
||||
('mode', None), ('selfcheck', None), ('position', None),
|
||||
('start_pos', None), ('time_delta', None), ('client', None)])
|
||||
def __init__(self, columns=(), **kwargs):
|
||||
self.columns = columns
|
||||
for column in columns:
|
||||
setattr(self, column, None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.attrs_dict = self.class_basic_attrs.copy()
|
||||
self.attrs_dict.update(kwargs)
|
||||
|
||||
for attr, value in self.attrs_dict.items():
|
||||
for attr, value in kwargs.items():
|
||||
setattr(self, attr, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, self.attrs_dict.keys()[key])
|
||||
if key in self.columns:
|
||||
return getattr(self, key)
|
||||
return getattr(self, self.columns[key])
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, self.attrs_dict.keys()[key], value)
|
||||
if key in self.columns:
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
setattr(self, self.columns[key], value)
|
||||
|
||||
def __repr__(self):
|
||||
return str({key: self[key] for key in self.columns})
|
||||
|
||||
|
||||
class StatedCopterData(CopterData):
|
||||
class_basic_states = indexed.IndexedOrderedDict([("checked", 0), ("selfchecked", None), ("takeoff_ready", None),
|
||||
("copter_id", True), ])
|
||||
def __init__(self, columns=(), checks_defaults=None, checks_class=ModelChecks, **kwargs):
|
||||
if checks_defaults is None:
|
||||
checks_defaults = {}
|
||||
|
||||
def __init__(self, checks_class=ModelChecks, **kwargs):
|
||||
self.states = CopterData(**self.class_basic_states)
|
||||
self.checks = ModelChecks
|
||||
self.__dict__['states'] = CopterData(columns, **checks_defaults)
|
||||
self.__dict__['checks'] = checks_class
|
||||
self.__dict__['all_checks'] = None
|
||||
|
||||
super(StatedCopterData, self).__init__(**kwargs)
|
||||
super().__init__(columns, **kwargs)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self.__dict__[key] = value
|
||||
|
||||
if key in self.class_basic_attrs.keys():
|
||||
try:
|
||||
if key in self.columns:
|
||||
with suppress(KeyError):
|
||||
self.states.__dict__[key] = \
|
||||
ModelChecks.checks_dict[self.attrs_dict.keys().index(key)](value)
|
||||
if key == 'start_pos':
|
||||
if (self.__dict__['position'] is not None) and (self.__dict__['start_pos'] is not None):
|
||||
current_pos = get_position(self.__dict__['position'])
|
||||
start_pos = get_position(self.__dict__['start_pos'])
|
||||
delta = get_position_delta(current_pos, start_pos)
|
||||
if delta != 'NO_POS':
|
||||
self.states.__dict__[key] = (delta < start_pos_delta_max)
|
||||
except KeyError: # No check present for that col
|
||||
pass
|
||||
else: # update selfchecked and takeoff_ready
|
||||
self.states.__dict__["selfchecked"] = all(
|
||||
[self.states[i] for i in ModelChecks.checks_dict.keys()]
|
||||
)
|
||||
|
||||
self.states.__dict__["takeoff_ready"] = all(
|
||||
[self.states[i] for i in ModelChecks.takeoff_checklist]
|
||||
)
|
||||
|
||||
def get_position(pos_array):
|
||||
if pos_array[0] != 'nan' and pos_array != 'NO_POS':
|
||||
pos = []
|
||||
for i in range(3):
|
||||
pos.append(pos_array[i])
|
||||
else:
|
||||
pos = 'NO_POS'
|
||||
return pos
|
||||
|
||||
|
||||
def get_position_delta(pos1, pos2):
|
||||
if pos1 != 'NO_POS' and pos2 != 'NO_POS':
|
||||
delta_squared = 0
|
||||
for i in range(3):
|
||||
delta_squared += (pos1[i]-pos2[i])**2
|
||||
return math.sqrt(delta_squared)
|
||||
return 'NO_POS'
|
||||
self.checks.check(key, self)
|
||||
self.states.__dict__["all_checks"] = all([self.states[i] for i in self.checks.checks_dict.keys()])
|
||||
|
||||
|
||||
class ModelFormatter:
|
||||
view_formatters = {}
|
||||
place_formatters = {}
|
||||
VIEW_FORMATTER = False
|
||||
PLACE_FORMATTER = True
|
||||
VIEW_FORMATTER = 1
|
||||
PLACE_FORMATTER = 2
|
||||
|
||||
@classmethod
|
||||
def format_view(cls, col, value):
|
||||
if col in cls.view_formatters:
|
||||
return cls.view_formatters[col](value)
|
||||
return value
|
||||
def get_formatter(cls, formatter_type):
|
||||
if formatter_type == cls.PLACE_FORMATTER:
|
||||
return cls.place_formatters
|
||||
if formatter_type == cls.VIEW_FORMATTER:
|
||||
return cls.view_formatters
|
||||
raise ValueError('Unknown formatter type')
|
||||
|
||||
@classmethod
|
||||
def format_place(cls, col, value):
|
||||
if col in cls.place_formatters:
|
||||
return cls.place_formatters[col](value)
|
||||
return value
|
||||
def format(cls, column, value, formatter_type):
|
||||
formatters_dict = cls.get_formatter(formatter_type)
|
||||
if isinstance(column, int):
|
||||
column = CopterDataModel.columns[column]
|
||||
try:
|
||||
return formatters_dict[column](value)
|
||||
except KeyError:
|
||||
return value # when there is no formatter for the column
|
||||
|
||||
format_place = partialmethod(format, formatter_type=PLACE_FORMATTER)
|
||||
format_view = partialmethod(format, formatter_type=VIEW_FORMATTER)
|
||||
|
||||
@classmethod
|
||||
def col_format(cls, col, format_type):
|
||||
def column_formatter(cls, column, formatter_type):
|
||||
def inner(f):
|
||||
if format_type:
|
||||
cls.place_formatters[col] = f
|
||||
else:
|
||||
cls.view_formatters[col] = f
|
||||
formatters_dict = cls.get_formatter(formatter_type)
|
||||
formatters_dict[column] = f
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
def wrapper(value):
|
||||
return f(value)
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner
|
||||
|
||||
place_formatter = partialmethod(column_formatter, formatter_type=PLACE_FORMATTER)
|
||||
view_formatter = partialmethod(column_formatter, formatter_type=VIEW_FORMATTER)
|
||||
|
||||
@ModelFormatter.col_format(0, ModelFormatter.PLACE_FORMATTER)
|
||||
|
||||
@ModelFormatter.place_formatter("copter_id")
|
||||
def place_id(value):
|
||||
value = value.stip()
|
||||
value = str(value).strip()
|
||||
# check user hostname spelling http://man7.org/linux/man-pages/man7/hostname.7.html
|
||||
# '-' (hyphen) not first; latin letters/numbers/hyphens; length form 1 to 63
|
||||
# or matches command pattern
|
||||
@@ -234,7 +277,7 @@ def place_id(value):
|
||||
return None
|
||||
|
||||
|
||||
@ModelFormatter.col_format(3, ModelFormatter.PLACE_FORMATTER)
|
||||
@ModelFormatter.place_formatter("battery")
|
||||
def place_battery(value):
|
||||
if isinstance(value, list):
|
||||
battery_v, battery_p = value
|
||||
@@ -243,62 +286,98 @@ def place_battery(value):
|
||||
return value
|
||||
|
||||
|
||||
@ModelFormatter.col_format(3, ModelFormatter.VIEW_FORMATTER)
|
||||
@ModelFormatter.view_formatter("battery")
|
||||
def view_battery(value):
|
||||
if isinstance(value, list):
|
||||
battery_v, battery_p = value
|
||||
return "{:.1f}V {:d}%".format(battery_v, int(battery_p*100))
|
||||
return f"{battery_v:4.1f}V {min(battery_p, 1):4.0%}"
|
||||
return value
|
||||
|
||||
@ModelFormatter.col_format(7, ModelFormatter.VIEW_FORMATTER)
|
||||
|
||||
@ModelFormatter.view_formatter("selfcheck")
|
||||
def view_selfcheck(value):
|
||||
if isinstance(value, list):
|
||||
if len(value)==1:
|
||||
if len(value[0]) <= 8:
|
||||
return value[0]
|
||||
if len(value) == 1 and len(value[0]) <= 8:
|
||||
return value[0]
|
||||
return "ERROR"
|
||||
return value
|
||||
|
||||
@ModelFormatter.col_format(8, ModelFormatter.VIEW_FORMATTER)
|
||||
def view_selfcheck(value):
|
||||
|
||||
@ModelFormatter.view_formatter("current_position")
|
||||
def view_current_position(value):
|
||||
if isinstance(value, list):
|
||||
x, y, z, yaw, frame = value
|
||||
return "{:.2f} {:.2f} {:.2f} {:d} {}".format(x, y, z, int(yaw), frame)
|
||||
return f"{x: .2f} {y: .2f} {z: .2f} {int(yaw): d} {frame}"
|
||||
return value
|
||||
|
||||
@ModelFormatter.col_format(9, ModelFormatter.VIEW_FORMATTER)
|
||||
def view_selfcheck(value):
|
||||
|
||||
@ModelFormatter.view_formatter("start_position")
|
||||
def view_start_position(value):
|
||||
if isinstance(value, list):
|
||||
x, y, z = value
|
||||
return "{:.2f} {:.2f} {:.2f}".format(x, y, z)
|
||||
return f"{x: .2f} {y: .2f} {z: .2f}"
|
||||
return value
|
||||
|
||||
@ModelFormatter.col_format(10, ModelFormatter.PLACE_FORMATTER)
|
||||
|
||||
@ModelFormatter.place_formatter("last_task")
|
||||
def place_last_task(value):
|
||||
if value is None: # TODO possible behaviour deviation
|
||||
return 'No task'
|
||||
return value
|
||||
|
||||
|
||||
@ModelFormatter.place_formatter("time_delta")
|
||||
def place_time_delta(value):
|
||||
return abs(value - time.time())
|
||||
|
||||
|
||||
@ModelFormatter.col_format(10, ModelFormatter.VIEW_FORMATTER)
|
||||
@ModelFormatter.view_formatter("time_delta")
|
||||
def view_time_delta(value):
|
||||
return "{:.3f}".format(value)
|
||||
return f"{value:.3f}"
|
||||
|
||||
|
||||
class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
columns_dict = {'copter_id': 'copter ID',
|
||||
'git_version': 'version',
|
||||
'config_version': 'configuration',
|
||||
'animation_id': ' animation ID ',
|
||||
'battery': ' battery ',
|
||||
'fcu_status': 'FCU status',
|
||||
'calibration_status': 'sensors',
|
||||
'mode': ' mode ',
|
||||
'selfcheck': ' checks ',
|
||||
'current_position': 'current x y z yaw frame_id',
|
||||
'start_position': ' start x y z ',
|
||||
'last_task': 'last task',
|
||||
'time_delta': 'dt',
|
||||
}
|
||||
|
||||
columns = list(columns_dict.keys())
|
||||
|
||||
selected_ready_signal = QtCore.pyqtSignal(bool)
|
||||
selected_takeoff_ready_signal = QtCore.pyqtSignal(bool)
|
||||
selected_flip_ready_signal = QtCore.pyqtSignal(bool) # TODO fix this signals
|
||||
selected_calibrating_signal = QtCore.pyqtSignal(bool)
|
||||
selected_calibration_ready_signal = QtCore.pyqtSignal(bool)
|
||||
|
||||
def __init__(self, checks=ModelChecks, formatter=ModelFormatter, parent=None):
|
||||
update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant, QtCore.QVariant)
|
||||
add_client_signal = QtCore.pyqtSignal(object)
|
||||
remove_row_signal = QtCore.pyqtSignal(int)
|
||||
remove_client_signal = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, checks=ModelChecks, formatter=ModelFormatter, data_model=StatedCopterData, parent=None):
|
||||
super(CopterDataModel, self).__init__(parent)
|
||||
self.headers = ('copter ID', 'version', ' animation ID ', ' battery ', ' system ', 'sensors',
|
||||
' mode ', ' checks ', 'current x y z yaw frame_id', ' start x y z ', 'dt')
|
||||
self.headers = list(self.columns_dict.values())
|
||||
self.data_contents = []
|
||||
|
||||
self.checks = checks
|
||||
self.formatter = formatter
|
||||
self.data_model = data_model
|
||||
|
||||
self.first_col_is_checked = False
|
||||
self.update_data_signal.connect(self._update_data)
|
||||
self.add_client_signal.connect(self._add_client)
|
||||
self.remove_row_signal.connect(self._remove_row)
|
||||
self.remove_client_signal.connect(self._remove_row_data)
|
||||
|
||||
def insertRows(self, contents, position='last', parent=QtCore.QModelIndex()):
|
||||
rows = len(contents)
|
||||
@@ -312,48 +391,44 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
self.beginRemoveRows(QtCore.QModelIndex(), position, position + rows - 1)
|
||||
self.data_contents = self.data_contents[:position] + self.data_contents[position + rows:]
|
||||
self.endRemoveRows()
|
||||
|
||||
self.emit_signals()
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_column(cls, index, column_name):
|
||||
return index.column() == cls.columns.index(column_name)
|
||||
|
||||
def user_selected(self, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(lambda x: x.states.checked == Qt.Checked, contents)
|
||||
return self.filter(lambda x: x.states.checked == Qt.Checked, contents)
|
||||
|
||||
def selfchecked_ready(self, contents=()):
|
||||
def filter(self, f, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(lambda x: x.states.selfchecked, contents)
|
||||
return filter(f, contents)
|
||||
|
||||
def takeoff_ready(self, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(lambda x: x.states.takeoff_ready, contents)
|
||||
def selected_check(self, f, selected=()):
|
||||
selected = selected or set(self.user_selected())
|
||||
return bool(selected) and all(f(item) for item in selected) # selected.issubset(self.filter(f))
|
||||
|
||||
def flip_ready(self, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(flip_checks, contents) # possibly change as takeoff checks
|
||||
|
||||
def calibrating(self, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(calibrating_check, contents)
|
||||
|
||||
def calibration_ready(self, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(calibration_ready_check, contents)
|
||||
def get_row_data(self, index):
|
||||
row = index.row()
|
||||
if row == -1:
|
||||
return None
|
||||
try:
|
||||
return self.data_contents[row]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def get_row_index(self, row_data):
|
||||
try:
|
||||
index = self.data_contents.index(row_data)
|
||||
return self.data_contents.index(row_data)
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
return index
|
||||
|
||||
def get_row_by_attr(self, attr, value):
|
||||
try:
|
||||
row_data = next(filter(lambda x: getattr(x, attr, None) == value, self.data_contents))
|
||||
return next(filter(lambda x: getattr(x, attr, None) == value, self.data_contents))
|
||||
except StopIteration:
|
||||
return None
|
||||
else:
|
||||
return row_data
|
||||
|
||||
def rowCount(self, n=None):
|
||||
return len(self.data_contents)
|
||||
@@ -362,32 +437,25 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
return len(self.headers)
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if role == Qt.DisplayRole:
|
||||
if orientation == Qt.Horizontal:
|
||||
return self.headers[section]
|
||||
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
|
||||
return self.headers[section]
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
row = index.row()
|
||||
col = index.column()
|
||||
if role == Qt.DisplayRole or role == Qt.EditRole: # Separate editRole in case of editing non-text
|
||||
item = self.data_contents[row][col]
|
||||
return str(self.formatter.format_view(col, item)) if item is not None else ""
|
||||
return str(self.formatter.format_view(self.columns[col], item)) if item is not None else ""
|
||||
elif role == ModelDataRole:
|
||||
return self.data_contents[row][col]
|
||||
|
||||
elif role == Qt.BackgroundRole:
|
||||
try:
|
||||
item = self.data_contents[row]
|
||||
result = item.states[col]
|
||||
except KeyError:
|
||||
return QtGui.QBrush(Qt.white)
|
||||
else:
|
||||
if result is None:
|
||||
return QtGui.QBrush(Qt.yellow)
|
||||
if result:
|
||||
return QtGui.QBrush(Qt.green)
|
||||
else:
|
||||
return QtGui.QBrush(Qt.red)
|
||||
state = self.data_contents[row].states[col]
|
||||
if state is None:
|
||||
state = missing_state
|
||||
elif isinstance(state, bool):
|
||||
state = true_state if state else false_state
|
||||
return state.brush
|
||||
|
||||
elif role == Qt.CheckStateRole and col == 0:
|
||||
return self.data_contents[row].states.checked
|
||||
@@ -395,17 +463,14 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
if role == QtCore.Qt.TextAlignmentRole and col != 0:
|
||||
return QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
|
||||
|
||||
def update_model(self, index=QtCore.QModelIndex(), role=QtCore.Qt.EditRole):
|
||||
def emit_signals(self):
|
||||
selected = set(self.user_selected())
|
||||
|
||||
self.selected_ready_signal.emit(selected.issubset(self.selfchecked_ready()))
|
||||
self.selected_takeoff_ready_signal.emit(selected.issubset(self.takeoff_ready()))
|
||||
|
||||
self.selected_flip_ready_signal.emit(selected.issubset(self.flip_ready()))
|
||||
self.selected_calibrating_signal.emit(selected.issubset(self.calibrating()))
|
||||
self.selected_calibration_ready_signal.emit(selected.issubset(self.calibration_ready()))
|
||||
|
||||
self.dataChanged.emit(index, index, (role,))
|
||||
self.selected_ready_signal.emit(self.selected_check(lambda x: x.states.all_checks, selected))
|
||||
self.selected_takeoff_ready_signal.emit(self.selected_check(takeoff_checks, selected))
|
||||
self.selected_flip_ready_signal.emit(self.selected_check(flip_checks, selected))
|
||||
self.selected_calibrating_signal.emit(self.selected_check(calibrating_check, selected))
|
||||
self.selected_calibration_ready_signal.emit(self.selected_check(calibration_ready_check, selected))
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
@@ -418,70 +483,150 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
if role == Qt.CheckStateRole:
|
||||
self.data_contents[row].states.checked = value
|
||||
elif role == Qt.EditRole: # For user/outer actions with data, place modifiers applied
|
||||
formatted_value = self.formatter.format_place(col, value)
|
||||
if formatted_value is not None: # todo use new := syntax
|
||||
self.data_contents[row][col] = formatted_value
|
||||
formatted_value = self.formatter.format_place(self.columns[col], value)
|
||||
if formatted_value is None: # todo use new := syntax
|
||||
return False
|
||||
|
||||
if col == 0:
|
||||
self.data_contents[row].client.send_message("id", {"new_id": formatted_value})
|
||||
self.data_contents[row].client.remove()
|
||||
self.data_contents[row][col] = formatted_value
|
||||
|
||||
elif role == ModelDataRole: # For inner setting\editing of data
|
||||
if col == 0:
|
||||
self.data_contents[row].client.send_message("id", kwargs={"new_id": formatted_value})
|
||||
|
||||
elif role == ModelDataRole: # For inner setting\editing of raw data
|
||||
self.data_contents[row][col] = value
|
||||
elif role == ModelStateRole:
|
||||
self.data_contents[row].states[col] = value
|
||||
else:
|
||||
return False
|
||||
|
||||
self.update_model(index, role)
|
||||
self.emit_signals()
|
||||
self.dataChanged.emit(index, index, (role,))
|
||||
return True
|
||||
|
||||
def select_all(self): # probably NOT thread-safe!
|
||||
self.first_col_is_checked = not self.first_col_is_checked
|
||||
for row_num, copter in enumerate(self.data_contents):
|
||||
copter.states.checked = int(self.first_col_is_checked)*2
|
||||
self.update_model(self.index(row_num, 0), Qt.CheckStateRole)
|
||||
|
||||
def flags(self, index):
|
||||
roles = Qt.ItemIsSelectable | Qt.ItemIsEnabled
|
||||
if index.column() == 0:
|
||||
roles |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable
|
||||
if self.is_column(index, "config_version"):
|
||||
roles |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
|
||||
return roles
|
||||
|
||||
def supportedDropActions(self):
|
||||
return Qt.CopyAction | Qt.MoveAction
|
||||
|
||||
def mimeTypes(self):
|
||||
return ['text/uri-list']
|
||||
|
||||
def mimeData(self, indexes):
|
||||
index = indexes[0]
|
||||
if self.is_column(index, "config_version"):
|
||||
return self._config_mime(index)
|
||||
|
||||
return None
|
||||
|
||||
def _config_mime(self, index):
|
||||
mimedata = QtCore.QMimeData()
|
||||
path = os.path.join(QDir.tempPath(), "config_{}.ini".format(
|
||||
self.data_contents[index.row()].copter_id))
|
||||
|
||||
with suppress(OSError): # remove if file exists
|
||||
os.remove(path)
|
||||
|
||||
self.data_contents[index.row()].client.get_file("config/client.ini", path, )
|
||||
mimedata.setData("application/copter_row_info",
|
||||
bytes(self.data_contents[index.row()].copter_id, encoding="UTF-8"))
|
||||
mimedata.setUrls([QUrl.fromLocalFile(path)])
|
||||
|
||||
return mimedata
|
||||
|
||||
def dropMimeData(self, mimedata, action, row, column, index):
|
||||
if action == Qt.IgnoreAction:
|
||||
return True
|
||||
|
||||
if self.is_column(index, "config_version"):
|
||||
if not mimedata.hasUrls():
|
||||
return False
|
||||
if str(mimedata.data("application/copter_row_info")) == self.data_contents[index.row()].copter_id:
|
||||
return False # to protect from dropping to the same cell
|
||||
|
||||
# print(mimedata.hasUrls(), mimedata.urls, mimedata.formats())
|
||||
return self.drop_config(mimedata.urls()[0].toLocalFile(), index.row())
|
||||
|
||||
return True
|
||||
|
||||
def drop_config(self, path, row):
|
||||
if not ConfigManager.config_exists(path):
|
||||
return False
|
||||
config = ConfigManager()
|
||||
config.load_only_config(path)
|
||||
config_dict = config.full_dict(include_defaults=False)
|
||||
config_dict.pop("PRIVATE", None)
|
||||
|
||||
self.data_contents[row].client.send_message("config", kwargs={
|
||||
"config": config_dict, "mode": "rewrite"})
|
||||
return False
|
||||
|
||||
# Thread-safe wrappers
|
||||
def add_client(self, **kwargs):
|
||||
default_states = {"checked": 0, "copter_id": True}
|
||||
self.add_client_signal.emit(self.data_model(self.columns, default_states, **kwargs))
|
||||
|
||||
def remove_client_data(self, row_data):
|
||||
self.remove_client_signal.emit(row_data)
|
||||
|
||||
def remove_row(self, row):
|
||||
self.remove_row_signal.emit(row)
|
||||
|
||||
def update_data(self, row, col, data, role=ModelDataRole):
|
||||
self.update_data_signal.emit(row, col, data, role)
|
||||
|
||||
@QtCore.pyqtSlot(int, int, QtCore.QVariant, QtCore.QVariant)
|
||||
def update_item(self, row, col, value, role=Qt.EditRole):
|
||||
def _update_data(self, row, col, value, role=Qt.EditRole):
|
||||
self.setData(self.index(row, col), value, role)
|
||||
|
||||
@QtCore.pyqtSlot(object)
|
||||
def add_client(self, client):
|
||||
def _add_client(self, client):
|
||||
self.insertRows([client])
|
||||
|
||||
@QtCore.pyqtSlot(int) # Probably deprecated now
|
||||
def remove_row(self, row):
|
||||
def _remove_row(self, row):
|
||||
self.removeRows(row)
|
||||
|
||||
@QtCore.pyqtSlot(object)
|
||||
def remove_row_data(self, data):
|
||||
def _remove_row_data(self, data):
|
||||
row = self.get_row_index(data)
|
||||
if row is not None:
|
||||
self.removeRows(row)
|
||||
|
||||
|
||||
def check_checklist(copter_item, checklist=()):
|
||||
return all(copter_item.states[col] for col in checklist)
|
||||
|
||||
|
||||
def takeoff_checks(copter_item):
|
||||
checklist = ("battery", "fcu_status", "mode", "selfcheck", "current_position")
|
||||
return check_checklist(copter_item, checklist)
|
||||
|
||||
|
||||
def flip_checks(copter_item):
|
||||
for col in ModelChecks.takeoff_checklist:
|
||||
if col != 4 or col != 7:
|
||||
if not ModelChecks.checks_dict[col](copter_item[col]):
|
||||
return False
|
||||
elif copter_item[4] != "ACTIVE":
|
||||
return False
|
||||
checklist = ("battery", "mode", "current_position")
|
||||
if not check_checklist(copter_item, checklist):
|
||||
return False
|
||||
if copter_item["fcu_status"] != "ACTIVE":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# for col in checklist:
|
||||
# if not copter_item.state[col]: # ModelChecks.check(col, copter_item):
|
||||
# return False
|
||||
|
||||
def calibrating_check(copter_item):
|
||||
return copter_item[5] == "CALIBRATING"
|
||||
return copter_item["calibration_status"] == "CALIBRATING"
|
||||
|
||||
|
||||
def calibration_ready_check(copter_item):
|
||||
if not ModelChecks.checks_dict[4](copter_item[4]):
|
||||
if not copter_item.states["fcu_status"]: # ModelChecks.check("fcu_status", copter_item):
|
||||
return False
|
||||
return not calibrating_check(copter_item)
|
||||
|
||||
@@ -505,18 +650,10 @@ class CopterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
return self.human_sort_prepare(leftData) < self.human_sort_prepare(rightData)
|
||||
|
||||
|
||||
class SignalManager(QtCore.QObject):
|
||||
update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant, QtCore.QVariant)
|
||||
add_client_signal = QtCore.pyqtSignal(object)
|
||||
remove_row_signal = QtCore.pyqtSignal(int)
|
||||
remove_client_signal = QtCore.pyqtSignal(object)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
def timer():
|
||||
idc = 1001
|
||||
while True:
|
||||
@@ -524,11 +661,12 @@ if __name__ == '__main__':
|
||||
idc += 1
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
app = QtWidgets.QApplication.instance()
|
||||
if app is None:
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
tableView = QtWidgets.QTableView()
|
||||
myModel = CopterDataModel(None)
|
||||
myModel = CopterDataModel()
|
||||
proxyModel = CopterProxyModel()
|
||||
|
||||
proxyModel.setDynamicSortFilter(True)
|
||||
@@ -549,14 +687,15 @@ if __name__ == '__main__':
|
||||
msg = "[{}]: Failure: {}".format("FCU connection2", "Angular velocities estimation is not available")
|
||||
msgs.append(msg)
|
||||
|
||||
myModel.add_client(StatedCopterData(copter_id=1000, checked=0, selfcheck=msgs, time_utc=1))
|
||||
myModel.add_client(StatedCopterData(checked=2, selfcheck="OK", time_utc=2))
|
||||
myModel.add_client(StatedCopterData(checked=2, selfcheck="not ok", time_utc="no"))
|
||||
# myModel._add_client(StatedCopterData(copter_id=1000, checked=0, selfcheck=msgs, time_utc=1))
|
||||
# myModel._add_client(StatedCopterData(checked=2, selfcheck="OK", time_utc=2))
|
||||
# myModel._add_client(StatedCopterData(checked=2, selfcheck="not ok", time_utc="no"))
|
||||
myModel.add_client(copter_id=1000, client=None, git_version='11318ca', selfcheck=msgs)
|
||||
# myModel.setData(myModel.index(0, 1), "test")
|
||||
|
||||
myModel.setData(myModel.index(0, 1), "test")
|
||||
|
||||
t = threading.Thread(target=timer, daemon=True)
|
||||
t.start()
|
||||
# t = threading.Thread(target=timer, daemon=True)
|
||||
# t.start()
|
||||
print(QtCore.QT_VERSION_STR)
|
||||
|
||||
print(get_git_version())
|
||||
myModel.update_data(0, 3, [1, 2], role=Qt.EditRole)
|
||||
app.exec_()
|
||||
|
||||
BIN
Server/icons/coex_splash.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
Server/icons/image.ico
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
234
Server/server.py
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
@@ -7,60 +8,53 @@ import datetime
|
||||
import threading
|
||||
import selectors
|
||||
import collections
|
||||
import configparser
|
||||
import traceback
|
||||
|
||||
import os, inspect # Add parent dir to PATH to import messaging_lib
|
||||
import inspect # Add parent dir to PATH to import messaging_lib and config_lib
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import messaging_lib as messaging
|
||||
from config import ConfigManager
|
||||
|
||||
random.seed()
|
||||
|
||||
now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
path = 'server_logs'
|
||||
if not os.path.exists(path):
|
||||
log_path = 'server_logs'
|
||||
if not os.path.exists(log_path):
|
||||
try:
|
||||
os.mkdir(path)
|
||||
os.mkdir(log_path)
|
||||
except OSError:
|
||||
print("Creation of the directory %s failed" % path)
|
||||
print("Creation of the directory {} failed".format(log_path))
|
||||
else:
|
||||
print("Successfully created the directory %s " % path)
|
||||
print("Successfully created the directory {}".format(log_path))
|
||||
|
||||
logging.basicConfig( # TODO all prints as logs
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s [%(name)-7.7s] [%(threadName)-19.19s] [%(levelname)-7.7s] %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler("server_logs/{}.log".format(now)),
|
||||
logging.StreamHandler()
|
||||
])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ConfigOption = collections.namedtuple("ConfigOption", ["section", "option", "value"])
|
||||
|
||||
|
||||
class Server(messaging.Singleton):
|
||||
def __init__(self, server_id=None, config_path="server_config.ini", on_stop=None):
|
||||
def __init__(self, server_id=None, config_path="config/server.ini"):
|
||||
self.id = server_id if server_id else str(random.randint(0, 9999)).zfill(4)
|
||||
self.time_started = 0
|
||||
|
||||
self.on_stop = on_stop
|
||||
|
||||
# Init socket
|
||||
self.sel = selectors.DefaultSelector()
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
messaging.set_keepalive(self.server_socket)
|
||||
self.server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
self.host = socket.gethostname()
|
||||
self.ip = messaging.get_ip_address()
|
||||
|
||||
# Init configs
|
||||
self.config = ConfigManager()
|
||||
self.config_path = config_path
|
||||
self.config = configparser.ConfigParser()
|
||||
self.load_config()
|
||||
|
||||
# Init threads
|
||||
self.autoconnect_thread = threading.Thread(target=self._client_processor, daemon=True,
|
||||
@@ -69,63 +63,63 @@ class Server(messaging.Singleton):
|
||||
|
||||
self.broadcast_thread = threading.Thread(target=self._ip_broadcast, daemon=True,
|
||||
name='IP broadcast sender')
|
||||
self.broadcast_thread_running = threading.Event()
|
||||
self.broadcast_thread_running = threading.Event() # TODO replace by interrupt
|
||||
self.broadcast_thread_interrupt = threading.Event()
|
||||
|
||||
self.listener_thread = threading.Thread(target=self._broadcast_listen, daemon=True,
|
||||
name='IP broadcast listener')
|
||||
self.listener_thread_running = threading.Event()
|
||||
|
||||
def load_config(self):
|
||||
self.config.read(self.config_path)
|
||||
self.port = int(self.config['SERVER']['port']) # TODO try, init def
|
||||
self.BUFFER_SIZE = int(self.config['SERVER']['buffer_size']) # TODO connect to connection manager
|
||||
self.config.load_config_and_spec(self.config_path)
|
||||
|
||||
self.remove_disconnected = self.config.getboolean('SERVER', 'remove_disconnected')
|
||||
def start(self):
|
||||
# load config on startup
|
||||
self.load_config()
|
||||
|
||||
self.use_broadcast = self.config.getboolean('BROADCAST', 'use_broadcast')
|
||||
self.broadcast_port = int(self.config['BROADCAST']['broadcast_port'])
|
||||
self.BROADCAST_DELAY = int(self.config['BROADCAST']['broadcast_delay'])
|
||||
|
||||
self.USE_NTP = self.config.getboolean('NTP', 'use_ntp')
|
||||
self.NTP_HOST = self.config['NTP']['host']
|
||||
self.NTP_PORT = int(self.config['NTP']['port'])
|
||||
|
||||
def start(self, do_ip_broadcast=None): # do_auto_connect=True, , do_listen_broadcast=False
|
||||
self.time_started = time.time()
|
||||
|
||||
if do_ip_broadcast is None:
|
||||
do_ip_broadcast = self.use_broadcast
|
||||
|
||||
logging.info("Starting server with id: {} on {}:{} !".format(self.id, self.ip, self.port))
|
||||
logging.info("Starting server socket!")
|
||||
self.server_socket.bind((self.ip, self.port))
|
||||
logging.info("Starting server with id: {} on {}:{} ({})!".format(self.id, self.ip, self.config.server_port,
|
||||
socket.gethostname()))
|
||||
logging.info("Binding server socket!")
|
||||
self.server_socket.bind((self.ip, self.config.server_port))
|
||||
|
||||
logging.info("Starting client processor thread!")
|
||||
self.client_processor_thread_running.set()
|
||||
self.autoconnect_thread.start()
|
||||
|
||||
if do_ip_broadcast:
|
||||
if self.config.broadcast_send:
|
||||
logging.info("Starting broadcast sender thread!")
|
||||
self.broadcast_thread_running.set()
|
||||
self.broadcast_thread.start()
|
||||
|
||||
logging.info("Starting broadcast listener thread!")
|
||||
self.listener_thread_running.set()
|
||||
self.listener_thread.start()
|
||||
if self.config.broadcast_listen:
|
||||
logging.info("Starting broadcast listener thread!")
|
||||
self.listener_thread_running.set()
|
||||
self.listener_thread.start()
|
||||
|
||||
def stop(self):
|
||||
logging.info("Stopping server")
|
||||
|
||||
self.client_processor_thread_running.clear()
|
||||
|
||||
self.broadcast_thread_interrupt.set()
|
||||
self.broadcast_thread_running.clear()
|
||||
|
||||
self.listener_thread_running.clear()
|
||||
|
||||
messaging.NotifierSock().notify()
|
||||
|
||||
self.server_socket.close()
|
||||
self.sel.close()
|
||||
|
||||
messaging.NotifierSock().close()
|
||||
|
||||
logging.info("Server stopped")
|
||||
|
||||
if self.on_stop is not None:
|
||||
self.on_stop()
|
||||
|
||||
sys.exit("Stopped")
|
||||
def terminate(self, reason="Terminated"):
|
||||
self.stop()
|
||||
logging.critical(reason)
|
||||
|
||||
@staticmethod
|
||||
def get_ntp_time(ntp_host, ntp_port):
|
||||
@@ -137,11 +131,10 @@ class Server(messaging.Singleton):
|
||||
return int.from_bytes(msg[-8:], 'big') / 2 ** 32 - NTP_DELTA
|
||||
|
||||
def time_now(self):
|
||||
if self.USE_NTP:
|
||||
timenow = self.get_ntp_time(self.NTP_HOST, self.NTP_PORT)
|
||||
else:
|
||||
timenow = time.time()
|
||||
return timenow
|
||||
if self.config.ntp_use:
|
||||
return self.get_ntp_time(self.config.ntp_host, self.config.ntp_port)
|
||||
|
||||
return time.time()
|
||||
|
||||
# noinspection PyArgumentList
|
||||
def _client_processor(self):
|
||||
@@ -151,14 +144,11 @@ class Server(messaging.Singleton):
|
||||
|
||||
self.server_socket.listen()
|
||||
self.server_socket.setblocking(False)
|
||||
self.sel.register(self.server_socket, selectors.EVENT_READ, data=None) #| selectors.EVENT_WRITE
|
||||
self.sel.register(self.server_socket, selectors.EVENT_READ, data=None)
|
||||
|
||||
while self.client_processor_thread_running.is_set():
|
||||
events = self.sel.select()
|
||||
#logging.error('tick')
|
||||
events = self.sel.select(timeout=1)
|
||||
for key, mask in events:
|
||||
# logging.error(mask)
|
||||
# logging.error(str(key.data))
|
||||
client = key.data
|
||||
if client is None:
|
||||
self._connect_client(key.fileobj)
|
||||
@@ -167,6 +157,7 @@ class Server(messaging.Singleton):
|
||||
client.process_events(mask)
|
||||
except Exception as error:
|
||||
logging.error("Exception {} occurred for {}! Resetting connection!".format(error, client.addr))
|
||||
traceback.print_exc()
|
||||
client.close(True)
|
||||
else: # Notifier
|
||||
client.process_events(mask)
|
||||
@@ -174,13 +165,18 @@ class Server(messaging.Singleton):
|
||||
logging.info("Client autoconnect thread stopped!")
|
||||
|
||||
def _connect_client(self, sock):
|
||||
conn, addr = sock.accept()
|
||||
try:
|
||||
conn, addr = sock.accept()
|
||||
except OSError:
|
||||
logging.error("Error while connecting socket!")
|
||||
return
|
||||
|
||||
logging.info("Got connection from: {}".format(str(addr)))
|
||||
conn.setblocking(False)
|
||||
|
||||
if not any([client_addr == addr[0] for client_addr in Client.clients.keys()]):
|
||||
client = Client(addr[0])
|
||||
client.buffer_size = self.BUFFER_SIZE
|
||||
client.buffer_size = self.config.server_buffer_size
|
||||
logging.info("New client")
|
||||
else:
|
||||
client = Client.clients[addr[0]]
|
||||
@@ -191,72 +187,82 @@ class Server(messaging.Singleton):
|
||||
|
||||
def _ip_broadcast(self):
|
||||
logging.info("Broadcast sender thread started!")
|
||||
msg = messaging.MessageManager.create_simple_message(
|
||||
"server_ip", {"host": self.ip, "port": str(self.port), "id": self.id, "start_time": str(self.time_started)})
|
||||
msg = messaging.MessageManager.create_action_message(
|
||||
"server_ip", kwargs={"host": self.ip, "port": str(self.config.server_port), "id": self.id,
|
||||
"start_time": str(self.time_started)})
|
||||
logging.debug("Formed broadcast message to {}:{}: {}".format(self.config.broadcast_send_ip, self.config.broadcast_port, msg))
|
||||
|
||||
broadcast_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
broadcast_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
broadcast_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
logging.info("Formed broadcast message: {}".format(msg))
|
||||
|
||||
time.sleep(self.BROADCAST_DELAY)
|
||||
|
||||
while self.broadcast_thread_running.is_set():
|
||||
try:
|
||||
broadcast_sock.sendto(msg, ('255.255.255.255', self.broadcast_port))
|
||||
except OSError as e:
|
||||
logging.error("Exception occured while broadcasting: {}".format(e))
|
||||
except Exception as e:
|
||||
broadcast_sock.close()
|
||||
logging.info("Broadcast sender thread stopped, socked closed!")
|
||||
else:
|
||||
logging.debug("Broadcast sent")
|
||||
finally:
|
||||
time.sleep(self.BROADCAST_DELAY)
|
||||
try:
|
||||
while self.broadcast_thread_running.is_set():
|
||||
self.broadcast_thread_interrupt.wait(timeout=self.config.broadcast_delay)
|
||||
try:
|
||||
broadcast_sock.sendto(msg, (self.config.broadcast_send_ip, self.config.broadcast_port))
|
||||
except OSError as e:
|
||||
logging.error(f"Cannot send broadcast due error {e}")
|
||||
else:
|
||||
logging.debug("Broadcast sent")
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error {e}!")
|
||||
raise
|
||||
|
||||
def _broadcast_listen(self):
|
||||
logging.info("Broadcast listener thread started!")
|
||||
broadcast_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
broadcast_client.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
# broadcast_client.settimeout(1)
|
||||
try:
|
||||
broadcast_client.bind(("", self.broadcast_port))
|
||||
broadcast_client.bind(("", self.config.broadcast_port))
|
||||
except OSError:
|
||||
logging.critical("Another server is running on this computer, shutting down!")
|
||||
# TODO popup and as function
|
||||
self.stop()
|
||||
self.terminate("Another server is running on this computer, shutting down!")
|
||||
return
|
||||
|
||||
try:
|
||||
while self.listener_thread_running.is_set():
|
||||
data, addr = broadcast_client.recvfrom(1024) # TODO nonblock
|
||||
try:
|
||||
data, addr = broadcast_client.recvfrom(1024) # TODO nonblock
|
||||
except OSError:
|
||||
logging.error(f"Cannot receive broadcast due error {e}")
|
||||
continue
|
||||
|
||||
message = messaging.MessageManager()
|
||||
message.income_raw = data
|
||||
message.process_message()
|
||||
if message.content:
|
||||
if message.content["command"] == "server_ip":
|
||||
if message.content["args"]["id"] != str(self.id) \
|
||||
and float(message.content["args"]["start_time"]) <= self.time_started:
|
||||
content = message.content
|
||||
|
||||
# younger server should shut down
|
||||
logging.critical("Another server detected over the network, shutting down!")
|
||||
# TODO popup
|
||||
self.stop()
|
||||
right_command = (content and message.jsonheader["action"] == "server_ip")
|
||||
|
||||
if right_command:
|
||||
different_id = content["kwargs"]["id"] != str(self.id)
|
||||
self_younger = float(content["kwargs"]["start_time"]) <= self.time_started
|
||||
|
||||
if different_id and self_younger:
|
||||
# younger server should shut down
|
||||
self.terminate("Another server detected over the network, shutting down!")
|
||||
|
||||
else:
|
||||
logging.warning("Got wrong broadcast message from {}".format(addr))
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error {e}!")
|
||||
raise
|
||||
|
||||
finally:
|
||||
broadcast_client.close()
|
||||
logging.info("Broadcast listener thread stopped, socked closed!")
|
||||
|
||||
def send_starttime(self, copter, start_time):
|
||||
print('start_time: {}'.format(start_time))
|
||||
copter.send_message("start", {"time": str(start_time)})
|
||||
copter.send_message("start", kwargs={"time": str(start_time)})
|
||||
|
||||
|
||||
def requires_connect(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
if args[0].connected:
|
||||
return f(*args, **kwargs)
|
||||
else:
|
||||
logging.warning("Function requires client to be connected!")
|
||||
logging.warning("Function requires client to be connected!")
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -265,8 +271,7 @@ def requires_any_connected(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
if Client.clients:
|
||||
return f(*args, **kwargs)
|
||||
else:
|
||||
logging.warning("No clients were connected!")
|
||||
logging.warning("No clients were connected!")
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -279,7 +284,7 @@ class Client(messaging.ConnectionManager):
|
||||
on_disconnect = None
|
||||
|
||||
def __init__(self, ip):
|
||||
super(Client, self).__init__()
|
||||
super().__init__()
|
||||
self.copter_id = None
|
||||
self.connected = False
|
||||
|
||||
@@ -296,12 +301,12 @@ class Client(messaging.ConnectionManager):
|
||||
if not self.resume_queue:
|
||||
self._send_queue = collections.deque()
|
||||
|
||||
super(Client, self).connect(client_selector, client_socket, client_addr)
|
||||
super().connect(client_selector, client_socket, client_addr)
|
||||
|
||||
self.connected = True
|
||||
|
||||
if self.copter_id is None:
|
||||
self.get_response("id", self._got_id)
|
||||
#if self.copter_id is None:
|
||||
self.get_response("id", self._got_id)
|
||||
|
||||
if self.on_connect:
|
||||
self.on_connect(self)
|
||||
@@ -321,9 +326,9 @@ class Client(messaging.ConnectionManager):
|
||||
self.on_disconnect(self)
|
||||
|
||||
if inner:
|
||||
super(Client, self)._close()
|
||||
super()._close()
|
||||
else:
|
||||
super(Client, self).close()
|
||||
super().close()
|
||||
|
||||
logging.info("Connection to {} closed!".format(self.copter_id))
|
||||
|
||||
@@ -340,18 +345,9 @@ class Client(messaging.ConnectionManager):
|
||||
|
||||
@requires_connect
|
||||
def _send(self, data):
|
||||
super(Client, self)._send(data)
|
||||
super()._send(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))
|
||||
sending_options = [{'section': option.section, 'option': option.option, 'value': option.value}
|
||||
for option in options]
|
||||
print(sending_options)
|
||||
self.send_message(
|
||||
'config_write', {"options": sending_options, "reload": reload_config}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@requires_any_connected
|
||||
def broadcast(message, force_all=False):
|
||||
@@ -361,13 +357,21 @@ class Client(messaging.ConnectionManager):
|
||||
|
||||
@classmethod
|
||||
@requires_any_connected
|
||||
def broadcast_message(cls, command, args=None, force_all=False):
|
||||
cls.broadcast(messaging.MessageManager.create_simple_message(command, args), force_all)
|
||||
def broadcast_message(cls, command, args=(), kwargs=None, force_all=False):
|
||||
cls.broadcast(messaging.MessageManager.create_action_message(command, args, kwargs), force_all)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s [%(name)-7.7s] [%(threadName)-19.19s] [%(levelname)-7.7s] %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler("server_logs/{}.log".format(now)),
|
||||
logging.StreamHandler()
|
||||
])
|
||||
|
||||
server = Server()
|
||||
server.start()
|
||||
|
||||
while True:
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
[SERVER]
|
||||
port = 25000
|
||||
buffer_size = 10000
|
||||
remove_disconnected = False
|
||||
|
||||
[CHECKS]
|
||||
battery_percentage_min = 50
|
||||
start_pos_delta_max = 1
|
||||
time_delta_max = 1
|
||||
|
||||
[BROADCAST]
|
||||
use_broadcast = True
|
||||
broadcast_port = 8181
|
||||
broadcast_delay = 5
|
||||
|
||||
[NTP]
|
||||
use_ntp = False
|
||||
host = ntp1.stratum2.ru
|
||||
port = 123
|
||||
@@ -13,7 +13,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
class Ui_MainWindow(object):
|
||||
def setupUi(self, MainWindow):
|
||||
MainWindow.setObjectName("MainWindow")
|
||||
MainWindow.resize(1360, 761)
|
||||
MainWindow.resize(1360, 816)
|
||||
self.centralwidget = QtWidgets.QWidget(MainWindow)
|
||||
self.centralwidget.setEnabled(True)
|
||||
self.centralwidget.setObjectName("centralwidget")
|
||||
@@ -51,23 +51,23 @@ class Ui_MainWindow(object):
|
||||
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.formLayout.setWidget(0, 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.formLayout.setWidget(0, 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")
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.music_text)
|
||||
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.music_text)
|
||||
self.music_delay_spin = QtWidgets.QDoubleSpinBox(self.centralwidget)
|
||||
self.music_delay_spin.setDecimals(1)
|
||||
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.formLayout.setWidget(1, 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.formLayout.setWidget(2, 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)
|
||||
@@ -76,12 +76,12 @@ class Ui_MainWindow(object):
|
||||
self.music_checkbox.setSizePolicy(sizePolicy)
|
||||
self.music_checkbox.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.music_checkbox.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
|
||||
self.music_checkbox.setLayoutDirection(QtCore.Qt.RightToLeft)
|
||||
self.music_checkbox.setLayoutDirection(QtCore.Qt.LeftToRight)
|
||||
self.music_checkbox.setAutoFillBackground(False)
|
||||
self.music_checkbox.setText("")
|
||||
self.music_checkbox.setChecked(False)
|
||||
self.music_checkbox.setObjectName("music_checkbox")
|
||||
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.music_checkbox)
|
||||
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.music_checkbox)
|
||||
self.verticalLayout.addLayout(self.formLayout)
|
||||
self.line = QtWidgets.QFrame(self.centralwidget)
|
||||
self.line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
@@ -117,10 +117,10 @@ class Ui_MainWindow(object):
|
||||
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.formLayout_5.setWidget(0, 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.formLayout_5.setWidget(1, 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)
|
||||
@@ -146,14 +146,13 @@ class Ui_MainWindow(object):
|
||||
self.formLayout_3 = QtWidgets.QFormLayout()
|
||||
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.formLayout_3.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.disarm_all_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.formLayout_3.setWidget(1, 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)
|
||||
@@ -164,22 +163,17 @@ class Ui_MainWindow(object):
|
||||
self.formLayout_4.setLabelAlignment(QtCore.Qt.AlignCenter)
|
||||
self.formLayout_4.setFormAlignment(QtCore.Qt.AlignCenter)
|
||||
self.formLayout_4.setObjectName("formLayout_4")
|
||||
self.flip_button = QtWidgets.QPushButton(self.centralwidget)
|
||||
self.flip_button.setObjectName("flip_button")
|
||||
self.formLayout_4.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.flip_button)
|
||||
self.leds_button = QtWidgets.QPushButton(self.centralwidget)
|
||||
self.leds_button.setObjectName("leds_button")
|
||||
self.formLayout_4.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.leds_button)
|
||||
self.takeoff_button = QtWidgets.QPushButton(self.centralwidget)
|
||||
self.takeoff_button.setEnabled(True)
|
||||
self.takeoff_button.setObjectName("takeoff_button")
|
||||
self.formLayout_4.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.takeoff_button)
|
||||
self.leds_button = QtWidgets.QPushButton(self.centralwidget)
|
||||
self.leds_button.setObjectName("leds_button")
|
||||
self.formLayout_4.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.leds_button)
|
||||
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout_2.setContentsMargins(-1, 0, -1, -1)
|
||||
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
|
||||
self.z_checkbox = QtWidgets.QCheckBox(self.centralwidget)
|
||||
self.z_checkbox.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||
self.z_checkbox.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
self.z_checkbox.setLayoutDirection(QtCore.Qt.LeftToRight)
|
||||
self.z_checkbox.setObjectName("z_checkbox")
|
||||
self.horizontalLayout_2.addWidget(self.z_checkbox)
|
||||
@@ -189,7 +183,10 @@ class Ui_MainWindow(object):
|
||||
self.z_spin.setProperty("value", 1.0)
|
||||
self.z_spin.setObjectName("z_spin")
|
||||
self.horizontalLayout_2.addWidget(self.z_spin)
|
||||
self.formLayout_4.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_2)
|
||||
self.formLayout_4.setLayout(2, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_2)
|
||||
self.flip_button = QtWidgets.QPushButton(self.centralwidget)
|
||||
self.flip_button.setObjectName("flip_button")
|
||||
self.formLayout_4.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.flip_button)
|
||||
self.verticalLayout.addLayout(self.formLayout_4)
|
||||
self.line_4 = QtWidgets.QFrame(self.centralwidget)
|
||||
self.line_4.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
@@ -215,27 +212,29 @@ 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, 22))
|
||||
self.menubar.setGeometry(QtCore.QRect(0, 0, 1360, 25))
|
||||
self.menubar.setObjectName("menubar")
|
||||
self.menuOptions = QtWidgets.QMenu(self.menubar)
|
||||
self.menuOptions.setObjectName("menuOptions")
|
||||
self.menuDeveloper_mode = QtWidgets.QMenu(self.menuOptions)
|
||||
self.menuMusic_2 = QtWidgets.QMenu(self.menuOptions)
|
||||
self.menuMusic_2.setObjectName("menuMusic_2")
|
||||
self.menuTable = QtWidgets.QMenu(self.menubar)
|
||||
self.menuTable.setObjectName("menuTable")
|
||||
self.menuDrone_2 = QtWidgets.QMenu(self.menubar)
|
||||
self.menuDrone_2.setObjectName("menuDrone_2")
|
||||
self.menuSend = QtWidgets.QMenu(self.menuDrone_2)
|
||||
self.menuSend.setObjectName("menuSend")
|
||||
self.menuRestart = QtWidgets.QMenu(self.menuDrone_2)
|
||||
self.menuRestart.setObjectName("menuRestart")
|
||||
self.menuDeveloper_mode = QtWidgets.QMenu(self.menuDrone_2)
|
||||
self.menuDeveloper_mode.setObjectName("menuDeveloper_mode")
|
||||
self.menuAnimation = QtWidgets.QMenu(self.menubar)
|
||||
self.menuAnimation.setObjectName("menuAnimation")
|
||||
self.menuDrone = QtWidgets.QMenu(self.menubar)
|
||||
self.menuDrone.setObjectName("menuDrone")
|
||||
self.menuDeveloper_mode_2 = QtWidgets.QMenu(self.menuDrone)
|
||||
self.menuDeveloper_mode_2.setObjectName("menuDeveloper_mode_2")
|
||||
self.menuMusic = QtWidgets.QMenu(self.menubar)
|
||||
self.menuMusic.setObjectName("menuMusic")
|
||||
MainWindow.setMenuBar(self.menubar)
|
||||
self.action_send_animations = QtWidgets.QAction(MainWindow)
|
||||
self.action_send_animations.setObjectName("action_send_animations")
|
||||
self.action_send_configurations = QtWidgets.QAction(MainWindow)
|
||||
self.action_send_configurations.setObjectName("action_send_configurations")
|
||||
self.action_send_Aruco_map = QtWidgets.QAction(MainWindow)
|
||||
self.action_send_Aruco_map.setObjectName("action_send_Aruco_map")
|
||||
self.action_send_aruco_map = QtWidgets.QAction(MainWindow)
|
||||
self.action_send_aruco_map.setObjectName("action_send_aruco_map")
|
||||
self.action_update_client_repo = QtWidgets.QAction(MainWindow)
|
||||
self.action_update_client_repo.setObjectName("action_update_client_repo")
|
||||
self.actionSend_launch_file_for_clever = QtWidgets.QAction(MainWindow)
|
||||
@@ -280,38 +279,74 @@ class Ui_MainWindow(object):
|
||||
self.action_restart_chrony.setObjectName("action_restart_chrony")
|
||||
self.action_send_fcu_parameters = QtWidgets.QAction(MainWindow)
|
||||
self.action_send_fcu_parameters.setObjectName("action_send_fcu_parameters")
|
||||
self.menuDeveloper_mode.addAction(self.action_send_any_file)
|
||||
self.menuDeveloper_mode.addAction(self.action_send_any_command)
|
||||
self.menuOptions.addAction(self.action_send_animations)
|
||||
self.menuOptions.addAction(self.action_send_configurations)
|
||||
self.menuOptions.addAction(self.action_send_launch_file)
|
||||
self.menuOptions.addAction(self.action_send_Aruco_map)
|
||||
self.menuOptions.addAction(self.action_send_calibrations)
|
||||
self.menuOptions.addAction(self.action_send_fcu_parameters)
|
||||
self.action_toggle_select = QtWidgets.QAction(MainWindow)
|
||||
self.action_toggle_select.setObjectName("action_toggle_select")
|
||||
self.action_select_all = QtWidgets.QAction(MainWindow)
|
||||
self.action_select_all.setObjectName("action_select_all")
|
||||
self.action_deselect_all = QtWidgets.QAction(MainWindow)
|
||||
self.action_deselect_all.setObjectName("action_deselect_all")
|
||||
self.action_edit_server_config = QtWidgets.QAction(MainWindow)
|
||||
self.action_edit_server_config.setObjectName("action_edit_server_config")
|
||||
self.action_edit_any_config = QtWidgets.QAction(MainWindow)
|
||||
self.action_edit_any_config.setObjectName("action_edit_any_config")
|
||||
self.action_update_server_git = QtWidgets.QAction(MainWindow)
|
||||
self.action_update_server_git.setEnabled(False)
|
||||
self.action_update_server_git.setVisible(False)
|
||||
self.action_update_server_git.setObjectName("action_update_server_git")
|
||||
self.action_retrive_any_file = QtWidgets.QAction(MainWindow)
|
||||
self.action_retrive_any_file.setObjectName("action_retrive_any_file")
|
||||
self.action_restart_server = QtWidgets.QAction(MainWindow)
|
||||
self.action_restart_server.setObjectName("action_restart_server")
|
||||
self.action_configure_columns = QtWidgets.QAction(MainWindow)
|
||||
self.action_configure_columns.setObjectName("action_configure_columns")
|
||||
self.actionSomething = QtWidgets.QAction(MainWindow)
|
||||
self.actionSomething.setObjectName("actionSomething")
|
||||
self.menuMusic_2.addAction(self.action_select_music_file)
|
||||
self.menuMusic_2.addAction(self.action_play_music)
|
||||
self.menuMusic_2.addAction(self.action_stop_music)
|
||||
self.menuOptions.addAction(self.menuMusic_2.menuAction())
|
||||
self.menuOptions.addSeparator()
|
||||
self.menuOptions.addAction(self.menuDeveloper_mode.menuAction())
|
||||
self.menuOptions.addAction(self.action_edit_server_config)
|
||||
self.menuOptions.addAction(self.action_edit_any_config)
|
||||
self.menuOptions.addSeparator()
|
||||
self.menuOptions.addAction(self.action_select_all_rows)
|
||||
self.menuAnimation.addAction(self.action_set_start_to_current_position)
|
||||
self.menuAnimation.addAction(self.action_reset_start)
|
||||
self.menuDeveloper_mode_2.addAction(self.action_restart_clever)
|
||||
self.menuDeveloper_mode_2.addAction(self.action_restart_clever_show)
|
||||
self.menuDeveloper_mode_2.addAction(self.action_update_client_repo)
|
||||
self.menuDeveloper_mode_2.addAction(self.action_reboot_all)
|
||||
self.menuDrone.addAction(self.action_set_z_offset_to_ground)
|
||||
self.menuDrone.addAction(self.action_reset_z_offset)
|
||||
self.menuDrone.addSeparator()
|
||||
self.menuDrone.addAction(self.action_restart_chrony)
|
||||
self.menuDrone.addAction(self.action_remove_row)
|
||||
self.menuDrone.addSeparator()
|
||||
self.menuDrone.addAction(self.menuDeveloper_mode_2.menuAction())
|
||||
self.menuMusic.addAction(self.action_select_music_file)
|
||||
self.menuMusic.addAction(self.action_play_music)
|
||||
self.menuMusic.addAction(self.action_stop_music)
|
||||
self.menuOptions.addAction(self.action_update_server_git)
|
||||
self.menuOptions.addAction(self.action_restart_server)
|
||||
self.menuTable.addAction(self.action_toggle_select)
|
||||
self.menuTable.addAction(self.action_select_all)
|
||||
self.menuTable.addAction(self.action_deselect_all)
|
||||
self.menuTable.addSeparator()
|
||||
self.menuTable.addAction(self.action_remove_row)
|
||||
self.menuTable.addSeparator()
|
||||
self.menuTable.addAction(self.action_configure_columns)
|
||||
self.menuSend.addAction(self.action_send_animations)
|
||||
self.menuSend.addAction(self.action_send_configurations)
|
||||
self.menuSend.addAction(self.action_send_launch_file)
|
||||
self.menuSend.addAction(self.action_send_aruco_map)
|
||||
self.menuSend.addAction(self.action_send_calibrations)
|
||||
self.menuSend.addAction(self.action_send_fcu_parameters)
|
||||
self.menuSend.addSeparator()
|
||||
self.menuSend.addAction(self.action_send_any_file)
|
||||
self.menuSend.addAction(self.action_send_any_command)
|
||||
self.menuRestart.addAction(self.action_restart_chrony)
|
||||
self.menuRestart.addAction(self.action_restart_clever)
|
||||
self.menuRestart.addAction(self.action_restart_clever_show)
|
||||
self.menuRestart.addSeparator()
|
||||
self.menuDeveloper_mode.addAction(self.action_update_client_repo)
|
||||
self.menuDrone_2.addAction(self.menuSend.menuAction())
|
||||
self.menuDrone_2.addAction(self.action_retrive_any_file)
|
||||
self.menuDrone_2.addAction(self.menuRestart.menuAction())
|
||||
self.menuDrone_2.addSeparator()
|
||||
self.menuDrone_2.addAction(self.action_set_start_to_current_position)
|
||||
self.menuDrone_2.addAction(self.action_reset_start)
|
||||
self.menuDrone_2.addAction(self.action_set_z_offset_to_ground)
|
||||
self.menuDrone_2.addAction(self.action_reset_z_offset)
|
||||
self.menuDrone_2.addSeparator()
|
||||
self.menuDrone_2.addAction(self.menuDeveloper_mode.menuAction())
|
||||
self.menuDrone_2.addSeparator()
|
||||
self.menuDrone_2.addAction(self.action_reboot_all)
|
||||
self.menubar.addAction(self.menuDrone_2.menuAction())
|
||||
self.menubar.addAction(self.menuOptions.menuAction())
|
||||
self.menubar.addAction(self.menuDrone.menuAction())
|
||||
self.menubar.addAction(self.menuAnimation.menuAction())
|
||||
self.menubar.addAction(self.menuMusic.menuAction())
|
||||
self.menubar.addAction(self.menuTable.menuAction())
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
@@ -334,43 +369,58 @@ class Ui_MainWindow(object):
|
||||
self.emergency_land_button.setText(_translate("MainWindow", "Emergency land"))
|
||||
self.disarm_all_button.setText(_translate("MainWindow", "Disarm ALL"))
|
||||
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"))
|
||||
self.takeoff_button.setText(_translate("MainWindow", "Takeoff"))
|
||||
self.z_checkbox.setText(_translate("MainWindow", " Z ="))
|
||||
self.z_spin.setSuffix(_translate("MainWindow", " m"))
|
||||
self.flip_button.setText(_translate("MainWindow", "Flip"))
|
||||
self.reboot_fcu.setText(_translate("MainWindow", "Reboot FCU"))
|
||||
self.calibrate_gyro.setText(_translate("MainWindow", "Calibrate gyro"))
|
||||
self.calibrate_level.setText(_translate("MainWindow", "Calibrate level"))
|
||||
self.menuOptions.setTitle(_translate("MainWindow", "Server"))
|
||||
self.menuMusic_2.setTitle(_translate("MainWindow", "Music"))
|
||||
self.menuTable.setTitle(_translate("MainWindow", "Table"))
|
||||
self.menuDrone_2.setTitle(_translate("MainWindow", "Selected drones"))
|
||||
self.menuSend.setTitle(_translate("MainWindow", "Send"))
|
||||
self.menuRestart.setTitle(_translate("MainWindow", "Restart service"))
|
||||
self.menuDeveloper_mode.setTitle(_translate("MainWindow", "Developer mode"))
|
||||
self.menuAnimation.setTitle(_translate("MainWindow", "Animation"))
|
||||
self.menuDrone.setTitle(_translate("MainWindow", "Drone"))
|
||||
self.menuDeveloper_mode_2.setTitle(_translate("MainWindow", "Developer mode"))
|
||||
self.menuMusic.setTitle(_translate("MainWindow", "Music"))
|
||||
self.action_send_animations.setText(_translate("MainWindow", "Send animations"))
|
||||
self.action_send_configurations.setText(_translate("MainWindow", "Send configurations"))
|
||||
self.action_send_Aruco_map.setText(_translate("MainWindow", "Send aruco map"))
|
||||
self.action_send_animations.setText(_translate("MainWindow", "Animations"))
|
||||
self.action_send_configurations.setText(_translate("MainWindow", "Configuration"))
|
||||
self.action_send_aruco_map.setText(_translate("MainWindow", "Aruco map"))
|
||||
self.action_update_client_repo.setText(_translate("MainWindow", "Update clever-show git"))
|
||||
self.actionSend_launch_file_for_clever.setText(_translate("MainWindow", "Send launch file for clever"))
|
||||
self.action_send_launch_file.setText(_translate("MainWindow", "Send launch files"))
|
||||
self.action_restart_clever.setText(_translate("MainWindow", "Restart clever service"))
|
||||
self.action_restart_clever_show.setText(_translate("MainWindow", "Restart clever-show service"))
|
||||
self.action_send_launch_file.setText(_translate("MainWindow", "Launch files"))
|
||||
self.action_restart_clever.setText(_translate("MainWindow", "clever"))
|
||||
self.action_restart_clever_show.setText(_translate("MainWindow", "clever-show"))
|
||||
self.action_select_all_rows.setText(_translate("MainWindow", "Select all drones"))
|
||||
self.action_select_all_rows.setShortcut(_translate("MainWindow", "Ctrl+A"))
|
||||
self.action_set_start_to_current_position.setText(_translate("MainWindow", "Set start X Y to current position"))
|
||||
self.action_reset_start.setText(_translate("MainWindow", "Reset start position"))
|
||||
self.action_set_z_offset_to_ground.setText(_translate("MainWindow", "Set Z offset to ground"))
|
||||
self.action_reset_z_offset.setText(_translate("MainWindow", "Reset Z offset"))
|
||||
self.action_select_music_file.setText(_translate("MainWindow", "Select music file"))
|
||||
self.action_play_music.setText(_translate("MainWindow", "Play music"))
|
||||
self.action_select_music_file.setText(_translate("MainWindow", "Select file"))
|
||||
self.action_play_music.setText(_translate("MainWindow", "Play"))
|
||||
self.action_test_music_after.setText(_translate("MainWindow", "Test music after"))
|
||||
self.actionFill.setText(_translate("MainWindow", "fill"))
|
||||
self.action_send_any_file.setText(_translate("MainWindow", "Send any file"))
|
||||
self.action_send_any_command.setText(_translate("MainWindow", "Send any command"))
|
||||
self.action_stop_music.setText(_translate("MainWindow", "Stop music"))
|
||||
self.action_remove_row.setText(_translate("MainWindow", "Remove from table"))
|
||||
self.action_send_calibrations.setText(_translate("MainWindow", "Send camera calibrations"))
|
||||
self.action_reboot_all.setText(_translate("MainWindow", "Reboot all"))
|
||||
self.action_restart_chrony.setText(_translate("MainWindow", "Restart chrony"))
|
||||
self.action_send_fcu_parameters.setText(_translate("MainWindow", "Send FCU parameters"))
|
||||
self.action_send_any_file.setText(_translate("MainWindow", "File"))
|
||||
self.action_send_any_command.setText(_translate("MainWindow", "Command"))
|
||||
self.action_stop_music.setText(_translate("MainWindow", "Stop"))
|
||||
self.action_remove_row.setText(_translate("MainWindow", "Remove selected drones"))
|
||||
self.action_remove_row.setShortcut(_translate("MainWindow", "Ctrl+Del"))
|
||||
self.action_send_calibrations.setText(_translate("MainWindow", "Camera calibrations"))
|
||||
self.action_reboot_all.setText(_translate("MainWindow", "Reboot"))
|
||||
self.action_restart_chrony.setText(_translate("MainWindow", "chrony"))
|
||||
self.action_send_fcu_parameters.setText(_translate("MainWindow", "FCU parameters"))
|
||||
self.action_toggle_select.setText(_translate("MainWindow", "Toggle select"))
|
||||
self.action_toggle_select.setShortcut(_translate("MainWindow", "Ctrl+A"))
|
||||
self.action_select_all.setText(_translate("MainWindow", "Select all"))
|
||||
self.action_select_all.setShortcut(_translate("MainWindow", "Shift+A"))
|
||||
self.action_deselect_all.setText(_translate("MainWindow", "Deselect all"))
|
||||
self.action_deselect_all.setShortcut(_translate("MainWindow", "Alt+A"))
|
||||
self.action_edit_server_config.setText(_translate("MainWindow", "Edit server config"))
|
||||
self.action_edit_any_config.setText(_translate("MainWindow", "Edit any config"))
|
||||
self.action_update_server_git.setText(_translate("MainWindow", "Update server git"))
|
||||
self.action_retrive_any_file.setText(_translate("MainWindow", "Retrive file"))
|
||||
self.action_restart_server.setText(_translate("MainWindow", "Restart server"))
|
||||
self.action_configure_columns.setText(_translate("MainWindow", "Configure columns"))
|
||||
self.actionSomething.setText(_translate("MainWindow", "something"))
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1360</width>
|
||||
<height>761</height>
|
||||
<height>816</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -76,7 +76,7 @@
|
||||
<property name="formAlignment">
|
||||
<set>Qt::AlignHCenter|Qt::AlignTop</set>
|
||||
</property>
|
||||
<item row="2" column="0">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="start_text">
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::RightToLeft</enum>
|
||||
@@ -89,14 +89,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="0" column="1">
|
||||
<widget class="QSpinBox" name="start_delay_spin">
|
||||
<property name="suffix">
|
||||
<string> s</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="music_text">
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::RightToLeft</enum>
|
||||
@@ -106,7 +106,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="music_delay_spin">
|
||||
<property name="suffix">
|
||||
<string> s</string>
|
||||
@@ -119,7 +119,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="music_play_text">
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::RightToLeft</enum>
|
||||
@@ -129,7 +129,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="music_checkbox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
@@ -144,7 +144,7 @@
|
||||
<enum>Qt::DefaultContextMenu</enum>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::RightToLeft</enum>
|
||||
<enum>Qt::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
@@ -224,14 +224,14 @@
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="1">
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="land_selected_button">
|
||||
<property name="text">
|
||||
<string>Land selected</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="land_all_button">
|
||||
<property name="text">
|
||||
<string>Land ALL</string>
|
||||
@@ -286,17 +286,14 @@
|
||||
<property name="formAlignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item row="3" column="1">
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="disarm_all_button">
|
||||
<property name="text">
|
||||
<string>Disarm ALL</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="disarm_selected_button">
|
||||
<property name="text">
|
||||
<string>Disarm selected</string>
|
||||
@@ -320,10 +317,10 @@
|
||||
<property name="formAlignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<item row="7" column="1">
|
||||
<widget class="QPushButton" name="flip_button">
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="leds_button">
|
||||
<property name="text">
|
||||
<string>Flip</string>
|
||||
<string>Test leds</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -337,26 +334,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="leds_button">
|
||||
<property name="text">
|
||||
<string>Test leds</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="z_checkbox">
|
||||
<property name="cursor">
|
||||
<cursorShape>ArrowCursor</cursorShape>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::NoFocus</enum>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::LeftToRight</enum>
|
||||
</property>
|
||||
@@ -383,6 +367,13 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="flip_button">
|
||||
<property name="text">
|
||||
<string>Flip</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
@@ -435,85 +426,104 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1360</width>
|
||||
<height>22</height>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuOptions">
|
||||
<property name="title">
|
||||
<string>Server</string>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuMusic_2">
|
||||
<property name="title">
|
||||
<string>Music</string>
|
||||
</property>
|
||||
<addaction name="action_select_music_file"/>
|
||||
<addaction name="action_play_music"/>
|
||||
<addaction name="action_stop_music"/>
|
||||
</widget>
|
||||
<addaction name="menuMusic_2"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_edit_server_config"/>
|
||||
<addaction name="action_edit_any_config"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_update_server_git"/>
|
||||
<addaction name="action_restart_server"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuTable">
|
||||
<property name="title">
|
||||
<string>Table</string>
|
||||
</property>
|
||||
<addaction name="action_toggle_select"/>
|
||||
<addaction name="action_select_all"/>
|
||||
<addaction name="action_deselect_all"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_remove_row"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_configure_columns"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuDrone_2">
|
||||
<property name="title">
|
||||
<string>Selected drones</string>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuSend">
|
||||
<property name="title">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
<addaction name="action_send_animations"/>
|
||||
<addaction name="action_send_configurations"/>
|
||||
<addaction name="action_send_launch_file"/>
|
||||
<addaction name="action_send_aruco_map"/>
|
||||
<addaction name="action_send_calibrations"/>
|
||||
<addaction name="action_send_fcu_parameters"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_send_any_file"/>
|
||||
<addaction name="action_send_any_command"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuRestart">
|
||||
<property name="title">
|
||||
<string>Restart service</string>
|
||||
</property>
|
||||
<addaction name="action_restart_chrony"/>
|
||||
<addaction name="action_restart_clever"/>
|
||||
<addaction name="action_restart_clever_show"/>
|
||||
<addaction name="separator"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuDeveloper_mode">
|
||||
<property name="title">
|
||||
<string>Developer mode</string>
|
||||
</property>
|
||||
<addaction name="action_send_any_file"/>
|
||||
<addaction name="action_send_any_command"/>
|
||||
<addaction name="action_update_client_repo"/>
|
||||
</widget>
|
||||
<addaction name="action_send_animations"/>
|
||||
<addaction name="action_send_configurations"/>
|
||||
<addaction name="action_send_launch_file"/>
|
||||
<addaction name="action_send_Aruco_map"/>
|
||||
<addaction name="action_send_calibrations"/>
|
||||
<addaction name="action_send_fcu_parameters"/>
|
||||
<addaction name="menuSend"/>
|
||||
<addaction name="action_retrive_any_file"/>
|
||||
<addaction name="menuRestart"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menuDeveloper_mode"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_select_all_rows"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuAnimation">
|
||||
<property name="title">
|
||||
<string>Animation</string>
|
||||
</property>
|
||||
<addaction name="action_set_start_to_current_position"/>
|
||||
<addaction name="action_reset_start"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuDrone">
|
||||
<property name="title">
|
||||
<string>Drone</string>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuDeveloper_mode_2">
|
||||
<property name="title">
|
||||
<string>Developer mode</string>
|
||||
</property>
|
||||
<addaction name="action_restart_clever"/>
|
||||
<addaction name="action_restart_clever_show"/>
|
||||
<addaction name="action_update_client_repo"/>
|
||||
<addaction name="action_reboot_all"/>
|
||||
</widget>
|
||||
<addaction name="action_set_z_offset_to_ground"/>
|
||||
<addaction name="action_reset_z_offset"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_restart_chrony"/>
|
||||
<addaction name="action_remove_row"/>
|
||||
<addaction name="menuDeveloper_mode"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="menuDeveloper_mode_2"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuMusic">
|
||||
<property name="title">
|
||||
<string>Music</string>
|
||||
</property>
|
||||
<addaction name="action_select_music_file"/>
|
||||
<addaction name="action_play_music"/>
|
||||
<addaction name="action_stop_music"/>
|
||||
<addaction name="action_reboot_all"/>
|
||||
</widget>
|
||||
<addaction name="menuDrone_2"/>
|
||||
<addaction name="menuOptions"/>
|
||||
<addaction name="menuDrone"/>
|
||||
<addaction name="menuAnimation"/>
|
||||
<addaction name="menuMusic"/>
|
||||
<addaction name="menuTable"/>
|
||||
</widget>
|
||||
<action name="action_send_animations">
|
||||
<property name="text">
|
||||
<string>Send animations</string>
|
||||
<string>Animations</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_send_configurations">
|
||||
<property name="text">
|
||||
<string>Send configurations</string>
|
||||
<string>Configuration</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_send_Aruco_map">
|
||||
<action name="action_send_aruco_map">
|
||||
<property name="text">
|
||||
<string>Send aruco map</string>
|
||||
<string>Aruco map</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_update_client_repo">
|
||||
@@ -528,17 +538,17 @@
|
||||
</action>
|
||||
<action name="action_send_launch_file">
|
||||
<property name="text">
|
||||
<string>Send launch files</string>
|
||||
<string>Launch files</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_restart_clever">
|
||||
<property name="text">
|
||||
<string>Restart clever service</string>
|
||||
<string>clever</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_restart_clever_show">
|
||||
<property name="text">
|
||||
<string>Restart clever-show service</string>
|
||||
<string>clever-show</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_select_all_rows">
|
||||
@@ -571,12 +581,12 @@
|
||||
</action>
|
||||
<action name="action_select_music_file">
|
||||
<property name="text">
|
||||
<string>Select music file</string>
|
||||
<string>Select file</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_play_music">
|
||||
<property name="text">
|
||||
<string>Play music</string>
|
||||
<string>Play</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_test_music_after">
|
||||
@@ -591,42 +601,110 @@
|
||||
</action>
|
||||
<action name="action_send_any_file">
|
||||
<property name="text">
|
||||
<string>Send any file</string>
|
||||
<string>File</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_send_any_command">
|
||||
<property name="text">
|
||||
<string>Send any command</string>
|
||||
<string>Command</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_stop_music">
|
||||
<property name="text">
|
||||
<string>Stop music</string>
|
||||
<string>Stop</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_remove_row">
|
||||
<property name="text">
|
||||
<string>Remove from table</string>
|
||||
<string>Remove selected drones</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Del</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_send_calibrations">
|
||||
<property name="text">
|
||||
<string>Send camera calibrations</string>
|
||||
<string>Camera calibrations</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_reboot_all">
|
||||
<property name="text">
|
||||
<string>Reboot all</string>
|
||||
<string>Reboot</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_restart_chrony">
|
||||
<property name="text">
|
||||
<string>Restart chrony</string>
|
||||
<string>chrony</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_send_fcu_parameters">
|
||||
<property name="text">
|
||||
<string>Send FCU parameters</string>
|
||||
<string>FCU parameters</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_toggle_select">
|
||||
<property name="text">
|
||||
<string>Toggle select</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+A</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_select_all">
|
||||
<property name="text">
|
||||
<string>Select all</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Shift+A</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_deselect_all">
|
||||
<property name="text">
|
||||
<string>Deselect all</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Alt+A</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_edit_server_config">
|
||||
<property name="text">
|
||||
<string>Edit server config</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_edit_any_config">
|
||||
<property name="text">
|
||||
<string>Edit any config</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_update_server_git">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Update server git</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_retrive_any_file">
|
||||
<property name="text">
|
||||
<string>Retrive file</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_restart_server">
|
||||
<property name="text">
|
||||
<string>Restart server</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_configure_columns">
|
||||
<property name="text">
|
||||
<string>Configure columns</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSomething">
|
||||
<property name="text">
|
||||
<string>something</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
import sys
|
||||
from functools import partial
|
||||
|
||||
from lib import b_partial
|
||||
|
||||
# TODO: previous step and reset
|
||||
class VisualLandDialog(QtWidgets.QDialog):
|
||||
@@ -26,8 +27,8 @@ class VisualLandDialog(QtWidgets.QDialog):
|
||||
self.ui.setupUi(self)
|
||||
self.ui.one_button.clicked.connect(partial(self.selection_choice, 1))
|
||||
self.ui.two_button.clicked.connect(partial(self.selection_choice, 2))
|
||||
self.ui.land_emergency_button.clicked.connect(partial(self.send_to_selected, "land", None))
|
||||
self.ui.disarm_emergency_button.clicked.connect(partial(self.send_to_selected, "disarm", None))
|
||||
self.ui.land_emergency_button.clicked.connect(b_partial(self.send_to_selected, "land"))
|
||||
self.ui.disarm_emergency_button.clicked.connect(b_partial(self.send_to_selected, "disarm"))
|
||||
|
||||
self.ui.one_button.setShortcut(QKeySequence("1"))
|
||||
self.ui.two_button.setShortcut(QKeySequence("2"))
|
||||
@@ -38,10 +39,10 @@ class VisualLandDialog(QtWidgets.QDialog):
|
||||
def row_mid(self):
|
||||
return int(math.ceil((self.row_min + self.row_max) / 2.0))
|
||||
|
||||
def send_to_row(self, row, message, args=None):
|
||||
logging.debug(f"Send {message}: {args} to {row}")
|
||||
self.model.data_contents[row].client.send_message(message, args)
|
||||
# test[row] = args # for testing
|
||||
def send_to_row(self, row, message, args=(), kwargs=None):
|
||||
logging.debug(f"Send {message}: {args}, {kwargs} to {row}")
|
||||
self.model.data_contents[row].client.send_message(message, args=args, kwargs=kwargs)
|
||||
# test[row] = args, kwargs # for testing
|
||||
# print(test)
|
||||
|
||||
def clear_leds(self, rows):
|
||||
@@ -56,16 +57,17 @@ class VisualLandDialog(QtWidgets.QDialog):
|
||||
|
||||
def send_led_indication(self):
|
||||
for row in range(self.row_min, self.row_mid):
|
||||
self.send_to_row(row, "led_fill", {"green": 255})
|
||||
self.send_to_row(row, "led_fill", kwargs={"green": 255})
|
||||
|
||||
for row in range(self.row_mid, self.row_max + 1):
|
||||
self.send_to_row(row, "led_fill", {"red": 255})
|
||||
self.send_to_row(row, "led_fill", kwargs={"red": 255})
|
||||
|
||||
@pyqtSlot()
|
||||
def selection_choice(self, choice):
|
||||
if self.row_min == self.row_max:
|
||||
# self.ui.one_button.setDisabled(True) # maybe?
|
||||
# self.ui.two_button.setDisabled(True)
|
||||
self.send_to_selected("land")
|
||||
return
|
||||
|
||||
if choice == 1:
|
||||
@@ -83,9 +85,9 @@ class VisualLandDialog(QtWidgets.QDialog):
|
||||
self.send_led_indication()
|
||||
|
||||
@pyqtSlot()
|
||||
def send_to_selected(self, message, args=None):
|
||||
def send_to_selected(self, message, args=(), kwargs=None):
|
||||
for row in range(self.row_min, self.row_max + 1):
|
||||
self.send_to_row(row, message, args)
|
||||
self.send_to_row(row, message, args, kwargs)
|
||||
|
||||
self._finished = True
|
||||
self.close()
|
||||
@@ -104,7 +106,7 @@ if __name__ == '__main__':
|
||||
import copter_table_models
|
||||
model = copter_table_models.CopterDataModel()
|
||||
for i in range(10):
|
||||
model.add_client(copter_table_models.StatedCopterData())
|
||||
model.add_client()
|
||||
|
||||
dialog = VisualLandDialog(model)
|
||||
test = list(range(10))
|
||||
|
||||
357
config.py
Normal file
@@ -0,0 +1,357 @@
|
||||
import os
|
||||
import copy
|
||||
import collections
|
||||
|
||||
from configobj import ConfigObj, Section, flatten_errors
|
||||
from validate import Validator, is_tuple, is_boolean, is_integer
|
||||
|
||||
|
||||
def modify_filename(path, pattern): # TODO move to core
|
||||
old_path, filename = os.path.split(path)
|
||||
filename, ext = os.path.splitext(filename)
|
||||
newfilename = pattern.format(filename) + ext
|
||||
return os.path.join(old_path, newfilename)
|
||||
|
||||
|
||||
def parent_path(path, levels=1):
|
||||
for i in range(levels):
|
||||
path = os.path.abspath(os.path.join(path, os.pardir))
|
||||
return path
|
||||
|
||||
|
||||
def parent_dir(path):
|
||||
return os.path.basename(os.path.normpath(path))
|
||||
|
||||
|
||||
def is_preset_param(value):
|
||||
parsed = is_tuple(value, min=2, max=2)
|
||||
return is_boolean(parsed[0]), is_integer(parsed[1], min=0)
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
def __init__(self, message, config, errors):
|
||||
super(ValidationError, self).__init__(message)
|
||||
self.config = config
|
||||
self.errors = errors
|
||||
|
||||
def __str__(self):
|
||||
return "{} - {}".format(self.args[0], " ".join(self.flatten_errors()))
|
||||
|
||||
def flatten_errors(self):
|
||||
for entry in flatten_errors(self.config, self.errors):
|
||||
section_list, key, error = entry
|
||||
if key is not None:
|
||||
section_list.append(key)
|
||||
else:
|
||||
section_list.append('[missing section]')
|
||||
section_string = ', '.join(section_list)
|
||||
if error == False: # Important syntax
|
||||
error = 'Missing value or section.'
|
||||
yield "[{}]: {}".format(section_string, error)
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
def __init__(self, config=None):
|
||||
self.config = ConfigObj() if config is None else config
|
||||
|
||||
self._name_dict = {}
|
||||
|
||||
def get(self, section, option):
|
||||
return self.config[section][option]
|
||||
|
||||
def set(self, section, option, value, write=False):
|
||||
self.config[section][option] = value
|
||||
if write:
|
||||
self.write()
|
||||
|
||||
def get_chain(self, *keys):
|
||||
current = self.config
|
||||
for key in keys:
|
||||
current = current[key]
|
||||
return current
|
||||
|
||||
def set_chain(self, value, *keys): # will create new sections!
|
||||
current = self.config
|
||||
for key in keys[:-1]:
|
||||
current = current.setdefault(key, {})
|
||||
current[keys[-1]] = value
|
||||
|
||||
def write(self):
|
||||
self.config.write()
|
||||
|
||||
@property
|
||||
def validated(self):
|
||||
return self.config.configspec is not None
|
||||
|
||||
def set_config(self, config):
|
||||
self.config = config
|
||||
self._name_dict = self.flatten_keys(config)
|
||||
|
||||
def validate_config(self, config=None, copy_defaults=False):
|
||||
config = self.config if config is None else config
|
||||
vdt = Validator({"preset_param": is_preset_param})
|
||||
|
||||
test = config.validate(vdt, copy=copy_defaults, preserve_errors=True)
|
||||
if test != True: # Important syntax, do no change
|
||||
raise ValidationError('Some config values are wrong', config, test)
|
||||
|
||||
self.set_config(config)
|
||||
|
||||
@classmethod
|
||||
def _full_dict(cls, item, include_defaults=False):
|
||||
if not isinstance(item, Section):
|
||||
return item
|
||||
|
||||
data = collections.OrderedDict()
|
||||
default_values = item.default_values
|
||||
defaults = item.defaults
|
||||
comments = item.comments
|
||||
inline_comments = item.inline_comments
|
||||
|
||||
for key, value in item.items():
|
||||
result = cls._full_dict(value, include_defaults)
|
||||
if not isinstance(result, dict):
|
||||
item_d = {'__value__': value}
|
||||
|
||||
comment = comments.get(key, [])
|
||||
if comment and comment != ['']:
|
||||
item_d.update({'comments': comment})
|
||||
|
||||
inline_comment = inline_comments.get(key, None)
|
||||
if inline_comment:
|
||||
item_d.update({'inline_comment': inline_comments})
|
||||
|
||||
if include_defaults:
|
||||
item_d.update({'default': default_values.get(key, None),
|
||||
'unchanged': key in defaults,
|
||||
})
|
||||
data[key] = item_d
|
||||
else:
|
||||
data[key] = result
|
||||
|
||||
return data
|
||||
|
||||
def full_dict(self, include_defaults=False):
|
||||
d = self._full_dict(self.config, include_defaults=include_defaults)
|
||||
d['initial_comment'] = self.config.initial_comment
|
||||
d['final_comment'] = self.config.final_comment
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def flatten_keys(cls, d, parent_keys=(), sep='_'):
|
||||
items = {}
|
||||
for key, value in d.items():
|
||||
keys = parent_keys + (key,)
|
||||
if isinstance(value, dict):
|
||||
items.update(cls.flatten_keys(value, keys, sep=sep))
|
||||
formatted_keys = [key.lower().strip().replace(' ', sep) for key in keys]
|
||||
formatted_key = sep.join(formatted_keys)
|
||||
items.update({formatted_key: keys})
|
||||
return dict(items)
|
||||
|
||||
def __getattr__(self, item):
|
||||
try:
|
||||
keys = self.__dict__['_name_dict'][item]
|
||||
return self.get_chain(*keys)
|
||||
except (ValueError, KeyError):
|
||||
return self.__dict__[item]
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
try:
|
||||
keys = self.__dict__['_name_dict'][key]
|
||||
self.set_chain(value, *keys)
|
||||
except (ValueError, KeyError):
|
||||
self.__dict__[key] = value
|
||||
|
||||
@staticmethod
|
||||
def config_exists(path):
|
||||
return os.path.isfile(path) and os.path.splitext(path)[1] == '.ini'
|
||||
|
||||
@staticmethod
|
||||
def _get_spec_path(path):
|
||||
return modify_filename(path, 'spec/configspec_{}')
|
||||
|
||||
@staticmethod
|
||||
def _get_config_path(path):
|
||||
filename = os.path.split(path)[1]
|
||||
return os.path.join(parent_path(path, levels=2),
|
||||
filename.replace('configspec_', ''))
|
||||
|
||||
def load_from_file(self, path):
|
||||
if not self.config_exists(path):
|
||||
raise ValueError('Config file do not exist!')
|
||||
|
||||
f_path, filename = os.path.split(path)
|
||||
if filename.startswith('configspec_'):
|
||||
config_path = self._get_config_path(path)
|
||||
|
||||
if self.config_exists(config_path):
|
||||
return self.load_config_and_spec(config_path)
|
||||
|
||||
generate_file = parent_dir(f_path) == 'spec'
|
||||
if generate_file:
|
||||
self.generate_default_config(config_path)
|
||||
|
||||
return self.load_only_spec(path, generate_file)
|
||||
|
||||
else:
|
||||
spec_path = self._get_spec_path(path)
|
||||
if self.config_exists(spec_path):
|
||||
return self.load_config_and_spec(path)
|
||||
|
||||
return self.load_only_config(path)
|
||||
|
||||
def load_config_and_spec(self, path):
|
||||
self.generate_default_config(path)
|
||||
config = ConfigObj(infile=path,
|
||||
configspec=self._get_spec_path(path))
|
||||
|
||||
self.validate_config(config)
|
||||
|
||||
def load_only_config(self, path):
|
||||
config = ConfigObj(infile=path)
|
||||
self.set_config(config)
|
||||
|
||||
def load_only_spec(self, path, generate_filename=True):
|
||||
config = ConfigObj(configspec=path)
|
||||
if generate_filename:
|
||||
config.filename = self._get_config_path(path)
|
||||
|
||||
self.validate_config(config, copy_defaults=True)
|
||||
|
||||
@classmethod
|
||||
def generate_default_config(cls, cfg_path):
|
||||
if cls.config_exists(cfg_path):
|
||||
return False
|
||||
|
||||
vdt = Validator()
|
||||
config = ConfigObj(configspec=cls._get_spec_path(cfg_path))
|
||||
config.filename = cfg_path
|
||||
config.validate(vdt, copy=True)
|
||||
config.indent_type = ''
|
||||
config.initial_comment = ('This is generated config with default values',
|
||||
'Modify to configure')
|
||||
config.write()
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _extract_values(cls, d):
|
||||
result = collections.OrderedDict()
|
||||
for key, val in d.items():
|
||||
if not isinstance(val, dict): # Pure dict option
|
||||
result[key] = val
|
||||
elif '__value__' in val: # Full-dict option with params
|
||||
if not val.get('unchanged', False):
|
||||
result[key] = val.get('__value__')
|
||||
else: # Section
|
||||
result[key] = cls._extract_values(val)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _load_comments(cls, d, section):
|
||||
comments = section.comments
|
||||
inline_comments = section.inline_comments
|
||||
|
||||
for key, val in d.items():
|
||||
if not isinstance(val, dict): # Pure dict option
|
||||
comments[key] = []
|
||||
inline_comments[key] = None
|
||||
elif '__value__' in val: # Full-dict option with params
|
||||
comment = val.get('comments', [])
|
||||
comments[key] = [] if comment == [''] else comment
|
||||
inline_comments[key] = val.get('inline_comment', None)
|
||||
else: # Section
|
||||
cls._load_comments(val, section[key])
|
||||
comments[key] = ['']
|
||||
inline_comments[key] = None
|
||||
|
||||
section.comments = comments
|
||||
section.inline_comments = inline_comments
|
||||
|
||||
def load_from_dict(self, d, configspec=None):
|
||||
initial_comment = d.pop('initial_comment', [''])
|
||||
final_comment = d.pop('final_comment', [''])
|
||||
|
||||
kwargs = {'infile': self._extract_values(d), 'indent_type': ''}
|
||||
filename = None
|
||||
if isinstance(configspec, dict):
|
||||
kwargs.update({'configspec': configspec})
|
||||
elif isinstance(configspec, str):
|
||||
spec_path = self._get_spec_path(configspec)
|
||||
if self.config_exists(spec_path): # when 'configspec' points to configuration file and configspec exists
|
||||
kwargs.update({'configspec': spec_path})
|
||||
filename = configspec
|
||||
elif self.config_exists(configspec): # when 'configspec' points to configspec file
|
||||
kwargs.update({'configspec': configspec})
|
||||
if parent_dir(configspec) == 'spec':
|
||||
filename = self._get_config_path(configspec)
|
||||
else:
|
||||
raise ValueError("Configspec does not exist")
|
||||
|
||||
config = ConfigObj(**kwargs)
|
||||
config.filename = filename
|
||||
config.initial_comment = initial_comment
|
||||
config.final_comment = final_comment
|
||||
|
||||
if config.configspec is not None:
|
||||
self.validate_config(config)
|
||||
else:
|
||||
self.set_config(config)
|
||||
|
||||
self._load_comments(d, self.config)
|
||||
|
||||
def merge(self, config, validate=True):
|
||||
current = copy.deepcopy(self.config)
|
||||
current.merge(config.config)
|
||||
if validate:
|
||||
self.validate_config(current)
|
||||
else:
|
||||
self.set_config(current)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cfg = ConfigManager()
|
||||
cfg.load_from_file('Drone/config/client.ini')
|
||||
# cfg.load_from_file('Server/config/server.ini')
|
||||
#cfg.load_from_file('Drone/config/spec/configspec_client.ini')
|
||||
print(dict(cfg.full_dict(include_defaults=True)))
|
||||
cfg.config.pop("PRIVATE", None)
|
||||
print(cfg.config)
|
||||
|
||||
|
||||
# cfg.load_config_and_spec('Drone/config/client.ini')
|
||||
# #print(cfg.config.comments)
|
||||
# #print(cfg.server_host)
|
||||
# cfg.server_host = '192.168.1.103'
|
||||
#
|
||||
# print(cfg.get('SERVER', 'host'))
|
||||
# cfg.set('SERVER', 'host', '192.168.1.103')
|
||||
# print(cfg.get('SERVER', 'host'))
|
||||
#
|
||||
# print(cfg.config.initial_comment, cfg.config.final_comment)
|
||||
#
|
||||
# # print(cfg.config)
|
||||
# # print(cfg.default_values)
|
||||
# # print(cfg.unchanged_defaults)
|
||||
#
|
||||
# # print(11111)
|
||||
import pprint
|
||||
#pprint.pprint(cfg.full_dict)
|
||||
# cfg2 = ConfigManager()
|
||||
# #cfg2.load_from_dict({"PRIVATE": {"offset": [1, 2, 3]}}, configspec='Drone/config/spec/configspec_client.ini')
|
||||
# cfg2.load_from_dict({"PRIVATE": {"id": "heh"}})
|
||||
# #pprint.pprint(cfg2.full_dict)
|
||||
# #cfg.merge(cfg2)
|
||||
# #pprint.pprint(cfg.full_dict)
|
||||
# print(cfg2.full_dict(include_defaults=True))
|
||||
#print(dict(cfg2.config.configspec))
|
||||
#print(cfg2.config.PRIVATE)
|
||||
#print(dict(ConfigManager(cfg.config.configspec).config))
|
||||
|
||||
# #print(cfg.full_dict)
|
||||
#
|
||||
# #cfg.load_from_dict(cfg.full_dict, 'Drone/config/client.ini')
|
||||
# #print(cfg.config.initial_comment, cfg.config.final_comment)
|
||||
# #cfg.write()
|
||||
#
|
||||
|
||||
BIN
docs/assets/server-column-editor.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
docs/assets/server-column-popup.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
docs/assets/server-drone-restart.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
docs/assets/server-drone-send.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 16 KiB |
BIN
docs/assets/server-table.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
@@ -5,101 +5,40 @@
|
||||
* [Установка и запуск](start-tutorial.md#установка-и-запуск-клиента)
|
||||
* [Настройка клиента](#настройка-клиента)
|
||||
|
||||
## Схема работы клиента
|
||||
|
||||
Клиент является сервисом `clever-show` в операционной системе коптера. Сервис запускает скрипт [copter_client.py](../../Drone/copter_client.py) и автоматически запускается при загрузке операционной системы. В случае необходимости применения параметров обновлённой конфигурации клиента сервис может быть перезапущен. Сервис `clever-show` предназначен для управления и настройки коптера для группового полёта с помощью приложения сервера.
|
||||
|
||||
Вместе с клиентом в операционной системе зарегистрирован сервис экстренной защиты дрона `visual_pose_watchdog`. Данный сервис запускает скрипт [visual_pose_watchdog.py](../../Drone/visual_pose_watchdog.py) и автоматически запускается при загрузке операционной системы. В случае необходимости применения параметров обновлённой конфигурации клиента сервис может быть перезапущен. Сервис `visual_pose_watchdog` предназначен для автоматической посадки коптера в экстренных ситуациях: при отсутствии сообщений о позиции дрона из топика `/mavros/vision_pose/pose` и при столкновении с объектами в полёте, когда расстояние между текущей точкой, где находится коптер, и точкой, где ему следует находиться по полётному заданию, превышает пороговое значение. Также `visual_pose_watchdog` предоставляет ROS сервис `/emergency_land`, к которому может обратиться клиент по команде с сервера для экстренной посадки.
|
||||
|
||||
Логи обоих сервисов записываются в файл `/var/log/syslog` операционной системы бортового компьютера Raspberry Pi на дроне. Логи текущего сеанса доступны для просмотра при выполнении команд `journalctl -u clever-show` для клиента и `journalctl -u visual_pose_watchdog` для сервиса экстренной защиты дрона в терминале, подключенном к Raspberry Pi. Логи могут быть полезны при анализе возникших нештатных или аварийных ситуаций при полёте коптера под управлением приложения клиента.
|
||||
|
||||
## Настройка клиента
|
||||
|
||||
### Файл конфигурации
|
||||
|
||||
Конфигурация клиента задаётся в файле [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
|
||||
|
||||
```
|
||||
Конфигурация клиента создаётся согласно [спецификации](../../Drone/config/spec/configspec_client.ini), в ней можно посмотреть значения по умолчанию для любого параметра после ключевого слова `default`. Все изменения сохраняются в файл конфигурации `client.ini` в папке `clever-show/Drone/config`. Доступно редактирование конфигурации клиента через GUI модуль `Config editor` в приложении сервера. Для редактирования конфигурации с сервера нужно выбрать строку с клиентом, для которого требуется изменение конфигурации, кликнуть левой кнопкой мыши по любой ячейке из строки и выбрать `Edit config` из контекстного меню.
|
||||
|
||||
Конфигурация по умолчанию является полностью работоспособной и не требует изменений для быстрого старта клиента.
|
||||
|
||||
Для централизованной загрузки конфигурации на все коптеры нужно использовать пункт меню `Send configurations` на [сервере](server.md#раздел-server). Допускается загрузка неполного файла параметров конфигурации, с отсутствующими разделами или параметрами относительно конфигурации по умолчанию.
|
||||
Для централизованной загрузки конфигурации на все коптеры нужно использовать пункт меню `Send configurations` на [сервере](server.md#раздел-server). Допускается загрузка неполного файла параметров конфигурации, с отсутствующими разделами или параметрами относительно конфигурации по умолчанию. Также доступна опция загрузки конфигурации конкретного клиента на выделенные коптеры: для этого нужно с сервера выбрать строку с клиентом, с которого требуется скопировать конфигурацию на выделенные клиенты, кликнуть левой кнопкой мыши по любой ячейке из строки и выбрать `Copy config to selected` из контекстного меню.
|
||||
|
||||
### Описание параметров
|
||||
|
||||
#### Раздел SERVER
|
||||
|
||||
В этом разделе задаются параметры сетевого взаимодействия клиента с сервером. Доступны следующие параметры:
|
||||
|
||||
* `port` - TCP порт, на который будут приниматься входящие соединения от сервера. При использовании настройки [use_broadcast](server.md#раздел-broadcast) на сервере, данный порт будет сконфигурирован у клиента автоматически. *Рекомендуется изменить значение по умолчанию в целях безопасности* (любое пятизначное и более число, если другое ПО не использует выбранный порт).
|
||||
* `broadcast_port` - UDP порт, на который по широковещательному каналу сервер передаёт свои настройки. С помощью данного механизма возможно автоматическое подключение клиента к серверу.
|
||||
* `host` - IP адрес сервера.
|
||||
* `buffer_size` - размер буфера при приёме и передаче данных. *Не рекомендуется изменять. Рекомендуется использовать единое значение у сервера и клиентов.*
|
||||
|
||||
#### Раздел VISUAL_POSE_WATCHDOG
|
||||
#### Раздел BROADCAST
|
||||
|
||||
В данном разделе настраивается программа экстренной защиты коптера от потери позиции или столкновения с объектом.
|
||||
В этом разделе включается/выключается механизм получения broadcast пакетов по UDP с сервера.
|
||||
|
||||
* `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` - время, через которое коптер безусловно выключает моторы после срабатывания экстренной защиты, в секундах.
|
||||
* `use` - логическое значение, определяет, использовать или не использовать механизм получения broadcast пакетов по UDP с сервера.
|
||||
* `port` - порт UDP для приёма broadcast сообщений.
|
||||
|
||||
#### Раздел TELEMETRY
|
||||
|
||||
@@ -107,9 +46,28 @@ port = 123
|
||||
|
||||
* `transmit` - логическое значение, определяет, нужно ли передавать данные на сервер.
|
||||
* `frequency` - частота передачи данных на сервер, целочисленное значение, количество раз в секунду.
|
||||
* `log_cpu_and_memory` - логическое значение, определяет, будет ли записываться в лог сервиса клиента clever-show состояние процессора и памяти.
|
||||
* `log_resources` - логическое значение, определяет, будет ли записываться в лог сервиса клиента clever-show состояние бортового компьютера Raspberry Pi: загрузка процессора и оперативной памяти, температура процессора, состояние температуры, состояние системы питания.
|
||||
|
||||
#### Раздел COPTERS
|
||||
#### Раздел POSITION WATCHDOG
|
||||
|
||||
В данном разделе настраивается программа экстренной защиты коптера от потери позиции или столкновения с объектом.
|
||||
|
||||
* `enabled` - логическое значение, определяет, использовать или нет экстренную защиту при потере визуальной позиции или столкновении с объектом.
|
||||
* `log_state` - логическое значение, определяет, будет ли записываться в лог сервиса состояние коптера: `armed: {} | mode: {} | vis_dt: {:.2f} | pos_delta: {:.2f} | pos_dt: {:.2f} | range: {:.2f} | watchdog_action: {}`.
|
||||
* `action` - действие при срабатывании экстренной защиты. Доступные варианты: `land` - посадка коптера в режиме полётного контроллера AUTO.LAND, `emergency_land` - посадка коптера с постепенным уменьшением мощности моторов, `disarm`- выключение моторов. **Внимание!** Не рекомендуется использовать режим AUTO.LAND с выключенным барометром - при потере источника высоты в полёте, например показаний лазера или визуальной позиции, режим AUTO.LAND не гарантирует посадку коптера, так как ориентируется на показания высоты. Для посадки коптера при его позиционировании с использованием визуальной позиции или лазера и возможности потери данных с этих систем рекомендуется использовать режим `emergency_land`.
|
||||
* `vision_pose_delay_after_arm` - время после взлёта коптера в секундах, которое требуется для получения визуальной позиции. В течение этого времени после взлёта защита по потере визуальной позиции не будет срабатывать. Этот параметр полезен при использовании модуля экстренной защиты совместно с системой позиционирования по aruco маркерам, расположенным на полу: при взлёте коптер какое-то время не имеет визуальной позиции.
|
||||
* `vision_pose_timeout` - время в секундах после потери визуальной позиции, через которое срабатывает экстренная защита.
|
||||
* `position_delta_max` - максимальная разница между текущим положением и точкой, в которой сейчас должен находиться коптер, в метрах. Требуется для проверки на столкновение коптера с объектом. Если расстояние между текущим положением коптера и положением, в котором он должен сейчас находиться, больше этого числа (в метрах), срабатывает экстренная защита.
|
||||
* `disarm_timeout` - время, через которое коптер безусловно выключает моторы после срабатывания экстренной защиты, в секундах.
|
||||
|
||||
#### Раздел EMERGENCY LAND
|
||||
|
||||
Настройки параметров экстренной посадки при действии `emergency_land` экстренной защиты или при вызове ROS сервиса `/emergency_land`.
|
||||
|
||||
* `thrust` - начальная мощность, подаваемая на моторы в случае выбора действия `emergency_land` при срабатывании экстренной защиты. Безразмерная величина, от 0 (отсутствие мошности) до 1 (полная мощность). Для гарантированной посадки рекомендуется устанавливать в значение, меньшее по величине на 5-10 процентов, чем газ висения (параметр `MPC_THR_HOVER` в px4). **Внимание!** Неправильная настройка этого параметра может привести к взлёту коптера вверх вместо посадки!
|
||||
* `decrease_thrust_after` - время, через которое мощность на моторах плавно начинает уменьшаться в случае выбора действия `emergency_land` при срабатывании экстренной защиты, в секундах.
|
||||
|
||||
#### Раздел COPTER
|
||||
|
||||
В данном разделе находятся настройки, влияющие на процесс полёта коптера.
|
||||
|
||||
@@ -119,23 +77,17 @@ port = 123
|
||||
* `safe_takeoff` - логическое значение, определяет, нужно ли производить посадку в безопасном режиме.
|
||||
* `reach_first_point_time` - максимальное время полёта к первой точке анимации, в секундах.
|
||||
* `land_time` - время зависания в конечной точке анимации перед посадкой, в секундах.
|
||||
* `x0_common` - смещение по оси x, общее для всех коптеров, в метрах.
|
||||
* `y0_common` - смещение по оси y, общее для всех коптеров, в метрах.
|
||||
* `z0_common` - смещение по оси z, общее для всех коптеров, в метрах.
|
||||
* `yaw` - поворот коптера при полёте по точкам, в градусах. Если значение `nan` - коптер сохраняет изначальную ориентацию в полёте.
|
||||
* `land_timeout` - время таймаута посадки, после которого происходит выключение моторов коптера, в секундах.
|
||||
* `common_offset` - смещение координат относительно текущей системы, общее для всех коптеров, в метрах. Список из 3 величин (x, y, z): каждая величина задаёт смещение по соответствующей оси.
|
||||
|
||||
#### Раздел FLOOR_FRAME
|
||||
|
||||
Данный раздел описывает смещение системы координат с названием `floor` и используется только при указании параметра `frame_id` как `floor` в разделе [COPTERS](#раздел-copters).
|
||||
|
||||
* `enabled` - логическое значение, определяет, нужно ли публиковать фрейм `floor`
|
||||
* `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`, в градусах.
|
||||
* `translation` - смещение системы координат `floor` по осям (x, y, z) относительно системы координат `parent`, в метрах.
|
||||
* `rotation` - поворот системы координат `floor` на углы (roll, pitch, yaw) вокруг осей (x, y, z) относительно системы координат `parent`, в градусах.
|
||||
|
||||
**Внимание!** Повороты `roll`, `pitch`, `yaw` производятся последовательно в указанном порядке.
|
||||
|
||||
@@ -143,24 +95,33 @@ port = 123
|
||||
|
||||
В данном разделе настраивается обработка анимации.
|
||||
|
||||
* `takeoff_animation_check` - логическое значение, определяет, будет ли производиться автоматическая обработка старта анимации. **Если значение True**, при загрузке анимации проверяется взлёт коптеров. Если в файле анимации коптер взлетает с земли, при старте анимации будет применена *логика немедленного воспроизведения*: коптер сразу начинает следовать точкам, указанным в анимации. Если в файле анимации коптер начинает полёт в воздухе, при старте анимации будет применена *логика полёта к первой точке*: коптер в начале взлетает на высоту `takeoff_height` за время `takeoff_time`, затем перемещается к первой точке за время `reach_first_point_time`, и затем начинает следовать точкам, указанным в анимации. **Если значение False**, при загрузке анимации не проверяется взлёт коптеров, а при старте анимации действует *логика полёта к первой точке*.
|
||||
* `land_animation_check` - логическое значение, определяет, будет ли производиться автоматическая обработка завершения анимации. **Если значение True**, при загрузке анимации проверяется посадка коптеров. Если в файле анимации коптер садится на землю и стоит до завершения анимации, проверка удалит все точки в анимации после начала посадки коптера. Таким образом, коптер в конце анимации зависнет над точкой посадки на время `land_time`, сядет автоматически и выключит моторы. **Если значение False**, при загрузке анимации не проверяется посадка коптеров и точкой посадки считается последняя точка в анимации. Например, если анимация посадки нарисована полностью и коптер стоит после посадки на земле некоторое время, а значение данного параметра **False**, всё это время у коптера будут включены моторы и он будет пытаться удержать указанную позицию посадки вплоть до завершении файла анимации, затем через время `land_time` перейдёт в редим посадки.
|
||||
* `takeoff_detection` - логическое значение, определяет, будет ли производиться автоматическая обработка старта анимации. **Если значение True**, при загрузке анимации проверяется взлёт коптеров. Если в файле анимации коптер взлетает с земли, при старте анимации будет применена *логика немедленного воспроизведения*: коптер сразу начинает следовать точкам, указанным в анимации. Если в файле анимации коптер начинает полёт в воздухе, при старте анимации будет применена *логика полёта к первой точке*: коптер в начале взлетает на высоту `takeoff_height` за время `takeoff_time`, затем перемещается к первой точке за время `reach_first_point_time`, и затем начинает следовать точкам, указанным в анимации. **Если значение False**, при загрузке анимации не проверяется взлёт коптеров, а при старте анимации действует *логика полёта к первой точке*.
|
||||
* `land_detection` - логическое значение, определяет, будет ли производиться автоматическая обработка завершения анимации. **Если значение True**, при загрузке анимации проверяется посадка коптеров. Если в файле анимации коптер садится на землю и стоит до завершения анимации, проверка удалит все точки в анимации после начала посадки коптера. Таким образом, коптер в конце анимации зависнет над точкой посадки на время `land_time`, сядет автоматически и выключит моторы. **Если значение False**, при загрузке анимации не проверяется посадка коптеров и точкой посадки считается последняя точка в анимации. Например, если анимация посадки нарисована полностью и коптер стоит после посадки на земле некоторое время, а значение данного параметра **False**, всё это время у коптера будут включены моторы и он будет пытаться удержать указанную позицию посадки вплоть до завершении файла анимации, затем через время `land_time` перейдёт в редим посадки.
|
||||
* `frame_delay` - время воспроизведения одного кадра в секундах.
|
||||
* `x_ratio` - масштаб анимации по оси x
|
||||
* `y_ratio` - масштаб анимации по оси y
|
||||
* `z_ratio` - масштаб анимации по оси z
|
||||
* `ratio` - масштаб анимации (ratio_x, ratio_y, ratio_z) по осям (x, y, z)
|
||||
* `yaw` - поворот коптера при полёте по точкам, в градусах. Если значение `nan` - коптер сохраняет изначальную ориентацию в полёте. Если значение `animation` - коптер берёт поворот по yaw из файла с анимацией.
|
||||
|
||||
#### Раздел LED
|
||||
|
||||
Настройки адресуемой светодиодной ленты на коптере
|
||||
|
||||
* `use` - логическое значение, определяет, используется или нет светодиодная лента.
|
||||
* `pin` - номер пина GPIO на Raspberry, к которому подключается лента
|
||||
* `count` - количество задействованных светодиодов в ленте
|
||||
|
||||
#### Раздел PRIVATE
|
||||
|
||||
В данном разделе находятся параметры, специфичные для конкретного коптера.
|
||||
|
||||
* `id` - имя коптера, отображаемое в таблице. Если значение `/hostname` - имя определяется из файла `/etc/hostname`.
|
||||
* `restart_dhcpcd` - логический параметр, определяет, требуется ли перезагрузка коптера при переименовании его `id` удалённо с сервера.
|
||||
* `use_leds` - логический параметр, определяет, использует ли коптер светодиодную ленту.
|
||||
* `led_pin` - номер пина GPIO на Raspberry Pi, к которому подключена светодиодная лента.
|
||||
* `x0` - смещение по оси x, только для данного коптера.
|
||||
* `y0` - смещение по оси y, только для данного коптера.
|
||||
* `z0` - смещение по оси z, только для данного коптера.
|
||||
* `offset` - смещение в метрах по осям (x, y, z) относительно текущей системы координат, только для данного коптера.
|
||||
|
||||
#### Раздел SYSTEM
|
||||
|
||||
Системные настройки служебных команд клиента
|
||||
|
||||
* `change_hostname` - логический параметр, определяет, требуется ли смена hostname при переименовании `id` коптера.
|
||||
* `restart_after_rename` - логический параметр, определяет, требуется ли перезагрузка коптера при переименовании его `id` удалённо с сервера.
|
||||
|
||||
#### Раздел NTP
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
|
||||
### Таблица состояния коптеров
|
||||
|
||||
При первом подключении клиента к серверу в таблицу добавляется строка для отображения состояния клиента, содержащая только имя клиента (`copter ID`). Если на клиентах настроена автоматическая передача телеметрии, данные в таблице будут обновляться автоматически. Так же возможно запросить телеметрию выбранных клиентов с помощью кнопки [`Preflight check`](#управление) Строки можно сортировать по возрастанию или убыванию значений любого из столбцов, кликнув по его заголовку.
|
||||
При первом подключении клиента к серверу в таблицу добавляется строка для отображения состояния клиента, содержащая только имя клиента (`copter ID`). Если на клиентах настроена автоматическая передача телеметрии, данные в таблице будут обновляться автоматически. Так же возможно запросить телеметрию выбранных клиентов с помощью кнопки [`Preflight check`](#управление).
|
||||
|
||||
Строки можно сортировать по возрастанию или убыванию значений любого из столбцов, кликнув по его заголовку.
|
||||
|
||||
Столбцы можно менять местами и изменять их ширину: все изменения сохраняются в настройках сервера.
|
||||
|
||||
Ячейки таблицы подсвечиваются:
|
||||
|
||||
@@ -30,64 +34,100 @@
|
||||
#### Столбцы таблицы
|
||||
|
||||
* `copter ID` - имя клиента. Может быть сконфигурирован на стороне клиента. Отображается сразу при подключении клиента. Рядом с каждым ID коптера расположен чекбокс - коптеры, чей ID отмечен чекбоксом положительно (галочка), считаются *выбранными*. Ячейки в этом столбце всегда проходят проверку.
|
||||
* `version` - хеш-код текущей git версии клиента. Ячейки в этом столбце всегда проходят проверку.
|
||||
* При двойном нажатии на это поле можно ввести новый `copter ID` клиента и переименовать его. В качестве имени допустимы сочетания латинских букв, цифр и тире (A-Z, a-z, 0-9, '-') длинной не более 63 символов. Тире не может являться первым символом.
|
||||
* `version` - хеш-код текущей git версии клиента. Ячейки в этом столбце проверяются при включенном (значение `true`) параметре [check_git_version](#раздел-checks), задаваемом в настройках сервера. Ячейка в данном столбце проходит проверку если хеш-код git версии данного клиента и сервера совпадают (если сервер не расположен в git-репозитории, то проверка проходится автоматически).
|
||||
* `configuration` - заданная пользователем версия конфигурации клиента. Ячейки в этом столбце всегда проходят проверку.
|
||||
* Ячейки этого столбца поддерживают *drag-and-drop*. При перетаскивании ячейки в любое стороннее приложение, поддерживающее файлы (к примеру, "Проводник"), файл конфигурации клиента будет скопирован в указанное место. При перетаскивании ячейки на другую ячейку файл конфигурации будет скопирован с одного на другой. При перетаскивании файла на ячейку он будет записан на клиент в качестве конфигурации (при условии валидации). При передаче конфигурации на клиент секция `PRIVATE` не будет отправляться.
|
||||
* `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`. В остальных случаях, если ячейка не пустая, она проходит проверку.
|
||||
* При двойном клике на ячейку при наличии ошибок будет показано диалоговое окно с полной детализацией всех ошибок.
|
||||
* `current x y z yaw frame_id` - текущее положение коптера с указанием названия системы координат. Ячейка автоматически проходит проверку если у параметра [check_current_position ](#раздел-checks) установлено значение `false`. Иначе, ячейка в данном столбце не проходит проверку, если её значение `NO_POS` или содержит `nan`. В остальных случаях, если ячейка не пустая, она проходит проверку.
|
||||
* `start x y z` - стартовое положение коптера для воспроизведения анимации. Ячейка в данном столбце не проходит проверку, если её значение `NO_POS` или разница между текущим и стартовым положением коптера больше значения [start_pos_delta_max](#раздел-checks). В остальных случаях, если ячейка не пустая, она проходит проверку.
|
||||
* `dt` - разница между временем на сервере и клиенте в секундах, включая сетевую задержку. Ячейка в данном столбце проходит проверку, если её значение меньше значения [time_delta_max](#раздел-checks), задаваемого в настройках сервера. В остальных случаях, если ячейка не пустая, она не проходит проверку. При слишком больших значениях сигнализирует об отсутствии синхронизации времени между коптером и клиентом.
|
||||
|
||||
### Меню
|
||||
|
||||
#### Раздел Server
|
||||
#### Раздел Selected drones
|
||||
|
||||

|
||||

|
||||
|
||||
Данный раздел содержит несколько утилит по отправке различных данных на *выбранные* клиенты. **Внимание!** Не используйте данные команды во время полёта коптеров!
|
||||
Данный раздел содержит несколько утилит по отправке различных данных и команд на *выбранные* клиенты. **Внимание!** Не используйте данные команды во время полёта коптеров!
|
||||
|
||||
* `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`) - выделяет все коптеры в таблице. При следующем вызове команды, выделение всех коптеров будет отменено.
|
||||
* Подраздел `Send`
|
||||
|
||||
#### Раздел Drone
|
||||
* `Animations` - отправка файлов анимации, экспортированных аддоном к Blender, на выбранные коптеры. В диалоговом окне необходимо выбрать *папку*, содержащую файлы анимации. Каждый файл анимации будет отправлен на клиент с именем, соответствующим имени файла без расширения.
|
||||
|
||||

|
||||
* `Configuration` - отправка *единого* файла конфигурации клиента на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл конфигурации в установленном формате. Файл конфигурации может быть неполным, в таком случае будут перезаписаны лишь указанные в файле параметры. **Внимание!** Не рекомендуется использовать данное действие для массовой перезаписи `copter ID`, кроме значения `/hostname`. **Внимание!** НЕ отправляйте на клиенты файл конфигурации сервера.
|
||||
|
||||
* `Launch files` - отправка launch-файлов конфигурации сервиса `clever`. В диалоговом окне необходимо выбрать *папку*, содержащую файлы конфигурации с сширением `.launch`. Все файлы с таким расширением будут отправлены *на каждый* из клиентов. **Внимание!** Существующие файлы конфигурации на коптерах будут перезаписаны, однако файлы, не отправленные сервером, не будут удалены или модифицированы.
|
||||
|
||||
* `Aruco map` - отправка *единого* файла карты aruco маркеров на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл карты в установленном формате. Файл на клиенте будет перезаписан. После получения и записи файла клиент автоматически перезапустит сервис `clever`. Для возобновления работоспособности полётных функций и получения некоторых значений телеметрии *необходимо подождать* некоторое время до полного запуска сервиса.
|
||||
|
||||
* `Camera calibrations` - отправка yaml-файлов калибровки камеры для сервиса `clever`. В диалоговом окне необходимо выбрать *папку*, содержащую файлы конфигурации с расширением `.yaml`. Каждый файл калибровки будет отправлен на клиент с именем (copter ID), соответствующим имени файла без расширения. **Внимание!** Существующий файл калибровки на коптере будет перезаписан.
|
||||
|
||||
* `FCU parameters` - отправка и запись *единого* файла конфигураций полётного контроллера (FCU) на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл параметров в установленном формате. Параметры на полётном контроллере будут перезаписаны.
|
||||
|
||||
* `File` - отправка *одного* любого файла на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл. Далее, необходимо указать путь, по которому данный файл будет записан на клиенты (не включая имя файла).
|
||||
|
||||
* `Command` - отправка и выполнение любой команды терминала на все выбранные клиенты. В диалоговом окне необходимо ввести требуемую команду. Команды *могут* использовать `sudo`-права.
|
||||
|
||||
------
|
||||
|
||||
* `Retrive file` - позволяет скачать любой файл с клиентов в выбранную директорию в файловой системе сервера. Если при скачивании был выбран более чем один клиент, то к имени файла от каждого клиента будет добавлен его ID. В диалоговом окне сначала введите путь к требуемому файлу на клиенте. Далее, в диалоговом окне необходимо указать путь, по которому данный файл будет записан на сервер.
|
||||
|
||||
------
|
||||
|
||||
* Подраздел `Restart Service`
|
||||
|
||||

|
||||
|
||||
* `chrony` - перезапускает сервис синхронизации времени `chrony` на выбранных клиентах. Используйте для ручной синхронизации в случаях, если время между сервером и клиентами не синхронизировано.
|
||||
* `clever` - перезапускает сервис `clever` на выбранных клиентах. Для возобновления работоспособности полётных функций и получения некоторых значений телеметрии *необходимо подождать* некоторое время до полного запуска сервиса.
|
||||
* `clever-show` - перезапускает сервис шоу коптеров `clever-show` на выбранных клиентах. Во время перезапуска клиенты будут отключены.
|
||||
|
||||
------
|
||||
|
||||
* `Set start X Y to current position` - устанавливает точку старта анимации у выбранных клиентов в значения текущей позиции по X Y.
|
||||
|
||||
* `Reset start position` - устанавливает точку старта анимации у выбранных клиентов в значения `0.0`, `0.0`.
|
||||
|
||||
* `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` - полностью перезагружает полётный контроллер и компьютер на выбранных коптерах. Во время перезапуска клиенты будут отключены.
|
||||
* `Reboot` - полностью перезагружает полётный контроллер и компьютер на выбранных коптерах. Во время перезапуска клиенты будут отключены.
|
||||
|
||||
#### Раздел Animation
|
||||
#### Раздел Server
|
||||
|
||||

|
||||
* Подраздел `Music`
|
||||
|
||||
* `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` - останавливает воспроизведение проигрываемой музыки.
|
||||
|
||||

|
||||
------
|
||||
|
||||
* `Select music file` - загружает выбранный музыкальный файл для дальнейшего воспроизведения вручную или через определённое время после старта анимации. Поддерживаемые расширения: `.mp3` или `.wav`.
|
||||
* `Play music` - воспроизводит загруженную музыку.
|
||||
* `Stop music` - останавливает воспроизведение проигрываемой музыки.
|
||||
* `Edit server config` - открывает [встроенный редактор конфигурационных файлов](#config-editor) с текущей конфигурацией сервера для редактирования. Доступен чекбокс `Restart` - в случае, если он нажат, то при сохранении конфигурации сервер будет перезапущен. **Внимание!** Изменённые параметры конфигурации будут применены к серверу только после его перезапуска (ручного или автоматического).
|
||||
|
||||
* `Edit any config` - открывает [встроенный редактор конфигурационных файлов](#config-editor) и позволяет выбрать для редактирования в файловой системе любой файл конфигурации c расширением `.ini` или же открыть файл спецификации конфигурации для создания файла конфигурации на его основе.
|
||||
|
||||
* `Restart server` - полностью перезапускает сервер. **Внимание!** После перезапуска сервер более не будет соединён с консолью, из которой был запущен, если сервер изначально был запущен из консоли.
|
||||
|
||||
#### Раздел Table
|
||||
|
||||

|
||||
|
||||
* `Toggle select` (`Ctrl+A`) - выделят все коптеры\снимает выделение со всех коптеров. Если в таблице выбраны не все коптеры, то *выделяет все* коптеры. Иначе (если были выделены все коптеры) *снимает выделение* со всех коптеров.
|
||||
* `Select all` - выделят все коптеры в таблице.
|
||||
* `Deselect all` - снимает выделение со всех коптеров в таблице.
|
||||
* `Remove selected drones` - удаляет выбранные коптеры из таблицы. **Внимание!** В случае, если клиент был подключен, будет произведено отключение. В случае если удалённый таким образом клиент исправно функционировал, он переподключится в кратчайшие сроки.
|
||||
* `Configure columns` - открывает [встроенный конфигуратор](#column-preset-editor) наборов настроек столбцов таблицы.
|
||||
|
||||
### Боковая панель команд
|
||||
|
||||
@@ -112,7 +152,7 @@
|
||||
* Кнопка `Land ALL` - ВСЕ коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно переходят в режим посадки AUTO.LAND. **Используйте в экстренных случаях как одно из средств перехвата.**
|
||||
|
||||
* Кнопка `Emergency land` - все выбранные коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно переходят в режим экстренной посадки - на все моторы подаётся небольшая мощность, которая уменьшается через определённое время до нуля. **Используйте в экстренных случаях как одно из средств перехвата.**
|
||||
* Кнопка `Visual land` - открывает диалоговое окно модуля визуальной посадки неисправного коптера. Полное описание находится в [конце статьи](#visual-land).
|
||||
* Кнопка `Visual land` - открывает [диалоговое окно](#visual-land) модуля визуальной посадки неисправного коптера.
|
||||
|
||||
* Кнопка `Disarm selected` - все выбранные коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно отключают моторы. Это может привести к падению и повреждению коптеров.
|
||||
* Кнопка `Disarm ALL` - ВСЕ коптеры прекращают выполнение своих полётных заданий, очищают очередь заданий и немедленно отключают моторы. Это может привести к падению и повреждению коптеров **Используйте в крайних случаях как последнее из средств перехвата.**
|
||||
@@ -139,31 +179,18 @@
|
||||
|
||||
### Файл конфигурации
|
||||
|
||||
Конфигурация сервера задаётся в файле [server_config.ini](../../Server/server_config.ini), имеющем следующий вид по умолчанию:
|
||||
Конфигурация сервера создаётся согласно [спецификации](../../Server/config/spec/configspec_server.ini), в ней можно посмотреть значения по умолчанию для любого параметра после ключевого слова `default`. Все изменения сохраняются в файл конфигурации `server.ini` в папке `clever-show/Server/config`.
|
||||
|
||||
```ini
|
||||
[SERVER]
|
||||
port = 25000
|
||||
buffer_size = 1024
|
||||
remove_disconnected = False
|
||||
Доступно редактирование конфигурации сервера через GUI модуль `Config editor` через меню `Server -> Edit server config`.
|
||||
|
||||
[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.0
|
||||
|
||||
[NTP]
|
||||
use_ntp = False
|
||||
host = ntp1.stratum2.ru
|
||||
port = 123
|
||||
```
|
||||
Конфигурация по умолчанию является полностью работоспособной и не требует изменений для быстрого старта клиента.
|
||||
|
||||
Конфигурация по умолчанию является полностью работоспособной и не требует изменений для быстрого начала работы системы.
|
||||
### Описание параметров
|
||||
|
||||
#### Корневой раздел
|
||||
|
||||
* `config_name` - Произвольная строка, название файла конфигурации для удобства хранения и быстрого отличия одного файла конфигурации от другого.
|
||||
* `config_version` - Произвольное дробное число, версия файла конфигурации для удобства хранения и быстрого отличия одного файла конфигурации от другого.
|
||||
|
||||
#### Раздел SERVER
|
||||
|
||||
@@ -171,12 +198,25 @@ port = 123
|
||||
|
||||
* `port` - TCP порт, на который будут приниматься входящие соединения от клиентов. При использовании broadcast данный порт будет сконфигурирован у клиента автоматически. *Рекомендуется изменить значение по умолчанию в целях безопасности* (любое пятизначное и более число, если другое ПО не использует выбранный порт).
|
||||
* `buffer_size` - размер буфера при приёме и передаче данных. *Не рекомендуется изменять. Рекомендуется использовать единое значение у сервера и клиентов.*
|
||||
|
||||
#### Раздел TABLE
|
||||
|
||||
* `remove_disconnected` - Определяет поведение при разрыве связи с клиентом. При значении `True` вся информация о клиенте *будет удалена* как из внутренней памяти, так и *из таблицы*. *Это может привести к 'скачкам' таблицы при отключении клиентов.* При значении `False` отключённые клиенты *не будут* удалены из таблицы, но будут отображены с подсвечиванием ячейки в столбце `copter ID` красным цветом. Все данные будут сохранены. При переподключении клиента, он будет ассоциирован с той же строкой таблицы, а ячейка со значением `copter ID` вновь станет зелёного цвета.
|
||||
|
||||
##### Подраздел PRESETS
|
||||
|
||||
Не рекомендуется изменять данный раздел вручную - для редактирования данных параметров можно взаимодействовать с таблицей или используя встроенный диалог конфигурации таблицы.
|
||||
|
||||
* `current` - Название текущего выбранного набора настроек столбцов таблицы
|
||||
* `<название_набора>`
|
||||
* `<название_столбца>` - значение представляет собой список (через ",") из булевого значения (отображается ли столбец в таблице) и целого числа больше 0 (ширину столбца в пикселах)
|
||||
|
||||
#### Раздел CHECKS
|
||||
|
||||
В этом разделе задаются параметры проверок коптера, которые регулируются на стороне сервера. Доступны следующие параметры:
|
||||
|
||||
* `check_git_version` - Будет ли производиться проверка соответствия git-версий клиента и сервера для индикации в ячейках столбца `version`
|
||||
* `check_current_position` - Будет ли производиться проверка корректности текущих координат коптера для индикации в ячейках столбца `current x y z yaw frame_id`.
|
||||
* `battery_percentage_min` - Минимальный заряд батарии коптера, допустимый для взлёта. Указывается *в процентах* (дробное значение от 0 до 100). Значение меньше указанного будет отмечено в столбце `battery` как неудовлетворительное.
|
||||
* `start_pos_delta_max` - Максимальное расстояние от текущего положения коптера до его точки взлёта в файле анимации, допустимое для взлёта. Указывается *в метрах* (дробное значение от 0 до 'inf'). Значение больше указанного будет отмечено в столбце `start x y z` как неудовлетворительное. Допустимо использование строки 'inf' для любого допустимого расстояния.
|
||||
* `time_delta_max` - Максимальная разница (абсолютное значение) между временем сервера и клиента (включая сетевую задержку), допустимая для взлёта. Указывается *в секундах* (дробное значение от 0 до 'inf'). Значение больше указанного будет отмечено в столбце `dt` как неудовлетворительное.
|
||||
@@ -185,19 +225,20 @@ port = 123
|
||||
|
||||
Сервер может использовать UDP broadcast, чтобы передавать клиентам актуальную информацию о конфигурации сервера. Таким образом становится возможным автоматическое подключение клиентов к серверу без необходимости дополнительной ручной конфигурации. В данном разделе задаются параметры этого механизма:
|
||||
|
||||
* `use_broadcast` - будут ли использованы broadcast'ы для передачи данных (при значении `False` broadcast'ы НЕ будут отправляться). Используйте `False` в случае повышенных требований безопасности, перегруженности сети или невозможности передачи по широковещательному каналу (из-за конфигурации брандмауэра или сети)
|
||||
* `broadcast_port` - UDP порт, по которому будет осуществляться отправка сообщений. *Рекомендуется изменить значение по умолчанию в целях безопасности.* **Внимание!** При изменении этого параметра клиенты НЕ смогут принимать сообщения автоконфигурации до изменения (вручную) соответствующего параметра в конфигурации клиента на равное значение.
|
||||
* `broadcast_delay` - периодичность (в секундах, целочисленное значение), с которой будет происходить отправка broadcast сообщений. Увеличьте задержку для уменьшения нагрузки на сеть. Уменьшите задержку для уменьшения времени отклика и подключения при первом запуске клиентов.
|
||||
* `send` - будут ли использованы broadcast'ы для передачи данных (при значении `False` broadcast'ы НЕ будут отправляться). Используйте `False` в случае повышенных требований безопасности, перегруженности сети или невозможности передачи по широковещательному каналу (из-за конфигурации брандмауэра или сети)
|
||||
* `listen` - будет ли сервер прослушивать порт бродкастов для автоматического выключения во избежание наличия нескольких серверов в одной сети.
|
||||
* `port` - UDP порт, по которому будет осуществляться отправка сообщений. *Рекомендуется изменить значение по умолчанию в целях безопасности.* **Внимание!** При изменении этого параметра клиенты НЕ смогут принимать сообщения автоконфигурации до изменения (вручную) соответствующего параметра в конфигурации клиента на равное значение.
|
||||
* `delay` - периодичность (в секундах, дробное значение), с которой будет происходить отправка broadcast сообщений. Увеличьте задержку для уменьшения нагрузки на сеть. Уменьшите задержку для уменьшения времени отклика и подключения при первом запуске клиентов.
|
||||
|
||||
#### Раздел NTP
|
||||
|
||||
Помимо синхронизации времени (с миллисекундной точностью) с помощью пакета chrony, предоставляется альтернатива - возможность использования внешних (при наличии соединения локальной сети с интернетом) или внутрисетевых NTP-серверов. **Внимание!** Для корректной работы системы, **и сервер, и клиенты** должны использовать единый способ синхронизации времени (набор параметров в этом разделе). Данный раздел полностью унифицирован и для сервера, и для клиентов.
|
||||
|
||||
* `use_ntp` - определяет, будет ли использоваться синхронизация времени с помощью NTP. (при значении `False` будет использовано локальное время ОС (синхронизируется автоматически при использовании chrony). *Рекомендуется использование chrony, а не NTP*
|
||||
* `use` - определяет, будет ли использоваться синхронизация времени с помощью NTP. (при значении `False` будет использовано локальное время ОС (синхронизируется автоматически при использовании chrony). *Рекомендуется использование chrony, а не NTP*
|
||||
* `host` - имя хоста или IP адрес NTP сервера (локального или удаленного)
|
||||
* `port` - порт, используемый NTP сервером
|
||||
|
||||
## Дополнительные операции
|
||||
## Дополнительные операции и окна
|
||||
|
||||
### Visual land
|
||||
|
||||
@@ -209,6 +250,14 @@ port = 123
|
||||
|
||||
При нажатии на кнопку `Visual land` все коптеры делятся на 2 равные группы по порядку расположения в таблице. Первая половина коптеров зажигает светодиодную ленту зелёным цветом, вторая - красным. При нажатии на зелёную или красную кнопку происходит выбор группы, соответствующей цвету нажатой кнопки. Коптеры выбранного цвета снова делятся на две половины и каждая половина зажигает светодиодную ленту зелёным и красным цветом соответственно. Остальные коптеры выключают светодиодную ленту.
|
||||
|
||||
Нажимая на кнопки, соответствующие цвету группы, в которой находится неисправный коптер, можно определить его номер и выполнить экстренную посадку за логорифмическое количество шагов от количества коптеров, т.е. гораздо быстрее, чем перебирая коптеры по одному.
|
||||
Нажимая на кнопки, соответствующие цвету группы, в которой находится неисправный коптер, можно определить его номер и выполнить экстренную посадку за логарифмическое количество шагов от количества коптеров, т.е. гораздо быстрее, чем перебирая коптеры по одному.
|
||||
|
||||
На любом шаге можно произвести посадку или выключение моторов всех коптеров, на которых включена светодиодная лента, нажав кнопку `Land` или `Disarm`.
|
||||
|
||||
### Config editor
|
||||
|
||||
...
|
||||
|
||||
### Column preset editor
|
||||
|
||||
...
|
||||
|
||||
3
lib.py
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
def b_partial(func, *args, **kwargs): # call argument blocker partial
|
||||
return lambda *a: func(*args, **kwargs)
|
||||
271
messaging_lib.py
@@ -5,10 +5,11 @@ import json
|
||||
import socket
|
||||
import struct
|
||||
import random
|
||||
import inspect
|
||||
import logging
|
||||
import threading
|
||||
import collections
|
||||
import platform
|
||||
import traceback
|
||||
|
||||
from contextlib import closing
|
||||
|
||||
@@ -18,9 +19,6 @@ except ImportError:
|
||||
import selectors2 as selectors
|
||||
|
||||
|
||||
# import logging_lib
|
||||
|
||||
|
||||
class Namespace:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
@@ -38,9 +36,6 @@ class PendingRequest(Namespace): pass
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# logger = logging_lib.Logger(_logger, True)
|
||||
|
||||
|
||||
def get_ip_address():
|
||||
try:
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as ip_socket:
|
||||
@@ -51,6 +46,30 @@ def get_ip_address():
|
||||
return "localhost"
|
||||
|
||||
|
||||
def set_keepalive(sock, after_idle_sec=1, interval_sec=3, max_fails=5):
|
||||
current_platform = platform.system() # could be empty
|
||||
if current_platform == "Linux":
|
||||
return _set_keepalive_linux(sock, after_idle_sec, interval_sec, max_fails)
|
||||
if current_platform == "Windows":
|
||||
return _set_keepalive_windows(sock, after_idle_sec, interval_sec)
|
||||
if current_platform == "Darwin":
|
||||
return _set_keepalive_osx(sock, interval_sec)
|
||||
|
||||
def _set_keepalive_linux(sock, after_idle_sec, interval_sec, max_fails):
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, after_idle_sec)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval_sec)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, max_fails)
|
||||
|
||||
def _set_keepalive_windows(sock, after_idle_sec, interval_sec):
|
||||
sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, after_idle_sec*1000, interval_sec*1000))
|
||||
|
||||
def _set_keepalive_osx(sock, interval_sec):
|
||||
TCP_KEEPALIVE = 0x10
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, TCP_KEEPALIVE, interval_sec)
|
||||
|
||||
|
||||
class _Singleton(type):
|
||||
""" A metaclass that creates a Singleton base class when called. """
|
||||
_instances = {}
|
||||
@@ -78,7 +97,7 @@ class MessageManager:
|
||||
@staticmethod
|
||||
def _json_decode(json_bytes, encoding="utf-8"):
|
||||
with io.TextIOWrapper(io.BytesIO(json_bytes), encoding=encoding, newline="") as tiow:
|
||||
obj = json.load(tiow)
|
||||
obj = json.load(tiow, object_pairs_hook=collections.OrderedDict)
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
@@ -100,35 +119,41 @@ class MessageManager:
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def create_json_message(cls, contents):
|
||||
message = cls.create_message(cls._json_encode(contents), "json", "message")
|
||||
def create_json_message(cls, contents, additional_headers=None):
|
||||
message = cls.create_message(cls._json_encode(contents), "json", "message",
|
||||
additional_headers=additional_headers)
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def create_simple_message(cls, command, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
message = cls.create_json_message({"command": command, "args": args})
|
||||
def create_action_message(cls, action, args=(), kwargs=None):
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
message = cls.create_json_message({"args": args, "kwargs": kwargs}, {"action": action, })
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def create_request(cls, requested_value, request_id, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
def create_request(cls, requested_value, request_id, args=(), kwargs=None):
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
contents = {"requested_value": requested_value,
|
||||
"request_id": request_id,
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
message = cls.create_message(cls._json_encode(contents), "json", "request")
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def create_response(cls, requested_value, request_id, value):
|
||||
contents = {"requested_value": requested_value,
|
||||
"request_id": request_id,
|
||||
"value": value,
|
||||
}
|
||||
message = cls.create_message(cls._json_encode(contents), "json", "response")
|
||||
def create_response(cls, requested_value, request_id, value, filetransfer=False):
|
||||
headers = {"requested_value": requested_value,
|
||||
"request_id": request_id, # TODO status
|
||||
}
|
||||
if filetransfer:
|
||||
contents = value
|
||||
else:
|
||||
contents = cls._json_encode({"value": value, })
|
||||
message = cls.create_message(contents, "binary" if filetransfer else "json",
|
||||
"response", additional_headers=headers)
|
||||
return message
|
||||
|
||||
def _process_protoheader(self):
|
||||
@@ -177,10 +202,10 @@ class MessageManager:
|
||||
self._process_content()
|
||||
|
||||
|
||||
def message_callback(string_command):
|
||||
def message_callback(action_string):
|
||||
def inner(f):
|
||||
ConnectionManager.messages_callbacks[string_command] = f
|
||||
logger.debug("Registered message function {} for {}".format(f, string_command))
|
||||
ConnectionManager.messages_callbacks[action_string] = f
|
||||
logger.debug("Registered message function {} for {}".format(f, action_string))
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
@@ -315,21 +340,23 @@ class ConnectionManager(object):
|
||||
def read(self):
|
||||
self._read()
|
||||
while self._recv_buffer:
|
||||
# add new message object if queue is empty or last message already processed
|
||||
if not self._received_queue or (self._received_queue[0].content is not None):
|
||||
self._received_queue.appendleft(MessageManager())
|
||||
|
||||
self._received_queue[0].income_raw += self._recv_buffer
|
||||
last_message = self._received_queue[0]
|
||||
|
||||
last_message.income_raw += self._recv_buffer
|
||||
self._recv_buffer = b''
|
||||
self._received_queue[0].process_message()
|
||||
last_message.process_message()
|
||||
|
||||
# if something left after processing message - put it back
|
||||
if self._received_queue[0].content and self._received_queue[0].income_raw:
|
||||
self._recv_buffer = self._received_queue[0].income_raw + self._recv_buffer
|
||||
self._received_queue[0].income_raw = b''
|
||||
if last_message.content is not None and last_message.income_raw:
|
||||
self._recv_buffer = last_message.income_raw + self._recv_buffer
|
||||
last_message.income_raw = b''
|
||||
|
||||
if self._received_queue:
|
||||
if self._received_queue[0].content:
|
||||
self.process_received(self._received_queue.popleft())
|
||||
if self._received_queue and last_message.content is not None:
|
||||
self.process_received(self._received_queue.popleft())
|
||||
|
||||
def _read(self):
|
||||
try:
|
||||
@@ -346,73 +373,106 @@ class ConnectionManager(object):
|
||||
|
||||
raise RuntimeError("Peer closed.")
|
||||
|
||||
def process_received(self, income_message):
|
||||
message_type = income_message.jsonheader["message-type"]
|
||||
def process_received(self, message):
|
||||
message_type = message.jsonheader["message-type"]
|
||||
content = message.content if message.jsonheader["content-type"] != "binary"\
|
||||
else message.content[:256]
|
||||
logger.debug(
|
||||
"Received message! Header: {}, content: {}".format(income_message.jsonheader, income_message.content))
|
||||
"Received message! Header: {}, content: {}".format(message.jsonheader, content))
|
||||
|
||||
if message_type == "message":
|
||||
self._process_message(income_message)
|
||||
self._process_message(message)
|
||||
elif message_type == "response":
|
||||
self._process_response(income_message)
|
||||
self._process_response(message)
|
||||
elif message_type == "request":
|
||||
self._process_request(income_message)
|
||||
elif message_type == "filetransfer":
|
||||
self._process_filetransfer(income_message)
|
||||
self._process_request(message)
|
||||
|
||||
def _process_message(self, message):
|
||||
command = message.content["command"]
|
||||
if message.jsonheader["action"] == "filetransfer":
|
||||
self._process_filetransfer(message.content, message.jsonheader["filepath"])
|
||||
else:
|
||||
self._process_action(message)
|
||||
|
||||
def _process_action(self, message):
|
||||
action = message.jsonheader["action"]
|
||||
args = message.content["args"]
|
||||
kwargs = message.content["kwargs"]
|
||||
callback = self.messages_callbacks.get(action, None)
|
||||
if callback is None:
|
||||
logger.warning("Action {} does not exist!".format(action))
|
||||
return
|
||||
try:
|
||||
self.messages_callbacks[command](self, **args)
|
||||
except KeyError:
|
||||
logger.warning("Command {} does not exist!".format(command))
|
||||
callback(self, *args, **kwargs)
|
||||
except Exception as error:
|
||||
logger.error("Error during command {} execution: {}".format(command, error))
|
||||
logger.error("Error during action {} execution: {}".format(action, error))
|
||||
traceback.print_exc()
|
||||
|
||||
def _process_request(self, message):
|
||||
command = message.content["requested_value"]
|
||||
requested_value = message.content["requested_value"]
|
||||
request_id = message.content["request_id"]
|
||||
args = message.content["args"]
|
||||
kwargs = message.content["kwargs"]
|
||||
|
||||
filetransfer = requested_value == "filetransfer"
|
||||
try:
|
||||
value = self.requests_callbacks[command](self, **args)
|
||||
except KeyError:
|
||||
logger.warning("Request {} does not exist!".format(command))
|
||||
if filetransfer:
|
||||
value = self._read_file(kwargs["filepath"])
|
||||
else:
|
||||
callback = self.requests_callbacks.get(requested_value, None)
|
||||
if callback is None:
|
||||
logger.warning("Request {} does not exist!".format(requested_value))
|
||||
return
|
||||
|
||||
value = callback(self, *args, **kwargs)
|
||||
except Exception as error: # TODO send response error\cancel
|
||||
logger.error("Error during request {} processing: {}".format(command, error))
|
||||
logger.error("Error during request {} processing: {}".format(requested_value, error))
|
||||
else:
|
||||
self._send_response(command, request_id, value)
|
||||
self._send_response(requested_value, request_id, value, filetransfer)
|
||||
|
||||
def _process_response(self, message):
|
||||
request_id, requested_value = message.content["request_id"], message.content["requested_value"]
|
||||
request_id, requested_value = message.jsonheader["request_id"], message.jsonheader["requested_value"]
|
||||
|
||||
with self._request_lock:
|
||||
request = self._request_queue.pop(request_id, None)
|
||||
if (request is None) or (request.requested_value != requested_value):
|
||||
logger.warning("Unexpected response!")
|
||||
return
|
||||
|
||||
if (request is not None) and (request.requested_value == requested_value):
|
||||
if requested_value == "filetransfer":
|
||||
value = True
|
||||
self._process_filetransfer(message.content, request.callback_kwargs["filepath"])
|
||||
logger.debug(
|
||||
"Request {} successfully closed with file bytes {}...".format(request, message.content[:256])
|
||||
)
|
||||
else:
|
||||
value = message.content["value"]
|
||||
logger.debug(
|
||||
"Request {} successfully closed with value {}".format(request, message.content["value"])
|
||||
)
|
||||
|
||||
f = request.callback
|
||||
f(self, value, *request.callback_args, **request.callback_kwargs)
|
||||
else:
|
||||
logger.warning("Unexpected response!")
|
||||
|
||||
def _process_filetransfer(self, message): # TODO path?
|
||||
if message.jsonheader["content-type"] == "binary":
|
||||
filepath = message.jsonheader["filepath"]
|
||||
if request.callback is not None:
|
||||
try:
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(message.content)
|
||||
except OSError as error:
|
||||
logger.error("File {} can not be written due error: {}".format(filepath, error))
|
||||
else:
|
||||
logger.info("File {} successfully received ".format(filepath))
|
||||
if self.whoami == "pi":
|
||||
logger.info("Return rights to pi:pi after file transfer")
|
||||
os.system("chown pi:pi {}".format(filepath))
|
||||
request.callback(self, value, *request.callback_args, **request.callback_kwargs)
|
||||
except Exception as error:
|
||||
logger.error("Error during response {} processing: {}".format(request, error))
|
||||
else:
|
||||
logger.info("No callback were registered for response: {}".format(request))
|
||||
|
||||
@staticmethod
|
||||
def _read_file(filepath):
|
||||
with open(filepath, mode='rb') as f:
|
||||
return f.read()
|
||||
|
||||
def _process_filetransfer(self, content, filepath):
|
||||
try:
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(content)
|
||||
except OSError as error:
|
||||
logger.error("File {} can not be written due error: {}".format(filepath, error))
|
||||
else:
|
||||
logger.info("File {} successfully received ".format(filepath))
|
||||
if self.whoami == "pi":
|
||||
logger.info("Return rights to pi:pi after file transfer")
|
||||
os.system("chown pi:pi {}".format(filepath))
|
||||
|
||||
def write(self):
|
||||
with self._send_lock:
|
||||
@@ -438,8 +498,7 @@ class ConnectionManager(object):
|
||||
else:
|
||||
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],))
|
||||
|
||||
logger.debug("Sent message to {}: sent {} bytes, {} bytes left.".format(self.addr, sent, left))
|
||||
|
||||
def _send(self, data):
|
||||
with self._send_lock:
|
||||
@@ -449,14 +508,15 @@ class ConnectionManager(object):
|
||||
self._set_selector_events_mask('rw')
|
||||
NotifierSock().notify()
|
||||
|
||||
def get_response(self, requested_value, callback, request_args=None, # timeout=30,
|
||||
callback_args=(), callback_kwargs=None):
|
||||
if request_args is None:
|
||||
request_args = {}
|
||||
def get_response(self, requested_value, callback, # timeout=30,
|
||||
request_args=(), request_kwargs=None,
|
||||
callback_args=(), callback_kwargs=None, ):
|
||||
if request_kwargs is None:
|
||||
request_kwargs = {}
|
||||
if callback_kwargs is None:
|
||||
callback_kwargs = {}
|
||||
|
||||
request_id = str(random.randint(0, 9999)).zfill(4)
|
||||
request_id = str(random.randint(0, 9999)).zfill(4) # maybe hash
|
||||
with self._request_lock:
|
||||
self._request_queue[request_id] = PendingRequest(
|
||||
requested_value=requested_value,
|
||||
@@ -466,24 +526,39 @@ class ConnectionManager(object):
|
||||
callback_args=callback_args,
|
||||
callback_kwargs=callback_kwargs,
|
||||
request_args=request_args,
|
||||
request_kwargs=request_kwargs,
|
||||
resend=True,
|
||||
)
|
||||
self._send(MessageManager.create_request(requested_value, request_id, request_args))
|
||||
self._send(MessageManager.create_request(requested_value, request_id, request_args, request_kwargs))
|
||||
|
||||
def get_file(self, client_filepath, filepath=None, callback=None,
|
||||
callback_args=(), callback_kwargs=None, ):
|
||||
if callback_kwargs is None:
|
||||
callback_kwargs = {}
|
||||
|
||||
if filepath is None:
|
||||
filepath = os.path.split(client_filepath)[1]
|
||||
|
||||
request_kwargs = {"filepath": client_filepath}
|
||||
callback_kwargs.update({"filepath": filepath})
|
||||
|
||||
self.get_response("filetransfer", callback, request_kwargs=request_kwargs,
|
||||
callback_args=callback_args, callback_kwargs=callback_kwargs)
|
||||
|
||||
def _resend_requests(self):
|
||||
with self._request_lock:
|
||||
for request_id, request in self._request_queue.items(): #TODO filter
|
||||
for request_id, request in self._request_queue.items(): # TODO filter
|
||||
if request.resend:
|
||||
self._send(MessageManager.create_request(
|
||||
request.requested_value, request_id, request.request_args.update(resend=request.resend))
|
||||
request.requested_value, request_id, request.request_kwargs.update(resend=request.resend))
|
||||
)
|
||||
request.resend = False
|
||||
|
||||
def send_message(self, command, args=None):
|
||||
self._send(MessageManager.create_simple_message(command, args))
|
||||
def send_message(self, action, args=(), kwargs=None):
|
||||
self._send(MessageManager.create_action_message(action, args, kwargs))
|
||||
|
||||
def _send_response(self, requested_value, request_id, value):
|
||||
self._send(MessageManager.create_response(requested_value, request_id, value))
|
||||
def _send_response(self, requested_value, request_id, value, filetransfer=False):
|
||||
self._send(MessageManager.create_response(requested_value, request_id, value, filetransfer))
|
||||
|
||||
def send_file(self, filepath, dest_filepath): # clever_restart=False
|
||||
try:
|
||||
@@ -493,9 +568,8 @@ class ConnectionManager(object):
|
||||
logger.warning("File can not be opened due error: ".format(error))
|
||||
else:
|
||||
logger.info("Sending file {} to {} (as: {})".format(filepath, self.addr, dest_filepath))
|
||||
self._send(MessageManager.create_message(
|
||||
data, "binary", "filetransfer", "binary", {"filepath": dest_filepath}
|
||||
))
|
||||
self._send(MessageManager.create_message(data, "binary", "message",
|
||||
additional_headers={"action": "filetransfer", "filepath": dest_filepath}))
|
||||
|
||||
|
||||
class NotifierSock(Singleton):
|
||||
@@ -527,9 +601,10 @@ class NotifierSock(Singleton):
|
||||
|
||||
def notify(self):
|
||||
with self._send_lock:
|
||||
if self._receiving_sock is not None:
|
||||
self._sending_sock.sendall(bytes(1))
|
||||
logger.debug("Notify socket: notified")
|
||||
if self._receiving_sock is None:
|
||||
return
|
||||
self._sending_sock.sendall(bytes(1))
|
||||
logger.debug("Notify socket: notified")
|
||||
|
||||
def process_events(self, mask):
|
||||
if mask & selectors.EVENT_READ and self._receiving_sock is not None:
|
||||
@@ -539,4 +614,12 @@ class NotifierSock(Singleton):
|
||||
except io.BlockingIOError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(e)
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._server_socket.close()
|
||||
self._sending_sock.close()
|
||||
self._receiving_sock.close()
|
||||
except (OSError, KeyError) as error:
|
||||
logger.error("Error during unregistring notifier socket: {}".format(error))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
indexed.py==0.0.1
|
||||
configobj==5.0.6
|
||||
numpy==1.18.1
|
||||
PyQt5==5.13.0
|
||||
PyQt5-sip==4.19.18
|
||||
Quamash==0.6.1
|
||||
selectors2==2.0.1
|
||||
six==1.13.0
|
||||
|
||||
14
update_configspec.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import config
|
||||
from Server.copter_table_models import CopterDataModel
|
||||
|
||||
cfg_server = config.ConfigObj('SERVER/config/spec/configspec_server.ini', list_values=False)
|
||||
widths = {"copter_id": 150}
|
||||
default_width = 100
|
||||
|
||||
default = {key: f"preset_param(default=list(True, {widths.get(key, default_width)}))"
|
||||
for key in CopterDataModel.columns}
|
||||
|
||||
cfg_server['TABLE']['PRESETS']['DEFAULT'] = default
|
||||
|
||||
cfg_server.write()
|
||||
print('Server configspec updated')
|
||||