Skip to content

Commit

Permalink
Merge pull request #109 from kevincar/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
kevincar authored Mar 6, 2024
2 parents 67aacd4 + 72ebf75 commit bd0bdf0
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 125 deletions.
36 changes: 33 additions & 3 deletions bless/backends/bluezdbus/dbus/advertisement.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
41 changes: 15 additions & 26 deletions bless/backends/bluezdbus/dbus/application.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

import bleak.backends.bluezdbus.defs as defs # type: ignore

from typing import List, Any, Callable, Optional, Union
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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:
"""
Expand All @@ -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

Expand All @@ -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:
"""
Expand Down
3 changes: 3 additions & 0 deletions bless/backends/bluezdbus/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions bless/backends/winrt/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 52 additions & 47 deletions examples/gattserver.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -47,48 +44,56 @@ 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
server.write_request_func = write_request

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))
Loading

0 comments on commit bd0bdf0

Please sign in to comment.