Merge pull request #63 from CopterExpress/qt-gui-update

Big update
This commit is contained in:
Arthur Golubtsov
2020-04-15 17:10:45 +03:00
committed by GitHub
44 changed files with 4346 additions and 1637 deletions

8
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

@@ -0,0 +1,3 @@
selectors2
psutil
configobj

View File

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

View File

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

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
Server/icons/image.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

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

View File

@@ -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
![Скриншот раздела Server](../assets/server-server.png)
![Скриншот раздела Selected drones - Send](../assets/server-drone-send.png)
Данный раздел содержит несколько утилит по отправке различных данных на *выбранные* клиенты. **Внимание!** Не используйте данные команды во время полёта коптеров!
Данный раздел содержит несколько утилит по отправке различных данных и команд на *выбранные* клиенты. **Внимание!** Не используйте данные команды во время полёта коптеров!
* `Send animations` - отправка файлов анимации, экспортированных аддоном к Blender, на выбранные коптеры. В диалоговом окне необходимо выбрать *папку*, содержащую файлы анимации. Каждый файл анимации будет отправлен на клиент с именем, соответствующим имени файла без расширения.
* `Send configurations` - отправка *единого* файла конфигурации клиента на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл конфигурации в установленном формате. Файл конфигурации может быть неполным, в таком случае будут перезаписаны лишь указанные в файле параметры. **Внимание!** Не рекомендуется использовать данное действие для массовой перезаписи `copter ID`, кроме значения `/hostname`. **Внимание!** НЕ отправляйте на клиенты файл конфигурации сервера.
* `Send launch files` - отправка launch-файлов конфигурации сервиса `clever`. В диалоговом окне необходимо выбрать *папку*, содержащую файлы конфигурации с сширением `.launch`. Все файлы с таким расширением будут отправлены *на каждый* из клиентов. **Внимание!** Существующие файлы конфигурации на коптерах будут перезаписаны, однако файлы, не отправленные сервером, не будут удалены или модифицированы.
* `Send aruco map` - отправка *единого* файла карты aruco маркеров на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл карты в установленном формате. Файл на клиенте будет перезаписан. После получения и записи файла клиент автоматически перезапустит сервис `clever`. Для возобновления работоспособности полётных функций и получения некоторых значений телеметрии *необходимо подождать* некоторое время до полного запуска сервиса.
* `Send camera calibrations` - отправка yaml-файлов калибровки камеры для сервиса `clever`. В диалоговом окне необходимо выбрать *папку*, содержащую файлы конфигурации с расширением `.yaml`. Каждый файл калибровки будет отправлен на клиент с именем (copter ID), соответствующим имени файла без расширения. **Внимание!** Существующий файл калибровки на коптере будет перезаписан.
* `Send FCU parameters` - отправка и запись *единого* файла конфигураций полётного контроллера (FCU) на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл параметров в установленном формате. Параметры на полётном контроллере будут перезаписаны.
* `Developer mode`: **Внимание!** Используйте данные действия с большой осторожностью.
* `Send any file` - отправка *одного* любого файла на все выбранные клиенты. В диалоговом окне необходимо выбрать *один* файл. Далее, необходимо указать путь, по которому данный файл будет записан на клиенты (не включая имя файла).
* `Send any command` - отправка и выполнение любой команды терминала на все выбранные клиенты. В диалоговом окне необходимо ввести требуемую команду. Команды *могут* использовать `sudo`-права.
* `Select all drones` (`Ctrl+A`) - выделяет все коптеры в таблице. При следующем вызове команды, выделение всех коптеров будет отменено.
* Подраздел `Send`
#### Раздел Drone
* `Animations` - отправка файлов анимации, экспортированных аддоном к Blender, на выбранные коптеры. В диалоговом окне необходимо выбрать *папку*, содержащую файлы анимации. Каждый файл анимации будет отправлен на клиент с именем, соответствующим имени файла без расширения.
![Скриншот раздела Drone](../assets/server-drone.png)
* `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`
![Скриншот раздела Selected drones - Restart](../assets/server-drone-restart.png)
* `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
![Скриншот раздела Animation](../assets/server-animation.png)
* Подраздел `Music`
* `Set start X Y to current position` - устанавливает точку старта анимации у выбранных клиентов в значения текущей позиции по X Y.
* `Reset start position` - устанавливает точку старта анимации у выбранных клиентов в значения `0.0`, `0.0`.
![Скриншот раздела Server - music](../assets/server-music.png)
#### Раздел Music
* `Select music file` - загружает выбранный музыкальный файл для дальнейшего воспроизведения вручную или через определённое время после старта анимации. Поддерживаемые расширения: `.mp3` или `.wav`.
* `Play music` - воспроизводит загруженную музыку.
* `Stop music` - останавливает воспроизведение проигрываемой музыки.
![Скриншот раздела Music](../assets/server-music.png)
------
* `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
![Скриншот раздела Table](../assets/server-table.png)
* `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
View File

@@ -0,0 +1,3 @@
def b_partial(func, *args, **kwargs): # call argument blocker partial
return lambda *a: func(*args, **kwargs)

View File

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

View File

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