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')
class CoreCommunicator(object): """ Uses a serial port to communicate with the Core and updates the output state. Provides methods to send CoreCommands. """ # Message constants. There are here for better code readability, you can't just simply change them START_OF_REQUEST = bytearray(b'STR') END_OF_REQUEST = bytearray(b'\r\n\r\n') START_OF_REPLY = bytearray(b'RTR') END_OF_REPLY = bytearray(b'\r\n') @Inject def __init__(self, controller_serial=INJECTED): # type: (Serial) -> None self._verbose = logger.level >= logging.DEBUG self._serial = controller_serial self._serial_write_lock = Lock() self._cid_lock = Lock() self._serial_bytes_written = 0 self._serial_bytes_read = 0 self._cid = None # type: Optional[int] # Reserved CIDs: 0 = Core events, 1 = uCAN transport, 2 = Slave transport self._cids_in_use = set() # type: Set[int] self._consumers = { } # type: Dict[int, List[Union[Consumer, BackgroundConsumer]]] self._last_success = 0.0 self._stop = False self._word_helper = WordField('') self._read_thread = None # type: Optional[BaseThread] self._command_total_histogram = Counter() # type: Counter self._command_success_histogram = Counter() # type: Counter self._command_timeout_histogram = Counter() # type: Counter self._communication_stats = { 'calls_succeeded': [], 'calls_timedout': [], 'bytes_written': 0, 'bytes_read': 0 } # type: Dict[str,Any] self._debug_buffer = { 'read': {}, 'write': {} } # type: Dict[str, Dict[float, bytearray]] self._debug_buffer_duration = 300 def start(self): """ Start the CoreComunicator, this starts the background read thread. """ self._stop = False self._read_thread = BaseThread(name='coreread', target=self._read) self._read_thread.setDaemon(True) self._read_thread.start() def stop(self): self._stop = True if self._read_thread is not None: self._read_thread.join() self._read_thread = None def get_communication_statistics(self): return self._communication_stats def reset_communication_statistics(self): self._communication_stats = { 'calls_succeeded': [], 'calls_timedout': [], 'bytes_written': 0, 'bytes_read': 0 } def get_command_histograms(self): return { 'total': dict(self._command_total_histogram), 'success': dict(self._command_success_histogram), 'timeout': dict(self._command_timeout_histogram) } def reset_command_histograms(self): self._command_total_histogram.clear() self._command_success_histogram.clear() self._command_timeout_histogram.clear() def get_debug_buffer(self): # type: () -> Dict[str,Dict[float,str]] def process(buffer): return {k: printable(v) for k, v in six.iteritems(buffer)} return { 'read': process(self._debug_buffer['read']), 'write': process(self._debug_buffer['write']) } def get_seconds_since_last_success(self): # type: () -> float """ Get the number of seconds since the last successful communication. """ if self._last_success == 0: return 0.0 # No communication - return 0 sec since last success else: return time.time() - self._last_success def _get_cid(self): # type: () -> int """ Get a communication id. 0 and 1 are reserved. """ def _increment_cid(current_cid): # type: (Optional[int]) -> int # Reserved CIDs: 0 = Core events, 1 = uCAN transport, 2 = Slave transport return current_cid + 1 if (current_cid is not None and current_cid < 255) else 3 def _available(candidate_cid): # type: (Optional[int]) -> bool if candidate_cid is None: return False if candidate_cid == self._cid: return False if candidate_cid in self._cids_in_use: return False return True with self._cid_lock: cid = self._cid # type: Optional[int] # Initial value while not _available(cid): cid = _increment_cid(cid) if cid == self._cid: # Seems there is no CID available at this moment raise RuntimeError('No available CID') if cid is None: # This is impossible due to `_available`, but mypy doesn't know that raise RuntimeError('CID should not be None') self._cid = cid self._cids_in_use.add(cid) return cid def _write_to_serial(self, data): # type: (bytearray) -> None """ Write data to the serial port. :param data: the data to write """ with self._serial_write_lock: if self._verbose: logger.debug('Writing to Core serial: {0}'.format( printable(data))) threshold = time.time() - self._debug_buffer_duration self._debug_buffer['write'][time.time()] = data for t in self._debug_buffer['write'].keys(): if t < threshold: del self._debug_buffer['write'][t] self._serial.write(data) self._serial_bytes_written += len(data) self._communication_stats['bytes_written'] += len(data) def register_consumer( self, consumer): # type: (Union[Consumer, BackgroundConsumer]) -> None """ Register a consumer :param consumer: The consumer to register. """ self._consumers.setdefault(consumer.get_hash(), []).append(consumer) def discard_cid(self, cid): # type: (int) -> None """ Discards a Command ID. """ with self._cid_lock: self._cids_in_use.discard(cid) def unregister_consumer( self, consumer): # type: (Union[Consumer, BackgroundConsumer]) -> None """ Unregister a consumer """ consumers = self._consumers.get(consumer.get_hash(), []) if consumer in consumers: consumers.remove(consumer) self.discard_cid(consumer.cid) def do_basic_action(self, action_type, action, device_nr=0, extra_parameter=0, timeout=2, log=True): # type: (int, int, int, int, Optional[int], bool) -> Optional[Dict[str, Any]] """ Sends a basic action to the Core with the given action type and action number """ if log: logger.info('BA: Execute {0} {1} {2} {3}'.format( action_type, action, device_nr, extra_parameter)) return self.do_command(CoreAPI.basic_action(), { 'type': action_type, 'action': action, 'device_nr': device_nr, 'extra_parameter': extra_parameter }, timeout=timeout) def do_command(self, command, fields, timeout=2): # type: (CoreCommandSpec, Dict[str, Any], Union[T_co, int]) -> Union[T_co, Dict[str, Any]] """ Send a command over the serial port and block until an answer is received. If the Core does not respond within the timeout period, a CommunicationTimedOutException is raised :param command: specification of the command to execute :param fields: A dictionary with the command input field values :param timeout: maximum allowed time before a CommunicationTimedOutException is raised """ cid = self._get_cid() consumer = Consumer(command, cid) command = consumer.command try: self._command_total_histogram.update({str(command.instruction): 1}) self._consumers.setdefault(consumer.get_hash(), []).append(consumer) self._send_command(cid, command, fields) except Exception: self.discard_cid(cid) raise try: result = None # type: Any if isinstance(consumer, Consumer) and timeout is not None: result = consumer.get(timeout) self._last_success = time.time() self._communication_stats['calls_succeeded'].append(time.time()) self._communication_stats[ 'calls_succeeded'] = self._communication_stats[ 'calls_succeeded'][-50:] self._command_success_histogram.update( {str(command.instruction): 1}) return result except CommunicationTimedOutException: self.unregister_consumer(consumer) self._communication_stats['calls_timedout'].append(time.time()) self._communication_stats[ 'calls_timedout'] = self._communication_stats[ 'calls_timedout'][-50:] self._command_timeout_histogram.update( {str(command.instruction): 1}) raise def _send_command( self, cid, command, fields): # type: (int, CoreCommandSpec, Dict[str, Any]) -> None """ Send a command over the serial port :param cid: The command ID :param command: The Core CommandSpec :param fields: A dictionary with the command input field values """ payload = command.create_request_payload(fields) checked_payload = (bytearray([cid]) + command.instruction + self._word_helper.encode(len(payload)) + payload) data = (CoreCommunicator.START_OF_REQUEST + checked_payload + bytearray(b'C') + CoreCommunicator._calculate_crc(checked_payload) + CoreCommunicator.END_OF_REQUEST) self._write_to_serial(data) @staticmethod def _calculate_crc(data): # type: (bytearray) -> bytearray """ Calculate the CRC of the data. :param data: Data for which to calculate the CRC :returns: CRC """ crc = 0 for byte in data: crc += byte return bytearray([crc % 256]) def _read(self): """ Code for the background read thread: reads from the serial port and forward certain messages to waiting consumers Request format: 'STR' + {CID, 1 byte} + {command, 2 bytes} + {length, 2 bytes} + {payload, `length` bytes} + 'C' + {checksum, 1 byte} + '\r\n\r\n' Response format: 'RTR' + {CID, 1 byte} + {command, 2 bytes} + {length, 2 bytes} + {payload, `length` bytes} + 'C' + {checksum, 1 byte} + '\r\n' """ data = bytearray() message_length = None header_fields = None header_length = len( CoreCommunicator.START_OF_REPLY ) + 1 + 2 + 2 # RTR + CID (1 byte) + command (2 bytes) + length (2 bytes) footer_length = 1 + 1 + len( CoreCommunicator.END_OF_REPLY) # 'C' + checksum (1 byte) + \r\n need_more_data = False while not self._stop: try: # Wait for data if more data is expected if need_more_data: readers, _, _ = select.select([self._serial], [], [], 1) if not readers: continue need_more_data = False # Read what's now on the serial port num_bytes = self._serial.inWaiting() if num_bytes > 0: data += self._serial.read(num_bytes) # Update counters self._serial_bytes_read += num_bytes self._communication_stats['bytes_read'] += num_bytes # Wait for the full message, or the header length min_length = message_length or header_length if len(data) < min_length: need_more_data = True continue if message_length is None: # Check if the data contains the START_OF_REPLY if CoreCommunicator.START_OF_REPLY not in data: need_more_data = True continue # Align with START_OF_REPLY if not data.startswith(CoreCommunicator.START_OF_REPLY): data = CoreCommunicator.START_OF_REPLY + data.split( CoreCommunicator.START_OF_REPLY, 1)[-1] if len(data) < header_length: continue header_fields = CoreCommunicator._parse_header(data) message_length = header_fields[ 'length'] + header_length + footer_length # If not all data is present, wait for more data if len(data) < message_length: continue message = data[:message_length] # type: bytearray data = data[message_length:] # A possible message is received, log where appropriate if self._verbose: logger.debug('Reading from Core serial: {0}'.format( printable(message))) threshold = time.time() - self._debug_buffer_duration self._debug_buffer['read'][time.time()] = message for t in self._debug_buffer['read'].keys(): if t < threshold: del self._debug_buffer['read'][t] # Validate message boundaries correct_boundaries = message.startswith( CoreCommunicator.START_OF_REPLY) and message.endswith( CoreCommunicator.END_OF_REPLY) if not correct_boundaries: logger.info('Unexpected boundaries: {0}'.format( printable(message))) # Reset, so we'll wait for the next RTR message_length = None data = message[ 3:] + data # Strip the START_OF_REPLY, and restore full data continue # Validate message CRC crc = bytearray([message[-3]]) payload = message[8:-4] # type: bytearray checked_payload = message[3:-4] # type: bytearray expected_crc = CoreCommunicator._calculate_crc(checked_payload) if crc != expected_crc: logger.info( 'Unexpected CRC ({0} vs expected {1}): {2}'.format( crc, expected_crc, printable(checked_payload))) # Reset, so we'll wait for the next RTR message_length = None data = message[ 3:] + data # Strip the START_OF_REPLY, and restore full data continue # A valid message is received, reliver it to the correct consumer consumers = self._consumers.get(header_fields['hash'], []) for consumer in consumers[:]: if self._verbose: logger.debug( 'Delivering payload to consumer {0}.{1}: {2}'. format(header_fields['command'], header_fields['cid'], printable(payload))) consumer.consume(payload) if isinstance(consumer, Consumer): self.unregister_consumer(consumer) self.discard_cid(header_fields['cid']) # Message processed, cleaning up message_length = None except Exception: logger.exception('Unexpected exception at Core read thread') data = bytearray() message_length = None @staticmethod def _parse_header( data): # type: (bytearray) -> Dict[str, Union[int, bytearray]] base = len(CoreCommunicator.START_OF_REPLY) return { 'cid': data[base], 'command': data[base + 1:base + 3], 'hash': Toolbox.hash(data[:base + 3]), 'length': struct.unpack('>H', data[base + 3:base + 5])[0] }
class 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()