diff --git a/cflib/crazyflie/log.py b/cflib/crazyflie/log.py index 021e230e0..f39c23360 100644 --- a/cflib/crazyflie/log.py +++ b/cflib/crazyflie/log.py @@ -52,6 +52,10 @@ import errno import logging import struct +from collections import deque +from contextlib import contextmanager +from threading import Lock +from threading import RLock from .toc import Toc from .toc import TocFetcher @@ -60,7 +64,7 @@ from cflib.utils.callbacks import Caller __author__ = 'Bitcraze AB' -__all__ = ['Log', 'LogTocElement'] +__all__ = ['Log', 'LogConfigError', 'LogTocElement'] # Channels used for the logging port CHAN_TOC = 0 @@ -92,6 +96,10 @@ logger = logging.getLogger(__name__) +class LogConfigError(Exception): + """Raised when a log configuration cannot change lifecycle state.""" + + class LogVariable(): """A logging variable""" @@ -143,8 +151,8 @@ def __init__(self, name, period_in_ms): self.added_cb = Caller() self.err_no = 0 - # These 3 variables are set by the log subsystem when the bock is added - self.id = 0 + # These 3 variables are set by the log subsystem when the block is added + self.id = None self.cf = None self.useV2 = False @@ -152,12 +160,34 @@ def __init__(self, name, period_in_ms): self.period_in_ms = period_in_ms self._added = False self._started = False + self._delete_pending = False self.pending = False self.valid = False self.variables = [] self.default_fetch_as = [] + self._resolved_default_variables = [] self.name = name + def _get_effective_variables(self): + return self.variables + self._resolved_default_variables + + def _detach(self): + previous_state = (self.started, self.added) + self._started = False + self._added = False + self._delete_pending = False + self.pending = False + self.id = None + self.cf = None + self._resolved_default_variables = [] + return previous_state + + def _call_detached_callbacks(self, was_started, was_added): + if was_started: + self.started_cb.call(self, False) + if was_added: + self.added_cb.call(self, False) + def add_variable(self, name, fetch_as=None): """Add a new variable to the configuration. @@ -189,17 +219,25 @@ def add_memory(self, name, fetch_as, stored_as, address): stored_as, address)) def _set_added(self, added): - if added != self._added: + if self._set_added_state(added): self.added_cb.call(self, added) + + def _set_added_state(self, added): + changed = added != self._added self._added = added + return changed def _get_added(self): return self._added def _set_started(self, started): - if started != self._started: + if self._set_started_state(started): self.started_cb.call(self, started) + + def _set_started_state(self, started): + changed = started != self._started self._started = started + return changed def _get_started(self): return self._started @@ -220,9 +258,10 @@ def _cmd_append_block(self): return CMD_APPEND_BLOCK def _setup_log_elements(self, pk, next_to_add): + variables = self._get_effective_variables() i = next_to_add - for i in range(next_to_add, len(self.variables)): - var = self.variables[i] + for i in range(next_to_add, len(variables)): + var = variables[i] if (var.is_toc_variable() is False): # Memory location logger.debug('Logging to raw memory %d, 0x%04X', var.get_storage_and_fetch_byte(), var.address) @@ -251,23 +290,29 @@ def _setup_log_elements(self, pk, next_to_add): def create(self): """Save the log configuration in the Crazyflie""" + cf = self.cf + if cf is None or self.id is None: + raise LogConfigError( + 'Log configuration must be added before it can be created') + with cf.log._config_command(self): + self._create() + + def _create(self): + cf = self.cf + block_id = self.id command = self._cmd_create_block() next_to_add = 0 is_done = False - num_variables = 0 - pending = 0 - for block in self.cf.log.log_blocks: - if block.pending or block.added or block.started: - pending += 1 - num_variables += len(block.variables) + pending, num_variables = cf.log._get_active_config_usage() if pending < Log.MAX_BLOCKS: # # The Crazyflie firmware can only handle 128 variables before # erroring out with ENOMEM. # - if num_variables + len(self.variables) > Log.MAX_VARIABLES: + if (num_variables + len(self._get_effective_variables()) > + Log.MAX_VARIABLES): raise AttributeError( ('Adding this configuration would exceed max number ' 'of variables (%d)' % Log.MAX_VARIABLES) @@ -276,67 +321,92 @@ def create(self): raise AttributeError( 'Configuration has max number of blocks (%d)' % Log.MAX_BLOCKS ) - self.pending += 1 - while not is_done: - pk = CRTPPacket() - pk.set_header(5, CHAN_SETTINGS) - pk.data = (command, self.id) - is_done, next_to_add = self._setup_log_elements(pk, next_to_add) - - logger.debug('Adding/appending log block id {}'.format(self.id)) - self.cf.send_packet(pk, expected_reply=(command, self.id)) - - # Use append if we have to add more variables - command = self._cmd_append_block() + self.pending = True + create_send_attempted = False + try: + while not is_done: + pk = CRTPPacket() + pk.set_header(5, CHAN_SETTINGS) + pk.data = (command, block_id) + is_done, next_to_add = self._setup_log_elements( + pk, next_to_add) + + logger.debug( + 'Adding/appending log block id {}'.format(block_id)) + create_send_attempted = True + cf.send_packet(pk, expected_reply=(command, block_id)) + if not cf.log._is_current_registration(self, cf, block_id): + raise LogConfigError( + 'Log configuration changed while being created') + + # Use append if we have to add more variables + command = self._cmd_append_block() + except BaseException: + if not create_send_attempted: + self.pending = False + else: + try: + cf.log._delete_config(self) + except BaseException: + logger.warning( + 'Failed to delete partial log block id=%d', + block_id, exc_info=True) + raise def start(self): """Start the logging for this entry""" - if (self.cf.link is not None): - if (self._added is False): - self.create() - logger.debug('First time block is started, add block') - else: - logger.debug('Block already registered, starting logging' - ' for id=%d', self.id) - pk = CRTPPacket() - pk.set_header(5, CHAN_SETTINGS) - pk.data = (CMD_START_LOGGING, self.id, self.period) - self.cf.send_packet(pk, expected_reply=( - CMD_START_LOGGING, self.id)) + cf = self.cf + if cf is None or self.id is None: + raise LogConfigError( + 'Log configuration must be added before it can be started') + with cf.log._config_command(self): + if (cf.link is not None): + if (self._added is False): + self._create() + logger.debug('First time block is started, add block') + else: + logger.debug( + 'Block already registered, starting logging for id=%d', + self.id) + pk = CRTPPacket() + pk.set_header(5, CHAN_SETTINGS) + pk.data = (CMD_START_LOGGING, self.id, self.period) + cf.send_packet(pk, expected_reply=( + CMD_START_LOGGING, self.id)) def stop(self): """Stop the logging for this entry""" - if (self.cf.link is not None): - if (self.id is None): - logger.warning('Stopping block, but no block registered') - else: - logger.debug('Sending stop logging for block id=%d', self.id) + cf = self.cf + if cf is None or self.id is None: + return + with cf.log._config_command(self, required=False) as registered: + if not registered: + return + block_id = self.id + if (cf.link is not None): + logger.debug('Sending stop logging for block id=%d', block_id) pk = CRTPPacket() pk.set_header(5, CHAN_SETTINGS) - pk.data = (CMD_STOP_LOGGING, self.id) - self.cf.send_packet( - pk, expected_reply=(CMD_STOP_LOGGING, self.id)) + pk.data = (CMD_STOP_LOGGING, block_id) + cf.send_packet( + pk, expected_reply=(CMD_STOP_LOGGING, block_id)) def delete(self): """Delete this entry in the Crazyflie""" - if (self.cf.link is not None): - if (self.id is None): - logger.warning('Delete block, but no block registered') - else: - logger.debug('LogEntry: Sending delete logging for block id=%d' - % self.id) - pk = CRTPPacket() - pk.set_header(5, CHAN_SETTINGS) - pk.data = (CMD_DELETE_BLOCK, self.id) - self.cf.send_packet( - pk, expected_reply=(CMD_DELETE_BLOCK, self.id)) + cf = self.cf + if cf is not None and self.id is not None: + cf.log._delete_config(self) def unpack_log_data(self, log_data, timestamp): """Unpack received logging data so it represent real values according to the configuration in the entry""" + ret_data = self._unpack_log_data(log_data) + self.data_received_cb.call(timestamp, ret_data, self) + + def _unpack_log_data(self, log_data): ret_data = {} data_index = 0 - for var in self.variables: + for var in self._get_effective_variables(): size = LogTocElement.get_size_from_id(var.fetch_as) name = var.name unpackstring = LogTocElement.get_unpack_string_from_id( @@ -345,7 +415,7 @@ def unpack_log_data(self, log_data, timestamp): unpackstring, log_data[data_index:data_index + size])[0] data_index += size ret_data[name] = value - self.data_received_cb.call(timestamp, ret_data, self) + return ret_data class LogTocElement: @@ -415,6 +485,7 @@ class Log(): """Create log configuration""" MAX_BLOCKS = 16 + MAX_CONFIG_IDS = 256 MAX_VARIABLES = 128 # These codes can be decoded using os.stderror, but @@ -436,6 +507,7 @@ def __init__(self, crazyflie=None): self.cf = crazyflie self.toc = None self.cf.add_port_callback(CRTPPort.LOGGING, self._new_packet_cb) + self.cf.disconnected.add_callback(self._disconnected) self.toc_updated = Caller() self.state = IDLE @@ -444,7 +516,15 @@ def __init__(self, crazyflie=None): self._refresh_callback = None self._toc_cache = None - self._config_id_counter = 1 + self._registration_lock = Lock() + self._command_lock = RLock() + self._command_depth = 0 + self._deferred_calls = deque() + self._dispatching_deferred_calls = False + self._available_config_ids = deque() + self._ids_ready = False + self._reset_pending = False + self._ids_before_reset = None self._useV2 = False @@ -458,35 +538,51 @@ def add_config(self, logconf): cannot be used. Since a valid TOC is required, a Crazyflie has to be connected when calling this method, otherwise it will fail.""" + with self._command_scope(): + self._add_config(logconf) + self._defer_call(self.block_added_cb.call, logconf) + + def _add_config(self, logconf): if not self.cf.link: - logger.error('Cannot add configs without being connected to a ' - 'Crazyflie!') - return + raise LogConfigError( + 'Cannot add log configurations without a connection') + + with self._registration_lock: + if logconf.id is not None or logconf.cf is not None: + raise LogConfigError( + 'Log configuration is already registered') + if not self._ids_ready: + raise LogConfigError( + 'Log configuration IDs are not ready') + toc = self.toc + + if toc is None: + raise LogConfigError('Log TOC is not available') # If the log configuration contains variables that we added without # type (i.e we want the stored as type for fetching as well) then # resolve this now and add them to the block again. + resolved_default_variables = [] for name in logconf.default_fetch_as: - var = self.toc.get_element_by_complete_name(name) + var = toc.get_element_by_complete_name(name) if not var: logger.warning( '%s not in TOC, this block cannot be used!', name) logconf.valid = False raise KeyError('Variable {} not in TOC'.format(name)) - # Now that we know what type this variable has, add it to the log - # config again with the correct type - logconf.add_variable(name, var.ctype) + resolved_default_variables.append(LogVariable(name, var.ctype)) # Now check that all the added variables are in the TOC and that # the total size constraint of a data packet with logging data is # not size = 0 - for var in logconf.variables: + effective_variables = logconf.variables + resolved_default_variables + for var in effective_variables: size += LogTocElement.get_size_from_id(var.fetch_as) # Check that we are able to find the variable in the TOC so # we can return error already now and not when the config is sent if var.is_toc_variable(): - if (self.toc.get_element_by_complete_name(var.name) is None): + if (toc.get_element_by_complete_name(var.name) is None): logger.warning( 'Log: %s not in TOC, this block cannot be used!', var.name) @@ -495,13 +591,23 @@ def add_config(self, logconf): if (size <= LogConfig.MAX_LEN and (logconf.period > 0 and logconf.period < 0xFF)): - logconf.valid = True - logconf.cf = self.cf - logconf.id = self._config_id_counter - logconf.useV2 = self._useV2 - self._config_id_counter = (self._config_id_counter + 1) % 255 - self.log_blocks.append(logconf) - self.block_added_cb.call(logconf) + with self._registration_lock: + if logconf.id is not None or logconf.cf is not None: + raise LogConfigError( + 'Log configuration is already registered') + if not self._ids_ready: + raise LogConfigError( + 'Log configuration IDs are not ready') + if not self._available_config_ids: + raise LogConfigError('No log configuration IDs available') + logconf.valid = True + logconf.cf = self.cf + logconf.id = self._available_config_ids.popleft() + logconf._delete_pending = False + logconf._resolved_default_variables = ( + resolved_default_variables) + logconf.useV2 = self._useV2 + self.log_blocks.append(logconf) else: logconf.valid = False raise AttributeError( @@ -512,32 +618,215 @@ def reset(self): """ Reset the log system and remove all log blocks """ - self.log_blocks = [] self._send_reset_packet() def refresh_toc(self, refresh_done_callback, toc_cache): """Start refreshing the table of loggale variables""" - self._useV2 = self.cf.platform.get_protocol_version() >= 4 + with self._command_scope(): + self._useV2 = self.cf.platform.get_protocol_version() >= 4 - self._toc_cache = toc_cache - self._refresh_callback = refresh_done_callback - self.toc = None + self._toc_cache = toc_cache + self._refresh_callback = refresh_done_callback + self.toc = None - self._send_reset_packet() + self._send_reset_packet() def _send_reset_packet(self): - pk = CRTPPacket() - pk.set_header(CRTPPort.LOGGING, CHAN_SETTINGS) - pk.data = (CMD_RESET_LOGGING,) - self.cf.send_packet(pk, expected_reply=(CMD_RESET_LOGGING,)) + with self._command_scope(): + if self.cf.link is None: + return + + with self._registration_lock: + if self._reset_pending: + return + self._ids_before_reset = ( + self._ids_ready, self._available_config_ids.copy()) + self._reset_pending = True + self._ids_ready = False + self._available_config_ids.clear() + + pk = CRTPPacket() + pk.set_header(CRTPPort.LOGGING, CHAN_SETTINGS) + pk.data = (CMD_RESET_LOGGING,) + try: + self.cf.send_packet( + pk, expected_reply=(CMD_RESET_LOGGING,)) + except BaseException: + self._restore_ids_after_failed_reset() + raise + + def _restore_ids_after_failed_reset(self): + with self._registration_lock: + if not self._reset_pending: + return False + self._reset_pending = False + if self._ids_before_reset is not None: + self._ids_ready, self._available_config_ids = ( + self._ids_before_reset) + self._ids_before_reset = None + return True + + def _detach_all_configs(self, restore_ids, require_reset_pending=False): + with self._registration_lock: + if require_reset_pending and not self._reset_pending: + return None + blocks = self.log_blocks + self.log_blocks = [] + if restore_ids: + self._available_config_ids = deque( + range(self.MAX_CONFIG_IDS)) + else: + self._available_config_ids.clear() + self._ids_ready = restore_ids + self._reset_pending = False + self._ids_before_reset = None + + callbacks = [] + for block in blocks: + callbacks.append((block, block._detach())) + + return callbacks + + def _disconnected(self, uri): + with self._command_scope(): + callbacks = self._detach_all_configs(restore_ids=False) + self._defer_detached_callbacks(callbacks) + + def _defer_detached_callbacks(self, callbacks): + for block, previous_state in callbacks: + self._defer_call( + block._call_detached_callbacks, *previous_state) + + @contextmanager + def _command_scope(self): + should_dispatch = False + try: + with self._command_lock: + self._command_depth += 1 + try: + yield + finally: + self._command_depth -= 1 + if (self._command_depth == 0 and + self._deferred_calls and + not self._dispatching_deferred_calls): + self._dispatching_deferred_calls = True + should_dispatch = True + except BaseException: + if should_dispatch: + try: + self._dispatch_deferred_calls() + except BaseException: + logger.warning( + 'Deferred callback failed while handling command error', + exc_info=True) + raise + else: + if should_dispatch: + self._dispatch_deferred_calls() + + def _defer_call(self, callback, *args): + self._deferred_calls.append((callback, args)) + + def _dispatch_deferred_calls(self): + first_error = None + while True: + with self._command_lock: + if not self._deferred_calls: + self._dispatching_deferred_calls = False + break + callback, args = self._deferred_calls.popleft() + try: + callback(*args) + except BaseException as error: + if first_error is None: + first_error = error + + if first_error is not None: + raise first_error + + def _get_active_config_usage(self): + with self._registration_lock: + log_blocks = list(self.log_blocks) + + active_blocks = [ + block for block in log_blocks + if block.pending or block.added or block.started] + variable_count = sum( + len(block._get_effective_variables()) for block in active_blocks) + return len(active_blocks), variable_count def _find_block(self, id): - for block in self.log_blocks: - if block.id == id: - return block + with self._registration_lock: + for block in self.log_blocks: + if block.id == id: + return block return None + @contextmanager + def _config_command(self, logconf, required=True): + with self._command_scope(): + registered = self._is_current_registration( + logconf, self.cf, logconf.id) + if required and not registered: + raise LogConfigError('Log configuration is not registered') + yield registered + + def _is_current_registration(self, logconf, cf, block_id): + with self._registration_lock: + return (self._ids_ready and + logconf in self.log_blocks and + logconf.cf is cf and + logconf.id == block_id and + block_id is not None and + not logconf._delete_pending) + + def _retire_config(self, logconf, block_id): + with self._registration_lock: + if (logconf not in self.log_blocks or + logconf.id != block_id or + not logconf._delete_pending): + return False + + self.log_blocks.remove(logconf) + if self._ids_ready: + self._available_config_ids.append(block_id) + elif self._reset_pending and self._ids_before_reset is not None: + ids_were_ready, available_config_ids = self._ids_before_reset + if ids_were_ready: + available_config_ids.append(block_id) + previous_state = logconf._detach() + + return logconf, previous_state + + def _delete_config(self, logconf): + with self._command_scope(): + with self._registration_lock: + if (not self._ids_ready or + logconf not in self.log_blocks or + logconf.id is None): + return + if logconf._delete_pending: + return + logconf._delete_pending = True + block_id = logconf.id + + logger.debug('LogEntry: Sending delete logging for block id=%d', + block_id) + pk = CRTPPacket() + pk.set_header(CRTPPort.LOGGING, CHAN_SETTINGS) + pk.data = (CMD_DELETE_BLOCK, block_id) + try: + self.cf.send_packet( + pk, expected_reply=(CMD_DELETE_BLOCK, block_id)) + except BaseException: + with self._registration_lock: + if (logconf.id == block_id and + logconf._delete_pending): + logconf._delete_pending = False + raise + def _new_packet_cb(self, packet): """Callback for newly arrived packets with TOC information""" chan = packet.channel @@ -545,90 +834,151 @@ def _new_packet_cb(self, packet): payload = packet.data[1:] if (chan == CHAN_SETTINGS): + self._handle_settings_packet(cmd, payload) + + if (chan == CHAN_LOGDATA): + with self._command_scope(): + id = packet.data[0] + block = self._find_block(id) + timestamps = struct.unpack('. +import errno +import struct +import threading +import unittest +from concurrent.futures import ThreadPoolExecutor +from unittest.mock import MagicMock +from unittest.mock import patch + +from cflib.crazyflie import Crazyflie +from cflib.crazyflie.log import CHAN_LOGDATA +from cflib.crazyflie.log import CHAN_SETTINGS +from cflib.crazyflie.log import CMD_APPEND_BLOCK_V2 +from cflib.crazyflie.log import CMD_CREATE_BLOCK +from cflib.crazyflie.log import CMD_CREATE_BLOCK_V2 +from cflib.crazyflie.log import CMD_DELETE_BLOCK +from cflib.crazyflie.log import CMD_RESET_LOGGING +from cflib.crazyflie.log import CMD_START_LOGGING +from cflib.crazyflie.log import CMD_STOP_LOGGING +from cflib.crazyflie.log import Log +from cflib.crazyflie.log import LogConfig +from cflib.crazyflie.log import LogConfigError +from cflib.crazyflie.toc import Toc +from cflib.crtp.crtpstack import CRTPPacket +from cflib.crtp.crtpstack import CRTPPort +from cflib.utils.callbacks import Caller + + +class LogTest(unittest.TestCase): + + def setUp(self): + self.cf = MagicMock(spec=Crazyflie) + self.cf.link = object() + self.cf.disconnected = Caller() + self.log = Log(self.cf) + self.cf.log = self.log + self.log.toc = Toc() + + def _acknowledge(self, command, block_id=0, error_status=0): + packet = CRTPPacket() + packet.set_header(CRTPPort.LOGGING, CHAN_SETTINGS) + packet.data = (command, block_id, error_status) + self.log._new_packet_cb(packet) + + def _make_config(self, name): + config = LogConfig(name, 100) + config.add_memory('value', 'uint8_t', 'uint8_t', 0x1000) + return config + + def _make_toc_config(self, name): + self.log.toc = MagicMock() + self.log.toc.get_element_by_complete_name.return_value = MagicMock() + self.log.toc.get_element_id.return_value = 1 + config = LogConfig(name, 100) + config.add_variable('group.value', 'uint8_t') + return config + + def _make_multi_packet_config(self): + self.log._useV2 = True + self.log.toc = MagicMock() + self.log.toc.get_element_by_complete_name.return_value = MagicMock() + self.log.toc.get_element_id.return_value = 1 + config = LogConfig('multi-packet', 100) + for i in range(20): + config.add_variable('group.value{}'.format(i), 'uint8_t') + self.log.add_config(config) + return config + + def _add_thread_cleanup(self, thread, unblock_event): + self.addCleanup(thread.join, 1.0) + self.addCleanup(unblock_event.set) + + def test_all_byte_values_are_available_as_log_config_ids(self): + self.log.reset() + self._acknowledge(CMD_RESET_LOGGING) + + configs = [self._make_config('config-{}'.format(i)) for i in range(256)] + for config in configs: + self.log.add_config(config) + + self.assertEqual(list(range(256)), [config.id for config in configs]) + with self.assertRaises(LogConfigError): + self.log.add_config(self._make_config('one-too-many')) + + def test_deleted_id_is_released_after_acknowledgement(self): + self.log.reset() + self._acknowledge(CMD_RESET_LOGGING) + deleted_config = self._make_config('deleted') + self.log.add_config(deleted_config) + deleted_config.delete() + for i in range(1, 256): + self.log.add_config(self._make_config('config-{}'.format(i))) + + with self.assertRaises(LogConfigError): + self.log.add_config(self._make_config('before-ack')) + + self._acknowledge(CMD_DELETE_BLOCK, deleted_config.id) + + self.assertIsNone(deleted_config.id) + self.assertIsNone(deleted_config.cf) + self.assertNotIn(deleted_config, self.log.log_blocks) + self.log.add_config(deleted_config) + self.assertEqual(0, deleted_config.id) + + def test_300_create_start_stop_delete_cycles(self): + self.log.reset() + self._acknowledge(CMD_RESET_LOGGING) + + for i in range(300): + config = self._make_toc_config('config-{}'.format(i)) + self.log.add_config(config) + received = [] + config.data_received_cb.add_callback( + lambda timestamp, data, logconf: received.append(data)) + config.start() + self._acknowledge(CMD_CREATE_BLOCK, config.id) + self._acknowledge(CMD_START_LOGGING, config.id) + + packet = CRTPPacket() + packet.set_header(CRTPPort.LOGGING, CHAN_LOGDATA) + packet.data = (config.id, 0, 0, 0, i % 256) + self.log._new_packet_cb(packet) + + config.stop() + self._acknowledge(CMD_STOP_LOGGING, config.id) + config.delete() + self._acknowledge(CMD_DELETE_BLOCK, config.id) + + self.assertEqual([{'group.value': i % 256}], received) + self.assertIsNone(config.id) + self.assertEqual([], self.log.log_blocks) + + def test_delete_is_idempotent_until_a_failed_acknowledgement(self): + self.log.reset() + self._acknowledge(CMD_RESET_LOGGING) + config = self._make_config('config') + self.log.add_config(config) + self.cf.send_packet.reset_mock() + + config.delete() + config.delete() + + self.assertEqual(1, self.cf.send_packet.call_count) + self._acknowledge(CMD_DELETE_BLOCK, config.id, errno.ENOMEM) + self.assertEqual(0, config.id) + self.assertIn(config, self.log.log_blocks) + + config.delete() + self.assertEqual(2, self.cf.send_packet.call_count) + + def test_reset_acknowledgement_detaches_configs_and_restores_ids(self): + self.log.reset() + self._acknowledge(CMD_RESET_LOGGING) + config = self._make_config('old-config') + self.log.add_config(config) + + self.log.reset() + + self.assertEqual(0, config.id) + with self.assertRaises(LogConfigError): + self.log.add_config(self._make_config('during-reset')) + + self._acknowledge(CMD_RESET_LOGGING) + + self.assertIsNone(config.id) + self.assertIsNone(config.cf) + self.assertEqual([], self.log.log_blocks) + new_config = self._make_config('new-config') + self.log.add_config(new_config) + self.assertEqual(0, new_config.id) + + def test_disconnect_detaches_configs_without_restoring_ids(self): + self.log.reset() + self._acknowledge(CMD_RESET_LOGGING) + config = self._make_config('config') + self.log.add_config(config) + + self.cf.disconnected.call('radio://test') + + self.assertIsNone(config.id) + self.assertIsNone(config.cf) + self.assertEqual([], self.log.log_blocks) + self.cf.link = None + with self.assertRaises(LogConfigError): + self.log.add_config(self._make_config('after-disconnect')) + + def test_detached_config_requires_registration_before_start(self): + config = self._make_config('config') + + config.stop() + config.delete() + with self.assertRaises(LogConfigError): + config.start() + + def test_config_cannot_be_registered_twice(self): + self.log.reset() + self._acknowledge(CMD_RESET_LOGGING) + config = self._make_config('config') + self.log.add_config(config) + + with self.assertRaises(LogConfigError): + self.log.add_config(config) + + other_config = self._make_config('other-config') + self.log.add_config(other_config) + self.assertEqual(1, other_config.id) + + def test_untyped_variables_are_resolved_fresh_when_reregistered(self): + self.log.reset() + self._acknowledge(CMD_RESET_LOGGING) + toc_element = MagicMock() + toc_element.ctype = 'uint8_t' + self.log.toc = MagicMock() + self.log.toc.get_element_by_complete_name.return_value = toc_element + config = LogConfig('config', 100) + config.add_variable('group.value') + self.log.add_config(config) + config.delete() + self._acknowledge(CMD_DELETE_BLOCK, config.id) + + toc_element.ctype = 'uint16_t' + self.log.add_config(config) + received = [] + config.data_received_cb.add_callback( + lambda timestamp, data, logconf: received.append(data)) + + config.unpack_log_data(struct.pack('