Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
recursive-include bless py.typed
17 changes: 11 additions & 6 deletions bless/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@

# Service
from bless.backends.corebluetooth.service import ( # noqa: F401
BlessGATTServiceCoreBluetooth as BlessGATTService
BlessGATTServiceCoreBluetooth as BlessGATTService,
)

# Characteristic Classes
from bless.backends.corebluetooth.characteristic import ( # noqa: F401
BlessGATTCharacteristicCoreBluetooth as BlessGATTCharacteristic,
)

# Descriptor Classes
from bless.backends.corebluetooth.descriptor import ( # noqa: F401
BlessGATTDescriptorCoreBluetooth as BlessGATTDescriptor,
)

elif sys.platform == "linux":

# Server
Expand All @@ -30,7 +35,7 @@

# Service
from bless.backends.bluezdbus.service import ( # noqa: F401
BlessGATTServiceBlueZDBus as BlessGATTService
BlessGATTServiceBlueZDBus as BlessGATTService,
)

# Characteristic Classes
Expand All @@ -52,29 +57,29 @@

# Service
from bless.backends.winrt.service import ( # noqa: F401
BlessGATTServiceWinRT as BlessGATTService
BlessGATTServiceWinRT as BlessGATTService,
)

# Characteristic Classes
from bless.backends.winrt.characteristic import ( # noqa: F401
BlessGATTCharacteristicWinRT as BlessGATTCharacteristic,
)

# type: ignore
from bless.backends.attribute import ( # noqa: E402 F401
GATTAttributePermissions,
)

# type: ignore
from bless.backends.characteristic import ( # noqa: E402 F401
GATTCharacteristicProperties,
)

# type: ignore
from bless.backends.descriptor import ( # noqa: E402 F401
GATTDescriptorProperties,
)

from bless.backends.request import BlessGATTRequest # noqa: F401
from bless.backends.session import BlessGATTSession # noqa: F401


def check_test() -> bool:
"""
Expand Down
38 changes: 36 additions & 2 deletions bless/backends/bluezdbus/characteristic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from uuid import UUID

from typing import Union, Optional, List, Dict, cast, TYPE_CHECKING, Literal
from typing import Dict, List, Literal, Optional, Set, TYPE_CHECKING, Union, cast

from bleak.backends.characteristic import ( # type: ignore
BleakGATTCharacteristic,
Expand All @@ -11,6 +11,9 @@
from bless.backends.characteristic import (
BlessGATTCharacteristic,
GATTCharacteristicProperties,
GATTReadCallback,
GATTWriteCallback,
GATTSubscribeCallback,
)
from bless.backends.bluezdbus.dbus.characteristic import (
Flags,
Expand Down Expand Up @@ -42,6 +45,7 @@ class BlessGATTCharacteristicBlueZDBus(
"""
BlueZ implementation of the BlessGATTCharacteristic
"""

gatt: "BlueZGattCharacteristic"

def __init__(
Expand All @@ -50,6 +54,10 @@ def __init__(
properties: GATTCharacteristicProperties,
permissions: GATTAttributePermissions,
value: Optional[bytearray],
on_read: Optional[GATTReadCallback] = None,
on_write: Optional[GATTWriteCallback] = None,
on_subscribe: Optional[GATTSubscribeCallback] = None,
on_unsubscribe: Optional[GATTSubscribeCallback] = None,
):
"""
Instantiates a new GATT Characteristic but is not yet assigned to any
Expand All @@ -66,9 +74,31 @@ def __init__(
Permissions that define the protection levels of the properties
value : Optional[bytearray]
The binary value of the characteristic
on_read : Optional[GATTReadCallback]
If defined, reads destined for this characteristic will be passed
to this function
on_write : Optional[GATTWriteCallback]
If defined, writes destined for this characteristic will be passed
to this function
on_subscribe : Optional[GATTSubscribeCallback]
If defined, subscriptions destined for this characteristic will be
passed to this function
on_unsubscribe : Optional[GATTSubscribeCallback]
If defined, unsubscriptions destined for this characteristic will
be passed to this function
"""
value = value if value is not None else bytearray(b"")
BlessGATTCharacteristic.__init__(self, uuid, properties, permissions, value)
BlessGATTCharacteristic.__init__(
self,
uuid,
properties,
permissions,
value,
on_read,
on_write,
on_subscribe,
on_unsubscribe,
)
self._value = value
self._descriptors: Dict[int, BleakGATTDescriptor] = {}

Expand Down Expand Up @@ -142,6 +172,10 @@ def description(self) -> str:
"""Description of this characteristic"""
return f"Characteristic {self._uuid}"

@property
def subscribed_centrals(self) -> Set[str]:
return set(list(self.obj._subscribed_centrals.keys()))


def transform_flags_with_permissions(
flag: Flags, permissions: GATTAttributePermissions
Expand Down
4 changes: 3 additions & 1 deletion bless/backends/bluezdbus/dbus/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
)
from bless.backends.bluezdbus.dbus.descriptor import BlueZGattDescriptor # type: ignore

from .session import NotifySession

LOGGER = logging.getLogger(__name__)

ReadCallback = Callable[[BlueZGattCharacteristic, Dict[str, Any]], bytes]
WriteCallback = Callable[[BlueZGattCharacteristic, bytes, Dict[str, Any]], None]
SubscribeCallback = Callable[[BlueZGattCharacteristic, Dict[str, Any]], None]
SubscribeCallback = Callable[[BlueZGattCharacteristic, NotifySession], None]


class BlueZGattApplication(ServiceInterface):
Expand Down
28 changes: 21 additions & 7 deletions bless/backends/bluezdbus/dbus/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import bleak.backends.bluezdbus.defs as defs # type: ignore

from dbus_next import DBusError # type: ignore
from dbus_next.aio import ProxyInterface # type: ignore
from dbus_next.constants import PropertyAccess # type: ignore
from dbus_next.service import ServiceInterface, method, dbus_property # type: ignore
from dbus_next.signature import Variant # type: ignore
from enum import Enum
from typing import List, TYPE_CHECKING, Any, Dict
from typing import List, TYPE_CHECKING, Any, Dict, cast

from .descriptor import BlueZGattDescriptor, DescriptorFlags # type: ignore
from .device import Device1
from .session import NotifySession # type: ignore

if TYPE_CHECKING:
Expand Down Expand Up @@ -111,7 +113,7 @@ def NotifyAcquired(self) -> "b": # type: ignore # noqa: F821
return len(self._subscribed_centrals) > 0

@method() # noqa: F722
def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821 N802 E501
async def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821 N802 E501
"""
Read the value of the characteristic.
This is to be fully implemented at the application level
Expand All @@ -126,6 +128,12 @@ def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821
bytes
The bytes that is the value of the characteristic
"""
device_path: str = options["device"]
device_interface: ProxyInterface = Device1.get_device(
self._service.bus, device_path
)
device: Device1 = cast(Device1, device_interface)
options["central_id"] = await device.get_address()
f = self._service.app.Read
if f is None:
raise NotImplementedError()
Expand All @@ -144,6 +152,12 @@ def WriteValue(self, value: "ay", options: "a{sv}"): # type: ignore # noqa
options : Dict
Some options for you to select from
"""
device_path: str = options["device"]
device_interface: ProxyInterface = Device1.get_device(
self._service.bus, device_path
)
device: Device1 = cast(Device1, device_interface)
options["central_id"] = device.get_address()
f = self._service.app.Write
if f is None:
raise NotImplementedError()
Expand Down Expand Up @@ -176,14 +190,14 @@ async def AcquireNotify(self, options: "a{sv}") -> "hq": # type: ignore # noqa
f = self._service.app.StartNotify
if f is None:
raise NotImplementedError()
f(self, {"device": address})

f(self, session)
self._subscribed_centrals[address] = session

async def close_rx():
logger.debug("Closing RX")
await asyncio.sleep(2)
os.close(rx)
# asyncio.get_running_loop().call_soon_threadsafe(os.close, rx)

asyncio.create_task(close_rx())
return [rx, mtu]
Expand All @@ -194,7 +208,7 @@ async def ReleaseNotify(self, session: NotifySession):
f = self._service.app.StopNotify
if f is None:
raise NotImplementedError()
f(self, {"device": address})
f(self, session)
del self._subscribed_centrals[address]

@method()
Expand All @@ -214,7 +228,7 @@ def StartNotify(self): # noqa: N802
f = self._service.app.StartNotify
if f is None:
raise NotImplementedError()
f(self, {})
f(self, None) # type: ignore
self._notifying_calls += 1

@method()
Expand All @@ -229,7 +243,7 @@ async def StopNotify(self): # noqa: N802
f = self._service.app.StopNotify
if f is None:
raise NotImplementedError()
f(self, {})
f(self, {}) # type: ignore
self._notifying_calls -= 1

def update_value(self) -> None:
Expand Down
16 changes: 16 additions & 0 deletions bless/backends/bluezdbus/dbus/device.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from bleak.backends.bluezdbus.defs import DEVICE_INTERFACE
from dbus_next.aio import MessageBus, ProxyInterface, ProxyObject # type: ignore
from dbus_next.constants import PropertyAccess # type: ignore
from dbus_next.introspection import Interface, Node # type: ignore
from dbus_next.service import ServiceInterface, method, dbus_property # type: ignore


Expand Down Expand Up @@ -145,3 +147,17 @@ def ServicesResolved(self) -> "a{say}": # type: ignore # noqa: F722
@dbus_property(access=PropertyAccess.READ)
def AdvertisingFlags(self) -> "ay": # type: ignore # noqa: F722 F821
raise NotImplementedError

@classmethod
def get_device(cls, bus: MessageBus, path: str) -> ProxyInterface:
# Query the device object
node: Node = Node.default(name=path)
device_iface: Interface = Device1().introspect()
node.interfaces.append(device_iface)

object: ProxyObject = bus.get_proxy_object("org.bluez", path, node)
return object.get_interface(DEVICE_INTERFACE)

# For typing
async def get_address(self) -> str:
raise NotImplementedError
25 changes: 11 additions & 14 deletions bless/backends/bluezdbus/dbus/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
import logging
import os

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

from asyncio import AbstractEventLoop, Event
from dbus_next.aio import ProxyInterface, ProxyObject, MessageBus # type: ignore
from dbus_next.introspection import Interface, Node # type: ignore
from dbus_next.aio import ProxyInterface, MessageBus # type: ignore
from select import poll, POLLHUP, POLLERR, POLLNVAL
from socket import socket, socketpair, AF_UNIX, SOCK_SEQPACKET
from typing import Callable, Coroutine, Optional, Union
Expand Down Expand Up @@ -39,6 +36,14 @@ def __init__(

self._tx: Optional[socket] = None
self._device: Optional[ProxyInterface] = None
self._address: Optional[str] = None

@property
def address(self) -> str:
if self._address is None:
raise Exception("NotifySession not started. Address property not obtained")

return self._address

def get_device(self) -> ProxyInterface:
if self._device is None:
Expand All @@ -63,16 +68,8 @@ async def watch_fd(self) -> None:
self.close()

async def start(self) -> int:

# Query the device object
node: Node = Node.default(name=self.device_path)
device_iface: Interface = Device1().introspect()
node.interfaces.append(device_iface)

object: ProxyObject = self.bus.get_proxy_object(
"org.bluez", self.device_path, node
)
self._device = object.get_interface(defs.DEVICE_INTERFACE)
self._device = Device1.get_device(self.bus, self.device_path)
self._address = await self.get_device_address()

# create a bluetooth socket pair
self._tx, rx = socketpair(AF_UNIX, SOCK_SEQPACKET)
Expand Down
31 changes: 31 additions & 0 deletions bless/backends/bluezdbus/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Dict, Optional, cast
from ..request import BlessGATTRequest


class BlessGATTRequestBlueZ(BlessGATTRequest):

@property
def options(self) -> Dict:
return cast(Dict, self.obj)

@property
def central_id(self) -> str:
"""
Note, that the device returned within the options on
BlueZ is a DBus path to the device object. It is the
receving calls responsibility to use the DBus to resolve
the device address and populate this field
"""
return self.obj["central_id"]

@property
def mtu(self) -> int:
return self.obj["mtu"]

@property
def offset(self) -> int:
return self.obj["offset"]

@property
def response_requested(self) -> Optional[bool]:
return None if "type" not in self.obj else (self.obj["type"] == "request")
Loading