mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-30 08:49:33 +00:00
Merge master
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[SERVER]
|
||||
port = 25000
|
||||
buffer_size = 1024
|
||||
remove_disconnected = True
|
||||
|
||||
[BROADCAST]
|
||||
use_broadcast = True
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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==
|
||||
-->
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user