Esempio n. 1
0
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)
Esempio n. 2
0
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())
Esempio n. 3
0
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]
        }