diff --git a/Drone/FlightLib/FlightLib.py b/Drone/FlightLib/FlightLib.py
index b7dae37..deae299 100644
--- a/Drone/FlightLib/FlightLib.py
+++ b/Drone/FlightLib/FlightLib.py
@@ -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)
diff --git a/Drone/animation_lib.py b/Drone/animation_lib.py
index 28b7374..4c12b92 100644
--- a/Drone/animation_lib.py
+++ b/Drone/animation_lib.py
@@ -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),
diff --git a/Drone/client.py b/Drone/client.py
index 2b6664b..aed8dc4 100644
--- a/Drone/client.py
+++ b/Drone/client.py
@@ -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()
diff --git a/Drone/client_config.ini b/Drone/client_config.ini
index 1170081..108adcd 100644
--- a/Drone/client_config.ini
+++ b/Drone/client_config.ini
@@ -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
diff --git a/Drone/copter_client.py b/Drone/copter_client.py
index 92cd73b..b093424 100644
--- a/Drone/copter_client.py
+++ b/Drone/copter_client.py
@@ -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,
diff --git a/README.md b/README.md
index 2f7bf21..cbde450 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/README_RU.md b/README_RU.md
index 19b7d9d..5659fdc 100644
--- a/README_RU.md
+++ b/README_RU.md
@@ -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).
diff --git a/Server/copter_table_models.py b/Server/copter_table_models.py
index b5f9e53..0905635 100644
--- a/Server/copter_table_models.py
+++ b/Server/copter_table_models.py
@@ -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")
diff --git a/Server/server.py b/Server/server.py
index e19ba2d..311b924 100644
--- a/Server/server.py
+++ b/Server/server.py
@@ -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):
diff --git a/Server/server_config.ini b/Server/server_config.ini
index 0614208..5160e2d 100644
--- a/Server/server_config.ini
+++ b/Server/server_config.ini
@@ -1,6 +1,7 @@
[SERVER]
port = 25000
buffer_size = 1024
+remove_disconnected = True
[BROADCAST]
use_broadcast = True
diff --git a/Server/server_gui.py b/Server/server_gui.py
index 0826274..794d524 100644
--- a/Server/server_gui.py
+++ b/Server/server_gui.py
@@ -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"))
diff --git a/Server/server_gui.ui b/Server/server_gui.ui
index abeb6af..7141df1 100644
--- a/Server/server_gui.ui
+++ b/Server/server_gui.ui
@@ -11,7 +11,7 @@
- Clever Drone Animation Player
+ Clever Drone Show
@@ -328,32 +328,28 @@
0
0
1220
- 25
+ 26
-
-
+
-
- Send Animations
+ Send animations
- Send Configurations
+ Send configurations
- Send Aruco map
+ Send aruco map
- Update client repo
+ Update clever-show git
@@ -411,7 +417,7 @@
- Send .launch file to clever
+ Send launch file to clever
@@ -426,7 +432,7 @@
- Select All
+ Select all drones
Ctrl+A
@@ -467,6 +473,36 @@
Test music after
+
+
+ fill
+
+
+
+
+ Send any file
+
+
+
+
+ Send any command
+
+
+
+
+ Stop music
+
+
+
+
+ Remove from table
+
+
+
+
+ Send camera calibrations
+
+
start_delay_spin
diff --git a/Server/server_qt.py b/Server/server_qt.py
index da30b9e..76343a7 100644
--- a/Server/server_qt.py
+++ b/Server/server_qt.py
@@ -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()
diff --git a/builder/assets/clever-show.service b/builder/assets/clever-show.service
index e1885e5..3c8466b 100644
--- a/builder/assets/clever-show.service
+++ b/builder/assets/clever-show.service
@@ -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
diff --git a/docs/blender-addon.md b/docs/ru/blender-addon.md
similarity index 100%
rename from docs/blender-addon.md
rename to docs/ru/blender-addon.md
diff --git a/docs/client.md b/docs/ru/client.md
similarity index 100%
rename from docs/client.md
rename to docs/ru/client.md
diff --git a/docs/image-building.md b/docs/ru/image-building.md
similarity index 100%
rename from docs/image-building.md
rename to docs/ru/image-building.md
diff --git a/docs/server.md b/docs/ru/server.md
similarity index 100%
rename from docs/server.md
rename to docs/ru/server.md
diff --git a/docs/start-tutorial.md b/docs/ru/start-tutorial.md
similarity index 55%
rename from docs/start-tutorial.md
rename to docs/ru/start-tutorial.md
index cf4edb2..4f565fe 100644
--- a/docs/start-tutorial.md
+++ b/docs/ru/start-tutorial.md
@@ -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
+```
+
+* Теперь при запуске серверного приложения настроенные коптеры будут отображаться в виде таблицы. Также можно подключаться к 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).
-
diff --git a/messaging_lib.py b/messaging_lib.py
index d2444fa..2e9a9b8 100644
--- a/messaging_lib.py
+++ b/messaging_lib.py
@@ -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