Example #1
0
    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)
Example #2
0
    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)
Example #4
0
    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)
Example #8
0
    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))
Example #9
0
    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)
Example #14
0
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
Example #15
0
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)