def test_reading_not_uploaded_message_returns_unused_status(self): storage = Mock() storage.read_status = Mock(return_value=UnusedLongMessageStatusInfo) storage.set_long_message = Mock() storage.get_long_message = Mock() handler = LongMessageHandler(storage) for mt in self.known_message_types: with self.subTest(mt=mt): handler.select_long_message_type(mt) status = handler.read_status() self.assertEqual(LongMessageStatus.UNUSED, status.status)
def test_finalize_notifies_for_already_stored_message_without_upload(self): info = LongMessageStatusInfo(LongMessageStatus.READY, b'\x01\x23\x45', 123) data = b'foobar' storage = Mock() storage.read_status = Mock(return_value=info) storage.set_long_message = Mock() storage.get_long_message = Mock(return_value=data) mock_callback = Mock() handler = LongMessageHandler(storage) handler.on_message_updated(mock_callback) for mt in self.known_message_types: mock_callback.reset_mock() with self.subTest(mt=mt): handler.select_long_message_type(mt) handler.finalize_message() self.assertTrue(mock_callback.called) self.assertEqual(0, storage.set_long_message.call_count) self.assertEqual(mt, mock_callback.call_args.args[0].message_type) self.assertEqual('012345', mock_callback.call_args.args[0].md5)
def test_reading_previously_uploaded_message_returns_ready_and_metadata( self): storage = Mock() storage.read_status = Mock(return_value=LongMessageStatusInfo( LongMessageStatus.READY, 'md5hash', 123)) storage.set_long_message = Mock() storage.get_long_message = Mock() handler = LongMessageHandler(storage) for mt in self.known_message_types: with self.subTest(mt=mt): handler.select_long_message_type(mt) status = handler.read_status() self.assertEqual(LongMessageStatus.READY, status.status) self.assertEqual('md5hash', status.md5) self.assertEqual(123, status.length)
def test_reading_unused_message_returns_zero(self): persistent = MemoryStorage() temp = MemoryStorage() storage = LongMessageStorage(persistent, temp) handler = LongMessageHandler(storage) ble = LongMessageProtocol(handler) ble.handle_write(0, [2]) # select long message 2 result = ble.handle_read() # unused long message response is a 0 byte self.assertEqual(b'\x00', result)
def test_upload_message_with_one_byte_is_accepted(self): persistent = MemoryStorage() temp = MemoryStorage() storage = LongMessageStorage(persistent, temp) handler = LongMessageHandler(storage) ble = LongMessageProtocol(handler) ble.handle_write(0, [2]) # select long message 2 ble.handle_write(1, bytes([0] * 16)) # init self.assertEqual( LongMessageProtocol.RESULT_SUCCESS, ble.handle_write(MessageType.UPLOAD_MESSAGE, bytes([2])))
def test_finalize_notifies_for_already_stored_message_without_upload(self): storage = Mock() storage.read_status = Mock(return_value=LongMessageStatusInfo( LongMessageStatus.READY, 'new_md5', 123)) storage.set_long_message = Mock() mock_callback = Mock() handler = LongMessageHandler(storage) handler.on_message_updated(mock_callback) for mt in self.known_message_types: mock_callback.reset_mock() with self.subTest(mt=mt): handler.select_long_message_type(mt) handler.finalize_message() self.assertEqual(1, mock_callback.call_count) self.assertEqual(0, storage.set_long_message.call_count)
def test_select_validates_longmessage_type(self): handler = LongMessageHandler(None) self.assertRaises(LongMessageError, lambda: handler.select_long_message_type(0)) self.assertRaises(LongMessageError, lambda: handler.select_long_message_type(5)) for mt in self.known_message_types: with self.subTest(mt=mt): handler.select_long_message_type(mt)
def test_read_returns_hash(self): persistent = MemoryStorage() persistent.write(2, b'abcd') md5_hash = hashlib.md5(b'abcd').hexdigest() temp = MemoryStorage() storage = LongMessageStorage(persistent, temp) handler = LongMessageHandler(storage) ble = LongMessageProtocol(handler) ble.handle_write(0, [2]) # select long message 2 (persistent) result = ble.handle_read() # reading a valid message returns its status, md5 hash and length self.assertEqual("03" + md5_hash + "00000004", bytes2hexdigest(result))
except Exception: device_name = f'Revvy_{serial}' print(f'Device name: {device_name}') device_name = Observable(device_name) device_name.subscribe( lambda v: device_storage.write('device-name', v.encode("utf-8"))) long_message_storage = LongMessageStorage(ble_storage, MemoryStorage()) extract_asset_longmessage(long_message_storage, writeable_assets_dir) with Robot() as robot: robot.assets.add_source(writeable_assets_dir) long_message_handler = LongMessageHandler(long_message_storage) robot_manager = RobotBLEController( robot, sw_version, RevvyBLE(device_name, serial, long_message_handler)) lmi = LongMessageImplementation(robot_manager, long_message_storage, writeable_assets_dir, False) long_message_handler.on_upload_started(lmi.on_upload_started) long_message_handler.on_upload_progress(lmi.on_upload_progress) long_message_handler.on_upload_finished(lmi.on_transmission_finished) long_message_handler.on_message_updated(lmi.on_message_updated) # noinspection PyBroadException try: robot_manager.start()
def test_finalize_validates_and_stores_uploaded_message(self, mock_hash): storage = Mock() storage.read_status = Mock(return_value=LongMessageStatusInfo( LongMessageStatus.READY, 'new_md5', 123)) storage.set_long_message = Mock() mock_hash.return_value = mock_hash mock_hash.update = Mock() mock_hash.hexdigest = Mock() handler = LongMessageHandler(storage) # message invalid for mt in self.known_message_types: mock_hash.reset_mock() mock_hash.hexdigest.return_value = 'invalid_md5' with self.subTest(mt='invalid ({})'.format(mt)): handler.select_long_message_type(mt) handler.init_transfer('new_md5') self.assertEqual(1, mock_hash.call_count) handler.upload_message(b'12345') self.assertEqual(1, mock_hash.update.call_count) handler.upload_message(b'67890') self.assertEqual(2, mock_hash.update.call_count) handler.finalize_message() self.assertEqual(1, mock_hash.hexdigest.call_count) status = handler.read_status() self.assertEqual(LongMessageStatus.VALIDATION_ERROR, status.status) self.assertEqual(0, storage.set_long_message.call_count) # message valid for mt in self.known_message_types: mock_hash.reset_mock() storage.reset_mock() mock_hash.hexdigest.return_value = 'new_md5' with self.subTest(mt='valid ({})'.format(mt)): handler.select_long_message_type(mt) handler.init_transfer('new_md5') self.assertEqual(1, mock_hash.call_count) handler.upload_message(b'12345') self.assertEqual(1, mock_hash.update.call_count) handler.upload_message(b'67890') self.assertEqual(2, mock_hash.update.call_count) handler.finalize_message() self.assertEqual(1, mock_hash.hexdigest.call_count) self.assertEqual(1, storage.set_long_message.call_count) status = handler.read_status() self.assertEqual(1, storage.read_status.call_count) self.assertEqual(LongMessageStatus.READY, status.status)
def test_reading_during_upload_returns_md5_and_current_length_of_new_message( self): storage = Mock() handler = LongMessageHandler(storage) for mt in self.known_message_types: with self.subTest(mt=mt): handler.select_long_message_type(mt) handler.init_transfer('new_md5') handler.upload_message(b'12345') status = handler.read_status() self.assertEqual(LongMessageStatus.UPLOAD, status.status) self.assertEqual('new_md5', status.md5) self.assertEqual(5, status.length) handler.upload_message(b'67890') status = handler.read_status() self.assertEqual(LongMessageStatus.UPLOAD, status.status) self.assertEqual('new_md5', status.md5) self.assertEqual(10, status.length)
def test_init_is_required_before_write_and_finalize(self): handler = LongMessageHandler(None) self.assertRaises(LongMessageError, lambda: handler.upload_message([1])) self.assertRaises(LongMessageError, handler.finalize_message)
def test_finalize_notifies_about_valid_message(self, mock_hash): storage = Mock() storage.read_status = Mock(return_value=LongMessageStatusInfo( LongMessageStatus.READY, 'new_md5', 123)) mock_hash.return_value = mock_hash mock_hash.update = Mock() mock_hash.hexdigest = Mock() mock_callback = Mock() handler = LongMessageHandler(storage) handler.on_message_updated(mock_callback) # message invalid for mt in self.known_message_types: mock_hash.reset_mock() mock_hash.hexdigest.return_value = 'invalid_md5' with self.subTest(mt='invalid ({})'.format(mt)): handler.select_long_message_type(mt) handler.init_transfer('new_md5') handler.upload_message(b'12345') handler.finalize_message() self.assertEqual(0, mock_callback.call_count) # message valid for mt in self.known_message_types: mock_callback.reset_mock() mock_hash.hexdigest.return_value = 'new_md5' with self.subTest(mt='valid ({})'.format(mt)): handler.select_long_message_type(mt) handler.init_transfer('new_md5') handler.upload_message(b'12345') handler.finalize_message() self.assertEqual(1, mock_callback.call_count)
def start_revvy(config: RobotConfig = None): current_installation = os.path.dirname(os.path.realpath(__file__)) os.chdir(current_installation) # base directories package_data_dir = os.path.join(current_installation, 'data') print('Revvy run from {} ({})'.format(current_installation, __file__)) # prepare environment serial = getserial() manifest = read_json('manifest.json') sound_files = { 'alarm_clock': 'alarm_clock.mp3', 'bell': 'bell.mp3', 'buzzer': 'buzzer.mp3', 'car_horn': 'car-horn.mp3', 'cat': 'cat.mp3', 'dog': 'dog.mp3', 'duck': 'duck.mp3', 'engine_revving': 'engine-revving.mp3', 'lion': 'lion.mp3', 'oh_no': 'oh-no.mp3', 'robot': 'robot.mp3', 'robot2': 'robot2.mp3', 'siren': 'siren.mp3', 'ta_da': 'tada.mp3', 'uh_oh': 'uh-oh.mp3', 'yee_haw': 'yee-haw.mp3', } def sound_path(file): return os.path.join(package_data_dir, 'assets', file) sound_paths = {key: sound_path(sound_files[key]) for key in sound_files} device_name = Observable("ROS") ble_storage_dir = os.path.join(current_installation, 'ble') ble_storage = FileStorage(ble_storage_dir) long_message_storage = LongMessageStorage(ble_storage, MemoryStorage()) long_message_handler = LongMessageHandler(long_message_storage) ble = RevvyBLE(device_name, serial, long_message_handler) # if the robot has never been configured, set the default configuration for the simple robot initial_config = default_robot_config with RevvyTransportI2C() as transport: robot_control = RevvyControl(transport.bind(0x2D)) robot = RobotManager(robot_control, ble, sound_paths, manifest['version'], initial_config) lmi = LongMessageImplementation(robot, config is not None) long_message_handler.on_upload_started(lmi.on_upload_started) long_message_handler.on_upload_finished(lmi.on_transmission_finished) long_message_handler.on_message_updated(lmi.on_message_updated) # noinspection PyBroadException try: robot.start() print("Press Enter to exit") input() # manual exit ret_val = RevvyStatusCode.OK except EOFError: robot.needs_interrupting = False while not robot.exited: time.sleep(1) ret_val = robot.status_code except KeyboardInterrupt: # manual exit or update request ret_val = robot.status_code except Exception: print(traceback.format_exc()) ret_val = RevvyStatusCode.ERROR finally: print('stopping') robot.stop() print('terminated.') return ret_val
def start_revvy(config: RobotConfig = None): current_installation = os.path.dirname(os.path.realpath(__file__)) os.chdir(current_installation) # base directories writeable_data_dir = os.path.join(current_installation, '..', '..', '..', 'user') package_data_dir = os.path.join(current_installation, 'data') ble_storage_dir = os.path.join(writeable_data_dir, 'ble') data_dir = os.path.join(writeable_data_dir, 'data') def log_uncaught_exception(exctype, value, tb): log_message = 'Uncaught exception: {}\n' \ 'Value: {}\n' \ 'Traceback: \n\t{}\n' \ '\n'.format(exctype, value, "\t".join(traceback.format_tb(tb))) print(log_message) logfile = os.path.join(data_dir, 'revvy_crash.log') with open(logfile, 'a') as logf: logf.write(log_message) sys.excepthook = log_uncaught_exception # self-test if not check_manifest(os.path.join(current_installation, 'manifest.json')): print('Revvy not started because manifest is invalid') return RevvyStatusCode.INTEGRITY_ERROR print('Revvy run from {} ({})'.format(current_installation, __file__)) # prepare environment serial = getserial() manifest = read_json('manifest.json') device_storage = FileStorage(data_dir) ble_storage = FileStorage(ble_storage_dir) sound_files = { 'alarm_clock': 'alarm_clock.mp3', 'bell': 'bell.mp3', 'buzzer': 'buzzer.mp3', 'car_horn': 'car-horn.mp3', 'cat': 'cat.mp3', 'dog': 'dog.mp3', 'duck': 'duck.mp3', 'engine_revving': 'engine-revving.mp3', 'lion': 'lion.mp3', 'oh_no': 'oh-no.mp3', 'robot': 'robot.mp3', 'robot2': 'robot2.mp3', 'siren': 'siren.mp3', 'ta_da': 'tada.mp3', 'uh_oh': 'uh-oh.mp3', 'yee_haw': 'yee-haw.mp3', } def sound_path(file): return os.path.join(package_data_dir, 'assets', file) sound_paths = {key: sound_path(sound_files[key]) for key in sound_files} dnp = DeviceNameProvider(device_storage, lambda: 'Revvy_{}'.format(serial)) device_name = Observable(dnp.get_device_name()) device_name.subscribe(dnp.update_device_name) long_message_storage = LongMessageStorage(ble_storage, MemoryStorage()) long_message_handler = LongMessageHandler(long_message_storage) ble = RevvyBLE(device_name, serial, long_message_handler) # if the robot has never been configured, set the default configuration for the simple robot initial_config = config if config is None: status = long_message_storage.read_status( LongMessageType.CONFIGURATION_DATA) if status.status != LongMessageStatus.READY: initial_config = default_robot_config with RevvyTransportI2C() as transport: robot_control = RevvyControl(transport.bind(0x2D)) bootloader_control = BootloaderControl(transport.bind(0x2B)) updater = McuUpdater(robot_control, bootloader_control) update_manager = McuUpdateManager( os.path.join(package_data_dir, 'firmware'), updater) update_manager.update_if_necessary() robot = RobotManager(robot_control, ble, sound_paths, manifest['version'], initial_config) lmi = LongMessageImplementation(robot, config is not None) long_message_handler.on_upload_started(lmi.on_upload_started) long_message_handler.on_upload_finished(lmi.on_transmission_finished) long_message_handler.on_message_updated(lmi.on_message_updated) # noinspection PyBroadException try: robot.start() print("Press Enter to exit") input() # manual exit ret_val = RevvyStatusCode.OK except EOFError: robot.needs_interrupting = False while not robot.exited: time.sleep(1) ret_val = robot.status_code except KeyboardInterrupt: # manual exit or update request ret_val = robot.status_code except Exception: print(traceback.format_exc()) ret_val = RevvyStatusCode.ERROR finally: print('stopping') robot.stop() print('terminated.') return ret_val
def test_upload_is_only_valid_if_chunk_count_matches_expected(self): storage = LongMessageStorage(MemoryStorage(), MemoryStorage()) chunks = [b'12345', b'1234568', b'98765432'] checksum = hashlib.md5() for chunk in chunks: checksum.update(chunk) expected_md5 = checksum.hexdigest() handler = LongMessageHandler(storage) handler.select_long_message_type(LongMessageType.FIRMWARE_DATA) expectations = ((len(chunks), LongMessageStatus.READY), (0, LongMessageStatus.READY), (len(chunks) - 1, LongMessageStatus.VALIDATION_ERROR), (len(chunks) + 1, LongMessageStatus.VALIDATION_ERROR)) for length, expected_status in expectations: handler.init_transfer(expected_md5, length) for chunk in chunks: handler.upload_message(chunk) handler.finalize_message() status = handler.read_status() self.assertEqual(expected_status, status.status)