def start_runtime(plugin_location=None): if plugin_location is None and (len(sys.argv) < 3 or sys.argv[1] != 'start_plugin'): sys.stderr.write('Usage: python {0} start_plugin <path>\n'.format( sys.argv[0])) sys.stderr.flush() sys.exit(1) elif not (len(sys.argv) < 3 or sys.argv[1] != 'start_plugin'): plugin_location = sys.argv[2] def watch_parent(): parent = os.getppid() # If the parent process gets kills, this process will be attached to init. # In that case the plugin should stop running. while True: if os.getppid() != parent: os._exit(1) time.sleep(1) # Keep an eye on our parent process watcher = BaseThread(name='pluginwatch', target=watch_parent) watcher.daemon = True watcher.start() # Start the runtime try: runtime = PluginRuntime(path=plugin_location) runtime.process_stdin() except BaseException as ex: writer = PluginIPCWriter(os.fdopen(sys.stdout.fileno(), 'wb', 0)) writer.log_exception('__main__', ex) os._exit(1) os._exit(0)
def start(self): # type: () -> None if self._running: raise Exception('PluginRunner is already running') self.logger('[Runner] Starting') python_executable = sys.executable if python_executable is None or len(python_executable) == 0: python_executable = '/usr/bin/python' self._proc = subprocess.Popen([ python_executable, 'runtime.py', 'start_plugin', self.plugin_path ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, cwd=self.runtime_path, close_fds=True) assert self._proc.stdout, 'Plugin stdout not available' self._process_running = True self._commands_executed = 0 self._commands_failed = 0 assert self._proc.stdin, 'Plugin stdin not defined' self._writer = PluginIPCWriter(stream=self._proc.stdin) self._reader = PluginIPCReader(stream=self._proc.stdout, logger=lambda message, ex: self.logger( '{0}: {1}'.format(message, ex)), command_receiver=self._process_command, name=self.name) self._reader.start() start_out = self._do_command('start', timeout=180) self.name = start_out['name'] self.version = start_out['version'] self.interfaces = start_out['interfaces'] self._decorators_in_use = start_out['decorators'] self._exposes = start_out['exposes'] self._metric_collectors = start_out['metric_collectors'] self._metric_receivers = start_out['metric_receivers'] exception = start_out.get('exception') if exception is not None: raise RuntimeError(exception) self._async_command_queue = Queue(1000) self._async_command_thread = BaseThread( name='plugincmd{0}'.format(self.plugin_path), target=self._perform_async_commands) self._async_command_thread.daemon = True self._async_command_thread.start() self._running = True if self._state_callback is not None: self._state_callback(self.name, PluginRunner.State.RUNNING) self.logger('[Runner] Started')
def start(self): # type: () -> bool self._stopped = False success = self._run() # Initial sync run self._thread = BaseThread(name='watchdog{0}'.format(self._plugin_runner.name), target=self.run) self._thread.daemon = True self._thread.start() return success
def _start_background_tasks(self): # type: () -> None """ Start all background tasks. """ for decorated_method in self._decorated_methods['background_task']: thread = BaseThread(name='plugin{}'.format( decorated_method.__name__), target=self._run_background_task, args=(decorated_method, )) thread.daemon = True thread.start()
def _handle_stop(self): def delayed_stop(): time.sleep(2) os._exit(0) stop_thread = BaseThread(name='pluginstop', target=delayed_stop) stop_thread.daemon = True stop_thread.start() self._stopped = True
def _start(self): def stop(signum, frame): """ This function is called on SIGTERM. """ _ = signum, frame self._stop = True signal(SIGTERM, stop) receiver = BaseThread(name='msgclientrecv', target=self._message_receiver) receiver.daemon = True receiver.start()
def start(self): """ Start the Passthrough service, this launches the two threads. """ self.__reader_thread = BaseThread(name='passthroughread', target=self.__reader) self.__reader_thread.daemon = True self.__reader_thread.start() self.__writer_thread = BaseThread(name='passthroughwrite', target=self.__writer) self.__writer_thread.daemon = True self.__writer_thread.start()
def open_maintenace_socket(self): """ Opens a TCP/SSL socket, connecting it with the maintenance service """ port = random.randint(6000, 7000) self._server_thread = BaseThread(name='maintenancesock', target=self._run_socket_server, args=[port]) self._server_thread.daemon = True self._server_thread.start() return port
class RunnerWatchdog(object): def __init__(self, plugin_runner, threshold=0.25, check_interval=60): # type: (PluginRunner, float, int) -> None self._plugin_runner = plugin_runner self._threshold = threshold self._check_interval = check_interval self._stopped = False self._thread = None # type: Optional[Thread] def stop(self): # type: () -> None self._stopped = True if self._thread is not None: self._thread.join() def start(self): # type: () -> bool self._stopped = False success = self._run() # Initial sync run self._thread = BaseThread(name='watchdog{0}'.format( self._plugin_runner.name), target=self.run) self._thread.daemon = True self._thread.start() return success def run(self): self._plugin_runner.logger('[Watchdog] Started') while not self._stopped: self._run() for _ in range(self._check_interval * 2): # Small sleep cycles, to be able to finish the thread quickly time.sleep(0.5) if self._stopped: break self._plugin_runner.logger('[Watchdog] Stopped') def _run(self): try: score = self._plugin_runner.error_score() if score > self._threshold: self._plugin_runner.logger( '[Watchdog] Stopping unhealthy runner') self._plugin_runner.stop() if not self._plugin_runner.is_running(): self._plugin_runner.logger( '[Watchdog] Starting stopped runner') self._plugin_runner.start() return True except Exception as e: self._plugin_runner.logger( '[Watchdog] Exception in watchdog: {0}'.format(e)) return False
def start(self): # type: () -> None """ Start the MasterComunicator, this starts the background read thread. """ if self.__init_master: self._flush_serial_input() if not self.__read_thread: self.__read_thread = BaseThread(name='masterread', target=self.__read) self.__read_thread.daemon = True if not self.__running: self.__running = True self.__read_thread.start()
class PluginIPCReader(object): """ This class handles IPC communications. It uses a stream of msgpack encoded dict values. """ def __init__(self, stream, logger, command_receiver=None, name=None): # type: (IO[bytes], Callable[[str,Exception],None], Callable[[Dict[str,Any]],None],Optional[str]) -> None self._command_queue = Queue() self._unpacker = msgpack.Unpacker( stream, read_size=1, raw=False) # type: msgpack.Unpacker[Dict[str,Any]] self._read_thread = None # type: Optional[Thread] self._logger = logger self._running = False self._command_receiver = command_receiver self._name = name def start(self): # type: () -> None self._running = True self._read_thread = BaseThread(name='ipcread', target=self._read) self._read_thread.daemon = True self._read_thread.start() def stop(self): # type: () -> None self._running = False if self._read_thread is not None: self._read_thread.join() def _read(self): # type: () -> None while self._running: try: command = next(self._unpacker) if not isinstance(command, dict): raise ValueError('invalid value %s' % command) if self._command_receiver is not None: self._command_receiver(command) else: self._command_queue.put(command) except StopIteration as ex: self._logger('PluginIPCReader %s stopped' % self._name, ex) self._running = False except Exception as ex: self._logger('Unexpected read exception', ex) def get(self, block=True, timeout=None): return self._command_queue.get(block, timeout)
class RS485(object): """ Replicates the pyserial interface. """ def __init__(self, serial): # type: (Serial) -> None """ Initialize a rs485 connection using the serial port. """ self._serial = serial fileno = serial.fileno() if fileno is not None: flags_rs485 = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND serial_rs485 = struct.pack('hhhhhhhh', flags_rs485, 0, 0, 0, 0, 0, 0, 0) fcntl.ioctl(fileno, TIOCSRS485, serial_rs485) self._serial.timeout = None self._running = False self._thread = BaseThread(name='rS485read', target=self._reader) self._thread.daemon = True # TODO why does this stream byte by byte? self.read_queue = Queue() # type: Queue[bytearray] def start(self): # type: () -> None if not self._running: self._running = True self._thread.start() def stop(self): # type: () -> None self._running = False def write(self, data): # type: (bytes) -> None """ Write data to serial port """ self._serial.write(data) def _reader(self): # type: () -> None try: while self._running: data = bytearray(self._serial.read(1)) if len(data) == 1: self.read_queue.put(data[:1]) size = self._serial.inWaiting() if size > 0: data = bytearray(self._serial.read(size)) for i in range(size): self.read_queue.put(data[i:i + 1]) except Exception as ex: print('Error in reader: {0}'.format(ex))
def activate(self): # type: () -> None """ Activates maintenance mode, If no data is send for too long, maintenance mode will be closed automatically. """ logger.info('Activating maintenance mode') self._last_maintenance_send_time = time.time() self._master_communicator.start_maintenance_mode() self._maintenance_timeout_timer = Timer(MaintenanceClassicCommunicator.MAINTENANCE_TIMEOUT, self._check_maintenance_timeout) self._maintenance_timeout_timer.start() self._stopped = False self._read_data_thread = BaseThread(target=self._read_data, name='maintenanceread') self._read_data_thread.daemon = True self._read_data_thread.start() self._deactivated_sent = False
class BackgroundConsumer(object): """ A consumer that runs in the background. The BackgroundConsumer does not provide get() but does a callback to a function whenever a message was consumed. """ def __init__( self, command, cid, callback ): # type: (CoreCommandSpec, int, Callable[[Dict[str, Any]], None]) -> None """ Create a background consumer using a cmd, cid and callback. :param command: the CoreCommand to consume. :param cid: the communication id. :param callback: function to call when an instance was found. """ self.cid = cid self.command = command self._callback = callback self._queue = Queue() # type: Queue[Dict[str, Any]] self._callback_thread = BaseThread(name='coredelivery', target=self._consumer) self._callback_thread.setDaemon(True) self._callback_thread.start() def _consumer(self): while True: try: self.deliver() except Exception: logger.exception( 'Unexpected exception delivering background consumer data') time.sleep(1) def get_hash(self): # type: () -> int """ Get an identification hash for this consumer. """ return Toolbox.hash(CoreCommunicator.START_OF_REPLY + bytearray([self.cid]) + self.command.response_instruction) def consume(self, payload): # type: (bytearray) -> None """ Consume payload. """ data = self.command.consume_response_payload(payload) self._queue.put(data) def deliver(self): """ Deliver data to the callback functions. """ self._callback(self._queue.get())
def __init__(self, command, cid, callback): # type: (CoreCommandSpec, int, Callable[[Dict[str, Any]], None]) -> None """ Create a background consumer using a cmd, cid and callback. :param command: the CoreCommand to consume. :param cid: the communication id. :param callback: function to call when an instance was found. """ self.cid = cid self.command = command self._callback = callback self._queue = Queue() # type: Queue[Dict[str, Any]] self._callback_thread = BaseThread(name='coredelivery', target=self._consumer) self._callback_thread.setDaemon(True) self._callback_thread.start()
def start_address_mode(self): # type: () -> None """ Start address mode. :raises: :class`InAddressModeException` if communicator is in maintenance mode. """ if self.__address_mode: raise InAddressModeException() self.__address_mode = True self.__address_mode_stop = False with self.__serial_lock: self.__address_thread = BaseThread(name='poweraddressmode', target=self.__do_address_mode) self.__address_thread.daemon = True self.__address_thread.start()
def __init__(self, serial): # type: (Serial) -> None """ Initialize a rs485 connection using the serial port. """ self._serial = serial fileno = serial.fileno() if fileno is not None: flags_rs485 = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND serial_rs485 = struct.pack('hhhhhhhh', flags_rs485, 0, 0, 0, 0, 0, 0, 0) fcntl.ioctl(fileno, TIOCSRS485, serial_rs485) self._serial.timeout = None self._running = False self._thread = BaseThread(name='rS485read', target=self._reader) self._thread.daemon = True # TODO why does this stream byte by byte? self.read_queue = Queue() # type: Queue[bytearray]
def _process(self): now = time.time() self.refresh_schedules() # Bug fix pending_schedules = [] for schedule_id in list(self._schedules.keys()): schedule_tuple = self._schedules.get(schedule_id) if schedule_tuple is None: continue schedule_dto, schedule = schedule_tuple if schedule_dto.status != 'ACTIVE': continue if schedule_dto.end is not None and schedule_dto.end < time.time(): schedule_dto.status = 'COMPLETED' schedule.status = 'COMPLETED' schedule.save() continue if schedule_dto.next_execution is not None and schedule_dto.next_execution < now - 60: continue pending_schedules.append(schedule_dto) # Sort the schedules according to their next_execution pending_schedules = list( sorted(pending_schedules, key=attrgetter('next_execution'))) if not pending_schedules: return next_start = pending_schedules[0].next_execution schedules_to_execute = [ schedule for schedule in pending_schedules if schedule.next_execution < next_start + 60 ] if not schedules_to_execute: return # Let this thread hang until it's time to execute the schedule logger.debug('next pending schedule %s, waiting %ss', datetime.fromtimestamp(next_start), next_start - now) self._event.wait(next_start - now) if self._event.isSet( ): # If a new schedule is saved, stop hanging and refresh the schedules self._event.clear() return thread = BaseThread(name='schedulingexc', target=self._execute_schedule, args=(schedules_to_execute, schedule)) thread.daemon = True thread.start()
def _process(self): for schedule_id in list(self._schedules.keys()): schedule_tuple = self._schedules.get(schedule_id) if schedule_tuple is None: continue schedule_dto, schedule = schedule_tuple if schedule_dto.status != 'ACTIVE': continue if schedule_dto.end is not None and schedule_dto.end < time.time(): schedule_dto.status = 'COMPLETED' schedule.status = 'COMPLETED' schedule.save() continue if schedule_dto.is_due: thread = BaseThread(name='schedulingexc', target=self._execute_schedule, args=(schedule_dto, schedule)) thread.daemon = True thread.start()
def send_passthrough_data(self, data): """ Send raw data on the serial port. :param data: string of bytes with raw command for the master. :raises: :class`InMaintenanceModeException` if master is in maintenance mode. """ if self.__maintenance_mode: raise InMaintenanceModeException() if not self.__passthrough_mode: self.__command_lock.acquire() self.__passthrough_done.clear() self.__passthrough_mode = True passthrough_thread = BaseThread(name='passthroughwait', target=self.__passthrough_wait) passthrough_thread.daemon = True passthrough_thread.start() self.__write_to_serial(data)
def __init__(self, cmd, cid, callback, send_to_passthrough=False): """ Create a background consumer using a cmd, cid and callback. :param cmd: the MasterCommand to consume. :param cid: the communication id. :param callback: function to call when an instance was found. :param send_to_passthrough: whether to send the command to the passthrough. """ self.cmd = cmd self.cid = cid self.callback = callback self.last_cmd_data = None # type: Optional[bytearray] # Keep the data of the last command. self.send_to_passthrough = send_to_passthrough self._queue = Queue() self._running = True self._callback_thread = BaseThread(name='masterdeliver', target=self._consumer) self._callback_thread.setDaemon(True) self._callback_thread.start()
class BackgroundConsumer(object): """ A consumer that runs in the background. The BackgroundConsumer does not provide get() but does a callback to a function whenever a message was consumed. """ def __init__(self, cmd, cid, callback, send_to_passthrough=False): """ Create a background consumer using a cmd, cid and callback. :param cmd: the MasterCommand to consume. :param cid: the communication id. :param callback: function to call when an instance was found. :param send_to_passthrough: whether to send the command to the passthrough. """ self.cmd = cmd self.cid = cid self.callback = callback self.last_cmd_data = None # type: Optional[bytearray] # Keep the data of the last command. self.send_to_passthrough = send_to_passthrough self._queue = Queue() self._running = True self._callback_thread = BaseThread(name='masterdeliver', target=self._consumer) self._callback_thread.setDaemon(True) self._callback_thread.start() def get_prefix(self): # type: () -> bytearray """ Get the prefix of the answer from the master. """ return bytearray(self.cmd.output_action) + bytearray([self.cid]) def stop(self): self._running = False self._callback_thread.join() def _consumer(self): while self._running: try: self._consume() except Empty: pass except Exception: logger.exception('Unexpected exception delivering background consumer data') time.sleep(1) def _consume(self): self.callback(self._queue.get(block=True, timeout=0.25)) def consume(self, data, partial_result): # type: (bytearray, Optional[Result]) -> Tuple[int, Result, bool] """ Consume data. """ (bytes_consumed, last_result, done) = self.cmd.consume_output(data, partial_result) self.last_cmd_data = (self.get_prefix() + last_result.actual_bytes) if done else None return bytes_consumed, last_result, done def deliver(self, output): # type: (Result) -> None self._queue.put(output)
if len(sys.argv) < 3 or sys.argv[1] != 'start': sys.stderr.write('Usage: python {0} start <path>\n'.format( sys.argv[0])) sys.stderr.flush() sys.exit(1) def watch_parent(): parent = os.getppid() # If the parent process gets kills, this process will be attached to init. # In that case the plugin should stop running. while True: if os.getppid() != parent: os._exit(1) time.sleep(1) # Keep an eye on our parent process watcher = BaseThread(name='pluginwatch', target=watch_parent) watcher.daemon = True watcher.start() # Start the runtime try: runtime = PluginRuntime(path=sys.argv[2]) runtime.process_stdin() except BaseException as ex: writer = PluginIPCWriter(os.fdopen(sys.stdout.fileno(), 'wb', 0)) writer.log_exception('__main__', ex) os._exit(1) os._exit(0)
class PassthroughService(object): """ The Passthrough service creates two threads: one for reading from and one for writing to the master. """ @Inject def __init__(self, master_communicator=INJECTED, passthrough_serial=INJECTED, verbose=False): self.__master_communicator = master_communicator self.__passthrough_serial = passthrough_serial self.__verbose = verbose self.__stopped = False self.__reader_thread = None self.__writer_thread = None self.__master_communicator.enable_passthrough() def start(self): """ Start the Passthrough service, this launches the two threads. """ self.__reader_thread = BaseThread(name='passthroughread', target=self.__reader) self.__reader_thread.daemon = True self.__reader_thread.start() self.__writer_thread = BaseThread(name='passthroughwrite', target=self.__writer) self.__writer_thread.daemon = True self.__writer_thread.start() def __reader(self): """ Reads from the master and writes to the passthrough serial. """ while not self.__stopped: data = self.__master_communicator.get_passthrough_data() if data and len(data) > 0: if self.__verbose: logger.info("Data for passthrough: %s", printable(data)) self.__passthrough_serial.write(data) def __writer(self): """ Reads from the passthrough serial and writes to the master. """ while not self.__stopped: data = self.__passthrough_serial.read(1) if data and len(data) > 0: num_bytes = self.__passthrough_serial.inWaiting() if num_bytes > 0: data += self.__passthrough_serial.read(num_bytes) try: if self.__verbose: logger.info("Data from passthrough: %s", printable(data)) self.__master_communicator.send_passthrough_data(data) except InMaintenanceModeException: logger.info( "Dropped passthrough communication in maintenance mode." ) def stop(self): """ Stop the Passthrough service. """ self.__stopped = True
class MaintenanceClassicCommunicator(MaintenanceCommunicator): """ The maintenance communicator handles maintenance communication with the Master """ MAINTENANCE_TIMEOUT = 600 @Inject def __init__(self, master_communicator=INJECTED, pubsub=INJECTED): # type: (MasterCommunicator, PubSub) -> None """ Construct a MaintenanceCommunicator. :param master_communicator: the communication with the master. :type master_communicator: master.master_communicator.MasterCommunicator """ self._master_communicator = master_communicator self._pubsub = pubsub self._deactivated_sent = False self._last_maintenance_send_time = 0.0 self._stopped = False self._maintenance_timeout_timer = None # type: Optional[Timer] self._read_data_thread = None # type: Optional[Thread] self._receiver_callback = None # type: Optional[Callable[[str],None]] def start(self): # type: () -> None pass # Classic doesn't have a permanent running maintenance def stop(self): # type: () -> None pass # Classic doesn't have a permanent running maintenance def set_receiver(self, callback): # type: (Callable[[str],None]) -> None self._receiver_callback = callback def is_active(self): # type: () -> bool return self._master_communicator.in_maintenance_mode() def activate(self): # type: () -> None """ Activates maintenance mode, If no data is send for too long, maintenance mode will be closed automatically. """ logger.info('Activating maintenance mode') self._last_maintenance_send_time = time.time() self._master_communicator.start_maintenance_mode() self._maintenance_timeout_timer = Timer(MaintenanceClassicCommunicator.MAINTENANCE_TIMEOUT, self._check_maintenance_timeout) self._maintenance_timeout_timer.start() self._stopped = False self._read_data_thread = BaseThread(target=self._read_data, name='maintenanceread') self._read_data_thread.daemon = True self._read_data_thread.start() self._deactivated_sent = False def deactivate(self, join=True): # type: (bool) -> None logger.info('Deactivating maintenance mode') self._stopped = True if join and self._read_data_thread is not None: self._read_data_thread.join() self._read_data_thread = None self._master_communicator.stop_maintenance_mode() if self._maintenance_timeout_timer is not None: self._maintenance_timeout_timer.cancel() self._maintenance_timeout_timer = None if self._deactivated_sent is False: master_event = MasterEvent(MasterEvent.Types.MAINTENANCE_EXIT, {}) self._pubsub.publish_master_event(PubSub.MasterTopics.MAINTENANCE, master_event) self._deactivated_sent = True def _check_maintenance_timeout(self): # type: () -> None """ Checks if the maintenance if the timeout is exceeded, and closes maintenance mode if required. """ timeout = MaintenanceClassicCommunicator.MAINTENANCE_TIMEOUT if self._master_communicator.in_maintenance_mode(): current_time = time.time() if self._last_maintenance_send_time + timeout < current_time: logger.info('Stopping maintenance mode because of timeout.') self.deactivate() else: wait_time = self._last_maintenance_send_time + timeout - current_time self._maintenance_timeout_timer = Timer(wait_time, self._check_maintenance_timeout) self._maintenance_timeout_timer.start() else: self.deactivate() def write(self, message): # type: (str) -> None self._last_maintenance_send_time = time.time() data = '{0}\r\n'.format(message.strip()).encode() self._master_communicator.send_maintenance_data(bytearray(data)) def _read_data(self): # type: () -> None """ Reads from the serial port and writes to the socket. """ buffer = '' while not self._stopped: try: data = self._master_communicator.get_maintenance_data() if data is None: continue buffer += data.decode() while '\n' in buffer: message, buffer = buffer.split('\n', 1) if self._receiver_callback is not None: try: self._receiver_callback(message.rstrip()) except Exception: logger.exception('Unexpected exception during maintenance callback') except Exception: logger.exception('Exception in maintenance read thread') break self.deactivate(join=False)
class MaintenanceController(object): SOCKET_TIMEOUT = 60 @Inject def __init__(self, maintenance_communicator=INJECTED, ssl_private_key=INJECTED, ssl_certificate=INJECTED): """ :type maintenance_communicator: gateway.maintenance_communicator.MaintenanceCommunicator """ self._consumers = {} self._privatekey_filename = ssl_private_key self._certificate_filename = ssl_certificate self._maintenance_communicator = maintenance_communicator if self._maintenance_communicator: self._maintenance_communicator.set_receiver(self._received_data) self._connection = None self._server_thread = None ####################### # Internal management # ####################### def start(self): # type: () -> None if self._maintenance_communicator: self._maintenance_communicator.start() def stop(self): # type: () -> None if self._maintenance_communicator: self._maintenance_communicator.stop() def _received_data(self, message): try: if self._connection is not None: self._connection.sendall('{0}\n'.format(message.rstrip())) except Exception: logger.exception( 'Exception forwarding maintenance data to socket connection.') for consumer_id, callback in self._consumers.items(): try: callback(message.rstrip()) except Exception: logger.exception( 'Exception forwarding maintenance data to consumer %s', str(consumer_id)) def _activate(self): if not self._maintenance_communicator.is_active(): self._maintenance_communicator.activate() def _deactivate(self): if self._maintenance_communicator.is_active(): self._maintenance_communicator.deactivate() ################# # Subscriptions # ################# def add_consumer(self, consumer_id, callback): self._consumers[consumer_id] = callback self._activate() def remove_consumer(self, consumer_id): self._consumers.pop(consumer_id, None) if not self._consumers: logger.info('Stopping maintenance mode due to no consumers.') self._deactivate() ########## # Socket # ########## def open_maintenace_socket(self): """ Opens a TCP/SSL socket, connecting it with the maintenance service """ port = random.randint(6000, 7000) self._server_thread = BaseThread(name='maintenancesock', target=self._run_socket_server, args=[port]) self._server_thread.daemon = True self._server_thread.start() return port def _run_socket_server(self, port): connection_timeout = MaintenanceController.SOCKET_TIMEOUT logger.info('Starting maintenance socket on port %s', port) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.settimeout(connection_timeout) sock = System.get_ssl_socket( sock, private_key_filename=self._privatekey_filename, certificate_filename=self._certificate_filename) sock.bind(('', port)) sock.listen(1) try: logger.info('Waiting for maintenance connection.') self._connection, address = sock.accept() logger.info('Maintenance connection from %s', str(address)) self._handle_connection() logger.info( 'Maintenance session ended, closing maintenance socket') sock.close() except socket.timeout: logger.info('Maintenance socket timed out, closing.') sock.close() except Exception: logger.exception('Error in maintenance service') sock.close() def _handle_connection(self): """ Handles one incoming connection. """ try: self._connection.settimeout(1) self._connection.sendall( 'Activating maintenance mode, waiting for other actions to complete ...\n' ) self._activate() self._connection.sendall('Connected\n') while self._maintenance_communicator.is_active(): try: try: data = self._connection.recv(1024) if not data: logger.info( 'Stopping maintenance mode due to no data.') break if data.startswith('exit'): logger.info( 'Stopping maintenance mode due to exit.') break self._maintenance_communicator.write(data) except Exception as exception: if System.handle_socket_exception( self._connection, exception, logger): continue else: logger.exception( 'Unexpected exception receiving connection data' ) break except Exception: logger.exception('Exception in maintenance mode') break except InMaintenanceModeException: self._connection.sendall('Maintenance mode already active.\n') finally: self._deactivate() logger.info('Maintenance mode deactivated') self._connection.close() self._connection = None ####### # I/O # ####### def write(self, message): self._maintenance_communicator.write(message)
class CoreCommunicator(object): """ Uses a serial port to communicate with the Core and updates the output state. Provides methods to send CoreCommands. """ # Message constants. There are here for better code readability, you can't just simply change them START_OF_REQUEST = bytearray(b'STR') END_OF_REQUEST = bytearray(b'\r\n\r\n') START_OF_REPLY = bytearray(b'RTR') END_OF_REPLY = bytearray(b'\r\n') @Inject def __init__(self, controller_serial=INJECTED): # type: (Serial) -> None self._verbose = logger.level >= logging.DEBUG self._serial = controller_serial self._serial_write_lock = Lock() self._cid_lock = Lock() self._serial_bytes_written = 0 self._serial_bytes_read = 0 self._cid = None # type: Optional[int] # Reserved CIDs: 0 = Core events, 1 = uCAN transport, 2 = Slave transport self._cids_in_use = set() # type: Set[int] self._consumers = { } # type: Dict[int, List[Union[Consumer, BackgroundConsumer]]] self._last_success = 0.0 self._stop = False self._word_helper = WordField('') self._read_thread = None # type: Optional[BaseThread] self._command_total_histogram = Counter() # type: Counter self._command_success_histogram = Counter() # type: Counter self._command_timeout_histogram = Counter() # type: Counter self._communication_stats = { 'calls_succeeded': [], 'calls_timedout': [], 'bytes_written': 0, 'bytes_read': 0 } # type: Dict[str,Any] self._debug_buffer = { 'read': {}, 'write': {} } # type: Dict[str, Dict[float, bytearray]] self._debug_buffer_duration = 300 def start(self): """ Start the CoreComunicator, this starts the background read thread. """ self._stop = False self._read_thread = BaseThread(name='coreread', target=self._read) self._read_thread.setDaemon(True) self._read_thread.start() def stop(self): self._stop = True if self._read_thread is not None: self._read_thread.join() self._read_thread = None def get_communication_statistics(self): return self._communication_stats def reset_communication_statistics(self): self._communication_stats = { 'calls_succeeded': [], 'calls_timedout': [], 'bytes_written': 0, 'bytes_read': 0 } def get_command_histograms(self): return { 'total': dict(self._command_total_histogram), 'success': dict(self._command_success_histogram), 'timeout': dict(self._command_timeout_histogram) } def reset_command_histograms(self): self._command_total_histogram.clear() self._command_success_histogram.clear() self._command_timeout_histogram.clear() def get_debug_buffer(self): # type: () -> Dict[str,Dict[float,str]] def process(buffer): return {k: printable(v) for k, v in six.iteritems(buffer)} return { 'read': process(self._debug_buffer['read']), 'write': process(self._debug_buffer['write']) } def get_seconds_since_last_success(self): # type: () -> float """ Get the number of seconds since the last successful communication. """ if self._last_success == 0: return 0.0 # No communication - return 0 sec since last success else: return time.time() - self._last_success def _get_cid(self): # type: () -> int """ Get a communication id. 0 and 1 are reserved. """ def _increment_cid(current_cid): # type: (Optional[int]) -> int # Reserved CIDs: 0 = Core events, 1 = uCAN transport, 2 = Slave transport return current_cid + 1 if (current_cid is not None and current_cid < 255) else 3 def _available(candidate_cid): # type: (Optional[int]) -> bool if candidate_cid is None: return False if candidate_cid == self._cid: return False if candidate_cid in self._cids_in_use: return False return True with self._cid_lock: cid = self._cid # type: Optional[int] # Initial value while not _available(cid): cid = _increment_cid(cid) if cid == self._cid: # Seems there is no CID available at this moment raise RuntimeError('No available CID') if cid is None: # This is impossible due to `_available`, but mypy doesn't know that raise RuntimeError('CID should not be None') self._cid = cid self._cids_in_use.add(cid) return cid def _write_to_serial(self, data): # type: (bytearray) -> None """ Write data to the serial port. :param data: the data to write """ with self._serial_write_lock: if self._verbose: logger.debug('Writing to Core serial: {0}'.format( printable(data))) threshold = time.time() - self._debug_buffer_duration self._debug_buffer['write'][time.time()] = data for t in self._debug_buffer['write'].keys(): if t < threshold: del self._debug_buffer['write'][t] self._serial.write(data) self._serial_bytes_written += len(data) self._communication_stats['bytes_written'] += len(data) def register_consumer( self, consumer): # type: (Union[Consumer, BackgroundConsumer]) -> None """ Register a consumer :param consumer: The consumer to register. """ self._consumers.setdefault(consumer.get_hash(), []).append(consumer) def discard_cid(self, cid): # type: (int) -> None """ Discards a Command ID. """ with self._cid_lock: self._cids_in_use.discard(cid) def unregister_consumer( self, consumer): # type: (Union[Consumer, BackgroundConsumer]) -> None """ Unregister a consumer """ consumers = self._consumers.get(consumer.get_hash(), []) if consumer in consumers: consumers.remove(consumer) self.discard_cid(consumer.cid) def do_basic_action(self, action_type, action, device_nr=0, extra_parameter=0, timeout=2, log=True): # type: (int, int, int, int, Optional[int], bool) -> Optional[Dict[str, Any]] """ Sends a basic action to the Core with the given action type and action number """ if log: logger.info('BA: Execute {0} {1} {2} {3}'.format( action_type, action, device_nr, extra_parameter)) return self.do_command(CoreAPI.basic_action(), { 'type': action_type, 'action': action, 'device_nr': device_nr, 'extra_parameter': extra_parameter }, timeout=timeout) def do_command(self, command, fields, timeout=2): # type: (CoreCommandSpec, Dict[str, Any], Union[T_co, int]) -> Union[T_co, Dict[str, Any]] """ Send a command over the serial port and block until an answer is received. If the Core does not respond within the timeout period, a CommunicationTimedOutException is raised :param command: specification of the command to execute :param fields: A dictionary with the command input field values :param timeout: maximum allowed time before a CommunicationTimedOutException is raised """ cid = self._get_cid() consumer = Consumer(command, cid) command = consumer.command try: self._command_total_histogram.update({str(command.instruction): 1}) self._consumers.setdefault(consumer.get_hash(), []).append(consumer) self._send_command(cid, command, fields) except Exception: self.discard_cid(cid) raise try: result = None # type: Any if isinstance(consumer, Consumer) and timeout is not None: result = consumer.get(timeout) self._last_success = time.time() self._communication_stats['calls_succeeded'].append(time.time()) self._communication_stats[ 'calls_succeeded'] = self._communication_stats[ 'calls_succeeded'][-50:] self._command_success_histogram.update( {str(command.instruction): 1}) return result except CommunicationTimedOutException: self.unregister_consumer(consumer) self._communication_stats['calls_timedout'].append(time.time()) self._communication_stats[ 'calls_timedout'] = self._communication_stats[ 'calls_timedout'][-50:] self._command_timeout_histogram.update( {str(command.instruction): 1}) raise def _send_command( self, cid, command, fields): # type: (int, CoreCommandSpec, Dict[str, Any]) -> None """ Send a command over the serial port :param cid: The command ID :param command: The Core CommandSpec :param fields: A dictionary with the command input field values """ payload = command.create_request_payload(fields) checked_payload = (bytearray([cid]) + command.instruction + self._word_helper.encode(len(payload)) + payload) data = (CoreCommunicator.START_OF_REQUEST + checked_payload + bytearray(b'C') + CoreCommunicator._calculate_crc(checked_payload) + CoreCommunicator.END_OF_REQUEST) self._write_to_serial(data) @staticmethod def _calculate_crc(data): # type: (bytearray) -> bytearray """ Calculate the CRC of the data. :param data: Data for which to calculate the CRC :returns: CRC """ crc = 0 for byte in data: crc += byte return bytearray([crc % 256]) def _read(self): """ Code for the background read thread: reads from the serial port and forward certain messages to waiting consumers Request format: 'STR' + {CID, 1 byte} + {command, 2 bytes} + {length, 2 bytes} + {payload, `length` bytes} + 'C' + {checksum, 1 byte} + '\r\n\r\n' Response format: 'RTR' + {CID, 1 byte} + {command, 2 bytes} + {length, 2 bytes} + {payload, `length` bytes} + 'C' + {checksum, 1 byte} + '\r\n' """ data = bytearray() message_length = None header_fields = None header_length = len( CoreCommunicator.START_OF_REPLY ) + 1 + 2 + 2 # RTR + CID (1 byte) + command (2 bytes) + length (2 bytes) footer_length = 1 + 1 + len( CoreCommunicator.END_OF_REPLY) # 'C' + checksum (1 byte) + \r\n need_more_data = False while not self._stop: try: # Wait for data if more data is expected if need_more_data: readers, _, _ = select.select([self._serial], [], [], 1) if not readers: continue need_more_data = False # Read what's now on the serial port num_bytes = self._serial.inWaiting() if num_bytes > 0: data += self._serial.read(num_bytes) # Update counters self._serial_bytes_read += num_bytes self._communication_stats['bytes_read'] += num_bytes # Wait for the full message, or the header length min_length = message_length or header_length if len(data) < min_length: need_more_data = True continue if message_length is None: # Check if the data contains the START_OF_REPLY if CoreCommunicator.START_OF_REPLY not in data: need_more_data = True continue # Align with START_OF_REPLY if not data.startswith(CoreCommunicator.START_OF_REPLY): data = CoreCommunicator.START_OF_REPLY + data.split( CoreCommunicator.START_OF_REPLY, 1)[-1] if len(data) < header_length: continue header_fields = CoreCommunicator._parse_header(data) message_length = header_fields[ 'length'] + header_length + footer_length # If not all data is present, wait for more data if len(data) < message_length: continue message = data[:message_length] # type: bytearray data = data[message_length:] # A possible message is received, log where appropriate if self._verbose: logger.debug('Reading from Core serial: {0}'.format( printable(message))) threshold = time.time() - self._debug_buffer_duration self._debug_buffer['read'][time.time()] = message for t in self._debug_buffer['read'].keys(): if t < threshold: del self._debug_buffer['read'][t] # Validate message boundaries correct_boundaries = message.startswith( CoreCommunicator.START_OF_REPLY) and message.endswith( CoreCommunicator.END_OF_REPLY) if not correct_boundaries: logger.info('Unexpected boundaries: {0}'.format( printable(message))) # Reset, so we'll wait for the next RTR message_length = None data = message[ 3:] + data # Strip the START_OF_REPLY, and restore full data continue # Validate message CRC crc = bytearray([message[-3]]) payload = message[8:-4] # type: bytearray checked_payload = message[3:-4] # type: bytearray expected_crc = CoreCommunicator._calculate_crc(checked_payload) if crc != expected_crc: logger.info( 'Unexpected CRC ({0} vs expected {1}): {2}'.format( crc, expected_crc, printable(checked_payload))) # Reset, so we'll wait for the next RTR message_length = None data = message[ 3:] + data # Strip the START_OF_REPLY, and restore full data continue # A valid message is received, reliver it to the correct consumer consumers = self._consumers.get(header_fields['hash'], []) for consumer in consumers[:]: if self._verbose: logger.debug( 'Delivering payload to consumer {0}.{1}: {2}'. format(header_fields['command'], header_fields['cid'], printable(payload))) consumer.consume(payload) if isinstance(consumer, Consumer): self.unregister_consumer(consumer) self.discard_cid(header_fields['cid']) # Message processed, cleaning up message_length = None except Exception: logger.exception('Unexpected exception at Core read thread') data = bytearray() message_length = None @staticmethod def _parse_header( data): # type: (bytearray) -> Dict[str, Union[int, bytearray]] base = len(CoreCommunicator.START_OF_REPLY) return { 'cid': data[base], 'command': data[base + 1:base + 3], 'hash': Toolbox.hash(data[:base + 3]), 'length': struct.unpack('>H', data[base + 3:base + 5])[0] }
class PowerCommunicator(object): """ Uses a serial port to communicate with the power modules. """ @Inject def __init__(self, power_serial=INJECTED, power_store=INJECTED, pubsub=INJECTED, time_keeper_period=60, address_mode_timeout=300): # type: (RS485, PowerStore, PubSub, int, int) -> None """ Default constructor. :param power_serial: Serial port to communicate with :type power_serial: Instance of :class`RS485` :param verbose: Print all serial communication to stdout. """ self.__verbose = logger.level >= logging.DEBUG self.__serial = power_serial self.__serial_lock = RLock() self.__cid = 1 self.__address_mode = False self.__address_mode_stop = False self.__address_thread = None # type: Optional[Thread] self.__address_mode_timeout = address_mode_timeout self.__power_store = power_store self.__pubsub = pubsub self.__last_success = 0 # type: float if time_keeper_period != 0: self.__time_keeper = TimeKeeper( self, power_store, time_keeper_period) # type: Optional[TimeKeeper] else: self.__time_keeper = None self.__communication_stats_calls = { 'calls_succeeded': [], 'calls_timedout': [] } # type: Dict[str, List] self.__communication_stats_bytes = { 'bytes_written': 0, 'bytes_read': 0 } # type: Dict[str, int] self.__debug_buffer = { 'read': {}, 'write': {} } # type: Dict[str,Dict[float,str]] self.__debug_buffer_duration = 300 def start(self): # type: () -> None """ Start the power communicator. """ if self.__time_keeper is not None: self.__time_keeper.start() def stop(self): # type: () -> None if self.__time_keeper is not None: self.__time_keeper.stop() def get_communication_statistics(self): # type: () -> Dict[str, Any] ret = {} # type: Dict[str, Any] ret.update(self.__communication_stats_calls) ret.update(self.__communication_stats_bytes) return ret def reset_communication_statistics(self): # type: () -> None self.__communication_stats_calls = { 'calls_succeeded': [], 'calls_timedout': [] } self.__communication_stats_bytes = { 'bytes_written': 0, 'bytes_read': 0 } def get_communicator_health(self): # type: () -> HEALTH stats = self.get_communication_statistics() calls_timedout = [call for call in stats['calls_timedout']] calls_succeeded = [call for call in stats['calls_succeeded']] if len(calls_timedout) == 0: # If there are no timeouts at all return CommunicationStatus.SUCCESS all_calls = sorted(calls_timedout + calls_succeeded) if len(all_calls) <= 10: # Not enough calls made to have a decent view on what's going on logger.warning( 'Observed energy communication failures, but not enough calls') return CommunicationStatus.UNSTABLE if not any(t in calls_timedout for t in all_calls[-10:]): logger.warning( 'Observed energy communication failures, but recent calls recovered' ) # The last X calls are successfull return CommunicationStatus.UNSTABLE calls_last_x_minutes = [t for t in all_calls if t > time.time() - 180] if len(calls_last_x_minutes) <= 5: logger.warning( 'Observed energy communication failures, but not recent enough' ) # Not enough recent calls return CommunicationStatus.UNSTABLE ratio = len([t for t in calls_last_x_minutes if t in calls_timedout ]) / float(len(calls_last_x_minutes)) if ratio < 0.25: # Less than 25% of the calls fail, let's assume everything is just "fine" logger.warning( 'Observed energy communication failures, but there\'s only a failure ratio of {:.2f}%' .format(ratio * 100)) return CommunicationStatus.UNSTABLE else: return CommunicationStatus.FAILURE def get_debug_buffer(self): # type: () -> Dict[str, Dict[Any, Any]] return self.__debug_buffer def get_seconds_since_last_success(self): # type: () -> float """ Get the number of seconds since the last successful communication. """ if self.__last_success == 0: return 0 # No communication - return 0 sec since last success else: return time.time() - self.__last_success def __get_cid(self): # type: () -> int """ Get a communication id """ (ret, self.__cid) = (self.__cid, (self.__cid % 255) + 1) return ret def __debug(self, action, data): # type: (str, Optional[bytearray]) -> None if self.__verbose and data is not None: logger.debug("%.3f %s power: %s" % (time.time(), action, printable(data))) def __write_to_serial(self, data): # type: (bytearray) -> None """ Write data to the serial port. :param data: the data to write """ self.__debug('writing to', data) self.__serial.write(data) self.__communication_stats_bytes['bytes_written'] += len(data) threshold = time.time() - self.__debug_buffer_duration self.__debug_buffer['write'][time.time()] = printable(data) for t in self.__debug_buffer['write'].keys(): if t < threshold: del self.__debug_buffer['write'][t] def do_command(self, address, cmd, *data): # type: (int, PowerCommand, DataType) -> Tuple[Any, ...] """ Send a command over the serial port and block until an answer is received. If the power module does not respond within the timeout period, a CommunicationTimedOutException is raised. :param address: Address of the power module :type address: 2 bytes string :param cmd: the command to execute :param data: data for the command :raises: :class`CommunicationTimedOutException` if power module did not respond in time :raises: :class`InAddressModeException` if communicator is in address mode :returns: dict containing the output fields of the command """ if self.__address_mode: raise InAddressModeException() def do_once(_address, _cmd, *_data): # type: (int, PowerCommand, DataType) -> Tuple[Any, ...] """ Send the command once. """ try: cid = self.__get_cid() send_data = _cmd.create_input(_address, cid, *_data) self.__write_to_serial(send_data) if _address == power_api.BROADCAST_ADDRESS: self.__communication_stats_calls['calls_succeeded'].append( time.time()) self.__communication_stats_calls[ 'calls_succeeded'] = self.__communication_stats_calls[ 'calls_succeeded'][-50:] return () # No reply on broadcast messages ! else: tries = 0 while True: # In this loop we might receive data that didn't match the expected header. This might happen # if we for some reason had a timeout on the previous call, and we now read the response # to that call. In this case, we just re-try (up to 3 times), as the correct data might be # next in line. header, response_data = self.__read_from_serial() if not _cmd.check_header(header, _address, cid): if _cmd.is_nack( header, _address, cid) and response_data == bytearray([2]): raise UnkownCommandException('Unknown command') tries += 1 logger.warning( "Header did not match command ({0})".format( tries)) if tries == 3: raise Exception( "Header did not match command ({0})". format(tries)) else: break self.__last_success = time.time() return_data = _cmd.read_output(response_data) self.__communication_stats_calls['calls_succeeded'].append( time.time()) self.__communication_stats_calls[ 'calls_succeeded'] = self.__communication_stats_calls[ 'calls_succeeded'][-50:] return return_data except CommunicationTimedOutException: self.__communication_stats_calls['calls_timedout'].append( time.time()) self.__communication_stats_calls[ 'calls_timedout'] = self.__communication_stats_calls[ 'calls_timedout'][-50:] raise with self.__serial_lock: try: return do_once(address, cmd, *data) except UnkownCommandException: # This happens when the module is stuck in the bootloader. logger.error("Got UnkownCommandException") do_once(address, power_api.bootloader_jump_application()) time.sleep(1) return self.do_command(address, cmd, *data) except CommunicationTimedOutException: # Communication timed out, try again. return do_once(address, cmd, *data) except Exception as ex: logger.exception("Unexpected error: {0}".format(ex)) time.sleep(0.25) return do_once(address, cmd, *data) def start_address_mode(self): # type: () -> None """ Start address mode. :raises: :class`InAddressModeException` if communicator is in maintenance mode. """ if self.__address_mode: raise InAddressModeException() self.__address_mode = True self.__address_mode_stop = False with self.__serial_lock: self.__address_thread = BaseThread(name='poweraddressmode', target=self.__do_address_mode) self.__address_thread.daemon = True self.__address_thread.start() def __do_address_mode(self): # type: () -> None """ This code is running in a thread when in address mode. """ if self.__power_store is None: self.__address_mode = False self.__address_thread = None return expire = time.time() + self.__address_mode_timeout address_mode = power_api.set_addressmode(power_api.ENERGY_MODULE) address_mode_p1c = power_api.set_addressmode(power_api.P1_CONCENTRATOR) want_an_address_8 = power_api.want_an_address(power_api.POWER_MODULE) want_an_address_12 = power_api.want_an_address(power_api.ENERGY_MODULE) want_an_address_p1c = power_api.want_an_address( power_api.P1_CONCENTRATOR) set_address = power_api.set_address(power_api.ENERGY_MODULE) set_address_p1c = power_api.set_address(power_api.P1_CONCENTRATOR) # AGT start data = address_mode.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.ADDRESS_MODE) self.__write_to_serial(data) data = address_mode_p1c.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.ADDRESS_MODE) self.__write_to_serial(data) # Wait for WAA and answer. while not self.__address_mode_stop and time.time() < expire: try: header, data = self.__read_from_serial() if set_address.check_header_partial( header) or set_address_p1c.check_header_partial( header): continue version = None if want_an_address_8.check_header_partial(header): version = power_api.POWER_MODULE elif want_an_address_12.check_header_partial(header): version = power_api.ENERGY_MODULE elif want_an_address_p1c.check_header_partial(header): version = power_api.P1_CONCENTRATOR if version is None: logger.warning( "Received unexpected message in address mode") else: (old_address, cid) = (header[:2][1], header[2:3]) # Ask power_controller for new address, and register it. new_address = self.__power_store.get_free_address() if self.__power_store.module_exists(old_address): self.__power_store.readdress_power_module( old_address, new_address) else: self.__power_store.register_power_module( new_address, version) # Send new address to module if version == power_api.P1_CONCENTRATOR: address_data = set_address_p1c.create_input( old_address, ord(cid), new_address) else: # Both power- and energy module share the same API address_data = set_address.create_input( old_address, ord(cid), new_address) self.__write_to_serial(address_data) except CommunicationTimedOutException: pass # Didn't receive a command, no problem. except Exception as exception: logger.exception("Got exception in address mode: %s", exception) # AGT stop data = address_mode.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.NORMAL_MODE) self.__write_to_serial(data) data = address_mode_p1c.create_input(power_api.BROADCAST_ADDRESS, self.__get_cid(), power_api.NORMAL_MODE) self.__write_to_serial(data) self.__address_mode = False def stop_address_mode(self): # type: () -> None """ Stop address mode. """ if not self.__address_mode: raise Exception("Not in address mode !") self.__address_mode_stop = True if self.__address_thread: self.__address_thread.join() self.__address_thread = None master_event = MasterEvent(MasterEvent.Types.POWER_ADDRESS_EXIT, {}) self.__pubsub.publish_master_event(PubSub.MasterTopics.POWER, master_event) def in_address_mode(self): # type: () -> bool """ Returns whether the PowerCommunicator is in address mode. """ return self.__address_mode def __read_from_serial(self): # type: () -> Tuple[bytearray, bytearray] """ Read a PowerCommand from the serial port. """ phase = 0 index = 0 header = bytearray() length = 0 data = bytearray() crc = 0 command = bytearray() try: while phase < 8: byte = self.__serial.read_queue.get(True, 0.25) command += byte self.__communication_stats_bytes['bytes_read'] += 1 if phase == 0: # Skip non 'R' bytes if byte == bytearray(b'R'): phase = 1 else: phase = 0 elif phase == 1: # Expect 'T' if byte == bytearray(b'T'): phase = 2 else: raise Exception("Unexpected character") elif phase == 2: # Expect 'R' if byte == bytearray(b'R'): phase = 3 index = 0 else: raise Exception("Unexpected character") elif phase == 3: # Read the header fields header += byte index += 1 if index == 8: length = ord(byte) if length > 0: phase = 4 index = 0 else: phase = 5 elif phase == 4: # Read the data data += byte index += 1 if index == length: phase = 5 elif phase == 5: # Read the CRC code crc = ord(byte) phase = 6 elif phase == 6: # Expect '\r' if byte == bytearray(b'\r'): phase = 7 else: raise Exception("Unexpected character") elif phase == 7: # Expect '\n' if byte == bytearray(b'\n'): phase = 8 else: raise Exception("Unexpected character") if PowerCommand.get_crc(header, data) != crc: raise Exception('CRC doesn\'t match') except Empty: raise CommunicationTimedOutException('Communication timed out') except Exception: self.__debug('reading from', command) raise finally: self.__debug('reading from', command) threshold = time.time() - self.__debug_buffer_duration self.__debug_buffer['read'][time.time()] = printable(command) for t in self.__debug_buffer['read'].keys(): if t < threshold: del self.__debug_buffer['read'][t] return header, data
def start(self): """ Start the CoreComunicator, this starts the background read thread. """ self._stop = False self._read_thread = BaseThread(name='coreread', target=self._read) self._read_thread.setDaemon(True) self._read_thread.start()
def start(self): self._stopped = False self._read_data_thread = BaseThread(name='maintenanceread', target=self._read_data) self._read_data_thread.daemon = True self._read_data_thread.start()