Example #1
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, basic_action, timeout=2):
        # type: (BasicAction, Optional[int]) -> Optional[Dict[str, Any]]
        """ Sends a basic action to the Core with the given action type and action number """
        logger.info('BA: Executed {0}'.format(basic_action))
        return self.do_command(
            CoreAPI.basic_action(),
            {'type': basic_action.action_type,
             'action': basic_action.action,
             'device_nr': basic_action.device_nr,
             'extra_parameter': basic_action.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]}
Example #2
0
class GroupActionTest(unittest.TestCase):
    """ Tests for MemoryFile """

    ADDRESS_START_PAGE = 256
    ACTIONS_START_PAGE = 281
    GANAMES_START_PAGE = 261

    @classmethod
    def setUpClass(cls):
        SetTestMode()
        logger = logging.getLogger('openmotics')
        logger.setLevel(logging.DEBUG)
        logger.propagate = False
        handler = logging.StreamHandler()
        handler.setLevel(logging.DEBUG)
        handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
        logger.addHandler(handler)

    def setUp(self):
        self.maxDiff = None
        self._word_helper = WordField('')

    @staticmethod
    def _encode_string(value):
        data = []
        for char in value:
            data.append(ord(char))
        data.append(255)
        return bytearray(data)

    @staticmethod
    def _write(memory_page, start_address, data):
        for i in range(len(data)):
            memory_page[start_address + i] = data[i]

    @staticmethod
    def _get_readable_ba_block(memory):
        entries = []
        for i in range(42):
            action_type = memory[GroupActionTest.ACTIONS_START_PAGE][i * 6]
            if action_type == 255:
                entries.append('_')
            elif action_type >= 100:
                entries.append('X')
            else:
                entries.append(str(action_type))
        return ''.join(entries)

    @staticmethod
    def _setup_master_communicator(memory):
        def _do_command(command, fields, timeout=None):
            _ = timeout
            if command.instruction == 'MW':
                page = fields['page']
                start = fields['start']
                page_data = memory.setdefault(page, [255] * 256)
                for index, data_byte in enumerate(fields['data']):
                    page_data[start + index] = data_byte

        def _do_basic_action(action_type, action, device_nr=0, extra_parameter=0, timeout=2, log=True):
            _ = device_nr, extra_parameter, timeout, log
            if action_type == 200 and action == 1:
                # Send EEPROM_ACTIVATE event
                eeprom_file._handle_event({'type': 254, 'action': 0, 'device_nr': 0, 'data': 0})

        master_communicator = Mock()
        master_communicator.do_command = _do_command
        master_communicator.do_basic_action = _do_basic_action

        SetUpTestInjections(master_communicator=master_communicator,
                            pubsub=Mock())
        eeprom_file = MemoryFile(MemoryTypes.EEPROM)
        eeprom_file._cache = memory
        SetUpTestInjections(memory_files={MemoryTypes.EEPROM: eeprom_file,
                                          MemoryTypes.FRAM: MemoryFile(MemoryTypes.FRAM)})

    def test_list_group_actions(self):
        memory = {}
        for page in range(256, 381):
            memory[page] = bytearray([255] * 256)
        GroupActionTest._setup_master_communicator(memory)

        group_actions = GroupActionController.load_group_actions()
        self.assertEqual(256, len(group_actions), 'There should be 256 GAs')
        for i in range(256):
            self.assertFalse(group_actions[i].in_use, 'GA {0} should not be in use'.format(i))

        # Set valid start address
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 0 * 4, self._word_helper.encode(0))

        group_actions = GroupActionController.load_group_actions()
        self.assertEqual(256, len(group_actions), 'There should still be 256 GAs')
        for i in range(256):
            self.assertFalse(group_actions[i].in_use, 'GA {0} should not be in use'.format(i))

        group_action = GroupActionController.load_group_action(0)
        self.assertEqual(group_actions[0], group_action, 'The GA is equal (same id, same name, same in_use state)')
        self.assertFalse(group_action.in_use, 'The GA is still not in use')

        # Add BA at start address and set a name
        basic_action_1 = BasicAction(0, 0)
        GroupActionTest._write(memory[GroupActionTest.ACTIONS_START_PAGE], 0 * 6, basic_action_1.encode())
        GroupActionTest._write(memory[GroupActionTest.GANAMES_START_PAGE], 0 * 16, GroupActionTest._encode_string('test'))

        group_action = GroupActionController.load_group_action(0)
        self.assertNotEqual(group_actions[0], group_action, 'The GA changed (name is set)')
        self.assertEqual('test', group_action.name)
        self.assertFalse(group_action.in_use, 'The GA is still not in use')

        # Write valid end address but remove BA
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 0 * 4 + 2, self._word_helper.encode(0))
        GroupActionTest._write(memory[GroupActionTest.ACTIONS_START_PAGE], 0 * 6, [255, 255, 255, 255, 255, 255])

        group_action = GroupActionController.load_group_action(0)
        self.assertFalse(group_action.in_use, 'The GA is not in use yet (no BAs defined)')

        # Restore BA
        GroupActionTest._write(memory[GroupActionTest.ACTIONS_START_PAGE], 0 * 6, basic_action_1.encode())

        group_action = GroupActionController.load_group_action(0)
        self.assertTrue(group_action.in_use, 'The GA is now in use (has name and BA)')
        self.assertEqual(1, len(group_action.actions), 'There should be one GA')
        self.assertEqual(basic_action_1, group_action.actions[0], 'The expected BA should be configured')

        # Make the GA point to two BAs
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 0 * 4 + 2, self._word_helper.encode(1))

        group_action = GroupActionController.load_group_action(0)
        self.assertTrue(group_action.in_use, 'The GA is still in use')
        self.assertEqual(1, len(group_action.actions), 'An empty BA should be excluded')
        self.assertEqual(basic_action_1, group_action.actions[0], 'The valid BA should still be included')

        # Write second BA
        basic_action_2 = BasicAction(0, 1)
        GroupActionTest._write(memory[GroupActionTest.ACTIONS_START_PAGE], 1 * 6, basic_action_2.encode())

        group_action = GroupActionController.load_group_action(0)
        self.assertTrue(group_action.in_use, 'The GA is still in use')
        self.assertEqual(2, len(group_action.actions), 'Both BAs should be included')
        self.assertEqual([basic_action_1, basic_action_2], group_action.actions, 'The valid BAs should be included')

        group_actions = GroupActionController.load_group_actions()
        self.assertEqual(256, len(group_actions), 'There should be 256 GAs')
        for i in range(1, 256):
            self.assertFalse(group_actions[i].in_use, 'GA {0} should not be in use'.format(i))
        self.assertEqual(group_action, group_actions[0], 'The list should correctly point to the first GA')

        # Set name of third GA, store BA and set addresses
        basic_action_3 = BasicAction(0, 2)
        GroupActionTest._write(memory[GroupActionTest.ACTIONS_START_PAGE], 2 * 6, basic_action_3.encode())
        GroupActionTest._write(memory[GroupActionTest.GANAMES_START_PAGE], 2 * 16, GroupActionTest._encode_string('three'))
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 2 * 4, self._word_helper.encode(2))
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 2 * 4 + 2, self._word_helper.encode(2))

        group_action_2 = GroupActionController.load_group_action(2)
        group_actions = GroupActionController.load_group_actions()
        self.assertEqual(256, len(group_actions), 'There should be 256 GAs')
        for i in range(0, 256):
            if i in [0, 2]:
                continue
            self.assertFalse(group_actions[i].in_use, 'GA {0} should not be in use'.format(i))
        self.assertEqual(group_action, group_actions[0], 'The list should correctly point to the first GA')
        self.assertEqual(group_action_2, group_actions[2], 'The list should correctly point to the first GA')

    def test_space_map(self):
        memory = {}
        for page in range(256, 381):
            memory[page] = bytearray([255] * 256)
        GroupActionTest._setup_master_communicator(memory)

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual({4200: [0]}, space_map, 'An empty map is expected')

        # Write a single start address
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 0 * 4, self._word_helper.encode(0))

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual({4200: [0]}, space_map, 'There should still be an empty map')

        # Write an end address
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 0 * 4 + 2, self._word_helper.encode(0))

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual({4199: [1]}, space_map, 'First address is used')

        # Write a few more addresses:
        # Range 0-0 already used by above code
        # Range 1-9 (0)
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 1 * 4, self._word_helper.encode(10) + self._word_helper.encode(14))
        # Range 15-19 (5)
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 2 * 4, self._word_helper.encode(20) + self._word_helper.encode(24))
        # Range 25-29 (5)
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 4 * 4, self._word_helper.encode(30) + self._word_helper.encode(34))
        # Range 35-99 (65)
        GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], 3 * 4, self._word_helper.encode(100) + self._word_helper.encode(163))
        # Range 164-4199 (4036)

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual({9: [1],
                          5: [15, 25],
                          65: [35],
                          4036: [164]}, space_map, 'Expect a few gaps')

    def test_save_configuration(self):
        memory = {}
        for page in range(256, 381):
            memory[page] = bytearray([255] * 256)
        GroupActionTest._setup_master_communicator(memory)

        group_action = GroupAction(id=5, name='five')
        GroupActionController.save_group_action(group_action, ['name'])

        self.assertEqual(GroupActionTest._encode_string('five'), memory[GroupActionTest.GANAMES_START_PAGE][5 * 16:5 * 16 + 5])

    def test_save_allocations(self):
        """
        This test validates whether writing a GA will store its BAs in the appropriate location. This
        is checked on two ways:
        1. A free space map is generated that is used to validate where the free slots are located
        2. A human readable / visual overview is generated of all BA's action type values in the first 42 addresses
           Legend: X = Used by a few pre-defined GAs
                   n = Actual data, as every insert uses different action types, this can be used to verify
                       whether data is written correctly, as whether the old data is overwritten when needed.
                   _ = Slot that was never used
        Note: As an allocation table is used, the BA space is not cleared, only the reference is removed!
        """
        memory = {}
        for page in range(256, 381):
            memory[page] = bytearray([255] * 256)
        GroupActionTest._setup_master_communicator(memory)

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('__________________________________________', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({4200: [0]}, space_map)

        # Generate "pre-defined" GAs
        for group_action_id, address in {10: 0, 11: 2, 12: 5, 13: 8, 14: 14, 15: 25, 16: (41, 4199)}.items():
            start, end = (address[0], address[1]) if isinstance(address, tuple) else (address, address)
            GroupActionTest._write(memory[GroupActionTest.ADDRESS_START_PAGE], group_action_id * 4, self._word_helper.encode(start) + self._word_helper.encode(end))
            memory[GroupActionTest.ACTIONS_START_PAGE][start * 6] = 100 + group_action_id

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X_X__X__X_____X__________X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({1: [1],
                          2: [3, 6],
                          5: [9],
                          10: [15],
                          15: [26]}, space_map)

        # Store GA with 1 BA
        group_action_1 = GroupAction(id=1, actions=[BasicAction(1, 0)])
        GroupActionController.save_group_action(group_action_1, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X1X__X__X_____X__________X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({2: [3, 6],
                          5: [9],
                          10: [15],
                          15: [26]}, space_map)

        # Store another GA with 1 BA
        group_action_2 = GroupAction(id=2, actions=[BasicAction(2, 0)])
        GroupActionController.save_group_action(group_action_2, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X1X2_X__X_____X__________X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({1: [4],
                          2: [6],
                          5: [9],
                          10: [15],
                          15: [26]}, space_map)

        # GA is update dto two BAs
        group_action_2 = GroupAction(id=2, actions=[BasicAction(3, 0),
                                                    BasicAction(3, 0)])
        GroupActionController.save_group_action(group_action_2, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X1X33X__X_____X__________X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({2: [6],
                          5: [9],
                          10: [15],
                          15: [26]}, space_map)

        # First GA is extended
        group_action_1 = GroupAction(id=1, actions=[BasicAction(4, 0),
                                                    BasicAction(4, 0)])
        GroupActionController.save_group_action(group_action_1, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X1X33X44X_____X__________X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({1: [1],
                          5: [9],
                          10: [15],
                          15: [26]}, space_map)

        # Add large GA
        group_action_3 = GroupAction(id=3, actions=[BasicAction(5, 0), BasicAction(5, 0), BasicAction(5, 0),
                                                    BasicAction(5, 0), BasicAction(5, 0), BasicAction(5, 0)])
        GroupActionController.save_group_action(group_action_3, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X1X33X44X_____X555555____X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({1: [1],
                          4: [21],
                          5: [9],
                          15: [26]}, space_map)

        # Large GA is reduced
        group_action_3 = GroupAction(id=3, actions=[BasicAction(6, 0), BasicAction(6, 0), BasicAction(6, 0)])
        GroupActionController.save_group_action(group_action_3, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X1X33X44X666__X555555____X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({1: [1],
                          2: [12],
                          10: [15],
                          15: [26]}, space_map, 'Reduced GA should be moved')

        # Another GA is added with only one BA
        group_action_4 = GroupAction(id=4, actions=[BasicAction(7, 0)])
        GroupActionController.save_group_action(group_action_4, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X7X33X44X666__X555555____X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({2: [12],
                          10: [15],
                          15: [26]}, space_map, 'Reduced GA should be moved')

        # Another large GA is added
        group_action_5 = GroupAction(id=5, actions=[BasicAction(8, 0), BasicAction(8, 0), BasicAction(8, 0),
                                                    BasicAction(8, 0)])
        GroupActionController.save_group_action(group_action_5, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X7X33X44X666__X888855____X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({2: [12],
                          6: [19],
                          15: [26]}, space_map, 'Reduced GA should be moved')

        # Large GA is "deleted"
        group_action_5 = GroupAction(id=5, actions=[])
        GroupActionController.save_group_action(group_action_5, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X7X33X44X666__X888855____X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({2: [12],
                          10: [15],
                          15: [26]}, space_map, 'Reduced GA should be moved')

        # A GA with too many BAs is added
        group_action_6 = GroupAction(id=6, actions=[BasicAction(8, 0)] * 16)
        with self.assertRaises(RuntimeError):
            GroupActionController.save_group_action(group_action_6, ['actions'])

        space_map = GroupActionController._free_address_space_map()
        self.assertEqual('X7X33X44X666__X888855____X_______________X', GroupActionTest._get_readable_ba_block(memory))
        #                 |    |    |    |    |    |    |    |    |
        #                 0    5    10   15   20   25   30   35   40
        self.assertEqual({2: [12],
                          10: [15],
                          15: [26]}, space_map, 'Memory is not changed')
Example #3
0
class BasicAction(object):
    def __init__(self,
                 action_type,
                 action,
                 device_nr=None,
                 extra_parameter=None
                 ):  # type: (int, int, Optional[int], Optional[int]) -> None
        self._word_helper = WordField('')
        self._byte_helper = ByteField('')
        self._action_type = self._byte_helper.encode(
            action_type)  # type: bytearray
        self._action = self._byte_helper.encode(action)  # type: bytearray
        self._device_nr = self._word_helper.encode(
            device_nr if device_nr is not None else 0)  # type: bytearray
        self._extra_parameter = self._word_helper.encode(
            extra_parameter
            if extra_parameter is not None else 0)  # type: bytearray

    @property
    def action_type(self):  # type: () -> int
        return self._byte_helper.decode(self._action_type)

    @property
    def action(self):  # type: () -> int
        return self._byte_helper.decode(self._action)

    @property
    def device_nr(self):  # type: () -> int
        return self._word_helper.decode(self._device_nr)

    @property
    def extra_parameter(self):  # type: () -> int
        return self._word_helper.decode(self._extra_parameter)

    def encode(self):  # type: () -> bytearray
        return self._action_type + self._action + self._device_nr + self._extra_parameter

    @property
    def in_use(self):  # type: () -> bool
        return self.action_type != 255 and self.action != 255

    @property
    def is_execute_group_action(self):  # type: () -> bool
        return self.action_type == 19 and self.action == 0

    @staticmethod
    def decode(data):  # type: (bytearray) -> BasicAction
        basic_action = BasicAction(action_type=data[0], action=data[1])
        basic_action._device_nr = data[2:4]
        basic_action._extra_parameter = data[4:6]
        return basic_action

    def __repr__(self):
        return 'BA({0},{1},{2},{3})'.format(self.action_type, self.action,
                                            self.device_nr,
                                            self.extra_parameter)

    def __eq__(self, other):  # type: (Any) -> bool
        if not isinstance(other, BasicAction):
            return False
        return self.encode() == other.encode()