예제 #1
0
 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)
예제 #2
0
 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)
예제 #3
0
    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)
예제 #4
0
    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)
예제 #5
0
    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)
예제 #6
0
 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.")
예제 #7
0
 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.")
예제 #8
0
                        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]]:
예제 #9
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)