diff --git a/bless/backends/bluezdbus/dbus/advertisement.py b/bless/backends/bluezdbus/dbus/advertisement.py index 553a0e3..345c3e2 100644 --- a/bless/backends/bluezdbus/dbus/advertisement.py +++ b/bless/backends/bluezdbus/dbus/advertisement.py @@ -20,6 +20,10 @@ class BlueZLEAdvertisement(ServiceInterface): org.bluez.LEAdvertisement1 interface implementation https://github.com/bluez/bluez/blob/5.64/doc/advertising-api.txt + https://python-dbus-next.readthedocs.io/en/latest/type-system/index.html + https://elixir.bootlin.com/linux/v5.11/source/include/net/bluetooth/mgmt.h#L794 + https://github.com/bluez/bluez/issues/527 + https://patches.linaro.org/project/linux-bluetooth/list/?series=31700 """ interface_name: str = "org.bluez.LEAdvertisement1" @@ -50,7 +54,17 @@ def __init__( self._solicit_uuids: List[str] = [""] self._service_data: Dict = {} - self._tx_power: int = 20 + # 3 options below are classified as Experimental in BlueZ and really + # work only: - when BlueZ is compiled with such option (usually it is) + # - and when "bluetoothd" daemon is started with -E, --experimental + # option (usually it's not) They are taken into account only with + # Kernel v5.11+ and BlueZ v5.65+. It's a known fact that BlueZ verions + # 5.63-5.64 have broken Dbus part for LEAdvertisingManager and do not + # work properly when the Experimental mode is enabled. + self._min_interval: int = 100 # in ms, range [20ms, 10,485s] + self._max_interval: int = 100 # in ms, range [20ms, 10,485s] + self._tx_power: int = 20 # range [-127 to +20] + self._local_name = app.app_name self.data = None @@ -86,11 +100,11 @@ def ManufacturerData(self, data: "a{qv}"): # type: ignore # noqa: F821 F722 N80 # @dbus_property() # def SolicitUUIDs(self) -> "as": # type: ignore # noqa: F821 F722 - # return self._solicit_uuids + # return self._solicit_uuids # @SolicitUUIDs.setter # type: ignore # def SolicitUUIDs(self, uuids: "as"): # type: ignore # noqa: F821 F722 - # self._solicit_uuids = uuids + # self._solicit_uuids = uuids @dbus_property() # noqa: F722 def ServiceData(self) -> "a{sv}": # type: ignore # noqa: F821 F722 N802 @@ -116,6 +130,22 @@ def TxPower(self) -> "n": # type: ignore # noqa: F821 N802 def TxPower(self, dbm: "n"): # type: ignore # noqa: F821 N802 self._tx_power = dbm + @dbus_property() + def MaxInterval(self) -> "u": # type: ignore # noqa: F821 N802 + return self._max_interval + + @MaxInterval.setter # type: ignore + def MaxInterval(self, interval: "u"): # type: ignore # noqa: F821 N802 + self._max_interval = interval + + @dbus_property() + def MinInterval(self) -> "u": # type: ignore # noqa: F821 N802 + return self._min_interval + + @MinInterval.setter # type: ignore + def MinInterval(self, interval: "u"): # type: ignore # noqa: F821 N802 + self._min_interval = interval + @dbus_property() def LocalName(self) -> "s": # type: ignore # noqa: F821 N802 return self._local_name diff --git a/bless/backends/bluezdbus/dbus/application.py b/bless/backends/bluezdbus/dbus/application.py index 6885161..45492d0 100644 --- a/bless/backends/bluezdbus/dbus/application.py +++ b/bless/backends/bluezdbus/dbus/application.py @@ -1,3 +1,5 @@ +import re + import bleak.backends.bluezdbus.defs as defs # type: ignore from typing import List, Any, Callable, Optional, Union @@ -22,12 +24,7 @@ class BlueZGattApplication(ServiceInterface): org.bluez.GattApplication1 interface implementation """ - def __init__( - self, - name: str, - destination: str, - bus: MessageBus - ): + def __init__(self, name: str, destination: str, bus: MessageBus): """ Initialize a new GattApplication1 @@ -45,14 +42,15 @@ def __init__( self.destination: str = destination self.bus: MessageBus = bus - self.base_path: str = "/org/bluez/" + self.app_name.replace(" ", "") + # Valid path must be ASCII characters "[A-Z][a-z][0-9]_" + # see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path # noqa E501 + + self.base_path: str = "/org/bluez/" + re.sub("[^A-Za-z0-9_]", "", self.app_name) self.advertisements: List[BlueZLEAdvertisement] = [] self.services: List[BlueZGattService] = [] self.Read: Optional[Callable[[BlueZGattCharacteristic], bytes]] = None - self.Write: Optional[ - Callable[[BlueZGattCharacteristic, bytes], None] - ] = None + self.Write: Optional[Callable[[BlueZGattCharacteristic, bytes], None]] = None self.StartNotify: Optional[Callable[[None], None]] = None self.StopNotify: Optional[Callable[[None], None]] = None @@ -124,7 +122,7 @@ async def set_name(self, adapter: ProxyObject, name: str): """ iface: ProxyInterface = adapter.get_interface("org.freedesktop.DBus.Properties") await iface.call_set( # type: ignore - "org.bluez.Adapter1", "Alias", Variant('s', name) + "org.bluez.Adapter1", "Alias", Variant("s", name) ) async def register(self, adapter: ProxyObject): @@ -137,10 +135,7 @@ async def register(self, adapter: ProxyObject): The adapter to register the application with """ iface: ProxyInterface = adapter.get_interface(defs.GATT_MANAGER_INTERFACE) - await iface.call_register_application( # type: ignore - self.path, - {} - ) + await iface.call_register_application(self.path, {}) # type: ignore async def unregister(self, adapter: ProxyObject): """ @@ -152,9 +147,7 @@ async def unregister(self, adapter: ProxyObject): The adapter on which the current application is registered """ iface: ProxyInterface = adapter.get_interface(defs.GATT_MANAGER_INTERFACE) - await iface.call_unregister_application( # type: ignore - self.path - ) + await iface.call_unregister_application(self.path) # type: ignore async def start_advertising(self, adapter: ProxyObject): """ @@ -178,9 +171,7 @@ async def start_advertising(self, adapter: ProxyObject): self.bus.export(advertisement.path, advertisement) iface: ProxyInterface = adapter.get_interface("org.bluez.LEAdvertisingManager1") - await iface.call_register_advertisement( # type: ignore - advertisement.path, {} - ) + await iface.call_register_advertisement(advertisement.path, {}) # type: ignore async def is_advertising(self, adapter: ProxyObject) -> bool: """ @@ -198,8 +189,7 @@ async def is_advertising(self, adapter: ProxyObject) -> bool: """ iface: ProxyInterface = adapter.get_interface(defs.PROPERTIES_INTERFACE) instances: Variant = await iface.call_get( # type: ignore - "org.bluez.LEAdvertisingManager1", - "ActiveInstances" + "org.bluez.LEAdvertisingManager1", "ActiveInstances" ) return instances.value > 0 @@ -215,9 +205,8 @@ async def stop_advertising(self, adapter: ProxyObject): await self.set_name(adapter, "") advertisement: BlueZLEAdvertisement = self.advertisements.pop() iface: ProxyInterface = adapter.get_interface("org.bluez.LEAdvertisingManager1") - await iface.call_unregister_advertisement( # type: ignore - advertisement.path - ) + await iface.call_unregister_advertisement(advertisement.path) # type: ignore + self.bus.unexport(advertisement.path) async def is_connected(self) -> bool: """ diff --git a/bless/backends/bluezdbus/server.py b/bless/backends/bluezdbus/server.py index 3115d5e..bba77ba 100644 --- a/bless/backends/bluezdbus/server.py +++ b/bless/backends/bluezdbus/server.py @@ -106,6 +106,9 @@ async def stop(self) -> bool: # Unregister await self.app.unregister(self.adapter) + # Remove our App + self.bus.unexport(self.app.path, self.app) + return True async def is_connected(self) -> bool: diff --git a/bless/backends/winrt/server.py b/bless/backends/winrt/server.py index 8a34f1f..16d474e 100644 --- a/bless/backends/winrt/server.py +++ b/bless/backends/winrt/server.py @@ -152,14 +152,14 @@ async def is_advertising(self) -> bool: bool True if advertising """ - all_services_advertising: bool = True + all_services_advertising: bool = False for uuid, service in self.services.items(): winrt_service: BlessGATTServiceWinRT = cast(BlessGATTServiceWinRT, service) service_is_advertising: bool = ( winrt_service.service_provider.advertisement_status == 2 ) all_services_advertising = ( - all_services_advertising and service_is_advertising + all_services_advertising or service_is_advertising ) return self._advertising and all_services_advertising diff --git a/examples/gattserver.py b/examples/gattserver.py index ef73fa6..ff4ddae 100644 --- a/examples/gattserver.py +++ b/examples/gattserver.py @@ -1,43 +1,40 @@ - """ Example for a BLE 4.0 Server using a GATT dictionary of services and characteristics """ - +import sys import logging import asyncio import threading -from typing import Any, Dict +from typing import Any, Dict, Union from bless import ( # type: ignore - BlessServer, - BlessGATTCharacteristic, - GATTCharacteristicProperties, - GATTAttributePermissions - ) + BlessServer, + BlessGATTCharacteristic, + GATTCharacteristicProperties, + GATTAttributePermissions, +) logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(name=__name__) -trigger: threading.Event = threading.Event() + +trigger: Union[asyncio.Event, threading.Event] +if sys.platform in ["darwin", "win32"]: + trigger = threading.Event() +else: + trigger = asyncio.Event() -def read_request( - characteristic: BlessGATTCharacteristic, - **kwargs - ) -> bytearray: +def read_request(characteristic: BlessGATTCharacteristic, **kwargs) -> bytearray: logger.debug(f"Reading {characteristic.value}") return characteristic.value -def write_request( - characteristic: BlessGATTCharacteristic, - value: Any, - **kwargs - ): +def write_request(characteristic: BlessGATTCharacteristic, value: Any, **kwargs): characteristic.value = value logger.debug(f"Char value set to {characteristic.value}") - if characteristic.value == b'\x0f': + if characteristic.value == b"\x0f": logger.debug("Nice") trigger.set() @@ -47,24 +44,28 @@ async def run(loop): # Instantiate the server gatt: Dict = { - "A07498CA-AD5B-474E-940D-16F1FBE7E8CD": { - "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B": { - "Properties": (GATTCharacteristicProperties.read | - GATTCharacteristicProperties.write | - GATTCharacteristicProperties.indicate), - "Permissions": (GATTAttributePermissions.readable | - GATTAttributePermissions.writeable), - "Value": None - } - }, - "5c339364-c7be-4f23-b666-a8ff73a6a86a": { - "bfc0c92f-317d-4ba9-976b-cc11ce77b4ca": { - "Properties": GATTCharacteristicProperties.read, - "Permissions": GATTAttributePermissions.readable, - "Value": bytearray(b'\x69') - } + "A07498CA-AD5B-474E-940D-16F1FBE7E8CD": { + "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B": { + "Properties": ( + GATTCharacteristicProperties.read + | GATTCharacteristicProperties.write + | GATTCharacteristicProperties.indicate + ), + "Permissions": ( + GATTAttributePermissions.readable + | GATTAttributePermissions.writeable + ), + "Value": None, } - } + }, + "5c339364-c7be-4f23-b666-a8ff73a6a86a": { + "bfc0c92f-317d-4ba9-976b-cc11ce77b4ca": { + "Properties": GATTCharacteristicProperties.read, + "Permissions": GATTAttributePermissions.readable, + "Value": bytearray(b"\x69"), + } + }, + } my_service_name = "Test Service" server = BlessServer(name=my_service_name, loop=loop) server.read_request_func = read_request @@ -72,23 +73,27 @@ async def run(loop): await server.add_gatt(gatt) await server.start() - logger.debug(server.get_characteristic( - "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B")) + logger.debug(server.get_characteristic("51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B")) logger.debug("Advertising") - logger.info("Write '0xF' to the advertised characteristic: " + - "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B") - trigger.wait() + logger.info( + "Write '0xF' to the advertised characteristic: " + + "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B" + ) + if trigger.__module__ == "threading": + trigger.wait() + else: + await trigger.wait() await asyncio.sleep(2) logger.debug("Updating") - server.get_characteristic("51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B").value = ( - bytearray(b"i") - ) + server.get_characteristic("51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B").value = bytearray( + b"i" + ) server.update_value( - "A07498CA-AD5B-474E-940D-16F1FBE7E8CD", - "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B" - ) + "A07498CA-AD5B-474E-940D-16F1FBE7E8CD", "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B" + ) await asyncio.sleep(5) await server.stop() + loop = asyncio.get_event_loop() loop.run_until_complete(run(loop)) diff --git a/examples/server.py b/examples/server.py index 158bcc8..14b07af 100644 --- a/examples/server.py +++ b/examples/server.py @@ -1,42 +1,40 @@ - """ Example for a BLE 4.0 Server """ - +import sys import logging import asyncio import threading -from typing import Any +from typing import Any, Union from bless import ( # type: ignore - BlessServer, - BlessGATTCharacteristic, - GATTCharacteristicProperties, - GATTAttributePermissions - ) + BlessServer, + BlessGATTCharacteristic, + GATTCharacteristicProperties, + GATTAttributePermissions, +) logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(name=__name__) -trigger: threading.Event = threading.Event() + +# NOTE: Some systems require different synchronization methods. +trigger: Union[asyncio.Event, threading.Event] +if sys.platform in ["darwin", "win32"]: + trigger = threading.Event() +else: + trigger = asyncio.Event() -def read_request( - characteristic: BlessGATTCharacteristic, - **kwargs - ) -> bytearray: +def read_request(characteristic: BlessGATTCharacteristic, **kwargs) -> bytearray: logger.debug(f"Reading {characteristic.value}") return characteristic.value -def write_request( - characteristic: BlessGATTCharacteristic, - value: Any, - **kwargs - ): +def write_request(characteristic: BlessGATTCharacteristic, value: Any, **kwargs): characteristic.value = value logger.debug(f"Char value set to {characteristic.value}") - if characteristic.value == b'\x0f': + if characteristic.value == b"\x0f": logger.debug("NICE") trigger.set() @@ -56,38 +54,31 @@ async def run(loop): # Add a Characteristic to the service my_char_uuid = "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B" char_flags = ( - GATTCharacteristicProperties.read | - GATTCharacteristicProperties.write | - GATTCharacteristicProperties.indicate - ) - permissions = ( - GATTAttributePermissions.readable | - GATTAttributePermissions.writeable - ) + GATTCharacteristicProperties.read + | GATTCharacteristicProperties.write + | GATTCharacteristicProperties.indicate + ) + permissions = GATTAttributePermissions.readable | GATTAttributePermissions.writeable await server.add_new_characteristic( - my_service_uuid, - my_char_uuid, - char_flags, - None, - permissions) - - logger.debug( - server.get_characteristic( - my_char_uuid - ) - ) + my_service_uuid, my_char_uuid, char_flags, None, permissions + ) + + logger.debug(server.get_characteristic(my_char_uuid)) await server.start() logger.debug("Advertising") logger.info(f"Write '0xF' to the advertised characteristic: {my_char_uuid}") - trigger.wait() + if trigger.__module__ == "threading": + trigger.wait() + else: + await trigger.wait() + await asyncio.sleep(2) logger.debug("Updating") server.get_characteristic(my_char_uuid) - server.update_value( - my_service_uuid, "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B" - ) + server.update_value(my_service_uuid, "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B") await asyncio.sleep(5) await server.stop() + loop = asyncio.get_event_loop() loop.run_until_complete(run(loop)) diff --git a/setup.py b/setup.py index 66c1f59..e10bdba 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="bless", - version="0.2.5", + version="0.2.6", author="Kevin Davis", author_email="kevincarrolldavis@gmail.com", description="A Bluetooth Low Energy Server supplement to Bleak", @@ -15,17 +15,16 @@ package_data={"bless": ["py.typed"]}, packages=setuptools.find_packages(exclude=("test", "examples")), include_package_data=True, - dependency_links=[ - "https://github.com/gwangyi/pysetupdi#egg=pysetupdi" - ], install_requires=[ "bleak", "pywin32;platform_system=='Windows'", + "dbus_next;platform_system=='Linux'", + "pysetupdi @ git+https://github.com/gwangyi/pysetupdi#egg=pysetupdi;platform_system=='Windows'", # noqa: E501 ], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], - python_requires='>=3.7', + python_requires=">=3.7", )