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
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'))
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) ]
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
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()
def sim(app): return commander.fget(app)
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}
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