Beispiel #1
0
 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()
Beispiel #2
0
    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)
Beispiel #4
0
 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()
Beispiel #5
0
    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)
Beispiel #6
0
    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)
Beispiel #7
0
    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())
Beispiel #8
0
 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)
Beispiel #9
0
 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)
Beispiel #10
0
 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)
Beispiel #11
0
 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)
Beispiel #12
0
 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)
Beispiel #13
0
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()
Beispiel #14
0
    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)
Beispiel #16
0
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])
Beispiel #18
0
    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')
Beispiel #21
0
 def setUp(self):
     self.saved_signals = save_signal_handlers()
     self.io_loop = IOLoop()
     self.io_loop.make_current()
     self.reactor = AsyncioSelectorReactor()
Beispiel #22
0
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!")
Beispiel #23
0
 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)
Beispiel #24
0
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)
Beispiel #25
0
    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
Beispiel #26
0
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
Beispiel #27
0
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))
Beispiel #28
0
    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()
Beispiel #31
0
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