mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-26 23:19:33 +00:00
* .client_connected > .new_client_connected
* Fixed 'confirmation_required' wrapper
* Logging impr
* Changed and optimized a lot checks behaviour
* Added indication of connected/disconnected copters
* update_data_signal changed signature
* Added client removing functionality
* Option for automatically remove disconnected copters from table
* Renaming copters from QT server table on the go + some improvements
* Server: Check if self.clients list is not empty when trying to pop element from it
* Probably fixes behaviour of non-immidiate data sending from server
* Added changing hostname of copter
* Updated config
* Preview of selfchecheck results on double click
* Delete doc_2019-10-16_17-57-17.bashrc
* Update table data models for selfcheck
* Server: modify set id request to message
* Update client_config default file
* Client: modify set new id function
* Client: add avahi-daemon to restart when restarting network
* Client: add new hostname to ssh motd message, do not change hostname if no network restart in config
* Client: add newline to motd message
* Optimized request behaviour
* Client: fix service file and restart order
* Client: Add SO_KEEPALIVE and TCP_NODELAY options to client socket
* Modify to last tests with ping
* Client: remove ping
* Client: select reboot option when change id and add execute command
* Server: Add SO_KEEPALIVE option to server socket
* Server: Change removing copter
* Request resending after disconnection
* Resending improval (for furthrer functionality & fixes
* Fix of client removing behaviour
* Debugging
* Revert dubug code; 'Remove' fix confirmed
* do not clear requests queue
* Update requirements.txt
* Added namespace class to fix resend
* Improvements and simplification of notifier + port to client
* Refactor of telemetry thread
* Simplify lambdas
* Compress hostname check to single regex
* Changes in telemetry
* Refactored formatting of telemetry in table. NOT DONE
* Fix
* Git checkout. REVERT later!
* Conection fix
* Compability fixes
* Update start position
* Fix for reconnection with notifier socket
* Added traceback for pyqt5
* Fixes in new telemetry display
* Added lock to Telemetry
* Fixes for table display
* Fix of doubling line of client in table
* Fix of mass-removing clients from table
* Fix for clinet double-connection+removal
* Fix lock in Telemetry
* Changed signature of response callbacks for better syntax & fixes (all tested)
* Revert "Git checkout. REVERT later!"
This reverts commit 6122352380.
* Server: fix formatters
* Client: Remove telemetry_loop, small refactor of Telemetry class
* Server: Add formatters
* Server: Very small refactor
* Server: Fix checks and formatters
* Client: Fix check_failsafe function, small code refactor
* Client: update default config file
253 lines
9.5 KiB
Python
253 lines
9.5 KiB
Python
import os
|
|
import time
|
|
import errno
|
|
import random
|
|
import socket
|
|
import struct
|
|
import logging
|
|
import collections
|
|
import ConfigParser
|
|
import selectors2 as selectors
|
|
import threading
|
|
|
|
from contextlib import closing
|
|
|
|
import os,sys,inspect # Add parent dir to PATH to import messaging_lib
|
|
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
parent_dir = os.path.dirname(current_dir)
|
|
sys.path.insert(0, parent_dir)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
import messaging_lib as messaging
|
|
|
|
ConfigOption = collections.namedtuple("ConfigOption", ["section", "option", "value"])
|
|
|
|
active_client = None # maybe needs to be refactored
|
|
|
|
class Client(object):
|
|
def __init__(self, config_path="client_config.ini"):
|
|
self.selector = selectors.DefaultSelector()
|
|
self.client_socket = None
|
|
|
|
self.server_connection = messaging.ConnectionManager("pi")
|
|
|
|
self.server_host = None
|
|
self.server_port = None
|
|
self.broadcast_port = None
|
|
|
|
self.connected = False
|
|
self.client_id = None
|
|
|
|
# Init configs
|
|
self.config_path = config_path
|
|
self.config = ConfigParser.ConfigParser()
|
|
self.load_config()
|
|
|
|
global active_client
|
|
active_client = self
|
|
|
|
# self._last_ping_time = 0
|
|
|
|
def load_config(self):
|
|
self.config.read(self.config_path)
|
|
|
|
self.broadcast_port = self.config.getint('SERVER', 'broadcast_port')
|
|
self.server_port = self.config.getint('SERVER', 'port')
|
|
self.server_host = self.config.get('SERVER', 'host')
|
|
self.BUFFER_SIZE = self.config.getint('SERVER', 'buffer_size')
|
|
self.USE_NTP = self.config.getboolean('NTP', 'use_ntp')
|
|
self.NTP_HOST = self.config.get('NTP', 'host')
|
|
self.NTP_PORT = self.config.getint('NTP', 'port')
|
|
|
|
self.files_directory = self.config.get('FILETRANSFER', 'files_directory') # not used?!
|
|
|
|
self.client_id = self.config.get('PRIVATE', 'id')
|
|
if self.client_id == '/default':
|
|
self.client_id = 'copter' + str(random.randrange(9999)).zfill(4)
|
|
self.write_config(False, ConfigOption('PRIVATE', 'id', self.client_id))
|
|
elif self.client_id == '/hostname':
|
|
self.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:
|
|
self.config.write(file)
|
|
os.system("chown -R pi:pi /home/pi/clever-show")
|
|
|
|
def write_config(self, reload_config=True, *config_options):
|
|
for config_option in config_options:
|
|
self.config.set(config_option.section, config_option.option, config_option.value)
|
|
self.rewrite_config()
|
|
|
|
if reload_config:
|
|
self.load_config()
|
|
|
|
@staticmethod
|
|
def get_ntp_time(ntp_host, ntp_port):
|
|
NTP_PACKET_FORMAT = "!12I"
|
|
NTP_DELTA = 2208988800L # 1970-01-01 00:00:00
|
|
NTP_QUERY = '\x1b' + 47 * '\0'
|
|
|
|
with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
|
|
s.sendto(bytes(NTP_QUERY), (ntp_host, ntp_port))
|
|
msg, address = s.recvfrom(1024)
|
|
unpacked = struct.unpack(NTP_PACKET_FORMAT, msg[0:struct.calcsize(NTP_PACKET_FORMAT)])
|
|
return unpacked[10] + float(unpacked[11]) / 2 ** 32 - NTP_DELTA
|
|
|
|
def time_now(self):
|
|
if self.USE_NTP:
|
|
timenow = self.get_ntp_time(self.NTP_HOST, self.NTP_PORT)
|
|
else:
|
|
timenow = time.time()
|
|
return timenow
|
|
|
|
def start(self):
|
|
logger.info("Starting client")
|
|
messaging.NotifierSock().init(self.selector)
|
|
|
|
try:
|
|
while True:
|
|
self._reconnect()
|
|
self._process_connections()
|
|
|
|
except (KeyboardInterrupt, ):
|
|
logger.critical("Caught interrupt, exiting!")
|
|
self.selector.close()
|
|
|
|
def _reconnect(self, timeout=3.0, attempt_limit=5):
|
|
logger.info("Trying to connect to {}:{} ...".format(self.server_host, self.server_port))
|
|
attempt_count = 0
|
|
while not self.connected:
|
|
logger.info("Waiting for connection, attempt {}".format(attempt_count))
|
|
try:
|
|
self.client_socket = socket.socket()
|
|
self.client_socket.settimeout(timeout)
|
|
self.client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
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):
|
|
if error.errno == errno.EINTR:
|
|
logger.critical("Shutting down on keyboard interrupt")
|
|
raise KeyboardInterrupt
|
|
|
|
logger.warning("Can not connect due error: {}".format(error))
|
|
attempt_count += 1
|
|
time.sleep(timeout)
|
|
|
|
else:
|
|
logger.info("Connection to server successful!")
|
|
self._connect()
|
|
break
|
|
|
|
if attempt_count >= attempt_limit:
|
|
logger.info("Too many attempts. Trying to get new server IP")
|
|
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))
|
|
|
|
def broadcast_bind(self, timeout=3.0, attempt_limit=5):
|
|
broadcast_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
broadcast_client.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
broadcast_client.bind(("", self.broadcast_port))
|
|
broadcast_client.settimeout(timeout)
|
|
|
|
attempt_count = 0
|
|
try:
|
|
while attempt_count <= attempt_limit:
|
|
try:
|
|
data, addr = broadcast_client.recvfrom(self.BUFFER_SIZE)
|
|
except socket.error as error:
|
|
logger.warning("Could not receive broadcast due error: {}".format(error))
|
|
attempt_count += 1
|
|
else:
|
|
message = messaging.MessageManager()
|
|
message.income_raw = data
|
|
message.process_message()
|
|
if message.content:
|
|
logger.info("Received broadcast message {} from {}".format(message.content, addr))
|
|
if message.content["command"] == "server_ip":
|
|
args = message.content["args"]
|
|
self.server_port = int(args["port"])
|
|
self.server_host = args["host"]
|
|
self.write_config(False,
|
|
ConfigOption("SERVER", "port", self.server_port),
|
|
ConfigOption("SERVER", "host", self.server_host))
|
|
logger.info("Binding to new IP: {}:{}".format(self.server_host, self.server_port))
|
|
self.on_broadcast_bind()
|
|
break
|
|
finally:
|
|
broadcast_client.close()
|
|
|
|
def on_broadcast_bind(self):
|
|
pass
|
|
|
|
def _process_connections(self):
|
|
while True:
|
|
events = self.selector.select(timeout=1)
|
|
# if time.time() - self._last_ping_time > 5:
|
|
# self.server_connection.send_message("ping")
|
|
# self._last_ping_time = time.time()
|
|
# logging.debug("tick")
|
|
|
|
for key, mask in events: # TODO add notifier to client!
|
|
connection = key.data
|
|
if connection is None:
|
|
pass
|
|
else:
|
|
try:
|
|
connection.process_events(mask)
|
|
|
|
except Exception as error:
|
|
logger.error(
|
|
"Exception {} occurred for {}! Resetting connection!".format(error, connection.addr)
|
|
)
|
|
self.server_connection._close()
|
|
self.connected = False
|
|
|
|
if isinstance(error, OSError):
|
|
if error.errno == errno.EINTR:
|
|
raise KeyboardInterrupt
|
|
|
|
mapping = self.selector.get_map().values()
|
|
notifier_key = self.selector.get_key(messaging.NotifierSock().get_sock())
|
|
notify_only= len(mapping) == 1 and notifier_key in mapping
|
|
if notify_only or not mapping:
|
|
logger.warning("No active connections left!")
|
|
return
|
|
|
|
|
|
@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()
|