예제 #1
0
 def __init__(self, app: web.Application):
     super().__init__(app)
     self._name = app['config']['name']
     self._cmder = commander.fget(app)
     self._store = block_store.fget(app)
     self._discovery_lock: asyncio.Lock = None
     self._conn_check_lock: asyncio.Lock = None
 def __init__(self, app: web.Application):
     super().__init__(app)
     self.codec = codec.fget(app)
     self.commander = commander.fget(app)
     self.service_store = service_store.fget(self.app)
     self.global_store = global_store.fget(self.app)
     self.block_store = block_store.fget(self.app)
async def test_check_connection(app, client, mocker):
    sim = connection_sim.fget(app)
    ctrl = controller.fget(app)
    cmder = commander.fget(app)

    s_noop = mocker.spy(cmder, 'noop')
    s_reconnect = mocker.spy(cmder, 'start_reconnect')

    await ctrl.noop()
    await ctrl._check_connection()
    assert s_noop.await_count == 2
    assert s_reconnect.await_count == 0

    cmder._timeout = 0.1
    with pytest.raises(exceptions.CommandTimeout):
        sim.next_error.append(None)
        await ctrl.noop()

    await asyncio.sleep(0.01)
    assert s_noop.await_count == 4

    with pytest.raises(exceptions.CommandTimeout):
        sim.next_error += [None, ErrorCode.INSUFFICIENT_HEAP]
        await ctrl.noop()

    await asyncio.sleep(0.01)
    assert s_reconnect.await_count == 1

    # Should be a noop if not connected
    await sim.end()
    await ctrl._check_connection()
    assert s_noop.await_count == 6
예제 #4
0
async def test_unexpected_message(app, client, mocker):
    m_log_error = mocker.patch(TESTED + '.LOGGER.error', autospec=True)
    cmder = commander.fget(app)
    message = EncodedResponse(
        msgId=123,
        error=ErrorCode.OK,
        payload=[]
    )
    _, enc_message = await codec.fget(app).encode((codec.RESPONSE_TYPE, None), message.dict())
    await cmder._data_callback(enc_message)
    m_log_error.assert_called_with(matching(r'.*Unexpected message'))
예제 #5
0
def test_main(mocker, app):
    mocker.patch(TESTED + '.service.run')
    mocker.patch(TESTED + '.service.create_app').return_value = app

    main.main()

    assert None not in [
        commander.fget(app),
        service_store.fget(app),
        block_store.fget(app),
        controller.fget(app),
        mqtt.handler(app),
        broadcaster.fget(app)
    ]
예제 #6
0
    async def _execute(self,
                       command_type: Type[commands.Command],
                       command_opts: CodecOpts,
                       content_: dict = None,
                       **kwargs) -> dict:
        # Allow a combination of a dict containing arguments, and loose kwargs
        content = content_ or dict()
        content.update(kwargs)

        cmder = commander.fget(self.app)
        resolver = SparkResolver(self.app)

        if await service_status.wait_updating(self.app, wait=False):
            raise exceptions.UpdateInProgress('Update is in progress')

        try:
            # pre-processing
            for afunc in [
                    resolver.convert_sid_nid,
                    resolver.convert_links_nid,
                    resolver.encode_data,
            ]:
                content = await afunc(content, command_opts)

            # execute
            retval = await cmder.execute(command_type.from_decoded(content))

            # post-processing
            for afunc in [
                    resolver.decode_data,
                    resolver.convert_links_sid,
                    resolver.add_sid,
                    resolver.add_service_id,
            ]:
                retval = await afunc(retval, command_opts)

            return retval

        except asyncio.CancelledError:  # pragma: no cover
            raise

        except exceptions.CommandTimeout as ex:
            # Wrap in a task to not delay the original response
            asyncio.create_task(self.check_connection())
            raise ex

        except Exception as ex:
            LOGGER.debug(f'Failed to execute {command_type}: {strex(ex)}')
            raise ex
    async def flash(self) -> dict:  # pragma: no cover
        sender = ymodem.FileSender(self._notify)
        cmder = commander.fget(self.app)
        address = service_status.desc(self.app).device_address

        self._notify(
            f'Started updating {self.name}@{address} to version {self.version} ({self.date})'
        )

        try:
            if not service_status.desc(self.app).is_connected:
                self._notify('Controller is not connected. Aborting update.')
                raise exceptions.NotConnected()

            if self.simulation:
                raise NotImplementedError(
                    'Firmware updates not available for simulation controllers'
                )

            self._notify('Sending update command to controller')
            service_status.set_updating(self.app)
            await asyncio.sleep(FLUSH_PERIOD_S
                                )  # Wait for in-progress commands to finish
            await cmder.execute(commands.FirmwareUpdateCommand.from_args())

            self._notify('Shutting down normal communication')
            await cmder.shutdown(self.app)

            self._notify('Waiting for normal connection to close')
            await asyncio.wait_for(service_status.wait_disconnected(self.app),
                                   STATE_TIMEOUT_S)

            self._notify(f'Connecting to {address}')
            conn = await self._connect(address)

            with conn.autoclose():
                await asyncio.wait_for(sender.transfer(conn),
                                       TRANSFER_TIMEOUT_S)

        except Exception as ex:
            self._notify(f'Failed to update firmware: {strex(ex)}')
            raise exceptions.FirmwareUpdateFailed(strex(ex))

        finally:
            self._notify('Scheduling service reboot')
            await shutdown_soon(self.app, UPDATE_SHUTDOWN_DELAY_S)

        self._notify('Firmware updated!')
        return {'address': address, 'version': self.version}
async def sparky(app, client, mocker):
    s = commander.fget(app)

    # Allow immediately responding to requests
    # This avoids having to schedule parallel calls to execute() and data_callback()
    s.__preloaded = []
    f_orig = s.add_request

    def m_add_request(request: str):
        fut = f_orig(request)
        for msg in s.__preloaded:
            s.data_callback(msg)
        s.__preloaded.clear()
        return fut

    mocker.patch.object(s, 'add_request', m_add_request)
    return s
예제 #9
0
    async def check_connection(self):
        """
        Sends a Noop command to controller to evaluate the connection.
        If this command also fails, prompt the commander to reconnect.

        Only do this when the service is synchronized,
        to avoid weird interactions when prompting for a handshake.
        """
        async with self._conn_check_lock:
            if await service_status.wait_synchronized(self.app, wait=False):
                LOGGER.info('Checking connection...')
                cmder = commander.fget(self.app)
                try:
                    cmd = commands.NoopCommand.from_args()
                    await cmder.execute(cmd)
                except Exception:
                    await cmder.start_reconnect()
예제 #10
0
def sim(app):
    return commander.fget(app)
예제 #11
0
    async def flash(self) -> dict:  # pragma: no cover
        ota = ymodem.OtaClient(self._notify)
        cmder = commander.fget(self.app)
        status_desc = service_status.desc(self.app)
        address = status_desc.device_address
        platform = status_desc.device_info.platform

        self._notify(
            f'Started updating {self.name}@{address} to version {self.version} ({self.date})'
        )

        try:
            if not status_desc.is_connected:
                self._notify('Controller is not connected. Aborting update.')
                raise exceptions.NotConnected()

            if self.simulation:
                raise NotImplementedError(
                    'Firmware updates not available for simulation controllers'
                )

            self._notify('Preparing update')
            service_status.set_updating(self.app)
            await asyncio.sleep(FLUSH_PERIOD_S
                                )  # Wait for in-progress commands to finish

            if platform != 'esp32':  # pragma: no cover
                self._notify('Sending update command to controller')
                await cmder.execute(commands.FirmwareUpdateCommand.from_args())

            self._notify('Waiting for normal connection to close')
            await cmder.shutdown(self.app)
            await asyncio.wait_for(service_status.wait_disconnected(self.app),
                                   STATE_TIMEOUT_S)

            if platform == 'esp32':  # pragma: no cover
                # ESP connections will always be a TCP address
                host, _ = address.split(':')
                self._notify(f'Sending update prompt to {host}')
                self._notify(
                    'The Spark will now download and apply the new firmware')
                self._notify('The update is done when the service reconnects')
                fw_url = ESP_URL_FMT.format(**self.app['ini'])
                await http.session(self.app
                                   ).post(f'http://{host}:80/firmware_update',
                                          data=fw_url)

            else:
                self._notify(f'Connecting to {address}')
                conn = await ymodem.connect(address)

                with conn.autoclose():
                    await asyncio.wait_for(
                        ota.send(conn, f'firmware/brewblox-{platform}.bin'),
                        TRANSFER_TIMEOUT_S)
                    self._notify('Update done!')

        except Exception as ex:
            self._notify(f'Failed to update firmware: {strex(ex)}')
            raise exceptions.FirmwareUpdateFailed(strex(ex))

        finally:
            self._notify('Restarting service...')
            await shutdown_soon(self.app, UPDATE_SHUTDOWN_DELAY_S)

        return {'address': address, 'version': self.version}
예제 #12
0
class SparkController(features.ServiceFeature):
    def __init__(self, app: web.Application):
        super().__init__(app)

    async def startup(self, app: web.Application):
        self._conn_check_lock = asyncio.Lock()

    async def validate(self, content_: dict = None, **kwargs) -> dict:
        content = content_ or dict()
        content.update(kwargs)
        opts = DecodeOpts()

        resolver = SparkResolver(self.app)

        content = await resolver.convert_sid_nid(content)
        content = await resolver.convert_links_nid(content)
        content = await resolver.encode_data(content)
        content = await resolver.decode_data(content, opts)
        content = await resolver.convert_links_sid(content)
        content = await resolver.add_sid(content)

        return content

    async def check_connection(self):
        """
        Sends a Noop command to controller to evaluate the connection.
        If this command also fails, prompt the commander to reconnect.

        Only do this when the service is synchronized,
        to avoid weird interactions when prompting for a handshake.
        """
        async with self._conn_check_lock:
            if await service_status.wait_synchronized(self.app, wait=False):
                LOGGER.info('Checking connection...')
                cmder = commander.fget(self.app)
                try:
                    cmd = commands.NoopCommand.from_args()
                    await cmder.execute(cmd)
                except Exception:
                    await cmder.start_reconnect()

    async def _execute(self,
                       command_type: Type[commands.Command],
                       decode_opts: list[DecodeOpts],
                       content_: dict = None,
                       /,
                       **kwargs) -> Union[dict, list[dict]]:
        # Allow a combination of a dict containing arguments, and loose kwargs
        content = (content_ or dict()) | kwargs

        cmder = commander.fget(self.app)
        resolver = SparkResolver(self.app)

        if await service_status.wait_updating(self.app, wait=False):
            raise exceptions.UpdateInProgress('Update is in progress')

        try:
            # pre-processing
            content = await resolver.convert_sid_nid(content)
            content = await resolver.convert_links_nid(content)
            content = await resolver.encode_data(content)

            # execute
            command_retval = await cmder.execute(
                command_type.from_decoded(content))

            # post-processing
            output: list[dict] = []

            for opts in decode_opts:
                retval = deepcopy(command_retval)
                retval = await resolver.decode_data(retval, opts)
                retval = await resolver.convert_links_sid(retval)
                retval = await resolver.add_sid(retval)
                retval = await resolver.add_service_id(retval)
                output.append(retval)

            # Multiple decoding opts is the exception
            # Don't unnecessarily wrap the output
            if len(decode_opts) == 1:
                return output[0]
            else:
                return output

        except exceptions.CommandTimeout as ex:
            # Wrap in a task to not delay the original response
            asyncio.create_task(self.check_connection())
            raise ex

        except Exception as ex:
            LOGGER.debug(f'Failed to execute {command_type}: {strex(ex)}')
            raise ex