async def _initializer(self, reactor: AsyncioSelectorReactor): reactor.listenTCP( self.__configuration.port(), ServiceFactory(configuration=self.__configuration, event_loop=asyncio.get_event_loop())) asyncio.get_event_loop().create_task(coro=get_client().start_up(), name="start-nats") reactor.run()
async def start(self): self._reactor = AsyncioSelectorReactor(self.loop) self._bus = await client.connect(self._reactor, "system").asFuture(self.loop) # Add signal listeners self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesAdded", ).asFuture(self.loop) ) self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesRemoved", ).asFuture(self.loop) ) self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.Properties", member="PropertiesChanged", ).asFuture(self.loop) ) # Find the HCI device to use for scanning and get cached device properties objects = await self._bus.callRemote( "/", "GetManagedObjects", interface=defs.OBJECT_MANAGER_INTERFACE, destination=defs.BLUEZ_SERVICE, ).asFuture(self.loop) self._adapter_path, self._interface = _filter_on_adapter(objects, self._device) self._cached_devices = dict(_filter_on_device(objects)) # Apply the filters await self._bus.callRemote( self._adapter_path, "SetDiscoveryFilter", interface="org.bluez.Adapter1", destination="org.bluez", signature="a{sv}", body=[self._filters], ).asFuture(self.loop) # Start scanning await self._bus.callRemote( self._adapter_path, "StartDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(self.loop)
def test_seconds(self): """L{seconds} should return a plausible epoch time.""" reactor = AsyncioSelectorReactor() result = reactor.seconds() # greater than 2020-01-01 self.assertGreater(result, 1577836800) # less than 2120-01-01 self.assertLess(result, 4733510400)
def setUp(self): self.saved_signals = save_signal_handlers() self.saved_policy = asyncio.get_event_loop_policy() if hasattr(asyncio, "WindowsSelectorEventLoopPolicy"): # Twisted requires a selector event loop, even if Tornado is # doing its own tricks in AsyncIOLoop to support proactors. # Setting an AddThreadSelectorEventLoop exposes various edge # cases so just use a regular selector. asyncio.set_event_loop_policy( asyncio.WindowsSelectorEventLoopPolicy()) # type: ignore self.io_loop = IOLoop() self.io_loop.make_current() self.reactor = AsyncioSelectorReactor()
def test_seconds(self): """L{seconds} should return a plausible epoch time.""" if hasWindowsSelectorEventLoopPolicy: set_event_loop_policy(WindowsSelectorEventLoopPolicy()) reactor = AsyncioSelectorReactor() result = reactor.seconds() # greater than 2020-01-01 self.assertGreater(result, 1577836800) # less than 2120-01-01 self.assertLess(result, 4733510400) if hasWindowsSelectorEventLoopPolicy: set_event_loop_policy(None)
def test_delayedCallResetToEarlier(self): """ L{DelayedCall.reset()} properly reschedules timer to earlier time """ if hasWindowsSelectorEventLoopPolicy: set_event_loop_policy(WindowsSelectorEventLoopPolicy()) reactor = AsyncioSelectorReactor() timer_called_at = [None] def on_timer(): timer_called_at[0] = reactor.seconds() start_time = reactor.seconds() dc = reactor.callLater(0.5, on_timer) dc.reset(0) reactor.callLater(1, reactor.stop) import io from contextlib import redirect_stderr stderr = io.StringIO() with redirect_stderr(stderr): reactor.run() self.assertEqual(stderr.getvalue(), "") self.assertIsNotNone(timer_called_at[0]) self.assertLess(timer_called_at[0] - start_time, 0.4) if hasWindowsSelectorEventLoopPolicy: set_event_loop_policy(None)
def __init__(self, name: str, loop: AbstractEventLoop = None, **kwargs): super(BlessServerBlueZDBus, self).__init__(loop=loop, **kwargs) self.name: str = name self.reactor: AsyncioSelectorReactor = AsyncioSelectorReactor(loop) self.services: Dict[str, BleakGATTServiceBlueZDBus] = {} self.setup_task: asyncio.Task = self.loop.create_task(self.setup())
def test_WindowsProactorEventLoopPolicy(self): """ L{AsyncioSelectorReactor} will raise a L{TypeError} if L{asyncio.WindowsProactorEventLoopPolicy} is default. """ set_event_loop_policy(WindowsProactorEventLoopPolicy()) with self.assertRaises(TypeError): AsyncioSelectorReactor() set_event_loop_policy(None)
def test_WindowsSelectorEventLoopPolicy(self): """ L{AsyncioSelectorReactor} will work if if L{asyncio.WindowsSelectorEventLoopPolicy} is default. """ set_event_loop_policy(WindowsSelectorEventLoopPolicy()) reactor = AsyncioSelectorReactor() self.assertReactorWorksWithAsyncioFuture(reactor) set_event_loop_policy(None)
def test_defaultSelectorEventLoopFromGlobalPolicy(self): """ L{AsyncioSelectorReactor} wraps the global policy's event loop by default. This ensures that L{asyncio.Future}s and coroutines created by library code that uses L{asyncio.get_event_loop} are bound to the same loop. """ reactor = AsyncioSelectorReactor() self.assertReactorWorksWithAsyncioFuture(reactor)
def test_WindowsSelectorEventLoop(self): """ L{WindowsSelectorEventLoop} works with L{AsyncioSelectorReactor} """ event_loop = WindowsSelectorEventLoopPolicy().new_event_loop() reactor = AsyncioSelectorReactor(event_loop) set_event_loop(event_loop) self.assertReactorWorksWithAsyncioFuture(reactor) set_event_loop_policy(None)
def test_newSelectorEventLoopFromDefaultEventLoopPolicy(self): """ If we use the L{asyncio.DefaultLoopPolicy} to create a new event loop, and then pass that event loop to a new L{AsyncioSelectorReactor}, this reactor should work properly with L{asyncio.Future}. """ event_loop = DefaultEventLoopPolicy().new_event_loop() reactor = AsyncioSelectorReactor(event_loop) set_event_loop(event_loop) self.assertReactorWorksWithAsyncioFuture(reactor) set_event_loop_policy(None)
class Application(object): def __init__(self, configuration: Configuration): self.__configuration = configuration self.logger = Logger(__file__) self.__event_loop = asyncio.get_event_loop() self.reactor = AsyncioSelectorReactor(eventloop=self.__event_loop) def run(self): build_information: BuildInformation = self.__configuration.build_information( ) return self.__initialize(name=build_information.name(), version=build_information.version()) def __initialize(self, name: str, version: str) -> None: self.logger.info("Starting {0} VER: {1}".format(name, version)) self.reactor.listenTCP( self.__configuration.port(), ServiceFactory(configuration=self.__configuration, event_loop=self.__event_loop)) asyncio.set_event_loop(self.__event_loop) self.reactor.callLater(seconds=5, f=get_client().start_up) self.reactor.run() async def _initializer(self, reactor: AsyncioSelectorReactor): reactor.listenTCP( self.__configuration.port(), ServiceFactory(configuration=self.__configuration, event_loop=asyncio.get_event_loop())) asyncio.get_event_loop().create_task(coro=get_client().start_up(), name="start-nats") reactor.run()
async def asyncSetUp(self): await super().asyncSetUp() twisted.internet.reactor = sys.modules[ 'twisted.internet.reactor'] = AsyncioSelectorReactor() logging.getLogger('lbrynet.p2p').setLevel(self.VERBOSITY) logging.getLogger('lbrynet.daemon').setLevel(self.VERBOSITY) lbry_conf.settings = None lbry_conf.initialize_settings(load_conf_file=False, data_dir=self.wallet_node.data_path, wallet_dir=self.wallet_node.data_path, download_dir=self.wallet_node.data_path) lbry_conf.settings['use_upnp'] = False lbry_conf.settings['reflect_uploads'] = False lbry_conf.settings['blockchain_name'] = 'lbrycrd_regtest' lbry_conf.settings['lbryum_servers'] = [('localhost', 50001)] lbry_conf.settings['known_dht_nodes'] = [] lbry_conf.settings.node_id = None await self.account.ensure_address_gap() address = (await self.account.receiving.get_addresses(limit=1, only_usable=True))[0] sendtxid = await self.blockchain.send_to_address(address, 10) await self.confirm_tx(sendtxid) await self.generate(5) def wallet_maker(component_manager): self.wallet_component = WalletComponent(component_manager) self.wallet_component.wallet_manager = self.manager self.wallet_component._running = True return self.wallet_component skip = [ DHT_COMPONENT, UPNP_COMPONENT, HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT, REFLECTOR_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT ] analytics_manager = FakeAnalytics() self.daemon = Daemon( analytics_manager, ComponentManager(analytics_manager=analytics_manager, skip_components=skip, wallet=wallet_maker)) await self.daemon.setup() self.daemon.wallet_manager = self.wallet_component.wallet_manager self.manager.old_db = self.daemon.storage
def test_delayedCallResetToLater(self): """ L{DelayedCall.reset()} properly reschedules timer to later time """ reactor = AsyncioSelectorReactor() timer_called_at = [None] def on_timer(): timer_called_at[0] = reactor.seconds() start_time = reactor.seconds() dc = reactor.callLater(0, on_timer) dc.reset(0.5) reactor.callLater(1, reactor.stop) reactor.run() self.assertIsNotNone(timer_called_at[0]) self.assertGreater(timer_called_at[0] - start_time, 0.4)
def get_reactor(loop: AbstractEventLoop): """Helper factory to get a Twisted reactor for the provided loop. Since the AsyncioSelectorReactor on POSIX systems leaks file descriptors even if stopped and presumably cleaned up, we lazily initialize them and cache them for each loop. In a normal use case you will only work on one event loop anyway, but in the case someone has different loops, this construct still works without leaking resources. Args: loop (asyncio.events.AbstractEventLoop): The event loop to use. Returns: A :py:class:`twisted.internet.asnycioreactor.AsyncioSelectorReactor` running on the provided asyncio event loop. """ if loop not in _reactors: _reactors[loop] = AsyncioSelectorReactor(loop) return _reactors[loop]
def test_defaultEventLoopFromGlobalPolicy(self): """ L{AsyncioSelectorReactor} wraps the global policy's event loop by default. This ensures that L{asyncio.Future}s and coroutines created by library code that uses L{asyncio.get_event_loop} are bound to the same loop. """ reactor = AsyncioSelectorReactor() future = asyncio.Future() result = [] def completed(future): result.append(future.result()) reactor.stop() future.add_done_callback(completed) future.set_result(True) self.assertEqual(result, []) self.runReactor(reactor, timeout=1) self.assertEqual(result, [True])
def test_delayedCallResetToLater(self): """ L{DelayedCall.reset()} properly reschedules timer to later time """ if hasWindowsSelectorEventLoopPolicy: set_event_loop_policy(WindowsSelectorEventLoopPolicy()) reactor = AsyncioSelectorReactor() timer_called_at = [None] def on_timer(): timer_called_at[0] = reactor.seconds() start_time = reactor.seconds() dc = reactor.callLater(0, on_timer) dc.reset(0.5) reactor.callLater(1, reactor.stop) reactor.run() self.assertIsNotNone(timer_called_at[0]) self.assertGreater(timer_called_at[0] - start_time, 0.4) if hasWindowsSelectorEventLoopPolicy: set_event_loop_policy(None)
def test_noCycleReferencesInCallLater(self): """ L{AsyncioSelectorReactor.callLater()} doesn't leave cyclic references """ gc_was_enabled = gc.isenabled() gc.disable() try: objects_before = len(gc.get_objects()) timer_count = 1000 reactor = AsyncioSelectorReactor() for _ in range(timer_count): reactor.callLater(0, lambda: None) reactor.runUntilCurrent() objects_after = len(gc.get_objects()) self.assertLess((objects_after - objects_before) / timer_count, 1) finally: if gc_was_enabled: gc.enable()
from pyppeteer import launch from pyppeteer.errors import PageError, TimeoutError from scrapy.http import HtmlResponse from scrapy.utils.python import global_object_name from twisted.internet.asyncioreactor import AsyncioSelectorReactor from twisted.internet.defer import Deferred from .pretend import SCRIPTS as PRETEND_SCRIPTS from .settings import * from textwrap import dedent from urllib.request import urlopen if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) reactor = AsyncioSelectorReactor(asyncio.get_event_loop()) # install AsyncioSelectorReactor twisted.internet.reactor = reactor sys.modules['twisted.internet.reactor'] = reactor def as_deferred(f): """ transform a Twisted Deffered to an Asyncio Future :param f: async function """ return Deferred.fromFuture(asyncio.ensure_future(f)) logger = logging.getLogger('daoke.aroay_translate')
def setUp(self): self.saved_signals = save_signal_handlers() self.io_loop = IOLoop() self.io_loop.make_current() self.reactor = AsyncioSelectorReactor()
class CompatibilityTests(unittest.TestCase): def setUp(self): self.saved_signals = save_signal_handlers() self.io_loop = IOLoop() self.io_loop.make_current() self.reactor = AsyncioSelectorReactor() def tearDown(self): self.reactor.disconnectAll() self.io_loop.clear_current() self.io_loop.close(all_fds=True) restore_signal_handlers(self.saved_signals) def start_twisted_server(self): class HelloResource(Resource): isLeaf = True def render_GET(self, request): return b"Hello from twisted!" site = Site(HelloResource()) port = self.reactor.listenTCP(0, site, interface="127.0.0.1") self.twisted_port = port.getHost().port def start_tornado_server(self): class HelloHandler(RequestHandler): def get(self): self.write("Hello from tornado!") app = Application([("/", HelloHandler)], log_function=lambda x: None) server = HTTPServer(app) sock, self.tornado_port = bind_unused_port() server.add_sockets([sock]) def run_reactor(self): # In theory, we can run the event loop through Tornado, # Twisted, or asyncio interfaces. However, since we're trying # to avoid installing anything as the global event loop, only # the twisted interface gets everything wired up correectly # without extra hacks. This method is a part of a # no-longer-used generalization that allowed us to test # different combinations. self.stop_loop = self.reactor.stop self.stop = self.reactor.stop self.reactor.run() def tornado_fetch(self, url, runner): client = AsyncHTTPClient() fut = client.fetch(url) fut.add_done_callback(lambda f: self.stop_loop()) runner() return fut.result() def twisted_fetch(self, url, runner): # http://twistedmatrix.com/documents/current/web/howto/client.html chunks = [] client = Agent(self.reactor) d = client.request(b"GET", utf8(url)) class Accumulator(Protocol): def __init__(self, finished): self.finished = finished def dataReceived(self, data): chunks.append(data) def connectionLost(self, reason): self.finished.callback(None) def callback(response): finished = Deferred() response.deliverBody(Accumulator(finished)) return finished d.addCallback(callback) def shutdown(failure): if hasattr(self, "stop_loop"): self.stop_loop() elif failure is not None: # loop hasn't been initialized yet; try our best to # get an error message out. (the runner() interaction # should probably be refactored). try: failure.raiseException() except: logging.error("exception before starting loop", exc_info=True) d.addBoth(shutdown) runner() self.assertTrue(chunks) return b"".join(chunks) def twisted_coroutine_fetch(self, url, runner): body = [None] @gen.coroutine def f(): # This is simpler than the non-coroutine version, but it cheats # by reading the body in one blob instead of streaming it with # a Protocol. client = Agent(self.reactor) response = yield client.request(b"GET", utf8(url)) with warnings.catch_warnings(): # readBody has a buggy DeprecationWarning in Twisted 15.0: # https://twistedmatrix.com/trac/changeset/43379 warnings.simplefilter("ignore", category=DeprecationWarning) body[0] = yield readBody(response) self.stop_loop() self.io_loop.add_callback(f) runner() return body[0] def testTwistedServerTornadoClientReactor(self): self.start_twisted_server() response = self.tornado_fetch( "http://127.0.0.1:%d" % self.twisted_port, self.run_reactor ) self.assertEqual(response.body, b"Hello from twisted!") def testTornadoServerTwistedClientReactor(self): self.start_tornado_server() response = self.twisted_fetch( "http://127.0.0.1:%d" % self.tornado_port, self.run_reactor ) self.assertEqual(response, b"Hello from tornado!") def testTornadoServerTwistedCoroutineClientReactor(self): self.start_tornado_server() response = self.twisted_coroutine_fetch( "http://127.0.0.1:%d" % self.tornado_port, self.run_reactor ) self.assertEqual(response, b"Hello from tornado!")
def __init__(self, configuration: Configuration): self.__configuration = configuration self.logger = Logger(__file__) self.__event_loop = asyncio.get_event_loop() self.reactor = AsyncioSelectorReactor(eventloop=self.__event_loop)
class BleakScannerBlueZDBus(BaseBleakScanner): """The native Linux Bleak BLE Scanner. Args: loop (asyncio.events.AbstractEventLoop): The event loop to use. Keyword Args: """ def __init__(self, loop: AbstractEventLoop = None, **kwargs): super(BleakScannerBlueZDBus, self).__init__(loop, **kwargs) self._device = kwargs.get("device", "hci0") self._reactor = None self._bus = None self._cached_devices = {} self._devices = {} self._rules = list() # Discovery filters self._filters = kwargs.get("filters", {}) self._filters["Transport"] = "le" self._adapter_path = None self._interface = None self._callback = None async def start(self): self._reactor = AsyncioSelectorReactor(self.loop) self._bus = await client.connect(self._reactor, "system").asFuture(self.loop) # Add signal listeners self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesAdded", ).asFuture(self.loop) ) self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesRemoved", ).asFuture(self.loop) ) self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.Properties", member="PropertiesChanged", ).asFuture(self.loop) ) # Find the HCI device to use for scanning and get cached device properties objects = await self._bus.callRemote( "/", "GetManagedObjects", interface=defs.OBJECT_MANAGER_INTERFACE, destination=defs.BLUEZ_SERVICE, ).asFuture(self.loop) self._adapter_path, self._interface = _filter_on_adapter(objects, self._device) self._cached_devices = dict(_filter_on_device(objects)) # Apply the filters await self._bus.callRemote( self._adapter_path, "SetDiscoveryFilter", interface="org.bluez.Adapter1", destination="org.bluez", signature="a{sv}", body=[self._filters], ).asFuture(self.loop) # Start scanning await self._bus.callRemote( self._adapter_path, "StartDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(self.loop) async def stop(self): await self._bus.callRemote( self._adapter_path, "StopDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(self.loop) for rule in self._rules: await self._bus.delMatch(rule).asFuture(self.loop) self._rules.clear() # Try to disconnect the System Bus. try: self._bus.disconnect() except Exception as e: logger.error("Attempt to disconnect system bus failed: {0}".format(e)) try: self._reactor.stop() except ReactorNotRunning: pass self._bus = None self._reactor = None async def set_scanning_filter(self, **kwargs): self._filters = kwargs.get("filters", {}) self._filters["Transport"] = "le" async def get_discovered_devices(self) -> List[BLEDevice]: # Reduce output. discovered_devices = [] for path, props in self._devices.items(): if not props: logger.debug( "Disregarding %s since no properties could be obtained." % path ) continue name, address, _, path = _device_info(path, props) if address is None: continue uuids = props.get("UUIDs", []) manufacturer_data = props.get("ManufacturerData", {}) discovered_devices.append( BLEDevice( address, name, {"path": path, "props": props}, uuids=uuids, manufacturer_data=manufacturer_data, ) ) return discovered_devices def register_detection_callback(self, callback: Callable): """Set a function to be called on each Scanner discovery. Documentation for the Event Handler: https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher.received Args: callback: Function accepting one argument of type ? """ self._callback = callback # Helper methods def parse_msg(self, message): if message.member == "InterfacesAdded": msg_path = message.body[0] try: device_interface = message.body[1].get("org.bluez.Device1", {}) except Exception as e: raise e self._devices[msg_path] = ( {**self._devices[msg_path], **device_interface} if msg_path in self._devices else device_interface ) elif message.member == "PropertiesChanged": iface, changed, invalidated = message.body if iface != defs.DEVICE_INTERFACE: return msg_path = message.path # the PropertiesChanged signal only sends changed properties, so we # need to get remaining properties from cached_devices. However, we # don't want to add all cached_devices to the devices dict since # they may not actually be nearby or powered on. if msg_path not in self._devices and msg_path in self._cached_devices: self._devices[msg_path] = self._cached_devices[msg_path] self._devices[msg_path] = ( {**self._devices[msg_path], **changed} if msg_path in self._devices else changed ) elif ( message.member == "InterfacesRemoved" and message.body[1][0] == defs.BATTERY_INTERFACE ): logger.info( "{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body ) ) return else: msg_path = message.path logger.info( "{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body ) ) logger.info( "{0}, {1} ({2} dBm), Object Path: {3}".format( *_device_info(msg_path, self._devices.get(msg_path)) ) ) if self._callback is not None: self._callback(message)
async def test_init(self): def read(char: BlueZGattCharacteristic): return self.val def write(char: BlueZGattCharacteristic, value: bytearray): char.value = value self.val = value def notify(char: BlueZGattCharacteristic): return def stop_notify(char: BlueZGattCharacteristic): return loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() reactor: AsyncioSelectorReactor = AsyncioSelectorReactor(loop) bus: client = await client.connect(reactor, "system").asFuture(loop) # Create the app app: BlueZGattApplication = BlueZGattApplication( "ble", "org.bluez.testapp", bus, loop) app.Read = read app.Write = write app.StartNotify = notify app.StopNotify = stop_notify # Add a service service_uuid: str = str(uuid.uuid4()) await app.add_service(service_uuid) # Add a characteristic char_uuid: str = str(uuid.uuid4()) flags: List[Flags] = [Flags.READ, Flags.WRITE, Flags.NOTIFY] await app.add_characteristic(service_uuid, char_uuid, bytearray(b'1'), flags) # Validate the app bus.exportObject(app) await bus.requestBusName(app.destination).asFuture(loop) response = await bus.callRemote( app.path, "GetManagedObjects", interface="org.freedesktop.DBus.ObjectManager", destination=app.destination).asFuture(loop) assert response == { '/org/bluez/ble/service1': { 'org.bluez.GattService1': { 'Primary': True, 'UUID': service_uuid }, 'org.freedesktop.DBus.Properties': {} }, '/org/bluez/ble/service1/char1': { 'org.bluez.GattCharacteristic1': { 'Flags': ['read', 'write', 'notify'], 'Notifying': False, 'Service': '/org/bluez/ble/service1', 'UUID': char_uuid, 'Value': [49] }, 'org.freedesktop.DBus.Properties': {} } } # Register the Application adapter: RemoteDBusObject = await get_adapter(bus, loop) await app.register(adapter) # Ensure we're not advertising assert await app.is_advertising(adapter) is False # Advertise await app.start_advertising(adapter) # Check assert await app.is_advertising(adapter) is True # We shouldn't be connected assert await app.is_connected() is False print("\nPlease connect now" + "and subscribe to the characteristic {}...".format(char_uuid)) await aioconsole.ainput("Press enter when ready...") assert await app.is_connected() is True # Read test rng: np.random._generator.Generator = np.random.default_rng() hex_val: str = ''.join(rng.choice(self.hex_words, 2, replace=False)) self.val = bytearray( int(f"0x{hex_val}", 16).to_bytes(length=int(np.ceil(len(hex_val) / 2)), byteorder='big')) print("Trigger a read and enter the hex value you see below") entered_value = await aioconsole.ainput("Value: ") assert entered_value == hex_val # Write test hex_val = ''.join(rng.choice(self.hex_words, 2, replace=False)) print(f"Set the characteristic to the following: {hex_val}") await aioconsole.ainput("Press enter when ready...") str_val: str = ''.join([hex(x)[2:] for x in self.val]).upper() assert str_val == hex_val # Notify test hex_val = ''.join(rng.choice(self.hex_words, 2, replace=False)) self.val = bytearray( int(f"0x{hex_val}", 16).to_bytes(length=int(np.ceil(len(hex_val) / 2)), byteorder='big')) print("A new value will be sent") await aioconsole.ainput("Press enter to receive the new value...") app.services[0].characteristics[0].value = self.val new_value: str = await aioconsole.ainput("Enter the New value: ") assert new_value == hex_val # unsubscribe print("Unsubscribe from the characteristic") await aioconsole.ainput("Press enter when ready...") assert await app.is_connected() is False # Stop Advertising await app.stop_advertising(adapter) assert await app.is_advertising(adapter) is False
async def discover(timeout=5.0, loop=None, **kwargs): """Discover nearby Bluetooth Low Energy devices. For possible values for `filter`, see the parameters to the ``SetDiscoveryFilter`` method in the `BlueZ docs <https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt?h=5.48&id=0d1e3b9c5754022c779da129025d493a198d49cf>`_ The ``Transport`` parameter is always set to ``le`` by default in Bleak. Args: timeout (float): Duration to scan for. loop (asyncio.AbstractEventLoop): Optional event loop to use. Keyword Args: device (str): Bluetooth device to use for discovery. filters (dict): A dict of filters to be applied on discovery. Returns: List of tuples containing name, address and signal strength of nearby devices. """ device = kwargs.get("device", "hci0") loop = loop if loop else asyncio.get_event_loop() cached_devices = {} devices = {} rules = list() reactor = AsyncioSelectorReactor(loop) # Discovery filters filters = kwargs.get("filters", {}) filters["Transport"] = "le" def parse_msg(message): if message.member == "InterfacesAdded": msg_path = message.body[0] try: device_interface = message.body[1].get("org.bluez.Device1", {}) except Exception as e: raise e devices[msg_path] = ( {**devices[msg_path], **device_interface} if msg_path in devices else device_interface ) elif message.member == "PropertiesChanged": iface, changed, invalidated = message.body if iface != defs.DEVICE_INTERFACE: return msg_path = message.path # the PropertiesChanged signal only sends changed properties, so we # need to get remaining properties from cached_devices. However, we # don't want to add all cached_devices to the devices dict since # they may not actually be nearby or powered on. if msg_path not in devices and msg_path in cached_devices: devices[msg_path] = cached_devices[msg_path] devices[msg_path] = ( {**devices[msg_path], **changed} if msg_path in devices else changed ) elif ( message.member == "InterfacesRemoved" and message.body[1][0] == defs.BATTERY_INTERFACE ): logger.info( "{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body ) ) return else: msg_path = message.path logger.info( "{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body ) ) logger.info( "{0}, {1} ({2} dBm), Object Path: {3}".format( *_device_info(msg_path, devices.get(msg_path)) ) ) bus = await client.connect(reactor, "system").asFuture(loop) # Add signal listeners rules.append( await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesAdded", path_namespace="/org/bluez", ).asFuture(loop) ) rules.append( await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesRemoved", path_namespace="/org/bluez", ).asFuture(loop) ) rules.append( await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.Properties", member="PropertiesChanged", path_namespace="/org/bluez", ).asFuture(loop) ) # Find the HCI device to use for scanning and get cached device properties objects = await bus.callRemote( "/", "GetManagedObjects", interface=defs.OBJECT_MANAGER_INTERFACE, destination=defs.BLUEZ_SERVICE, ).asFuture(loop) adapter_path, interface = _filter_on_adapter(objects, device) cached_devices = dict(_filter_on_device(objects)) # Running Discovery loop. await bus.callRemote( adapter_path, "SetDiscoveryFilter", interface="org.bluez.Adapter1", destination="org.bluez", signature="a{sv}", body=[filters], ).asFuture(loop) await bus.callRemote( adapter_path, "StartDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(loop) await asyncio.sleep(timeout) await bus.callRemote( adapter_path, "StopDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(loop) # Reduce output. discovered_devices = [] for path, props in devices.items(): if not props: logger.debug( "Disregarding %s since no properties could be obtained." % path ) continue name, address, _, path = _device_info(path, props) if address is None: continue uuids = props.get("UUIDs", []) manufacturer_data = props.get("ManufacturerData", {}) discovered_devices.append( BLEDevice( address, name, {"path": path, "props": props}, uuids=uuids, manufacturer_data=manufacturer_data, ) ) for rule in rules: await bus.delMatch(rule).asFuture(loop) # Try to disconnect the System Bus. try: bus.disconnect() except Exception as e: logger.error("Attempt to disconnect system bus failed: {0}".format(e)) try: reactor.stop() except ReactorNotRunning: # I think Bleak will always end up here, but I want to call stop just in case... pass return discovered_devices
class BleakClientBlueZDBus(BaseBleakClient): """A native Linux Bleak Client Implemented by using the `BlueZ DBUS API <https://docs.ubuntu.com/core/en/stacks/bluetooth/bluez/docs/reference/dbus-api>`_. Args: address (str): The MAC address of the BLE peripheral to connect to. loop (asyncio.events.AbstractEventLoop): The event loop to use. Keyword Args: timeout (float): Timeout for required ``discover`` call. Defaults to 2.0. """ def __init__(self, address, loop=None, **kwargs): super(BleakClientBlueZDBus, self).__init__(address, loop, **kwargs) self.device = kwargs.get("device") if kwargs.get("device") else "hci0" self.address = address # Backend specific, TXDBus objects and data self._device_path = None self._bus = None self._reactor = None self._rules = {} self._subscriptions = list() self._disconnected_callback = None self._char_path_to_uuid = {} # We need to know BlueZ version since battery level characteristic # are stored in a separate DBus interface in the BlueZ >= 5.48. p = subprocess.Popen(["bluetoothctl", "--version"], stdout=subprocess.PIPE) out, _ = p.communicate() s = re.search(b"(\\d+).(\\d+)", out.strip(b"'")) self._bluez_version = tuple(map(int, s.groups())) # Connectivity methods def set_disconnected_callback( self, callback: Callable[[BaseBleakClient, Future], None], **kwargs ) -> None: """Set the disconnected callback. The callback will be called on DBus PropChanged event with the 'Connected' key set to False. A disconnect callback must accept two positional arguments, the BleakClient and the Future that called it. Example: .. code-block::python async with BleakClient(mac_addr, loop=loop) as client: def disconnect_callback(client, future): print(f"Disconnected callback called on {client}!") client.set_disconnected_callback(disconnect_callback) Args: callback: callback to be called on disconnection. """ self._disconnected_callback = callback async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. Keyword Args: timeout (float): Timeout for required ``discover`` call. Defaults to 2.0. Returns: Boolean representing connection status. """ # A Discover must have been run before connecting to any devices. Do a quick one here # to ensure that it has been done. timeout = kwargs.get("timeout", self._timeout) await discover(timeout=timeout, device=self.device, loop=self.loop) self._reactor = AsyncioSelectorReactor(self.loop) # Create system bus self._bus = await txdbus_connect(self._reactor, busAddress="system").asFuture( self.loop ) # TODO: Handle path errors from txdbus/dbus self._device_path = get_device_object_path(self.device, self.address) def _services_resolved_callback(message): iface, changed, invalidated = message.body is_resolved = defs.DEVICE_INTERFACE and changed.get( "ServicesResolved", False ) if iface == is_resolved: logger.info("Services resolved.") self.services_resolved = True rule_id = await signals.listen_properties_changed( self._bus, self.loop, _services_resolved_callback ) logger.debug( "Connecting to BLE device @ {0} with {1}".format(self.address, self.device) ) try: await self._bus.callRemote( self._device_path, "Connect", interface="org.bluez.Device1", destination="org.bluez", ).asFuture(self.loop) except RemoteError as e: raise BleakError(str(e)) if await self.is_connected(): logger.debug("Connection successful.") else: raise BleakError( "Connection to {0} was not successful!".format(self.address) ) # Get all services. This means making the actual connection. await self.get_services() properties = await self._get_device_properties() if not properties.get("Connected"): raise BleakError("Connection failed!") await self._bus.delMatch(rule_id).asFuture(self.loop) self._rules["PropChanged"] = await signals.listen_properties_changed( self._bus, self.loop, self._properties_changed_callback ) return True async def _cleanup(self) -> None: for rule_name, rule_id in self._rules.items(): logger.debug("Removing rule {0}, ID: {1}".format(rule_name, rule_id)) try: await self._bus.delMatch(rule_id).asFuture(self.loop) except Exception as e: logger.error("Could not remove rule {0} ({1}): {2}".format(rule_id, rule_name, e)) self._rules = {} for _uuid in list(self._subscriptions): try: await self.stop_notify(_uuid) except Exception as e: logger.error("Could not remove notifications on characteristic {0}: {1}".format(_uuid, e)) self._subscriptions = [] async def disconnect(self) -> bool: """Disconnect from the specified GATT server. Returns: Boolean representing if device is disconnected. """ logger.debug("Disconnecting from BLE device...") # Remove all residual notifications. await self._cleanup() # Try to disconnect the actual device/peripheral try: await self._bus.callRemote( self._device_path, "Disconnect", interface=defs.DEVICE_INTERFACE, destination=defs.BLUEZ_SERVICE, ).asFuture(self.loop) except Exception as e: logger.error("Attempt to disconnect device failed: {0}".format(e)) # See if it has been disconnected. is_disconnected = not await self.is_connected() # Try to disconnect the System Bus. try: self._bus.disconnect() except Exception as e: logger.error("Attempt to disconnect system bus failed: {0}".format(e)) # Stop the Twisted reactor holding the connection to the DBus system. try: self._reactor.stop() except Exception as e: # I think Bleak will always end up here, but I want to call stop just in case... logger.debug("Attempt to stop Twisted reactor failed: {0}".format(e)) finally: self._bus = None self._reactor = None return is_disconnected async def is_connected(self) -> bool: """Check connection status between this client and the server. Returns: Boolean representing connection status. """ # TODO: Listen to connected property changes. return await self._bus.callRemote( self._device_path, "Get", interface=defs.PROPERTIES_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="ss", body=[defs.DEVICE_INTERFACE, "Connected"], returnSignature="v", ).asFuture(self.loop) # GATT services methods async def get_services(self) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. Returns: A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. """ if self._services_resolved: return self.services sleep_loop_sec = 0.02 total_slept_sec = 0 services_resolved = False while total_slept_sec < 5.0: properties = await self._get_device_properties() services_resolved = properties.get("ServicesResolved", False) if services_resolved: break await asyncio.sleep(sleep_loop_sec, loop=self.loop) total_slept_sec += sleep_loop_sec if not services_resolved: raise BleakError("Services discovery error") logger.debug("Get Services...") objs = await get_managed_objects( self._bus, self.loop, self._device_path + "/service" ) # There is no guarantee that services are listed before characteristics # Managed Objects dict. # Need multiple iterations to construct the Service Collection _chars, _descs = [], [] for object_path, interfaces in objs.items(): logger.debug(utils.format_GATT_object(object_path, interfaces)) if defs.GATT_SERVICE_INTERFACE in interfaces: service = interfaces.get(defs.GATT_SERVICE_INTERFACE) self.services.add_service( BleakGATTServiceBlueZDBus(service, object_path) ) elif defs.GATT_CHARACTERISTIC_INTERFACE in interfaces: char = interfaces.get(defs.GATT_CHARACTERISTIC_INTERFACE) _chars.append([char, object_path]) elif defs.GATT_DESCRIPTOR_INTERFACE in interfaces: desc = interfaces.get(defs.GATT_DESCRIPTOR_INTERFACE) _descs.append([desc, object_path]) for char, object_path in _chars: _service = list(filter(lambda x: x.path == char["Service"], self.services)) self.services.add_characteristic( BleakGATTCharacteristicBlueZDBus(char, object_path, _service[0].uuid) ) self._char_path_to_uuid[object_path] = char.get("UUID") for desc, object_path in _descs: _characteristic = list( filter( lambda x: x.path == desc["Characteristic"], self.services.characteristics.values(), ) ) self.services.add_descriptor( BleakGATTDescriptorBlueZDBus(desc, object_path, _characteristic[0].uuid) ) self._services_resolved = True return self.services # IO methods async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], **kwargs) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: _uuid (str or UUID): The uuid of the characteristics to read from. Returns: (bytearray) The read data. """ characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: # Special handling for BlueZ >= 5.48, where Battery Service (0000180f-0000-1000-8000-00805f9b34fb:) # has been moved to interface org.bluez.Battery1 instead of as a regular service. if _uuid == "00002a19-0000-1000-8000-00805f9b34fb" and ( self._bluez_version[0] == 5 and self._bluez_version[1] >= 48 ): props = await self._get_device_properties( interface=defs.BATTERY_INTERFACE ) # Simulate regular characteristics read to be consistent over all platforms. value = bytearray([props.get("Percentage", "")]) logger.debug( "Read Battery Level {0} | {1}: {2}".format( _uuid, self._device_path, value ) ) return value if str(_uuid) == '00002a00-0000-1000-8000-00805f9b34fb' and ( self._bluez_version[0] == 5 and self._bluez_version[1] >= 48 ): props = await self._get_device_properties( interface=defs.DEVICE_INTERFACE ) # Simulate regular characteristics read to be consistent over all platforms. value = bytearray(props.get("Name", "").encode('ascii')) logger.debug( "Read Device Name {0} | {1}: {2}".format( _uuid, self._device_path, value ) ) return value raise BleakError( "Characteristic with UUID {0} could not be found!".format(_uuid) ) value = bytearray( await self._bus.callRemote( characteristic.path, "ReadValue", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="a{sv}", body=[{}], returnSignature="ay", ).asFuture(self.loop) ) logger.debug( "Read Characteristic {0} | {1}: {2}".format( _uuid, characteristic.path, value ) ) return value async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: handle (int): The handle of the descriptor to read from. Returns: (bytearray) The read data. """ descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError("Descriptor with handle {0} was not found!".format(handle)) value = bytearray( await self._bus.callRemote( descriptor.path, "ReadValue", interface=defs.GATT_DESCRIPTOR_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="a{sv}", body=[{}], returnSignature="ay", ).asFuture(self.loop) ) logger.debug( "Read Descriptor {0} | {1}: {2}".format(handle, descriptor.path, value) ) return value async def write_gatt_char( self, _uuid: Union[str, uuid.UUID], data: bytearray, response: bool = False ) -> None: """Perform a write operation on the specified GATT characteristic. NB: the version check below is for the "type" option to the "Characteristic.WriteValue" method that was added to Bluez in 5.50 ttps://git.kernel.org/pub/scm/bluetooth/bluez.git/commit?id=fa9473bcc48417d69cc9ef81d41a72b18e34a55a Before that commit, "Characteristic.WriteValue" was only "Write with response". "Characteristic.AcquireWrite" was added in Bluez 5.46 https://git.kernel.org/pub/scm/bluetooth/bluez.git/commit/doc/gatt-api.txt?id=f59f3dedb2c79a75e51a3a0d27e2ae06fefc603e which can be used to "Write without response", but for older versions of Bluez, it is not possible to "Write without response". Args: _uuid (str or UUID): The uuid of the characteristics to write to. data (bytes or bytearray): The data to send. response (bool): If write-with-response operation should be done. Defaults to `False`. """ characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: raise BleakError("Characteristic {0} was not found!".format(_uuid)) if ( "write" not in characteristic.properties and "write-without-response" not in characteristic.properties ): raise BleakError( "Characteristic %s does not support write operations!" % str(_uuid) ) if not response and "write-without-response" not in characteristic.properties: response = True # Force response here, since the device only supports that. if ( response and "write" not in characteristic.properties and "write-without-response" in characteristic.properties ): response = False logger.warning( "Characteristic %s does not support Write with response. Trying without..." % str(_uuid) ) # See docstring for details about this handling. if not response and self._bluez_version[0] == 5 and self._bluez_version[1] < 46: raise BleakError("Write without response requires at least BlueZ 5.46") if response or (self._bluez_version[0] == 5 and self._bluez_version[1] > 50): # TODO: Add OnValueUpdated handler for response=True? await self._bus.callRemote( characteristic.path, "WriteValue", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="aya{sv}", body=[data, {"type": "request" if response else "command"}], returnSignature="", ).asFuture(self.loop) else: # Older versions of BlueZ don't have the "type" option, so we have # to write the hard way. This isn't the most efficient way of doing # things, but it works. fd, _ = await self._bus.callRemote( characteristic.path, "AcquireWrite", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="a{sv}", body=[{}], returnSignature="hq", ).asFuture(self.loop) os.write(fd, data) os.close(fd) logger.debug( "Write Characteristic {0} | {1}: {2}".format( _uuid, characteristic.path, data ) ) async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: """Perform a write operation on the specified GATT descriptor. Args: handle (int): The handle of the descriptor to read from. data (bytes or bytearray): The data to send. """ descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError("Descriptor with handle {0} was not found!".format(handle)) await self._bus.callRemote( descriptor.path, 'WriteValue', interface=defs.GATT_DESCRIPTOR_INTERFACE, destination=defs.BLUEZ_SERVICE, signature='aya{sv}', body=[data, {'type': 'command'}], returnSignature='', ).asFuture(self.loop) logger.debug( "Write Descriptor {0} | {1}: {2}".format( handle, descriptor.path, data ) ) async def start_notify( self, _uuid: Union[str, uuid.UUID], callback: Callable[[str, Any], Any], **kwargs ) -> None: """Activate notifications/indications on a characteristic. Callbacks must accept two inputs. The first will be a uuid string object and the second will be a bytearray. .. code-block:: python def callback(sender, data): print(f"{sender}: {data}") client.start_notify(char_uuid, callback) Args: _uuid (str or UUID): The uuid of the characteristics to start notification on. callback (function): The function to be called on notification. Keyword Args: notification_wrapper (bool): Set to `False` to avoid parsing of notification to bytearray. """ _wrap = kwargs.get("notification_wrapper", True) characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: # Special handling for BlueZ >= 5.48, where Battery Service (0000180f-0000-1000-8000-00805f9b34fb:) # has been moved to interface org.bluez.Battery1 instead of as a regular service. # The org.bluez.Battery1 on the other hand does not provide a notification method, so here we cannot # provide this functionality... # See https://kernel.googlesource.com/pub/scm/bluetooth/bluez/+/refs/tags/5.48/doc/battery-api.txt if str(_uuid) == "00002a19-0000-1000-8000-00805f9b34fb" and ( self._bluez_version[0] == 5 and self._bluez_version[1] >= 48 ): raise BleakError( "Notifications on Battery Level Char ({0}) is not " "possible in BlueZ >= 5.48. Use regular read instead.".format(_uuid) ) raise BleakError( "Characteristic with UUID {0} could not be found!".format(_uuid) ) await self._bus.callRemote( characteristic.path, "StartNotify", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="", body=[], returnSignature="", ).asFuture(self.loop) if _wrap: self._notification_callbacks[ characteristic.path ] = _data_notification_wrapper( callback, self._char_path_to_uuid ) # noqa | E123 error in flake8... else: self._notification_callbacks[ characteristic.path ] = _regular_notification_wrapper( callback, self._char_path_to_uuid ) # noqa | E123 error in flake8... self._subscriptions.append(str(_uuid)) async def stop_notify(self, _uuid: Union[str, uuid.UUID]) -> None: """Deactivate notification/indication on a specified characteristic. Args: _uuid: The characteristic to stop notifying/indicating on. """ characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: raise BleakError("Characteristic {0} was not found!".format(_uuid)) await self._bus.callRemote( characteristic.path, "StopNotify", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="", body=[], returnSignature="", ).asFuture(self.loop) self._notification_callbacks.pop(characteristic.path, None) self._subscriptions.remove(str(_uuid)) # DBUS introspection method for characteristics. async def get_all_for_characteristic(self, _uuid: Union[str, uuid.UUID]) -> dict: """Get all properties for a characteristic. This method should generally not be needed by end user, since it is a DBus specific method. Args: _uuid: The characteristic to get properties for. Returns: (dict) Properties dictionary """ characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: raise BleakError("Characteristic {0} was not found!".format(_uuid)) out = await self._bus.callRemote( characteristic.path, "GetAll", interface=defs.PROPERTIES_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="s", body=[defs.GATT_CHARACTERISTIC_INTERFACE], returnSignature="a{sv}", ).asFuture(self.loop) return out async def _get_device_properties(self, interface=defs.DEVICE_INTERFACE) -> dict: """Get properties of the connected device. Args: interface: Which DBus interface to get properties on. Defaults to `org.bluez.Device1`. Returns: (dict) The properties. """ return await self._bus.callRemote( self._device_path, "GetAll", interface=defs.PROPERTIES_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="s", body=[interface], returnSignature="a{sv}", ).asFuture(self.loop) # Internal Callbacks def _properties_changed_callback(self, message): """Notification handler. In the BlueZ DBus API, notifications come as PropertiesChanged callbacks on the GATT Characteristic interface that StartNotify has been called on. Args: message (): The PropertiesChanged DBus signal message relaying the new data on the GATT Characteristic. """ logger.debug( "DBUS: path: {}, domain: {}, body: {}".format( message.path, message.body[0], message.body[1] ) ) if message.body[0] == defs.GATT_CHARACTERISTIC_INTERFACE: if message.path in self._notification_callbacks: logger.info( "GATT Char Properties Changed: {0} | {1}".format( message.path, message.body[1:] ) ) self._notification_callbacks[message.path]( message.path, message.body[1] ) elif message.body[0] == defs.DEVICE_INTERFACE: device_path = "/org/bluez/%s/dev_%s" % ( self.device, self.address.replace(":", "_"), ) if message.path.lower() == device_path.lower(): message_body_map = message.body[1] if ( "Connected" in message_body_map and not message_body_map["Connected"] ): logger.debug("Device {} disconnected.".format(self.address)) task = self.loop.create_task(self._cleanup()) if self._disconnected_callback is not None: task.add_done_callback(partial(self._disconnected_callback, self))
async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. Keyword Args: timeout (float): Timeout for required ``discover`` call. Defaults to 2.0. Returns: Boolean representing connection status. """ # A Discover must have been run before connecting to any devices. Do a quick one here # to ensure that it has been done. timeout = kwargs.get("timeout", self._timeout) await discover(timeout=timeout, device=self.device, loop=self.loop) self._reactor = AsyncioSelectorReactor(self.loop) # Create system bus self._bus = await txdbus_connect(self._reactor, busAddress="system").asFuture( self.loop ) # TODO: Handle path errors from txdbus/dbus self._device_path = get_device_object_path(self.device, self.address) def _services_resolved_callback(message): iface, changed, invalidated = message.body is_resolved = defs.DEVICE_INTERFACE and changed.get( "ServicesResolved", False ) if iface == is_resolved: logger.info("Services resolved.") self.services_resolved = True rule_id = await signals.listen_properties_changed( self._bus, self.loop, _services_resolved_callback ) logger.debug( "Connecting to BLE device @ {0} with {1}".format(self.address, self.device) ) try: await self._bus.callRemote( self._device_path, "Connect", interface="org.bluez.Device1", destination="org.bluez", ).asFuture(self.loop) except RemoteError as e: raise BleakError(str(e)) if await self.is_connected(): logger.debug("Connection successful.") else: raise BleakError( "Connection to {0} was not successful!".format(self.address) ) # Get all services. This means making the actual connection. await self.get_services() properties = await self._get_device_properties() if not properties.get("Connected"): raise BleakError("Connection failed!") await self._bus.delMatch(rule_id).asFuture(self.loop) self._rules["PropChanged"] = await signals.listen_properties_changed( self._bus, self.loop, self._properties_changed_callback ) return True
class CompatibilityTests(unittest.TestCase): def setUp(self): self.saved_signals = save_signal_handlers() self.io_loop = IOLoop() self.io_loop.make_current() self.reactor = AsyncioSelectorReactor() def tearDown(self): self.reactor.disconnectAll() self.io_loop.clear_current() self.io_loop.close(all_fds=True) restore_signal_handlers(self.saved_signals) def start_twisted_server(self): class HelloResource(Resource): isLeaf = True def render_GET(self, request): return b"Hello from twisted!" site = Site(HelloResource()) port = self.reactor.listenTCP(0, site, interface='127.0.0.1') self.twisted_port = port.getHost().port def start_tornado_server(self): class HelloHandler(RequestHandler): def get(self): self.write("Hello from tornado!") app = Application([('/', HelloHandler)], log_function=lambda x: None) server = HTTPServer(app) sock, self.tornado_port = bind_unused_port() server.add_sockets([sock]) def run_reactor(self): # In theory, we can run the event loop through Tornado, # Twisted, or asyncio interfaces. However, since we're trying # to avoid installing anything as the global event loop, only # the twisted interface gets everything wired up correectly # without extra hacks. This method is a part of a # no-longer-used generalization that allowed us to test # different combinations. self.stop_loop = self.reactor.stop self.stop = self.reactor.stop self.reactor.run() def tornado_fetch(self, url, runner): client = AsyncHTTPClient() fut = client.fetch(url) fut.add_done_callback(lambda f: self.stop_loop()) runner() return fut.result() def twisted_fetch(self, url, runner): # http://twistedmatrix.com/documents/current/web/howto/client.html chunks = [] client = Agent(self.reactor) d = client.request(b'GET', utf8(url)) class Accumulator(Protocol): def __init__(self, finished): self.finished = finished def dataReceived(self, data): chunks.append(data) def connectionLost(self, reason): self.finished.callback(None) def callback(response): finished = Deferred() response.deliverBody(Accumulator(finished)) return finished d.addCallback(callback) def shutdown(failure): if hasattr(self, 'stop_loop'): self.stop_loop() elif failure is not None: # loop hasn't been initialized yet; try our best to # get an error message out. (the runner() interaction # should probably be refactored). try: failure.raiseException() except: logging.error('exception before starting loop', exc_info=True) d.addBoth(shutdown) runner() self.assertTrue(chunks) return b''.join(chunks) def twisted_coroutine_fetch(self, url, runner): body = [None] @gen.coroutine def f(): # This is simpler than the non-coroutine version, but it cheats # by reading the body in one blob instead of streaming it with # a Protocol. client = Agent(self.reactor) response = yield client.request(b'GET', utf8(url)) with warnings.catch_warnings(): # readBody has a buggy DeprecationWarning in Twisted 15.0: # https://twistedmatrix.com/trac/changeset/43379 warnings.simplefilter('ignore', category=DeprecationWarning) body[0] = yield readBody(response) self.stop_loop() self.io_loop.add_callback(f) runner() return body[0] def testTwistedServerTornadoClientReactor(self): self.start_twisted_server() response = self.tornado_fetch( 'http://127.0.0.1:%d' % self.twisted_port, self.run_reactor) self.assertEqual(response.body, b'Hello from twisted!') def testTornadoServerTwistedClientReactor(self): self.start_tornado_server() response = self.twisted_fetch( 'http://127.0.0.1:%d' % self.tornado_port, self.run_reactor) self.assertEqual(response, b'Hello from tornado!') def testTornadoServerTwistedCoroutineClientReactor(self): self.start_tornado_server() response = self.twisted_coroutine_fetch( 'http://127.0.0.1:%d' % self.tornado_port, self.run_reactor) self.assertEqual(response, b'Hello from tornado!')
def setUp(self): self.saved_signals = save_signal_handlers() self.io_loop = IOLoop() self.io_loop.make_current() self.reactor = AsyncioSelectorReactor()
async def scanner(loop: asyncio.AbstractEventLoop, outqueue: asyncio.Queue, stopevent: asyncio.Event, device: str = 'hci0', **kwargs): """Perform a continuous Bluetooth LE Scan Args: loop: async event loop outqueue: outgoing queue stopevent: stop event device: bluetooth device """ logger.info(f'>>> scanner:linux device:{device}') q = queue.Queue(QUEUE_SIZE) devices = {} cached_devices = {} rules = list() # ----------------------------------------------------------------------------- def queue_put(msg_path): try: if msg_path in devices: props = devices[msg_path] name, address, _, _ = _device_info(msg_path, props) # logger.debug(f'>>> {name} {path} {address}') if q and address: q.put( BLEDevice(address, name, { "path": msg_path, "props": props }, uuids=props.get("UUIDs", []), manufacturer_data=props.get( "ManufacturerData", {}))) except: logger.exception(f'>>> exception') # ----------------------------------------------------------------------------- def parse_msg(message): if message.member == "InterfacesAdded": logger.debug(f'>>> {message.member} {message.path}:{message.body}') msg_path = message.body[0] try: device_interface = message.body[1].get("org.bluez.Device1", {}) except Exception as e: raise e # store device devices[msg_path] = ({ **devices[msg_path], **device_interface } if msg_path in devices else device_interface) # put BLEDevice object to the queue logger.debug(f'>>> InterfacesAdded body:{msg_path}') queue_put(msg_path) elif message.member == "PropertiesChanged": logger.debug(f'>>> {message.member} {message.path}:{message.body}') msg_path = message.path iface, changed, _ = message.body if iface != DEVICE_INTERFACE: return # store changed info if msg_path not in devices and msg_path in cached_devices: devices[msg_path] = cached_devices[msg_path] devices[msg_path] = ({ **devices[msg_path], **changed } if msg_path in devices else changed) # put BLEDevice object to the queue logger.debug(f'>>> PropertiesChanged body:{msg_path}') queue_put(msg_path) elif message.member == "InterfacesRemoved": logger.debug(f'>>> {message.member} {message.path}:{message.body}') return else: msg_path = message.path logger.warning("{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body)) # ----------------------------------------------------------------------------- try: logger.info(f'>>> Starting...') # Connect to the txdbus reactor = AsyncioSelectorReactor(loop) bus = await client.connect(reactor, "system").asFuture(loop) # Add signal listeners rules.append(await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesAdded", ).asFuture(loop)) rules.append(await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesRemoved", ).asFuture(loop)) rules.append(await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.Properties", member="PropertiesChanged", ).asFuture(loop)) # Find the HCI device to use for scanning and get cached device properties objects = await bus.callRemote( "/", "GetManagedObjects", interface=OBJECT_MANAGER_INTERFACE, destination=BLUEZ_SERVICE, ).asFuture(loop) adapter_path, interface = _filter_on_adapter(objects, device) logger.info(f'>>> device:{device} adapter_path:{adapter_path}') logger.debug(f'>>> interface:{interface}') cached_devices = dict(_filter_on_device(objects)) logger.debug(f">>> cached_devices:{cached_devices}") # Running Discovery loop. await bus.callRemote( adapter_path, "SetDiscoveryFilter", interface="org.bluez.Adapter1", destination="org.bluez", signature="a{sv}", body=[{ "Transport": "le" }], ).asFuture(loop) await bus.callRemote( adapter_path, "StartDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(loop) # Run Communication loop while not stopevent.is_set(): try: l_data = q.get_nowait() if l_data and outqueue: await outqueue.put(l_data) except queue.Empty: try: await asyncio.sleep(0.1) except asyncio.CancelledError: logger.warning(f'>>> CancelledError') break except: logger.exception(f'>>> exception') break try: await bus.callRemote( adapter_path, "StopDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(loop) except RemoteError: logger.error(f'>>> RemoteError') except: logger.exception(f'>>> exception') # Stop discovery logger.info(f'>>> Disconnecting...') for rule in rules: await bus.delMatch(rule).asFuture(loop) rules.clear() # Disconnect txdbus client try: bus.disconnect() except Exception as l_e: logger.error(f'>>> Attempt to disconnect system bus failed: {l_e}') try: reactor.stop() except ReactorNotRunning as l_e: logger.error(f'>>> Attempt to stop reactor failed: {l_e}') bus = None reactor = None