Revert "Merge branch 'qt-gui-update' of https://github.com/CopterExpress/clever-show into qt-gui-update"

This reverts commit a6c4bd76f4, reversing
changes made to d0eed91f73.
This commit is contained in:
Arthur Golubtsov
2020-01-24 14:18:33 +03:00
parent a6c4bd76f4
commit bbc1525998
9 changed files with 121 additions and 217 deletions

View File

@@ -148,18 +148,18 @@ class Client(object):
message = messaging.MessageManager()
message.income_raw = data
message.process_message()
if message.content and message.jsonheader["action"] == "server_ip":
if message.content:
logger.info("Received broadcast message {} from {}".format(message.content, addr))
if message.content["command"] == "server_ip":
args = message.content["args"]
self.config.set("SERVER", "port", int(args["port"]))
self.config.set("SERVER", "host", args["host"])
self.config.write()
kwargs = message.content["kwargs"]
self.config.set("SERVER", "port", int(kwargs["port"]))
self.config.set("SERVER", "host", kwargs["host"])
self.config.write()
logger.info("Binding to new IP: {}:{}".format(
self.config.server_host, self.config.server_port))
self.on_broadcast_bind()
break
logger.info("Binding to new IP: {}:{}".format(
self.config.server_host, self.config.server_port))
self.on_broadcast_bind()
break
finally:
broadcast_client.close()
@@ -168,7 +168,6 @@ class Client(object):
def _process_connections(self):
while True:
#self.server_connection.send_message("telemetry", kwargs={"value":{"time": time.time()}})
events = self.selector.select(timeout=1)
for key, mask in events:
@@ -227,7 +226,6 @@ def _response_id(*args, **kwargs):
if new_id is not None:
active_client.config.set("PRIVATE", "id", new_id, True)
active_client.load_config()
# TODO renaming here
return active_client.client_id

View File

@@ -123,7 +123,7 @@ def execute_command(command):
os.system(command)
def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1): # TODO simplify
def configure_chrony_ip(ip, path="/etc/chrony/chrony.conf", ip_index=1):
try:
with open(path, 'r') as f:
raw_content = f.read()
@@ -245,7 +245,7 @@ def _execute(*args, **kwargs):
logger.info("Executing done")
@messaging.message_callback("id") # TODO redo
@messaging.message_callback("id")
def _response_id(*args, **kwargs):
new_id = kwargs.get("new_id", None)
if new_id is not None:
@@ -645,7 +645,6 @@ def _play_animation(*args, **kwargs):
)
# noinspection PyAttributeOutsideInit
class Telemetry:
params_default_dict = {
"git_version": None,
@@ -653,14 +652,13 @@ class Telemetry:
"battery": None,
"armed": False,
"fcu_status": None,
"calibration_status": None,
"cal_status": None,
"mode": None,
"selfcheck": None,
"current_position": None,
"start_position": None,
"task": None,
"time": None,
"config_version": None,
}
def __init__(self):
@@ -692,10 +690,6 @@ class Telemetry:
def get_git_version(cls):
return subprocess.check_output("git log --pretty=format:'%h' -n 1", shell=True)
@classmethod
def get_config_version(cls):
return "{} V{}".format(client.active_client.config.config_name, client.active_client.config.config_version)
@classmethod
def get_start_position(cls):
x_start, y_start = animation.get_start_xy(os.path.abspath("animation.csv"),
@@ -770,7 +764,6 @@ class Telemetry:
def update_telemetry_slow(self):
self.animation_id = animation.get_id()
self.git_version = self.get_git_version()
self.config_version = self.get_config_varsion()
try:
self.cal_status = get_calibration_status()
self.fcu_status = get_sys_status()
@@ -835,9 +828,9 @@ class Telemetry:
self._tasks_cleared = False
self._last_state = state
def transmit_message(self): # todo if connected
def transmit_message(self):
try:
client.active_client.server_connection.send_message('telemetry', kwargs={'value': self.create_msg_contents()})
client.active_client.server_connection.send_message('telemetry', args={'value': self.create_msg_contents()})
except AttributeError as e:
logger.debug(e)

View File

@@ -24,7 +24,6 @@ remove_disconnected = boolean(default=False)
start_position = boolean(default=True)
last_task = boolean(default=True)
time_delta = boolean(default=True)
config_version = boolean(default=True)
[[[__many__]]]
__many__ = boolean

View File

@@ -372,7 +372,7 @@ class ConfigModel(QtCore.QAbstractItemModel):
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
def mimeTypes(self):
return ['app/configitem']
return ['app/configitem', 'text/xml']
def mimeData(self, indexes):
mimedata = QtCore.QMimeData()

View File

@@ -49,10 +49,8 @@ class CopterTableWidget(QTableView):
self.setSortingEnabled(True)
self.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.resizeColumnsToContents()
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.doubleClicked.connect(self.on_double_click)
self.setDragEnabled(True)
def moved(self, logical_index, old_index, new_index):
name = self.current_columns.pop(old_index)

View File

@@ -1,14 +1,12 @@
import re
import sys
import os
import time
import math
import indexed
from contextlib import suppress
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt as Qt, QUrl, QDir
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt as Qt
ModelDataRole = 998
ModelStateRole = 999
@@ -118,18 +116,15 @@ columns_names = {'copter_id': 'copter ID',
'current_position': 'current x y z yaw frame_id',
'start_position': ' start x y z ',
'last_task': 'last task',
'time_delta': 'dt',
'config_version': 'configuration',
'time_delta': 'dt'
}
columns = list(columns_names.keys())
class CopterData:
class_basic_attrs = indexed.IndexedOrderedDict([('copter_id', None), ('git_ver', None), ('animation_id', None),
('battery', None), ('fcu_status', None), ('calibration_status', None),
('battery', None), ('fcu_status', None), ('cal_status', None),
('mode', None), ('selfcheck', None), ('current_position', None),
('start_position', None), ('last_task', None), ('time_delta', None),
("config_version", None), ('client', None)])
('start_position', None), ('last_task', None), ('time_delta', None), ('client', None)])
def __init__(self, **kwargs):
self.attrs_dict = self.class_basic_attrs.copy()
@@ -304,11 +299,7 @@ def place_time_delta(value):
@ModelFormatter.col_format(11, ModelFormatter.VIEW_FORMATTER)
def view_time_delta(value):
return "{:.3f}".format(value)
def is_column(index, column_name):
return index.column() == columns.index(column_name)
return "{:.3f}".format(value)
class CopterDataModel(QtCore.QAbstractTableModel):
@@ -479,7 +470,7 @@ class CopterDataModel(QtCore.QAbstractTableModel):
self.update_model(index, role)
return True
def select_all(self): # probably NOT thread-safe! TODO remake
def select_all(self): # probably NOT thread-safe!
self.first_col_is_checked = not self.first_col_is_checked
for row_num, copter in enumerate(self.data_contents):
copter.states.checked = int(self.first_col_is_checked)*2
@@ -489,37 +480,8 @@ class CopterDataModel(QtCore.QAbstractTableModel):
roles = Qt.ItemIsSelectable | Qt.ItemIsEnabled
if index.column() == 0:
roles |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable
if is_column(index, "config_version"):
roles |= Qt.ItemIsDragEnabled # | Qt.ItemIsDropEnabled
return roles
def supportedDropActions(self):
return QtCore.Qt.CopyAction
def mimeTypes(self):
return ['text/plain']
def mimeData(self, indexes):
index = indexes[0]
if is_column(index, "config_version"):
return self._config_mime(index)
return None
def _config_mime(self, index):
mimedata = QtCore.QMimeData()
path = os.path.join(QDir.tempPath(), "config_{}.ini".format(
self.data_contents[index.row()].copter_id))
with suppress(OSError): # remove if file exists
os.remove(path)
self.data_contents[index.row()].client.get_file("config/client.ini", path,)
mimedata.setUrls([QUrl.fromLocalFile(path)])
return mimedata
@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)

View File

@@ -8,7 +8,6 @@ import datetime
import threading
import selectors
import collections
import traceback
import inspect # Add parent dir to PATH to import messaging_lib and config_lib
@@ -157,7 +156,6 @@ class Server(messaging.Singleton):
client.process_events(mask)
except Exception as error:
logging.error("Exception {} occurred for {}! Resetting connection!".format(error, client.addr))
traceback.print_exc()
client.close(True)
else: # Notifier
client.process_events(mask)
@@ -187,9 +185,9 @@ class Server(messaging.Singleton):
def _ip_broadcast(self):
logging.info("Broadcast sender thread started!")
msg = messaging.MessageManager.create_action_message(
"server_ip", kwargs={"host": self.ip, "port": str(self.config.server_port), "id": self.id,
"start_time": str(self.time_started)})
msg = messaging.MessageManager.create_simple_message(
"server_ip", {"host": self.ip, "port": str(self.config.server_port), "id": self.id,
"start_time": str(self.time_started)})
logging.debug("Formed broadcast message: {}".format(msg))
broadcast_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
@@ -233,11 +231,11 @@ class Server(messaging.Singleton):
message.process_message()
content = message.content
right_command = (content and message.jsonheader["action"] == "server_ip")
right_command = (content and content["command"] == "server_ip")
if right_command:
different_id = content["kwargs"]["id"] != str(self.id)
self_younger = float(content["kwargs"]["start_time"]) <= self.time_started
different_id = content["args"]["id"] != str(self.id)
self_younger = float(message.content["args"]["start_time"]) <= self.time_started
if different_id and self_younger:
# younger server should shut down
@@ -255,7 +253,7 @@ class Server(messaging.Singleton):
logging.info("Broadcast listener thread stopped, socked closed!")
def send_starttime(self, copter, start_time):
copter.send_message("start", kwargs={"time": str(start_time)})
copter.send_message("start", {"time": str(start_time)})
def requires_connect(f):
@@ -358,7 +356,7 @@ class Client(messaging.ConnectionManager):
@classmethod
@requires_any_connected
def broadcast_message(cls, command, args=None, force_all=False):
cls.broadcast(messaging.MessageManager.create_action_message(command, args), force_all)
cls.broadcast(messaging.MessageManager.create_simple_message(command, args), force_all)
if __name__ == '__main__':

View File

@@ -127,7 +127,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.action_send_any_file.triggered.connect(self.send_any_file)
self.ui.action_send_any_command.triggered.connect(self.send_any_command)
self.ui.action_restart_clever.triggered.connect(
b_partial(self.send_to_selected, "service_restart", kwargs={"name": "clever"}))
b_partial(self.send_to_selected, "service_restart", {"name": "clever"}))
self.ui.action_restart_clever_show.triggered.connect(self.restart_clever_show)
self.ui.action_update_client_repo.triggered.connect(b_partial(self.send_to_selected, "update_repo"))
self.ui.action_reboot_all.triggered.connect(b_partial(self.send_to_selected, "reboot_all"))
@@ -200,9 +200,8 @@ class MainWindow(QtWidgets.QMainWindow):
yield f(copter, *args, **kwargs)
@pyqtSlot()
def send_to_selected(self, command, command_args=(), command_kwargs=None):
return list(self.iterate_selected(lambda copter: copter.client.send_message(
command, command_args, command_kwargs)))
def send_to_selected(self, command, command_args=None):
return list(self.iterate_selected(lambda copter: copter.client.send_message(command, command_args)))
def new_client_connected(self, client: Client):
logging.debug("Added client {}".format(client))
@@ -238,14 +237,13 @@ class MainWindow(QtWidgets.QMainWindow):
"animation_id": 2,
"battery": 3,
"fcu_status": 4,
"calibration_status": 5,
"cal_status": 5,
"mode": 6,
"selfcheck": 7,
"current_position": 8,
"start_position": 9,
"task": 10,
"time": 11,
"config_version": 12,
}
for key, value in telems.items():
@@ -288,7 +286,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.pause_button.setText('Resume')
else:
time_gap = 0.1 # TODO config? automatic delay detection?
self.send_to_selected("resume", kwargs={"time": server.time_now() + time_gap})
self.send_to_selected("resume", {"time": server.time_now() + time_gap})
self.ui.pause_button.setText('Pause')
@pyqtSlot()
@@ -297,7 +295,7 @@ class MainWindow(QtWidgets.QMainWindow):
for copter in self.model.user_selected():
if self.model.checks.takeoff_checks(copter):
if self.ui.z_checkbox.isChecked():
copter.client.send_message("takeoff_z", {"z": str(self.ui.z_spin.value())}) # todo int, merge commands
copter.client.send_message("takeoff_z", {"z": str(self.ui.z_spin.value())}) # todo int
else:
copter.client.send_message("takeoff")
@@ -427,7 +425,7 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot()
def send_aruco(self):
def callback(copter):
copter.client.send_message("service_restart", kwargs={"name": "clever"})
copter.client.send_message("service_restart", {"name": "clever"})
self.send_files("Select aruco map configuration file", "Aruco map files (*.txt)", onefile=True,
client_path="/home/pi/catkin_ws/src/clever/aruco_pose/map/",
@@ -467,20 +465,20 @@ class MainWindow(QtWidgets.QMainWindow):
copters = self.model.user_selected()
for copter in copters:
copter.client.send_message("config", kwargs={"config": data, "mode": mode.lower()})
copter.client.send_message("config", {"config": data, "mode": mode.lower()})
@pyqtSlot()
def send_any_command(self):
text, ok = QInputDialog.getText(self, "Enter command to send on copter",
"Command:", QLineEdit.Normal, "")
if ok and text:
self.send_to_selected("execute", kwargs={"command": text})
self.send_to_selected("execute", {"command": text})
@pyqtSlot()
def restart_clever_show(self):
for copter in self.model.user_selected():
copter.client.send_message("service_restart", kwargs={"name": "visual_pose_watchdog"})
copter.client.send_message("service_restart", kwargs={"name": "clever-show"})
copter.client.send_message("service_restart", {"name": "visual_pose_watchdog"})
copter.client.send_message("service_restart", {"name": "clever-show"})
@pyqtSlot()
def restart_chrony(self):

View File

@@ -94,41 +94,35 @@ class MessageManager:
return message
@classmethod
def create_json_message(cls, contents, additional_headers=None):
message = cls.create_message(cls._json_encode(contents), "json", "message",
additional_headers=additional_headers)
def create_json_message(cls, contents):
message = cls.create_message(cls._json_encode(contents), "json", "message")
return message
@classmethod
def create_action_message(cls, action, args=(), kwargs=None):
if kwargs is None:
kwargs = {}
message = cls.create_json_message({"args": args, "kwargs": kwargs}, {"action": action, })
def create_simple_message(cls, command, args=None):
if args is None:
args = {}
message = cls.create_json_message({"command": command, "args": args})
return message
@classmethod
def create_request(cls, requested_value, request_id, args=(), kwargs=None):
if kwargs is None:
kwargs = {}
def create_request(cls, requested_value, request_id, args=None):
if args is None:
args = {}
contents = {"requested_value": requested_value,
"request_id": request_id,
"args": args,
"kwargs": kwargs,
}
message = cls.create_message(cls._json_encode(contents), "json", "request")
return message
@classmethod
def create_response(cls, requested_value, request_id, value, filetransfer=False):
headers = {"requested_value": requested_value,
"request_id": request_id, # TODO status
}
if filetransfer:
contents = value
else:
contents = cls._json_encode({"value": value, })
message = cls.create_message(contents, "binary" if filetransfer else "json",
"response", additional_headers=headers)
def create_response(cls, requested_value, request_id, value):
contents = {"requested_value": requested_value,
"request_id": request_id,
"value": value,
}
message = cls.create_message(cls._json_encode(contents), "json", "response")
return message
def _process_protoheader(self):
@@ -177,10 +171,10 @@ class MessageManager:
self._process_content()
def message_callback(action_string):
def message_callback(string_command):
def inner(f):
ConnectionManager.messages_callbacks[action_string] = f
logger.debug("Registered message function {} for {}".format(f, action_string))
ConnectionManager.messages_callbacks[string_command] = f
logger.debug("Registered message function {} for {}".format(f, string_command))
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
@@ -346,105 +340,84 @@ class ConnectionManager(object):
raise RuntimeError("Peer closed.")
def process_received(self, message):
message_type = message.jsonheader["message-type"]
content = message.content if message.jsonheader["content-type"] != "binary"\
else message.content[:256]
def process_received(self, income_message):
message_type = income_message.jsonheader["message-type"]
content = income_message.content if message_type != "filetransfer" else income_message.content[:256]
logger.debug(
"Received message! Header: {}, content: {}".format(message.jsonheader, content))
"Received message! Header: {}, content: {}".format(income_message.jsonheader, content))
if message_type == "message":
self._process_message(message)
self._process_message(income_message)
elif message_type == "response":
self._process_response(message)
self._process_response(income_message)
elif message_type == "request":
self._process_request(message)
self._process_request(income_message)
elif message_type == "filetransfer":
self._process_filetransfer(income_message)
def _process_message(self, message):
if message.jsonheader["action"] == "filetransfer":
self._process_filetransfer(message.content, message.jsonheader["filepath"])
else:
self._process_action(message)
def _process_action(self, message):
action = message.jsonheader["action"]
command = message.content["command"]
args = message.content["args"]
kwargs = message.content["kwargs"]
callback = self.messages_callbacks.get(action, None)
callback = self.messages_callbacks.get(command, None)
if callback is None:
logger.warning("Action {} does not exist!".format(action))
logger.warning("Command {} does not exist!".format(command))
return
try:
callback(self, *args, **kwargs)
callback(self, **args)
except Exception as error:
logger.error("Error during action {} execution: {}".format(action, error))
logger.error("Error during command {} execution: {}".format(command, error))
def _process_request(self, message):
requested_value = message.content["requested_value"]
command = message.content["requested_value"]
request_id = message.content["request_id"]
args = message.content["args"]
kwargs = message.content["kwargs"]
filetransfer = requested_value == "filetransfer"
callback = self.requests_callbacks.get(command, None)
if callback is None:
logger.warning("Request {} does not exist!".format(command))
return
try:
if filetransfer:
value = self._read_file(kwargs["filepath"])
else:
callback = self.requests_callbacks.get(requested_value, None)
if callback is None:
logger.warning("Request {} does not exist!".format(requested_value))
return
value = callback(self, *args, **kwargs)
value = callback(self, **args)
except Exception as error: # TODO send response error\cancel
logger.error("Error during request {} processing: {}".format(requested_value, error))
logger.error("Error during request {} processing: {}".format(command, error))
else:
self._send_response(requested_value, request_id, value, filetransfer)
self._send_response(command, request_id, value)
def _process_response(self, message):
request_id, requested_value = message.jsonheader["request_id"], message.jsonheader["requested_value"]
request_id, requested_value = message.content["request_id"], message.content["requested_value"]
with self._request_lock:
request = self._request_queue.pop(request_id, None)
if (request is None) or (request.requested_value != requested_value):
logger.warning("Unexpected response!")
return
if requested_value == "filetransfer":
value = True
self._process_filetransfer(message.content, request.callback_kwargs["filepath"])
logger.debug(
"Request {} successfully closed with file bytes {}...".format(request, message.content[:256])
)
else:
if (request is not None) and (request.requested_value == requested_value):
value = message.content["value"]
logger.debug(
"Request {} successfully closed with value {}".format(request, message.content["value"])
)
if request.callback is not None:
print(self, "CALLBACK", request.callback, "VAL", value, "ARGS",request.callback_args, request.callback_kwargs)
try:
request.callback(self, value, *request.callback_args, **request.callback_kwargs)
except Exception as error:
logger.error("Error during response {} processing: {}".format(request, error))
else:
logger.info("No callback were registered for response: {}".format(request))
print(1)
except Exception as e:
logging.error("Error during callback call of request") # TODO more info
traceback.print_exc()
print(e)
@staticmethod
def _read_file(filepath):
with open(filepath, mode='rb') as f:
return f.read()
def _process_filetransfer(self, content, filepath):
try:
with open(filepath, 'wb') as f:
f.write(content)
except OSError as error:
logger.error("File {} can not be written due error: {}".format(filepath, error))
else:
logger.info("File {} successfully received ".format(filepath))
if self.whoami == "pi":
logger.info("Return rights to pi:pi after file transfer")
os.system("chown pi:pi {}".format(filepath))
logger.warning("Unexpected response!")
def _process_filetransfer(self, message): # TODO path?
if message.jsonheader["content-type"] == "binary":
filepath = message.jsonheader["filepath"]
try:
with open(filepath, 'wb') as f:
f.write(message.content)
except OSError as error:
logger.error("File {} can not be written due error: {}".format(filepath, error))
else:
logger.info("File {} successfully received ".format(filepath))
if self.whoami == "pi":
logger.info("Return rights to pi:pi after file transfer")
os.system("chown pi:pi {}".format(filepath))
def write(self):
with self._send_lock:
@@ -480,15 +453,14 @@ class ConnectionManager(object):
self._set_selector_events_mask('rw')
NotifierSock().notify()
def get_response(self, requested_value, callback, # timeout=30,
request_args=(), request_kwargs=None,
callback_args=(), callback_kwargs=None, ):
if request_kwargs is None:
request_kwargs = {}
def get_response(self, requested_value, callback, request_args=None, # timeout=30,
callback_args=(), callback_kwargs=None):
if request_args is None:
request_args = {}
if callback_kwargs is None:
callback_kwargs = {}
request_id = str(random.randint(0, 9999)).zfill(4) # maybe hash
request_id = str(random.randint(0, 9999)).zfill(4)
with self._request_lock:
self._request_queue[request_id] = PendingRequest(
requested_value=requested_value,
@@ -498,39 +470,24 @@ class ConnectionManager(object):
callback_args=callback_args,
callback_kwargs=callback_kwargs,
request_args=request_args,
request_kwargs=request_kwargs,
resend=True,
)
self._send(MessageManager.create_request(requested_value, request_id, request_args, request_kwargs))
def get_file(self, client_filepath, filepath=None, callback=None,
callback_args=(), callback_kwargs=None, ):
if callback_kwargs is None:
callback_kwargs = {}
if filepath is None:
filepath = os.path.split(client_filepath)[1]
request_kwargs = {"filepath": client_filepath}
callback_kwargs.update({"filepath": filepath})
self.get_response("filetransfer", callback, request_kwargs=request_kwargs,
callback_args=callback_args, callback_kwargs=callback_kwargs)
self._send(MessageManager.create_request(requested_value, request_id, request_args))
def _resend_requests(self):
with self._request_lock:
for request_id, request in self._request_queue.items(): # TODO filter
for request_id, request in self._request_queue.items(): #TODO filter
if request.resend:
self._send(MessageManager.create_request(
request.requested_value, request_id, request.request_kwargs.update(resend=request.resend))
request.requested_value, request_id, request.request_args.update(resend=request.resend))
)
request.resend = False
def send_message(self, action, args=(), kwargs=None):
self._send(MessageManager.create_action_message(action, args, kwargs))
def send_message(self, command, args=None):
self._send(MessageManager.create_simple_message(command, args))
def _send_response(self, requested_value, request_id, value, filetransfer=False):
self._send(MessageManager.create_response(requested_value, request_id, value, filetransfer))
def _send_response(self, requested_value, request_id, value):
self._send(MessageManager.create_response(requested_value, request_id, value))
def send_file(self, filepath, dest_filepath): # clever_restart=False
try:
@@ -540,8 +497,9 @@ class ConnectionManager(object):
logger.warning("File can not be opened due error: ".format(error))
else:
logger.info("Sending file {} to {} (as: {})".format(filepath, self.addr, dest_filepath))
self._send(MessageManager.create_message(data, "binary", "message",
additional_headers={"action": "filetransfer", "filepath": dest_filepath}))
self._send(MessageManager.create_message(
data, "binary", "filetransfer", "binary", {"filepath": dest_filepath}
))
class NotifierSock(Singleton):