Merge master

This commit is contained in:
Arthur Golubtsov
2019-10-21 08:39:00 +01:00
20 changed files with 715 additions and 270 deletions

View File

@@ -69,7 +69,7 @@ def check(check_name):
failures = f(*args, **kwargs)
msgs = []
for failure in failures:
msg = "[{}]: Failure: {}".format(check_name, failure)
msg = "[{}]: Err: {}".format(check_name, failure)
msgs.append(msg)
logger.warning(msg)

View File

@@ -40,7 +40,7 @@ def get_id(filepath="animation.csv"):
print("No animation id in file")
return anim_id
def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, ratio=1):
def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, x_ratio=1, y_ratio=1, z_ratio=1):
imported_frames = []
global anim_id
try:
@@ -62,9 +62,9 @@ def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, ratio=1):
frame_number, x, y, z, yaw, red, green, blue = row_0
imported_frames.append({
'number': int(frame_number),
'x': ratio*float(x) + x0,
'y': ratio*float(y) + y0,
'z': ratio*float(z) + z0,
'x': x_ratio*float(x) + x0,
'y': y_ratio*float(y) + y0,
'z': z_ratio*float(z) + z0,
'yaw': float(yaw),
'red': int(red),
'green': int(green),
@@ -74,9 +74,9 @@ def load_animation(filepath="animation.csv", x0=0, y0=0, z0=0, ratio=1):
frame_number, x, y, z, yaw, red, green, blue = row
imported_frames.append({
'number': int(frame_number),
'x': ratio*float(x) + x0,
'y': ratio*float(y) + y0,
'z': ratio*float(z) + z0,
'x': x_ratio*float(x) + x0,
'y': y_ratio*float(y) + y0,
'z': z_ratio*float(z) + z0,
'yaw': float(yaw),
'red': int(red),
'green': int(green),

View File

@@ -47,6 +47,8 @@ class Client(object):
global active_client
active_client = self
# self._last_ping_time = 0
def load_config(self):
self.config.read(self.config_path)
@@ -58,14 +60,16 @@ class Client(object):
self.NTP_HOST = self.config.get('NTP', 'host')
self.NTP_PORT = self.config.getint('NTP', 'port')
self.files_directory = self.config.get('FILETRANSFER', 'files_directory')
self.files_directory = self.config.get('FILETRANSFER', 'files_directory') # not used?!
self.client_id = self.config.get('PRIVATE', 'id')
if self.client_id == 'default':
if self.client_id == '/default':
self.client_id = 'copter' + str(random.randrange(9999)).zfill(4)
self.write_config(False, 'PRIVATE', 'id', self.client_id)
self.write_config(False, ConfigOption('PRIVATE', 'id', self.client_id))
elif self.client_id == '/hostname':
self.client_id = socket.gethostname()
elif self.client_id == '/ip':
self.client_id = messaging.get_ip_address()
def rewrite_config(self):
with open(self.config_path, 'w') as file:
@@ -103,7 +107,7 @@ class Client(object):
try:
while True:
self._reconnect()
#self._process_connections()
self._process_connections()
except (KeyboardInterrupt, ):
logger.critical("Caught interrupt, exiting!")
@@ -117,6 +121,8 @@ class Client(object):
try:
self.client_socket = socket.socket()
self.client_socket.settimeout(timeout)
self.client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.client_socket.connect((self.server_host, self.server_port))
except socket.error as error:
if isinstance(error, OSError):
@@ -138,15 +144,12 @@ class Client(object):
self.broadcast_bind(timeout*2, attempt_limit)
attempt_count = 0
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._process_connections()
def broadcast_bind(self, timeout=3.0, attempt_limit=5):
broadcast_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -187,6 +190,9 @@ class Client(object):
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: # TODO add notifier to client!
connection = key.data
@@ -200,7 +206,7 @@ class Client(object):
logger.error(
"Exception {} occurred for {}! Resetting connection!".format(error, connection.addr)
)
self.server_connection.close()
self.server_connection._close()
self.connected = False
if isinstance(error, OSError):
@@ -213,20 +219,28 @@ class Client(object):
return
@messaging.request_callback("id")
def _response_id():
return active_client.client_id
@messaging.request_callback("time")
def _response_time():
return active_client.time_now()
@messaging.message_callback("config_write")
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)
@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)
return active_client.client_id
@messaging.request_callback("time")
def _response_time(*args, **kwargs):
return active_client.time_now()
if __name__ == "__main__":
client = Client()
client.start()

View File

@@ -17,7 +17,9 @@ port = 123
takeoff_animation_check = True
land_animation_check = True
frame_delay = 0.1
ratio = 1.0
x_ratio = 1.0
y_ratio = 1.0
z_ratio = 1.0
[COPTERS]
frame_id = floor
@@ -41,7 +43,8 @@ yaw = -90
[PRIVATE]
id = /hostname
use_leds = False
restart_dhcpcd = True
use_leds = True
led_pin = 21
x0 = 0
y0 = 0

View File

@@ -49,13 +49,17 @@ class CopterClient(client.Client):
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.RATIO = self.config.getfloat('ANIMATION', 'ratio')
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')
self.RESTART_DHCPCD = self.config.getboolean('PRIVATE', 'restart_dhcpcd')
def on_broadcast_bind(self):
configure_chrony_ip(self.server_host)
restart_service("chrony")
@@ -97,6 +101,9 @@ class CopterClient(client.Client):
def restart_service(name):
os.system("systemctl restart {}".format(name))
def execute_command(command):
os.system(command)
def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1):
try:
with open(path, 'r') as f:
@@ -130,8 +137,114 @@ def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1):
return True
def configure_hostname(hostname):
path = "/etc/hostname"
try:
with open(path, 'r') as f:
raw_content = f.read()
except IOError as e:
print("Reading error {}".format(e))
return False
current_hostname = str(raw_content)
if current_hostname != hostname:
content = hostname + '\n'
try:
with open(path, 'w') as f:
f.write(content)
except IOError:
print("Error writing")
return False
return True
def configure_hosts(hostname):
path = "/etc/hosts"
try:
with open(path, 'r') as f:
raw_content = f.read()
except IOError as e:
print("Reading error {}".format(e))
return False
index_start = raw_content.find("127.0.1.1", )
index_stop = raw_content.find("\n", index_start)
_ip, current_hostname = raw_content[index_start:index_stop].split()
if current_hostname != hostname:
content = raw_content[:index_start] + "{} {}".format(_ip, hostname) + raw_content[index_stop:]
try:
with open(path, 'w') as f:
f.write(content)
except IOError:
print("Error writing")
return False
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:
with open(path, 'r') as f:
raw_content = f.read()
except IOError as e:
print("Reading error {}".format(e))
return False
index_start = raw_content.find("ROS_HOSTNAME='", ) + 14
index_stop = raw_content.find("'", index_start)
current_hostname = raw_content[index_start:index_stop]
if current_hostname != hostname:
content = raw_content[:index_start] + hostname + raw_content[index_stop:]
try:
with open(path, 'w') as f:
f.write(content)
except IOError:
print("Error writing")
return False
return True
@messaging.message_callback("execute")
def _execute(*args, **kwargs):
command = kwargs.get("command", None)
if command:
execute_command(command)
@messaging.message_callback("id")
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)
if new_id != '/hostname':
if client.active_client.RESTART_DHCPCD:
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")
restart_service("clever-show")
@messaging.request_callback("selfcheck")
def _response_selfcheck():
def _response_selfcheck(*args, **kwargs):
if check_state_topic(wait_new_status=True):
check = FlightLib.selfcheck()
return check if check else "OK"
@@ -141,7 +254,7 @@ def _response_selfcheck():
@messaging.request_callback("anim_id")
def _response_animation_id():
def _response_animation_id(*args, **kwargs):
# Load animation
result = animation.get_id()
if result != 'No animation':
@@ -150,7 +263,9 @@ def _response_animation_id():
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,
ratio=client.active_client.RATIO,
x_ratio=client.active_client.X_RATIO,
y_ratio=client.active_client.Y_RATIO,
z_ratio=client.active_client.Z_RATIO,
)
# Correct start and land frames in animation
corrected_frames, start_action, start_delay = animation.correct_animation(frames,
@@ -163,7 +278,7 @@ def _response_animation_id():
return result
@messaging.request_callback("batt_voltage")
def _response_batt():
def _response_batt(*args, **kwargs):
if check_state_topic(wait_new_status=True):
return FlightLib.get_telemetry('body').voltage
else:
@@ -172,7 +287,7 @@ def _response_batt():
@messaging.request_callback("cell_voltage")
def _response_cell():
def _response_cell(*args, **kwargs):
if check_state_topic(wait_new_status=True):
return FlightLib.get_telemetry('body').cell_voltage
else:
@@ -180,42 +295,45 @@ def _response_cell():
return float('nan')
@messaging.request_callback("sys_status")
def _response_sys_status():
def _response_sys_status(*args, **kwargs):
return get_sys_status()
@messaging.request_callback("cal_status")
def _response_cal_status():
def _response_cal_status(*args, **kwargs):
return get_calibration_status()
@messaging.request_callback("position")
def _response_position():
def _response_position(*args, **kwargs):
telem = FlightLib.get_telemetry(client.active_client.FRAME_ID)
return "{:.2f} {:.2f} {:.2f} {:.1f} {}".format(
telem.x, telem.y, telem.z, math.degrees(telem.yaw), client.active_client.FRAME_ID)
@messaging.request_callback("calibrate_gyro")
def _calibrate_gyro():
def _calibrate_gyro(*args, **kwargs):
calibrate('gyro')
return get_calibration_status()
@messaging.request_callback("calibrate_level")
def _calibrate_level():
def _calibrate_level(*args, **kwargs):
calibrate('level')
return get_calibration_status()
@messaging.message_callback("test")
def _command_test(**kwargs):
def _command_test(*args, **kwargs):
logger.info("logging info test")
print("stdout test")
@messaging.message_callback("move_start")
def _command_move_start_to_current_position(**kwargs):
def _command_move_start_to_current_position(*args, **kwargs):
# Load animation
frames = animation.load_animation(os.path.abspath("animation.csv"),
x0=client.active_client.X0_COMMON,
y0=client.active_client.Y0_COMMON,
ratio=client.active_client.RATIO,
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,
)
# Correct start and land frames in animation
corrected_frames, start_action, start_delay = animation.correct_animation(frames,
@@ -232,7 +350,7 @@ def _command_move_start_to_current_position(**kwargs):
print ("Start delta: {:.2f} {:.2f}".format(client.active_client.X0, client.active_client.Y0))
@messaging.message_callback("reset_start")
def _command_reset_start(**kwargs):
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()
@@ -240,7 +358,7 @@ def _command_reset_start(**kwargs):
print ("Reset start to {:.2f} {:.2f}".format(client.active_client.X0, client.active_client.Y0))
@messaging.message_callback("set_z_to_ground")
def _command_set_z(**kwargs):
def _command_set_z(*args, **kwargs):
telem = FlightLib.get_telemetry(client.active_client.FRAME_ID)
client.active_client.config.set('PRIVATE', 'z0', telem.z)
client.active_client.rewrite_config()
@@ -248,7 +366,7 @@ def _command_set_z(**kwargs):
print ("Set z offset to {:.2f}".format(client.active_client.Z0))
@messaging.message_callback("reset_z_offset")
def _command_reset_z(**kwargs):
def _command_reset_z(*args, **kwargs):
client.active_client.config.set('PRIVATE', 'z0', 0)
client.active_client.rewrite_config()
client.active_client.load_config()
@@ -256,36 +374,36 @@ def _command_reset_z(**kwargs):
@messaging.message_callback("update_repo")
def _command_update_repo(**kwargs):
def _command_update_repo(*args, **kwargs):
os.system("git reset --hard origin/master")
os.system("git fetch")
os.system("git pull")
os.system("chown -R pi:pi ~/CleverSwarm")
@messaging.message_callback("reboot_fcu")
def _command_reboot():
def _command_reboot(*args, **kwargs):
reboot_fcu()
@messaging.message_callback("service_restart")
def _command_service_restart(**kwargs):
def _command_service_restart(*args, **kwargs):
restart_service(kwargs["name"])
@messaging.message_callback("repair_chrony")
def _command_chrony_repair():
def _command_chrony_repair(*args, **kwargs):
configure_chrony_ip(client.active_client.server_host)
restart_service("chrony")
@messaging.message_callback("led_test")
def _command_led_test(**kwargs):
def _command_led_test(*args, **kwargs):
LedLib.chase(255, 255, 255)
time.sleep(2)
LedLib.off()
@messaging.message_callback("led_fill")
def _command_led_fill(**kwargs):
def _command_led_fill(*args, **kwargs):
r = kwargs.get("red", 0)
g = kwargs.get("green", 0)
b = kwargs.get("blue", 0)
@@ -294,11 +412,11 @@ def _command_led_fill(**kwargs):
@messaging.message_callback("flip")
def _copter_flip():
def _copter_flip(*args, **kwargs):
FlightLib.flip(frame_id=client.active_client.FRAME_ID)
@messaging.message_callback("takeoff")
def _command_takeoff(**kwargs):
def _command_takeoff(*args, **kwargs):
task_manager.add_task(time.time(), 0, animation.takeoff,
task_kwargs={
"z": client.active_client.TAKEOFF_HEIGHT,
@@ -310,7 +428,7 @@ def _command_takeoff(**kwargs):
@messaging.message_callback("land")
def _command_land(**kwargs):
def _command_land(*args, **kwargs):
task_manager.reset()
task_manager.add_task(0, 0, animation.land,
task_kwargs={
@@ -323,7 +441,7 @@ def _command_land(**kwargs):
@messaging.message_callback("disarm")
def _command_disarm(**kwargs):
def _command_disarm(*args, **kwargs):
task_manager.reset()
task_manager.add_task(-5, 0, FlightLib.arming_wrapper,
task_kwargs={
@@ -333,22 +451,22 @@ def _command_disarm(**kwargs):
@messaging.message_callback("stop")
def _command_stop(**kwargs):
def _command_stop(*args, **kwargs):
task_manager.reset()
@messaging.message_callback("pause")
def _command_pause(**kwargs):
def _command_pause(*args, **kwargs):
task_manager.pause()
@messaging.message_callback("resume")
def _command_resume(**kwargs):
def _command_resume(*args, **kwargs):
task_manager.resume(time_to_start_next_task=kwargs.get("time", 0))
@messaging.message_callback("start")
def _play_animation(**kwargs):
def _play_animation(*args, **kwargs):
start_time = float(kwargs["time"])
# Check if animation file is available
if animation.get_id() == 'No animation':
@@ -363,7 +481,9 @@ def _play_animation(**kwargs):
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,
ratio=client.active_client.RATIO,
x_ratio=client.active_client.X_RATIO,
y_ratio=client.active_client.Y_RATIO,
z_ratio=client.active_client.Z_RATIO,
)
# Correct start and land frames in animation
corrected_frames, start_action, start_delay = animation.correct_animation(frames,

View File

@@ -12,7 +12,9 @@ Software for making the drone show controlled by Raspberry Pi and COEX [Clever](
* [Raspberry Pi image](https://github.com/CopterExpress/clever-show/releases/latest) for quick launch software on the drones
## Documentation
Start tutorial is located [here](docs/start-tutorial.md).
> Documentation is available only in Russian for now.
Start tutorial is located [here](docs/ru/start-tutorial.md).
Detailed documentation is located in the [docs](https://github.com/CopterExpress/clever-show/tree/master/docs) folder.

View File

@@ -12,7 +12,7 @@
* [Образ для Raspberry Pi](https://github.com/CopterExpress/clever-show/releases/latest) для быстрого запуска ПО на коптере
## Документация
Инструкция по запуску ПО находится [здесь](docs/start-tutorial.md).
Инструкция по запуску ПО находится [здесь](docs/ru/start-tutorial.md).
Подробная документация расположена в папке [docs](https://github.com/CopterExpress/clever-show/tree/master/docs).

View File

@@ -1,32 +1,72 @@
import sys
import re
import collections
import indexed
from server import ConfigOption
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt as Qt
ModelDataRole = 998
ModelStateRole = 999
class CopterData:
class_attrs = collections.OrderedDict([('copter_id', None), ('anim_id', None), ('batt_v', None), ('batt_p', None),
('sys_status', None), ('cal_status', None), ('selfcheck', None), ('position', None), ("time_delta", None),
("client", None), ("checked", 0)], )
class_basic_attrs = indexed.IndexedOrderedDict([('copter_id', None), ('anim_id', None),
('batt_v', None), ('batt_p', None),
('sys_status', None), ('cal_status', None), ('selfcheck', None),
('position', None), ("time_delta", None),
("client", None), ])
def __init__(self, **kwargs):
self.attrs = self.class_attrs.copy()
self.attrs.update(kwargs)
self.attrs_dict = self.class_basic_attrs.copy()
self.attrs_dict.update(kwargs)
for attr, value in self.attrs.items():
for attr, value in self.attrs_dict.items():
setattr(self, attr, value)
def __getitem__(self, key):
return getattr(self, list(self.attrs.keys())[key])
return getattr(self, self.attrs_dict.keys()[key])
def __setitem__(self, key, value):
setattr(self, list(self.attrs.keys())[key], value)
setattr(self, self.attrs_dict.keys()[key], value)
class StatedCopterData(CopterData):
class_basic_states = indexed.IndexedOrderedDict([("checked", 0), ("selfchecked", None), ("takeoff_ready", None),
("copter_id", True), ])
def __init__(self, **kwargs):
self.states = CopterData(**self.class_basic_states)
super(StatedCopterData, self).__init__(**kwargs)
def __setattr__(self, key, value):
self.__dict__[key] = value
if key in self.class_basic_attrs.keys():
try:
self.states.__dict__[key] = \
Checks.all_checks[self.attrs_dict.keys().index(key)](value)
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 Checks.all_checks.keys()]
)
self.states.__dict__["takeoff_ready"] = all(
[self.states[i] for i in Checks.takeoff_checklist]
)
class Checks:
all_checks = {}
takeoff_checklist = (2, 3, 4, 5, 6)
class CopterDataModel(QtCore.QAbstractTableModel):
checks = {}
selected_ready_signal = QtCore.pyqtSignal(bool)
selected_takeoff_ready_signal = QtCore.pyqtSignal(bool)
selected_flip_ready_signal = QtCore.pyqtSignal(bool)
@@ -35,8 +75,12 @@ class CopterDataModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
super(CopterDataModel, self).__init__(parent)
self.headers = ('copter ID', ' animation ID ', 'batt V', 'batt %', ' system ', 'calibration', 'selfcheck', 'current x y z yaw frame_id', 'time delta')
self.headers = ('copter ID', ' animation ID ', 'batt V', 'batt %', ' system ',
'calibration', 'selfcheck', 'current x y z yaw frame_id', 'time delta')
self.data_contents = []
self.on_id_changed = None
self.first_col_is_checked = False
def insertRows(self, contents, position='last', parent=QtCore.QModelIndex()):
@@ -48,20 +92,28 @@ class CopterDataModel(QtCore.QAbstractTableModel):
self.endInsertRows()
def user_selected(self):
return filter(lambda x: x.checked == Qt.Checked, self.data_contents)
def removeRows(self, position, rows=1, index=QtCore.QModelIndex()):
self.beginRemoveRows(QtCore.QModelIndex(), position, position + rows - 1)
self.data_contents = self.data_contents[:position] + self.data_contents[position + rows:]
self.endRemoveRows()
return True
def user_selected(self, contents=()):
contents = contents or self.data_contents
return filter(lambda x: x.states.checked == Qt.Checked, contents)
def selfchecked_ready(self, contents=()):
contents = contents or self.data_contents
return filter(lambda x: all_checks(x), contents)
return filter(lambda x: x.states.selfchecked, contents)
def takeoff_ready(self, contents=()):
contents = contents or self.data_contents
return filter(lambda x: takeoff_checks(x), contents)
return filter(lambda x: x.states.takeoff_ready, contents)
def flip_ready(self, contents=()):
contents = contents or self.data_contents
return filter(lambda x: flip_checks(x), contents)
return filter(lambda x: flip_checks(x), contents) # possibly change as takeoff checks
def calibrating(self, contents=()):
contents = contents or self.data_contents
@@ -71,6 +123,22 @@ class CopterDataModel(QtCore.QAbstractTableModel):
contents = contents or self.data_contents
return filter(lambda x: calibration_ready_check(x), contents)
def get_row_index(self, row_data):
try:
index = 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))
except StopIteration:
return None
else:
return row_data
def rowCount(self, n=None):
return len(self.data_contents)
@@ -85,15 +153,19 @@ class CopterDataModel(QtCore.QAbstractTableModel):
def data(self, index, role=Qt.DisplayRole):
row = index.row()
col = index.column()
#print('row {}, col {}, role {}'.format(row, col, role))
if role == Qt.DisplayRole:
#print(self.data_contents[row][col])
return self.data_contents[row][col] or ""
if role == Qt.DisplayRole or role == Qt.EditRole: # Separate editRole in case of editing non-text
item = self.data_contents[row][col]
return str(item) if item is not None else ""
elif role == ModelDataRole:
return self.data_contents[row][col]
elif role == Qt.BackgroundRole:
if col in self.checks.keys():
item = self.data_contents[row][col]
result = self.checks[col](item)
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:
@@ -102,61 +174,85 @@ class CopterDataModel(QtCore.QAbstractTableModel):
return QtGui.QBrush(Qt.red)
elif role == Qt.CheckStateRole and col == 0:
return self.data_contents[row].checked
return self.data_contents[row].states.checked
if role == QtCore.Qt.TextAlignmentRole and col != 0:
return QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
def update_model(self, index=QtCore.QModelIndex()):
#self.modelReset.emit()
self.selected_ready_signal.emit(set(self.user_selected()).issubset(self.selfchecked_ready()))
self.selected_takeoff_ready_signal.emit(set(self.user_selected()).issubset(self.takeoff_ready()))
self.selected_flip_ready_signal.emit(set(self.user_selected()).issubset(self.flip_ready()))
self.selected_calibrating_signal.emit(set(self.user_selected()).issubset(self.calibrating()))
self.selected_calibration_ready_signal.emit(set(self.user_selected()).issubset(self.calibration_ready()))
self.dataChanged.emit(index, index, (QtCore.Qt.EditRole,))
def update_model(self, index=QtCore.QModelIndex(), role=QtCore.Qt.EditRole):
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,))
@QtCore.pyqtSlot()
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
if role == Qt.CheckStateRole:
self.data_contents[index.row()].checked = value
col = index.column()
row = index.row()
elif role == Qt.EditRole:
self.data_contents[index.row()][index.column()] = value
self.update_model(index)
if role == Qt.CheckStateRole:
self.data_contents[row].states.checked = value
elif role == Qt.EditRole: # For user actions with data
if col == 0:
# check user hostname spelling http://man7.org/linux/man-pages/man7/hostname.7.html
if value[0] != '-' and len(value) <= 63 and re.match("^[A-Za-z0-9-]*$", value):
self.data_contents[row].client.send_message("id", {"new_id": value})
self.data_contents[row].client.remove()
else:
msg = QtWidgets.QMessageBox()
msg.setIcon(QtWidgets.QMessageBox.Critical)
msg.setText("Wrong input for the copter name!\nPlease use only A-Z, a-z, 0-9, and '-' chars.\nDon't use '-' as first char.")
msg.exec_()
else:
self.data_contents[row][col] = value
elif role == ModelDataRole: # For inner setting\editing of 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)
return True
def select_all(self):
self.first_col_is_checked = not self.first_col_is_checked
for copter in self.data_contents:
copter.checked = int(self.first_col_is_checked)*2
for row in range(len(self.data_contents)):
self.update_model(self.index(row, 0))
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
roles |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable
return roles
@QtCore.pyqtSlot(int, int, QtCore.QVariant)
def update_item(self, row, col, value):
self.setData(self.index(row, col), value)
@QtCore.pyqtSlot(int, int, QtCore.QVariant, QtCore.QVariant)
def update_item(self, row, col, value, role=Qt.EditRole):
self.setData(self.index(row, col), value, role)
@QtCore.pyqtSlot(object)
def add_client(self, client):
self.insertRows([client])
@QtCore.pyqtSlot(int)
def remove_client(self, row):
self.removeRows(row)
def col_check(col):
def inner(f):
CopterDataModel.checks[col] = f
Checks.all_checks[col] = f
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
@@ -172,42 +268,49 @@ def check_anim(item):
return None
return str(item) != 'No animation'
@col_check(2)
def check_bat_v(item):
if not item:
return None
return float(item) > 3.2
@col_check(3)
def check_bat_p(item):
if not item:
return None
return float(item) > 30
@col_check(4)
def check_sys_status(item):
if not item:
return None
return item == "STANDBY"
@col_check(5)
def check_cal_status(item):
if not item:
return None
return item == "OK"
@col_check(6)
def check_selfcheck(item):
if not item:
return None
return item == "OK"
@col_check(7)
def check_cal_status(item):
if not item:
return None
return True
@col_check(8)
def check_time_delta(item):
if not item:
@@ -216,35 +319,40 @@ def check_time_delta(item):
def all_checks(copter_item):
for col, check in CopterDataModel.checks.items():
for col, check in Checks.all_checks.items():
if not check(copter_item[col]):
return False
return True
def takeoff_checks(copter_item):
for i in range(5):
if not CopterDataModel.checks[2+i](copter_item[2+i]):
for col in Checks.takeoff_checklist:
if not Checks.all_checks[col](copter_item[col]):
return False
return True
def flip_checks(copter_item):
for i in range(5):
if 2+i != 4:
if not CopterDataModel.checks[2+i](copter_item[2+i]):
for col in Checks.takeoff_checklist:
if col != 4:
if not Checks.all_checks[col](copter_item[col]):
return False
else:
if copter_item[4] != "ACTIVE":
return False
return True
def calibrating_check(copter_item):
return copter_item[5] == "CALIBRATING"
def calibration_ready_check(copter_item):
if not CopterDataModel.checks[4](copter_item[4]):
if not Checks.all_checks[4](copter_item[4]):
return False
return not calibrating_check(copter_item)
class CopterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(CopterProxyModel, self).__init__(parent)
@@ -265,8 +373,9 @@ class CopterProxyModel(QtCore.QSortFilterProxyModel):
class SignalManager(QtCore.QObject):
update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant)
update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant, QtCore.QVariant)
add_client_signal = QtCore.pyqtSignal(object)
remove_client_signal = QtCore.pyqtSignal(int)
if __name__ == '__main__':
@@ -296,9 +405,18 @@ if __name__ == '__main__':
tableView.setSortingEnabled(True)
tableView.show()
myModel.add_client(CopterData(copter_id=1000, checked=0, time_utc=1))
myModel.add_client(CopterData(checked=2, selfcheck="OK", time_utc=2))
myModel.add_client(CopterData(checked=2, selfcheck="not ok", time_utc="no"))
msgs = []
msg = "[{}]: Failure: {}".format("FCU connection", "Angular velocities estimation is not available")
msgs.append(msg)
msg = "[{}]: Failure: {}".format("FCU connection1", "Angular velocities estimation is not available")
msgs.append(msg)
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.setData(myModel.index(0, 1), "test")

View File

@@ -28,9 +28,7 @@ logging.basicConfig( # TODO all prints as logs
ConfigOption = collections.namedtuple("ConfigOption", ["section", "option", "value"])
class Server:
BUFFER_SIZE = 1024
class Server(messaging.Singleton):
def __init__(self, server_id=None, config_path="server_config.ini", on_stop=None):
self.id = server_id if server_id else str(random.randint(0, 9999)).zfill(4)
self.time_started = 0
@@ -41,8 +39,11 @@ class Server:
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)
self.server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.host = socket.gethostname()
self.ip = Server.get_ip_address()
self.ip = messaging.get_ip_address()
# Init configs
self.config_path = config_path
@@ -65,7 +66,9 @@ class Server:
def load_config(self):
self.config.read(self.config_path)
self.port = int(self.config['SERVER']['port']) # TODO try, init def
Server.BUFFER_SIZE = int(self.config['SERVER']['buffer_size'])
self.BUFFER_SIZE = int(self.config['SERVER']['buffer_size']) # TODO connect to connection manager
self.remove_disconnected = self.config.getboolean('SERVER', 'remove_disconnected')
self.use_broadcast = self.config.getboolean('BROADCAST', 'use_broadcast')
self.broadcast_port = int(self.config['BROADCAST']['broadcast_port'])
@@ -112,16 +115,6 @@ class Server:
sys.exit("Stopped")
@staticmethod
def get_ip_address():
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as ip_socket:
ip_socket.connect(("8.8.8.8", 80))
return ip_socket.getsockname()[0]
except OSError:
logging.warning("No network connection detected, starting on localhost")
return "localhost"
@staticmethod
def get_ntp_time(ntp_host, ntp_port):
NTP_DELTA = 2208988800 # 1970-01-01 00:00:00
@@ -149,7 +142,7 @@ class Server:
while self.client_processor_thread_running.is_set():
events = self.sel.select()
logging.error('tick')
#logging.error('tick')
for key, mask in events:
# logging.error(mask)
# logging.error(str(key.data))
@@ -161,7 +154,7 @@ class Server:
client.process_events(mask)
except Exception as error:
logging.error("Exception {} occurred for {}! Resetting connection!".format(error, client.addr))
client.close()
client.close(True)
else: # Notifier
client.process_events(mask)
@@ -218,7 +211,7 @@ class Server:
try:
while self.listener_thread_running.is_set():
data, addr = broadcast_client.recvfrom(1024) # TODO nonblock
data, addr = broadcast_client.recvfrom(1024) # TODO nonblock
message = messaging.MessageManager()
message.income_raw = data
message.process_message()
@@ -301,16 +294,28 @@ class Client(messaging.ConnectionManager):
def _got_id(self, value):
logging.info("Got copter id: {} for client {}".format(value, self.addr))
self.copter_id = value
if Client.on_first_connect:
Client.on_first_connect(self)
if self.on_first_connect:
self.on_first_connect(self)
def close(self):
def close(self, inner=False):
self.connected = False
if Client.on_disconnect:
Client.on_disconnect(self)
if self.on_disconnect:
self.on_disconnect(self)
super(Client, self).close()
if inner:
super(Client, self)._close()
else:
super(Client, self).close()
logging.info("Connection to {} closed!".format(self.copter_id))
def remove(self):
if self.connected:
self.close()
if self.clients:
self.clients.pop(self.addr[0])
logging.info("Client {} successfully removed!".format(self.copter_id))
@requires_connect
def _send(self, data):

View File

@@ -1,6 +1,7 @@
[SERVER]
port = 25000
buffer_size = 1024
remove_disconnected = True
[BROADCAST]
use_broadcast = True

View File

@@ -167,18 +167,18 @@ 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, 1220, 25))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1220, 26))
self.menubar.setObjectName("menubar")
self.menuOptions = QtWidgets.QMenu(self.menubar)
self.menuOptions.setObjectName("menuOptions")
self.menuDeveloper_mode = QtWidgets.QMenu(self.menuOptions)
self.menuDeveloper_mode.setObjectName("menuDeveloper_mode")
self.menuTable = QtWidgets.QMenu(self.menubar)
self.menuTable.setObjectName("menuTable")
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)
@@ -214,28 +214,46 @@ class Ui_MainWindow(object):
self.action_play_music.setObjectName("action_play_music")
self.action_test_music_after = QtWidgets.QAction(MainWindow)
self.action_test_music_after.setObjectName("action_test_music_after")
self.menuDeveloper_mode.addAction(self.action_send_launch_file)
self.menuDeveloper_mode.addAction(self.action_restart_clever)
self.menuDeveloper_mode.addAction(self.action_restart_clever_show)
self.menuDeveloper_mode.addAction(self.action_update_client_repo)
self.actionFill = QtWidgets.QAction(MainWindow)
self.actionFill.setObjectName("actionFill")
self.action_send_any_file = QtWidgets.QAction(MainWindow)
self.action_send_any_file.setObjectName("action_send_any_file")
self.actionSend_any_command = QtWidgets.QAction(MainWindow)
self.actionSend_any_command.setObjectName("actionSend_any_command")
self.action_stop_music = QtWidgets.QAction(MainWindow)
self.action_stop_music.setObjectName("action_stop_music")
self.action_remove_row = QtWidgets.QAction(MainWindow)
self.action_remove_row.setObjectName("action_remove_row")
self.action_send_calibrations = QtWidgets.QAction(MainWindow)
self.action_send_calibrations.setObjectName("action_send_calibrations")
self.menuDeveloper_mode.addAction(self.action_send_any_file)
self.menuDeveloper_mode.addAction(self.actionSend_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.addSeparator()
self.menuOptions.addAction(self.menuDeveloper_mode.menuAction())
self.menuTable.addAction(self.action_select_all_rows)
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.menuDrone.addAction(self.action_set_z_offset_to_ground)
self.menuDrone.addAction(self.action_reset_z_offset)
self.menuDrone.addSeparator()
self.menuDrone.addAction(self.menuDeveloper_mode_2.menuAction())
self.menuDrone.addAction(self.action_remove_row)
self.menuMusic.addAction(self.action_select_music_file)
self.menuMusic.addAction(self.action_play_music)
self.menuMusic.addAction(self.action_test_music_after)
self.menuMusic.addAction(self.action_stop_music)
self.menubar.addAction(self.menuOptions.menuAction())
self.menubar.addAction(self.menuAnimation.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)
@@ -243,7 +261,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Clever Drone Animation Player"))
MainWindow.setWindowTitle(_translate("MainWindow", "Clever Drone Show"))
self.music_text.setText(_translate("MainWindow", " Music after"))
self.music_delay_spin.setSuffix(_translate("MainWindow", " s"))
self.music_play_text.setText(_translate("MainWindow", " Play music"))
@@ -263,21 +281,21 @@ class Ui_MainWindow(object):
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", "Actions"))
self.menuOptions.setTitle(_translate("MainWindow", "Server"))
self.menuDeveloper_mode.setTitle(_translate("MainWindow", "Developer mode"))
self.menuTable.setTitle(_translate("MainWindow", "Table"))
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_update_client_repo.setText(_translate("MainWindow", "Update client repo"))
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_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 file to clever"))
self.action_send_launch_file.setText(_translate("MainWindow", "Send launch file to clever"))
self.action_restart_clever.setText(_translate("MainWindow", "Restart clever service"))
self.action_restart_clever_show.setText(_translate("MainWindow", "Restart clever-show service"))
self.action_select_all_rows.setText(_translate("MainWindow", "Select All"))
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"))
@@ -286,3 +304,9 @@ class Ui_MainWindow(object):
self.action_select_music_file.setText(_translate("MainWindow", "Select music file"))
self.action_play_music.setText(_translate("MainWindow", "Play music"))
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.actionSend_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"))

View File

@@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Clever Drone Animation Player</string>
<string>Clever Drone Show</string>
</property>
<widget class="QWidget" name="centralwidget">
<property name="enabled">
@@ -328,32 +328,28 @@
<x>0</x>
<y>0</y>
<width>1220</width>
<height>25</height>
<height>26</height>
</rect>
</property>
<widget class="QMenu" name="menuOptions">
<property name="title">
<string>Actions</string>
<string>Server</string>
</property>
<widget class="QMenu" name="menuDeveloper_mode">
<property name="title">
<string>Developer mode</string>
</property>
<addaction name="action_send_launch_file"/>
<addaction name="action_restart_clever"/>
<addaction name="action_restart_clever_show"/>
<addaction name="action_update_client_repo"/>
<addaction name="action_send_any_file"/>
<addaction name="actionSend_any_command"/>
</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="separator"/>
<addaction name="menuDeveloper_mode"/>
</widget>
<widget class="QMenu" name="menuTable">
<property name="title">
<string>Table</string>
</property>
<addaction name="separator"/>
<addaction name="action_select_all_rows"/>
</widget>
<widget class="QMenu" name="menuAnimation">
@@ -367,8 +363,19 @@
<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"/>
</widget>
<addaction name="action_set_z_offset_to_ground"/>
<addaction name="action_reset_z_offset"/>
<addaction name="separator"/>
<addaction name="menuDeveloper_mode_2"/>
<addaction name="action_remove_row"/>
</widget>
<widget class="QMenu" name="menuMusic">
<property name="title">
@@ -376,32 +383,31 @@
</property>
<addaction name="action_select_music_file"/>
<addaction name="action_play_music"/>
<addaction name="action_test_music_after"/>
<addaction name="action_stop_music"/>
</widget>
<addaction name="menuOptions"/>
<addaction name="menuAnimation"/>
<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>Send animations</string>
</property>
</action>
<action name="action_send_configurations">
<property name="text">
<string>Send Configurations</string>
<string>Send configurations</string>
</property>
</action>
<action name="action_send_Aruco_map">
<property name="text">
<string>Send Aruco map</string>
<string>Send aruco map</string>
</property>
</action>
<action name="action_update_client_repo">
<property name="text">
<string>Update client repo</string>
<string>Update clever-show git</string>
</property>
</action>
<action name="actionSend_launch_file_for_clever">
@@ -411,7 +417,7 @@
</action>
<action name="action_send_launch_file">
<property name="text">
<string>Send .launch file to clever</string>
<string>Send launch file to clever</string>
</property>
</action>
<action name="action_restart_clever">
@@ -426,7 +432,7 @@
</action>
<action name="action_select_all_rows">
<property name="text">
<string>Select All</string>
<string>Select all drones</string>
</property>
<property name="shortcut">
<string>Ctrl+A</string>
@@ -467,6 +473,36 @@
<string>Test music after</string>
</property>
</action>
<action name="actionFill">
<property name="text">
<string>fill</string>
</property>
</action>
<action name="action_send_any_file">
<property name="text">
<string>Send any file</string>
</property>
</action>
<action name="actionSend_any_command">
<property name="text">
<string>Send any command</string>
</property>
</action>
<action name="action_stop_music">
<property name="text">
<string>Stop music</string>
</property>
</action>
<action name="action_remove_row">
<property name="text">
<string>Remove from table</string>
</property>
</action>
<action name="action_send_calibrations">
<property name="text">
<string>Send camera calibrations</string>
</property>
</action>
</widget>
<tabstops>
<tabstop>start_delay_spin</tabstop>

View File

@@ -3,6 +3,7 @@ import glob
import math
import time
import asyncio
import functools
from PyQt5 import QtWidgets, QtMultimedia
from PyQt5.QtGui import QStandardItemModel, QStandardItem
@@ -36,6 +37,7 @@ def wait(end, interrupter=threading.Event(), maxsleep=0.1):
def confirmation_required(text="Are you sure?", label="Confirm operation?"):
def inner(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
reply = QMessageBox.question(
args[0], label,
@@ -43,11 +45,10 @@ def confirmation_required(text="Are you sure?", label="Confirm operation?"):
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
print("Dialog accepted")
#print(args)
return f(args[0])
else:
print("Dialog declined")
logging.debug("Dialog accepted")
return f(*args, **kwargs)
logging.debug("Dialog declined")
return wrapper
@@ -66,9 +67,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.model = CopterDataModel()
self.proxy_model = CopterProxyModel()
self.signals = SignalManager()
self.gyro_calibrated = {}
self.level_calibrated = {}
self.first_col_is_checked = False
self.player = QtMultimedia.QMediaPlayer()
self.init_model()
@@ -76,6 +74,8 @@ class MainWindow(QtWidgets.QMainWindow):
self.show()
def init_model(self):
# self.model.on_id_changed = self.set_copter_id
self.proxy_model.setDynamicSortFilter(True)
self.proxy_model.setSourceModel(self.model)
@@ -83,9 +83,12 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.tableView.setModel(self.proxy_model)
self.ui.tableView.resizeColumnsToContents()
self.ui.tableView.doubleClicked.connect(self.selfcheck_info_dialog)
# Connect signals to manipulate model from threads
self.signals.update_data_signal.connect(self.model.update_item)
self.signals.add_client_signal.connect(self.model.add_client)
self.signals.remove_client_signal.connect(self.model.remove_client)
# Connect model signals to UI
self.model.selected_ready_signal.connect(self.ui.start_button.setEnabled)
@@ -106,9 +109,18 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.action_select_all_rows.triggered.connect(self.model.select_all)
def new_client_connected(self, client: Client):
self.signals.add_client_signal.emit(StatedCopterData(copter_id=client.copter_id, client=client))
def client_connected(self, client: Client):
self.signals.add_client_signal.emit(CopterData(copter_id=client.copter_id, client=client))
def client_connection_changed(self, client: Client):
row_data = self.model.get_row_by_attr("client", client)
row_num = self.model.get_row_index(row_data)
if row_num is not None:
if Server().remove_disconnected and (not client.connected):
client.remove()
self.signals.remove_client_signal.emit(row_num)
else:
self.signals.update_data_signal.emit(row_num, 0, client.connected, ModelStateRole)
def init_ui(self):
# Connecting
@@ -130,7 +142,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.calibrate_gyro.clicked.connect(self.calibrate_gyro_selected)
self.ui.calibrate_level.clicked.connect(self.calibrate_level_selected)
self.ui.action_remove_row.triggered.connect(self.remove_selected)
self.ui.action_send_animations.triggered.connect(self.send_animations)
self.ui.action_send_calibrations.triggered.connect(self.send_calibrations)
self.ui.action_send_configurations.triggered.connect(self.send_configurations)
self.ui.action_send_Aruco_map.triggered.connect(self.send_aruco)
self.ui.action_send_launch_file.triggered.connect(self.send_launch)
@@ -143,7 +158,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.action_reset_z_offset.triggered.connect(self.reset_z_offset)
self.ui.action_select_music_file.triggered.connect(self.select_music_file)
self.ui.action_play_music.triggered.connect(self.play_music)
self.ui.action_test_music_after.triggered.connect(self.test_music_after)
self.ui.action_stop_music.triggered.connect(self.stop_music)
# Set most safety-important buttons disabled
self.ui.start_button.setEnabled(False)
@@ -152,21 +167,23 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot()
def selfcheck_selected(self):
for copter in self.model.user_selected():
client = copter.client
for copter_data_row in self.model.user_selected():
client = copter_data_row.client
client.get_response("anim_id", self._set_copter_data, callback_args=(1, copter.copter_id))
client.get_response("batt_voltage", self._set_copter_data, callback_args=(2, copter.copter_id))
client.get_response("cell_voltage", self._set_copter_data, callback_args=(3, copter.copter_id))
client.get_response("sys_status", self._set_copter_data, callback_args=(4, copter.copter_id))
client.get_response("cal_status", self._set_copter_data, callback_args=(5, copter.copter_id))
client.get_response("selfcheck", self._set_copter_data, callback_args=(6, copter.copter_id))
client.get_response("position", self._set_copter_data, callback_args=(7, copter.copter_id))
client.get_response("time", self._set_copter_data, callback_args=(8, copter.copter_id))
client.get_response("anim_id", self.set_copter_data, callback_args=(1, copter_data_row))
client.get_response("batt_voltage", self.set_copter_data, callback_args=(2, copter_data_row))
client.get_response("cell_voltage", self.set_copter_data, callback_args=(3, copter_data_row))
client.get_response("sys_status", self.set_copter_data, callback_args=(4, copter_data_row))
client.get_response("cal_status", self.set_copter_data, callback_args=(5, copter_data_row))
client.get_response("selfcheck", self.set_copter_data, callback_args=(6, copter_data_row))
client.get_response("position", self.set_copter_data, callback_args=(7, copter_data_row))
client.get_response("time", self.set_copter_data, callback_args=(8, copter_data_row))
def _set_copter_data(self, value, col, copter_id):
row = self.model.data_contents.index(next(
filter(lambda x: x.copter_id == copter_id, self.model.data_contents)))
def set_copter_data(self, value, col, copter_data_row):
row = self.model.get_row_index(copter_data_row)
if row is None:
logging.error("No such client!")
return
if col == 1:
data = value
@@ -180,23 +197,63 @@ class MainWindow(QtWidgets.QMainWindow):
elif col == 5:
data = str(value)
elif col == 6:
data = str(value)
data = value
elif col == 7:
data = str(value)
elif col == 8:
#data = time.ctime(int(value))
data = "{}".format(round(float(value) - time.time(), 3))
if abs(float(data)) > 1:
Client.get_by_id(copter_id).send_message("repair_chrony")
#self.signals.update_data_signal.emit(row, col + 1, data2)
copter_data_row.client.send_message("repair_chrony")
else:
print("No column matched for response")
logging.error("No column matched for response")
return
self.signals.update_data_signal.emit(row, col, data)
self.signals.update_data_signal.emit(row, col, data, ModelDataRole)
#def set_copter_id(self, value, copter_data_row):
# col = 0
# row = self.model.get_row_index(copter_data_row)
# if row is None:
# logging.error("No such client!")
# return
# logging.info("SET COPTER ID TO {}".format(value))
#
# copter_data_row.client.copter_id = value
# self.signals.update_data_signal.emit(row, col, value, ModelDataRole)
# self.signals.update_data_signal.emit(row, col, True, ModelStateRole)
@pyqtSlot(QtCore.QModelIndex)
def selfcheck_info_dialog(self, index):
col = index.column()
if col == 6:
data = self.proxy_model.data(index, role=ModelDataRole)
if data and data != "OK":
dialog = QMessageBox()
dialog.setIcon(QMessageBox.NoIcon)
dialog.setStandardButtons(QMessageBox.Ok)
dialog.setWindowTitle("Selfcheck info")
dialog.setText("\n".join(data[:10]))
dialog.setDetailedText("\n".join(data))
dialog.exec()
def _selfcheck_shortener(self, data):
shortened = []
for line in data:
if len(line) > 89:
pass
return shortened
@confirmation_required("This operation will takeoff selected copters with delay and start animation. Proceed?")
@pyqtSlot()
def remove_selected(self):
for copter in self.model.user_selected():
row_num = self.model.data_contents.index(copter)
copter.client.remove()
self.signals.remove_client_signal.emit(row_num)
logging.info("Client removed from table!")
@pyqtSlot()
@confirmation_required("This operation will takeoff selected copters with delay and start animation. Proceed?")
def send_starttime_selected(self, **kwargs):
time_now = server.time_now()
dt = self.ui.start_delay_spin.value()
@@ -243,15 +300,15 @@ class MainWindow(QtWidgets.QMainWindow):
def disarm_all(self):
Client.broadcast_message("disarm")
@confirmation_required("This operation will takeoff copters immediately. Proceed?")
@pyqtSlot()
@confirmation_required("This operation will takeoff copters immediately. Proceed?")
def takeoff_selected(self, **kwargs):
for copter in self.model.user_selected():
if takeoff_checks(copter):
copter.client.send_message("takeoff")
@confirmation_required("This operation will flip(!!!) copters immediately. Proceed?")
@pyqtSlot()
@confirmation_required("This operation will flip(!!!) copters immediately. Proceed?")
def flip_selected(self, **kwargs):
for copter in self.model.user_selected():
if flip_checks(copter):
@@ -269,36 +326,33 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot()
def calibrate_gyro_selected(self):
for copter in self.model.user_selected():
client = copter.client
for copter_data_row in self.model.user_selected():
client = copter_data_row.client
# Update calibration status
row = self.model.data_contents.index(next(filter(
lambda x: x.copter_id == client.copter_id, self.model.data_contents)))
row = self.model.get_row_index(copter_data_row)
col = 5
data = 'CALIBRATING'
self.signals.update_data_signal.emit(row, col, data)
self.signals.update_data_signal.emit(row, col, data, ModelDataRole)
# Send request
client.get_response("calibrate_gyro", self._get_calibration_info, callback_args=(5, copter.copter_id))
client.get_response("calibrate_gyro", self._get_calibration_info, callback_args=(copter_data_row, ))
@pyqtSlot()
def calibrate_level_selected(self):
for copter in self.model.user_selected():
client = copter.client
for copter_data_row in self.model.user_selected():
client = copter_data_row.client
# Update calibration status
row = self.model.data_contents.index(next(filter(
lambda x: x.copter_id == client.copter_id, self.model.data_contents)))
row = self.model.get_row_index(copter_data_row)
col = 5
data = 'CALIBRATING'
self.signals.update_data_signal.emit(row, col, data)
self.signals.update_data_signal.emit(row, col, data, ModelDataRole)
# Send request
client.get_response("calibrate_level", self._get_calibration_info, callback_args=(5, copter.copter_id))
client.get_response("calibrate_level", self._get_calibration_info, callback_args=(copter_data_row, ))
def _get_calibration_info(self, value, col, copter_id):
row = self.model.data_contents.index(next(
filter(lambda x: x.copter_id == copter_id, self.model.data_contents)))
def _get_calibration_info(self, value, copter_data_row):
col = 5
row = self.model.get_row_index(copter_data_row)
data = str(value)
self.signals.update_data_signal.emit(row, col, data)
self.signals.update_data_signal.emit(row, col, data, ModelDataRole)
@pyqtSlot()
def send_animations(self):
@@ -308,7 +362,7 @@ class MainWindow(QtWidgets.QMainWindow):
print("Selected directory:", path)
files = [file for file in glob.glob(path + '/*.csv')]
names = [os.path.basename(file).split(".")[0] for file in files]
print(files)
# print(files)
for file, name in zip(files, names):
for copter in self.model.user_selected():
if name == copter.copter_id:
@@ -316,6 +370,22 @@ class MainWindow(QtWidgets.QMainWindow):
else:
print("Filename has no matches with any drone selected")
@pyqtSlot()
def send_calibrations(self):
path = str(QFileDialog.getExistingDirectory(self, "Select directory with calibration files"))
if path:
print("Selected directory:", path)
files = [file for file in glob.glob(path + '/*.yaml')]
names = [os.path.basename(file).split(".")[0] for file in files]
# print(files)
for file, name in zip(files, names):
for copter in self.model.user_selected():
if name == copter.copter_id:
copter.client.send_file(file, "/home/pi/catkin_ws/src/clever/clever/camera_info/calibration.yaml")
else:
print("Filename has no matches with any drone selected")
@pyqtSlot()
def send_configurations(self):
path = QFileDialog.getOpenFileName(self, "Select configuration file", filter="Configs (*.ini *.txt .cfg)")[0]
@@ -390,47 +460,53 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot()
def select_music_file(self):
path = QFileDialog.getOpenFileName(self, "Select music file", filter="Music files (*.mp3)")[0]
path = QFileDialog.getOpenFileName(self, "Select music file", filter="Music files (*.mp3 *.wav)")[0]
if path:
media = QUrl.fromLocalFile(path)
content = QtMultimedia.QMediaContent(media)
self.player.setMedia(content)
self.ui.action_select_music_file.setText(self.ui.action_select_music_file.text() + " (selected)")
@pyqtSlot()
def play_music(self):
if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.InvalidMedia:
logger.info("Can't play media")
logging.info("Can't play media")
return
if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.NoMedia:
logger.info("No media file")
logging.info("No media file")
return
if self.player.state() == QtMultimedia.QMediaPlayer.StoppedState or \
self.player.state() == QtMultimedia.QMediaPlayer.PausedState:
self.ui.action_play_music.setText("Pause music")
self.player.play()
else:
self.ui.action_play_music.setText("Play music")
self.player.pause()
@pyqtSlot()
def stop_music(self):
if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.InvalidMedia:
logging.error("Can't stop media")
return
if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.NoMedia:
logging.error("No media file")
return
self.player.stop()
@asyncio.coroutine
def play_music_at_time(self, t):
if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.InvalidMedia:
logger.info("Can't play media")
logging.error("Can't play media")
return
if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.NoMedia:
logger.info("No media file")
logging.error("No media file")
return
self.player.stop()
yield from asyncio.sleep(t - time.time())
#wait(t)
logging.info("Playing music")
self.player.play()
@pyqtSlot()
def test_music_after(self):
dt = self.ui.music_delay_spin.value()
asyncio.ensure_future(self.play_music_at_time(dt+time.time()), loop=loop)
logging.info('Wait {} seconds to play music'.format(dt))
@pyqtSlot()
def emergency(self):
client_row_min = 0
@@ -487,7 +563,11 @@ if __name__ == "__main__":
#app.exec_()
with loop:
window = MainWindow()
Client.on_first_connect = window.client_connected
Client.on_first_connect = window.new_client_connected
Client.on_connect = window.client_connection_changed
Client.on_disconnect = window.client_connection_changed
server = Server(on_stop=app.quit)
server.start()
loop.run_forever()

View File

@@ -1,6 +1,8 @@
[Unit]
Description=Clever Show Client Service
After=clever.service
Requires=clever.service
Requires=network.target
After=network.target
[Service]
WorkingDirectory=/home/pi/clever-show/Drone

View File

@@ -7,51 +7,60 @@
* Wifi роутер, работающий на частоте 2.4 ГГц, либо 5.8 ГГц, если эту частоту поддерживают wifi модули коптеров и компьютера.
## Подготовка ПО
Скачайте на компьютер последний образ (CleverSwarm-XXX.img.zip) и исходный код (Source code) из последнего [релиза](https://github.com/artem30801/CleverSwarm/releases/latest). Разархивируйте исходный код в удобную директорию.
Скачайте на компьютер последний образ (clever-show_XXX.img.zip) и исходный код (Source code) из последнего [релиза](https://github.com/copterexpress/clever-show/releases/latest). Разархивируйте исходный код в удобную директорию.
## Настройка роутера
Для управления одним или несколькими коптерами требуется подключение коптеров и сервера к одной сети. Для этого требуется отдельный wifi роутер с известным SSID и паролем. Подключите компьютер, который будет использоваться в качестве сервера, к сети роутера и узнайте его ip адрес - он понадобится для дальнейшей настройки.
Для управления одним или несколькими коптерами требуется подключение коптеров и сервера к одной сети. Для этого требуется отдельный wifi роутер с известным SSID и паролем.
Подключите компьютер, который будет использоваться в качестве сервера, к сети роутера и узнайте его ip адрес - он понадобится для дальнейшей настройки.
## Настройка и запуск клиента
* Запишите образ на microSD карту, используя [Etcher](https://www.balena.io/etcher/).
* Вставьте флешку в Raspberry Pi, включите коптер. Дождитесь появления сети `CLEVERSHOW-XXXX`.
* Подключитесь к сети коптера, используя пароль `cleverwifi`.
* Настройте коптер, чтобы корректно работал режим позиции. По-умолчанию образ сконфигурирован для получения позиции с камеры с помощью aruco-маркеров и optical flow. Камера направлена вниз и вперёд, загружена тестовая карта меток. Если ваш способ позиционирования отличается - можно либо настроить данный образ, либо [собрать образ](image-building.md) со своими настройками.
* Перейдите в директорию клиента и запустите скрипт настройки клиента
* Подключитесь к Raspberry Pi на коптере с помощью ssh, используя статический ip `192.168.11.1`, имя пользователя `pi` и пароль `raspberry`.
```bash
cd ~/CleverSwarm/Drone
sudo ./client_setup.sh
ssh pi@192.168.11.1
```
* Выполните скрипт настройки клиента с указанными параметрами - SSID, пароль точки доступа, имя коптера, ip сервера.
* Коптер переключится в режим клиента указанной точки доступа и настроит автозапуск клиента copter_client.py с помощью сервиса clever-show
* Перейдите в директорию клиента и выполните скрипт настройки клиента с указанными параметрами - название точки доступа (`SSID`), пароль точки доступа (`password`), имя коптера (`copter name`), ip сервера (`server ip`). Коптер переключится в режим клиента указанной точки доступа и настроит автозапуск клиента на Raspberry Pi.
```bash
cd ~/clever-show/Drone
sudo ./client_setup.sh <SSID> <password> <copter name> <server ip>
```
* Теперь при запуске серверного приложения настроенные коптеры будут отображаться в виде таблицы. Также можно подключаться к Raspberry Pi на коптере по его имени через `ssh` в указанной при настройке wifi сети, например `ssh pi@clever-1`, пароль `cleverwifi`.
Документация по клиентской части находится [здесь](client.md).
## Настройка и запуск сервера
* Установите [chrony](https://chrony.tuxfamily.org/index.html) и Python 3 на ваш компьютер:
* Установите [chrony](https://chrony.tuxfamily.org/index.html), [samba](https://help.ubuntu.ru/wiki/samba) и Python 3 на ваш компьютер:
```bash
sudo apt install chrony python3 python3-pip
sudo apt install chrony samba python3 python3-pip
```
* Установите необходимые python-пакеты с помощью команды (запущенной из директории с исходным кодом)
```bash
pip3 install -r requirements.txt
```
* Подключитесь к wifi сети роутера, к которому подключены коптеры.
* Скопируйте [файл настроек chrony](../Server/chrony.conf) в `/etc/chrony/chrony.conf`. Если ip адрес сети начинается не с `192.168.`, то исправьте адрес после слова allow в скопированном файле настроек.
* Скопируйте [файл настроек chrony](../../Server/chrony.conf) в `/etc/chrony/chrony.conf`. Если ip адрес сети начинается не с `192.168.`, то исправьте адрес после слова allow в скопированном файле настроек.
* Перезапустите сервис chrony
```bash
cd source-code-dir
sudo systemctl restart chrony
```
* Перейдите в директорию сервера из директории с исходным кодом и запустите сервер
```bash
cd source-code-dir/Server
python3 server_qt.py
```
Документация по серверной части находится [здесь](server.md).
<!--stackedit_data:
eyJoaXN0b3J5IjpbLTIwNjI5MzIwMTFdfQ==
-->

View File

@@ -8,6 +8,8 @@ import logging
import threading
import collections
from contextlib import closing
try:
import selectors
except ImportError:
@@ -24,6 +26,16 @@ 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:
ip_socket.connect(("8.8.8.8", 80))
return ip_socket.getsockname()[0]
except OSError:
logging.warning("No network connection detected, using localhost")
return "localhost"
class _Singleton(type):
""" A metaclass that creates a Singleton base class when called. """
_instances = {}
@@ -185,9 +197,7 @@ class ConnectionManager(object):
self.socket = None
self.addr = None
self.selector = None
self.socket = None
self.addr = None
self._should_close = False
self._recv_buffer = b""
self._send_buffer = b""
@@ -198,6 +208,7 @@ class ConnectionManager(object):
self._send_lock = threading.Lock()
self._request_lock = threading.Lock()
self._close_lock = threading.Lock()
self.BUFFER_SIZE = 1024
self.resume_queue = False
@@ -225,8 +236,16 @@ class ConnectionManager(object):
self._set_selector_events_mask('r')
def close(self):
with self._close_lock:
self._should_close = True
self._set_selector_events_mask('w')
NotifierSock().notify()
def _close(self):
logger.info("Closing connection to {}".format(self.addr))
try:
logger.info("Unregistering selector of {}".format(self.addr))
self.selector.unregister(self.socket)
except AttributeError:
pass
@@ -236,6 +255,7 @@ class ConnectionManager(object):
self.selector = None
try:
logger.info("Closing socket of of {}".format(self.addr))
self.socket.close()
except AttributeError:
pass
@@ -244,7 +264,18 @@ class ConnectionManager(object):
finally:
self.socket = None
with self._close_lock:
self._should_close = False
logger.info("CLOSED connection to {}".format(self.addr))
def process_events(self, mask):
with self._close_lock:
close = self._should_close
if close:
self._close()
return
if mask & selectors.EVENT_READ:
self.read()
if mask & selectors.EVENT_WRITE:
@@ -304,7 +335,7 @@ class ConnectionManager(object):
command = message.content["command"]
args = message.content["args"]
try:
self.messages_callbacks[command](**args)
self.messages_callbacks[command](self, **args)
except KeyError:
logger.warning("Command {} does not exist!".format(command))
except Exception as error:
@@ -315,7 +346,7 @@ class ConnectionManager(object):
request_id = message.content["request_id"]
args = message.content["args"]
try:
value = self.requests_callbacks[command](**args)
value = self.requests_callbacks[command](self, **args)
except KeyError:
logger.warning("Request {} does not exist!".format(command))
except Exception as error: # TODO send response error\cancel