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 __write_to_serial(self, data): """ Write data to the serial port. :param data: the data to write :type data: string """ with self.__serial_write_lock: if self.__verbose: logger.info('Writing to Master serial: {0}'.format(printable(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] self.__serial.write(data) self.__communication_stats['bytes_written'] += len(data)
def __write_to_serial(self, data): """ Write data to the serial port. :param data: the data to write :type data: string """ with self.__serial_write_lock: if self.__verbose: print "%.3f writing to serial: %s" % (time.time(), printable(data)) self.__serial.write(data) self.__serial_bytes_written += len(data)
def __write_to_serial(self, data): """ Write data to the serial port. :param data: the data to write :type data: string """ with self.__serial_write_lock: if self.__verbose: LOGGER.info('Writing to Master serial: {0}'.format( printable(data))) self.__serial.write(data) self.__serial_bytes_written += len(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.")
return "" read_state = ReadState() data = "" while not self.__stop: data += self.__serial.read(1) num_bytes = self.__serial.inWaiting() if num_bytes > 0: data += self.__serial.read(num_bytes) if data != None and len(data) > 0: self.__serial_bytes_read += (1 + num_bytes) if self.__verbose: print "%.3f read from serial: %s" % (time.time(), printable(data)) if read_state.should_resume(): data = read_state.consume(data) # No else here: data might not be empty when current_consumer is done if read_state.should_find_consumer(): start_bytes = self.__get_start_bytes() leftovers = "" # for unconsumed bytes; these will go to the passthrough. while len(data) > 0: if data[0] in start_bytes: # Prefixes are 3 bytes, make sure we have enough data to match if len(data) >= 3: match = False for consumer in start_bytes[data[0]]:
class MasterCommunicator(object): """ Uses a serial port to communicate with the master and updates the output state. Provides methods to send MasterCommands, Passthrough and Maintenance. """ @Inject def __init__(self, controller_serial=INJECTED, init_master=True, verbose=False, passthrough_timeout=0.2): """ :param controller_serial: Serial port to communicate with :type controller_serial: Instance of :class`serial.Serial` :param init_master: Send an initialization sequence to the master to make sure we are in CLI mode. This can be turned of for testing. :type init_master: boolean. :param verbose: Print all serial communication to stdout. :type verbose: boolean. :param passthrough_timeout: The time to wait for an answer on a passthrough message (in sec) :type passthrough_timeout: float. """ self.__init_master = init_master self.__verbose = verbose self.__serial = controller_serial self.__serial_write_lock = Lock() self.__command_lock = Lock() self.__cid = 1 self.__maintenance_mode = False self.__maintenance_queue = Queue() self.__consumers = [] self.__passthrough_enabled = False self.__passthrough_mode = False self.__passthrough_timeout = passthrough_timeout self.__passthrough_queue = Queue() self.__passthrough_done = Event() self.__last_success = 0 self.__running = False self.__read_thread = Thread(target=self.__read, name="MasterCommunicator read thread") self.__read_thread.daemon = True self.__communication_stats = {'calls_succeeded': [], 'calls_timedout': [], 'bytes_written': 0, 'bytes_read': 0} self.__debug_buffer = {'read': {}, 'write': {}} self.__debug_buffer_duration = 300 def start(self): """ Start the MasterComunicator, this starts the background read thread. """ if self.__init_master: def flush_serial_input(): """ Try to read from the serial input and discard the bytes read. """ i = 0 data = self.__serial.read(1) while len(data) > 0 and i < 100: data = self.__serial.read(1) i += 1 self.__serial.timeout = 1 self.__serial.write(" "*18 + "\r\n") flush_serial_input() self.__serial.write("exit\r\n") flush_serial_input() self.__serial.write(" "*10) flush_serial_input() self.__serial.timeout = None if not self.__running: self.__running = True self.__read_thread.start() def stop(self): pass # Not supported/used def enable_passthrough(self): self.__passthrough_enabled = True def get_communication_statistics(self): return self.__communication_stats def get_debug_buffer(self): return self.__debug_buffer def get_seconds_since_last_success(self): """ 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): """ Get a communication id """ (ret, self.__cid) = (self.__cid, (self.__cid % 255) + 1) return ret def __write_to_serial(self, data): """ Write data to the serial port. :param data: the data to write :type data: string """ with self.__serial_write_lock: if self.__verbose: logger.info('Writing to Master serial: {0}'.format(printable(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] self.__serial.write(data) self.__communication_stats['bytes_written'] += len(data) def register_consumer(self, consumer): """ Register a customer consumer with the communicator. An instance of :class`Consumer` will be removed when consumption is done. An instance of :class`BackgroundConsumer` stays active and is thus able to consume multiple messages. :param consumer: The consumer to register. :type consumer: Consumer or BackgroundConsumer. """ self.__consumers.append(consumer) def do_basic_action(self, action_type, action_number): """ Sends a basic action to the master with the given action type and action number :param action_type: The action type to execute :type action_type: int :param action_number: The action number to execute :type action_number: int :raises: :class`CommunicationTimedOutException` if master did not respond in time :raises: :class`InMaintenanceModeException` if master is in maintenance mode :returns: dict containing the output fields of the command """ logger.info('BA: Execute {0} {1}'.format(action_type, action_number)) return self.do_command( master_api.basic_action(), {'action_type': action_type, 'action_number': action_number} ) def do_command(self, cmd, fields=None, timeout=2, extended_crc=False): """ Send a command over the serial port and block until an answer is received. If the master does not respond within the timeout period, a CommunicationTimedOutException is raised :param cmd: specification of the command to execute :type cmd: :class`MasterCommand.MasterCommandSpec` :param fields: an instance of one of the available fields :type fields :class`MasterCommand.FieldX` :param timeout: maximum allowed time before a CommunicationTimedOutException is raised :type timeout: int :raises: :class`CommunicationTimedOutException` if master did not respond in time :raises: :class`InMaintenanceModeException` if master is in maintenance mode :returns: dict containing the output fields of the command """ if self.__maintenance_mode: raise InMaintenanceModeException() if fields is None: fields = dict() cid = self.__get_cid() consumer = Consumer(cmd, cid) inp = cmd.create_input(cid, fields, extended_crc) with self.__command_lock: self.__consumers.append(consumer) self.__write_to_serial(inp) try: result = consumer.get(timeout).fields if cmd.output_has_crc() and not MasterCommunicator.__check_crc(cmd, result, extended_crc): raise CrcCheckFailedException() else: self.__last_success = time.time() self.__communication_stats['calls_succeeded'].append(time.time()) self.__communication_stats['calls_succeeded'] = self.__communication_stats['calls_succeeded'][-50:] return result except CommunicationTimedOutException: self.__communication_stats['calls_timedout'].append(time.time()) self.__communication_stats['calls_timedout'] = self.__communication_stats['calls_timedout'][-50:] raise @staticmethod def __check_crc(cmd, result, extended_crc=False): """ Calculate the CRC of the data for a certain master command. :param cmd: instance of MasterCommandSpec. :param result: A dict containing the result of the master command. :param extended_crc: Indicates whether the action should be included in the crc :returns: boolean """ crc = 0 if extended_crc: crc += ord(cmd.action[0]) crc += ord(cmd.action[1]) for field in cmd.output_fields: if Field.is_crc(field): break else: for byte in field.encode(result[field.name]): crc += ord(byte) return result['crc'] == [67, (crc / 256), (crc % 256)] def __passthrough_wait(self): """ Waits until the passthrough is done or a timeout is reached. """ if not self.__passthrough_done.wait(self.__passthrough_timeout): logger.info("Timed out on passthrough message") self.__passthrough_mode = False self.__command_lock.release() def __push_passthrough_data(self, data): if self.__passthrough_enabled: self.__passthrough_queue.put(data) 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 = Thread(target=self.__passthrough_wait) passthrough_thread.daemon = True passthrough_thread.start() self.__write_to_serial(data) def get_passthrough_data(self): """ Get data that wasn't consumed by do_command. Blocks if no data available or in maintenance mode. :returns: string containing unprocessed output """ data = self.__passthrough_queue.get() if data[-4:] == '\r\n\r\n': self.__passthrough_done.set() return data def start_maintenance_mode(self): """ Start maintenance mode. :raises: :class`InMaintenanceModeException` if master is in maintenance mode. """ if self.__maintenance_mode: raise InMaintenanceModeException() self.__maintenance_queue.clear() self.__maintenance_mode = True self.send_maintenance_data(master_api.to_cli_mode().create_input(0)) def send_maintenance_data(self, data): """ Send data to the master in maintenance mode. :param data: data to send to the master :type data: string """ if not self.__maintenance_mode: raise Exception("Not in maintenance mode !") self.__write_to_serial(data) def get_maintenance_data(self): """ Get data from the master in maintenance mode. :returns: string containing unprocessed output """ if not self.__maintenance_mode: raise Exception("Not in maintenance mode !") try: return self.__maintenance_queue.get(timeout=1) except Empty: return None def stop_maintenance_mode(self): """ Stop maintenance mode. """ if self.__maintenance_mode: self.send_maintenance_data("exit\r\n") self.__maintenance_mode = False def in_maintenance_mode(self): """ Returns whether the MasterCommunicator is in maintenance mode. """ return self.__maintenance_mode def __get_start_bytes(self): """ Create a dict that maps the start byte to a list of consumers. """ start_bytes = {} for consumer in self.__consumers: start_byte = consumer.get_prefix()[0] if start_byte in start_bytes: start_bytes[start_byte].append(consumer) else: start_bytes[start_byte] = [consumer] return start_bytes def __read(self): """ Code for the background read thread: reads from the serial port, checks if consumers for incoming bytes, if not: put in pass through buffer. """ def consumer_done(_consumer): """ Callback for when consumer is done. ReadState does not access parent directly. """ if isinstance(_consumer, Consumer): self.__consumers.remove(_consumer) elif isinstance(_consumer, BackgroundConsumer) and _consumer.send_to_passthrough: self.__push_passthrough_data(_consumer.last_cmd_data) class ReadState(object): """" The read state keeps track of the current consumer and the partial result for that consumer. """ def __init__(self): self.current_consumer = None self.partial_result = None def should_resume(self): """ Checks whether we should resume consuming data with the current_consumer. """ return self.current_consumer is not None def should_find_consumer(self): """ Checks whether we should find a new consumer. """ return self.current_consumer is None def set_consumer(self, _consumer): """ Set a new consumer. """ self.current_consumer = _consumer self.partial_result = None def consume(self, _data): """ Consume the bytes in data using the current_consumer, and return the bytes that were not used. """ try: bytes_consumed, result, done = read_state.current_consumer.consume(_data, read_state.partial_result) except ValueError, value_error: logger.error('Could not consume/decode message from the master: {0}'.format(value_error)) return '' if done: consumer_done(self.current_consumer) self.current_consumer.deliver(result) self.current_consumer = None self.partial_result = None return _data[bytes_consumed:] self.partial_result = result return '' read_state = ReadState() data = "" while self.__running: data += self.__serial.read(1) num_bytes = self.__serial.inWaiting() if num_bytes > 0: data += self.__serial.read(num_bytes) if data is not None and len(data) > 0: self.__communication_stats['bytes_read'] += (1 + num_bytes) threshold = time.time() - self.__debug_buffer_duration self.__debug_buffer['read'][time.time()] = printable(data) for t in self.__debug_buffer['read'].keys(): if t < threshold: del self.__debug_buffer['read'][t] if self.__verbose: logger.info('Reading from Master serial: {0}'.format(printable(data))) if read_state.should_resume(): data = read_state.consume(data) # No else here: data might not be empty when current_consumer is done if read_state.should_find_consumer(): start_bytes = self.__get_start_bytes() leftovers = "" # for unconsumed bytes; these will go to the passthrough. while len(data) > 0: if data[0] in start_bytes: # Prefixes are 3 bytes, make sure we have enough data to match if len(data) >= 3: match = False for consumer in start_bytes[data[0]]: if data[:3] == consumer.get_prefix(): # Found matching consumer read_state.set_consumer(consumer) data = read_state.consume(data[3:]) # Strip off prefix # Consumers might have changed, update start_bytes start_bytes = self.__get_start_bytes() match = True break if match: continue else: # All commands end with '\r\n', there are no prefixes that start # with \r\n so the last bytes of a command will not get stuck # waiting for the next serial.read() break leftovers += data[0] data = data[1:] if len(leftovers) > 0: if not self.__maintenance_mode: self.__push_passthrough_data(leftovers) else: self.__maintenance_queue.put(leftovers)