Skip to content

Commit 6bef5ce

Browse files
authored
Merge pull request #170 from kevincar/132-more-information-on-client-for-callback-read-and-write
132 more information on client for callback read and write
2 parents 2a40a43 + e58c626 commit 6bef5ce

35 files changed

+974
-313
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
recursive-include bless py.typed

bless/__init__.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@
1313

1414
# Service
1515
from bless.backends.corebluetooth.service import ( # noqa: F401
16-
BlessGATTServiceCoreBluetooth as BlessGATTService
16+
BlessGATTServiceCoreBluetooth as BlessGATTService,
1717
)
1818

1919
# Characteristic Classes
2020
from bless.backends.corebluetooth.characteristic import ( # noqa: F401
2121
BlessGATTCharacteristicCoreBluetooth as BlessGATTCharacteristic,
2222
)
2323

24+
# Descriptor Classes
25+
from bless.backends.corebluetooth.descriptor import ( # noqa: F401
26+
BlessGATTDescriptorCoreBluetooth as BlessGATTDescriptor,
27+
)
28+
2429
elif sys.platform == "linux":
2530

2631
# Server
@@ -30,7 +35,7 @@
3035

3136
# Service
3237
from bless.backends.bluezdbus.service import ( # noqa: F401
33-
BlessGATTServiceBlueZDBus as BlessGATTService
38+
BlessGATTServiceBlueZDBus as BlessGATTService,
3439
)
3540

3641
# Characteristic Classes
@@ -52,29 +57,29 @@
5257

5358
# Service
5459
from bless.backends.winrt.service import ( # noqa: F401
55-
BlessGATTServiceWinRT as BlessGATTService
60+
BlessGATTServiceWinRT as BlessGATTService,
5661
)
5762

5863
# Characteristic Classes
5964
from bless.backends.winrt.characteristic import ( # noqa: F401
6065
BlessGATTCharacteristicWinRT as BlessGATTCharacteristic,
6166
)
6267

63-
# type: ignore
6468
from bless.backends.attribute import ( # noqa: E402 F401
6569
GATTAttributePermissions,
6670
)
6771

68-
# type: ignore
6972
from bless.backends.characteristic import ( # noqa: E402 F401
7073
GATTCharacteristicProperties,
7174
)
7275

73-
# type: ignore
7476
from bless.backends.descriptor import ( # noqa: E402 F401
7577
GATTDescriptorProperties,
7678
)
7779

80+
from bless.backends.request import BlessGATTRequest # noqa: F401
81+
from bless.backends.session import BlessGATTSession # noqa: F401
82+
7883

7984
def check_test() -> bool:
8085
"""

bless/backends/bluezdbus/characteristic.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from uuid import UUID
22

3-
from typing import Union, Optional, List, Dict, cast, TYPE_CHECKING, Literal
3+
from typing import Dict, List, Literal, Optional, Set, TYPE_CHECKING, Union, cast
44

55
from bleak.backends.characteristic import ( # type: ignore
66
BleakGATTCharacteristic,
@@ -11,6 +11,9 @@
1111
from bless.backends.characteristic import (
1212
BlessGATTCharacteristic,
1313
GATTCharacteristicProperties,
14+
GATTReadCallback,
15+
GATTWriteCallback,
16+
GATTSubscribeCallback,
1417
)
1518
from bless.backends.bluezdbus.dbus.characteristic import (
1619
Flags,
@@ -42,6 +45,7 @@ class BlessGATTCharacteristicBlueZDBus(
4245
"""
4346
BlueZ implementation of the BlessGATTCharacteristic
4447
"""
48+
4549
gatt: "BlueZGattCharacteristic"
4650

4751
def __init__(
@@ -50,6 +54,10 @@ def __init__(
5054
properties: GATTCharacteristicProperties,
5155
permissions: GATTAttributePermissions,
5256
value: Optional[bytearray],
57+
on_read: Optional[GATTReadCallback] = None,
58+
on_write: Optional[GATTWriteCallback] = None,
59+
on_subscribe: Optional[GATTSubscribeCallback] = None,
60+
on_unsubscribe: Optional[GATTSubscribeCallback] = None,
5361
):
5462
"""
5563
Instantiates a new GATT Characteristic but is not yet assigned to any
@@ -66,9 +74,31 @@ def __init__(
6674
Permissions that define the protection levels of the properties
6775
value : Optional[bytearray]
6876
The binary value of the characteristic
77+
on_read : Optional[GATTReadCallback]
78+
If defined, reads destined for this characteristic will be passed
79+
to this function
80+
on_write : Optional[GATTWriteCallback]
81+
If defined, writes destined for this characteristic will be passed
82+
to this function
83+
on_subscribe : Optional[GATTSubscribeCallback]
84+
If defined, subscriptions destined for this characteristic will be
85+
passed to this function
86+
on_unsubscribe : Optional[GATTSubscribeCallback]
87+
If defined, unsubscriptions destined for this characteristic will
88+
be passed to this function
6989
"""
7090
value = value if value is not None else bytearray(b"")
71-
BlessGATTCharacteristic.__init__(self, uuid, properties, permissions, value)
91+
BlessGATTCharacteristic.__init__(
92+
self,
93+
uuid,
94+
properties,
95+
permissions,
96+
value,
97+
on_read,
98+
on_write,
99+
on_subscribe,
100+
on_unsubscribe,
101+
)
72102
self._value = value
73103
self._descriptors: Dict[int, BleakGATTDescriptor] = {}
74104

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

175+
@property
176+
def subscribed_centrals(self) -> Set[str]:
177+
return set(list(self.obj._subscribed_centrals.keys()))
178+
145179

146180
def transform_flags_with_permissions(
147181
flag: Flags, permissions: GATTAttributePermissions

bless/backends/bluezdbus/dbus/application.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
)
2424
from bless.backends.bluezdbus.dbus.descriptor import BlueZGattDescriptor # type: ignore
2525

26+
from .session import NotifySession
27+
2628
LOGGER = logging.getLogger(__name__)
2729

2830
ReadCallback = Callable[[BlueZGattCharacteristic, Dict[str, Any]], bytes]
2931
WriteCallback = Callable[[BlueZGattCharacteristic, bytes, Dict[str, Any]], None]
30-
SubscribeCallback = Callable[[BlueZGattCharacteristic, Dict[str, Any]], None]
32+
SubscribeCallback = Callable[[BlueZGattCharacteristic, NotifySession], None]
3133

3234

3335
class BlueZGattApplication(ServiceInterface):

bless/backends/bluezdbus/dbus/characteristic.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import bleak.backends.bluezdbus.defs as defs # type: ignore
66

77
from dbus_next import DBusError # type: ignore
8+
from dbus_next.aio import ProxyInterface # type: ignore
89
from dbus_next.constants import PropertyAccess # type: ignore
910
from dbus_next.service import ServiceInterface, method, dbus_property # type: ignore
1011
from dbus_next.signature import Variant # type: ignore
1112
from enum import Enum
12-
from typing import List, TYPE_CHECKING, Any, Dict
13+
from typing import List, TYPE_CHECKING, Any, Dict, cast
1314

1415
from .descriptor import BlueZGattDescriptor, DescriptorFlags # type: ignore
16+
from .device import Device1
1517
from .session import NotifySession # type: ignore
1618

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

113115
@method() # noqa: F722
114-
def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821 N802 E501
116+
async def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821 N802 E501
115117
"""
116118
Read the value of the characteristic.
117119
This is to be fully implemented at the application level
@@ -126,6 +128,12 @@ def ReadValue(self, options: "a{sv}") -> "ay": # type: ignore # noqa: F722 F821
126128
bytes
127129
The bytes that is the value of the characteristic
128130
"""
131+
device_path: str = options["device"]
132+
device_interface: ProxyInterface = Device1.get_device(
133+
self._service.bus, device_path
134+
)
135+
device: Device1 = cast(Device1, device_interface)
136+
options["central_id"] = await device.get_address()
129137
f = self._service.app.Read
130138
if f is None:
131139
raise NotImplementedError()
@@ -144,6 +152,12 @@ def WriteValue(self, value: "ay", options: "a{sv}"): # type: ignore # noqa
144152
options : Dict
145153
Some options for you to select from
146154
"""
155+
device_path: str = options["device"]
156+
device_interface: ProxyInterface = Device1.get_device(
157+
self._service.bus, device_path
158+
)
159+
device: Device1 = cast(Device1, device_interface)
160+
options["central_id"] = device.get_address()
147161
f = self._service.app.Write
148162
if f is None:
149163
raise NotImplementedError()
@@ -176,14 +190,14 @@ async def AcquireNotify(self, options: "a{sv}") -> "hq": # type: ignore # noqa
176190
f = self._service.app.StartNotify
177191
if f is None:
178192
raise NotImplementedError()
179-
f(self, {"device": address})
193+
194+
f(self, session)
180195
self._subscribed_centrals[address] = session
181196

182197
async def close_rx():
183198
logger.debug("Closing RX")
184199
await asyncio.sleep(2)
185200
os.close(rx)
186-
# asyncio.get_running_loop().call_soon_threadsafe(os.close, rx)
187201

188202
asyncio.create_task(close_rx())
189203
return [rx, mtu]
@@ -194,7 +208,7 @@ async def ReleaseNotify(self, session: NotifySession):
194208
f = self._service.app.StopNotify
195209
if f is None:
196210
raise NotImplementedError()
197-
f(self, {"device": address})
211+
f(self, session)
198212
del self._subscribed_centrals[address]
199213

200214
@method()
@@ -214,7 +228,7 @@ def StartNotify(self): # noqa: N802
214228
f = self._service.app.StartNotify
215229
if f is None:
216230
raise NotImplementedError()
217-
f(self, {})
231+
f(self, None) # type: ignore
218232
self._notifying_calls += 1
219233

220234
@method()
@@ -229,7 +243,7 @@ async def StopNotify(self): # noqa: N802
229243
f = self._service.app.StopNotify
230244
if f is None:
231245
raise NotImplementedError()
232-
f(self, {})
246+
f(self, {}) # type: ignore
233247
self._notifying_calls -= 1
234248

235249
def update_value(self) -> None:

bless/backends/bluezdbus/dbus/device.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from bleak.backends.bluezdbus.defs import DEVICE_INTERFACE
2+
from dbus_next.aio import MessageBus, ProxyInterface, ProxyObject # type: ignore
23
from dbus_next.constants import PropertyAccess # type: ignore
4+
from dbus_next.introspection import Interface, Node # type: ignore
35
from dbus_next.service import ServiceInterface, method, dbus_property # type: ignore
46

57

@@ -145,3 +147,17 @@ def ServicesResolved(self) -> "a{say}": # type: ignore # noqa: F722
145147
@dbus_property(access=PropertyAccess.READ)
146148
def AdvertisingFlags(self) -> "ay": # type: ignore # noqa: F722 F821
147149
raise NotImplementedError
150+
151+
@classmethod
152+
def get_device(cls, bus: MessageBus, path: str) -> ProxyInterface:
153+
# Query the device object
154+
node: Node = Node.default(name=path)
155+
device_iface: Interface = Device1().introspect()
156+
node.interfaces.append(device_iface)
157+
158+
object: ProxyObject = bus.get_proxy_object("org.bluez", path, node)
159+
return object.get_interface(DEVICE_INTERFACE)
160+
161+
# For typing
162+
async def get_address(self) -> str:
163+
raise NotImplementedError

bless/backends/bluezdbus/dbus/session.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
import logging
44
import os
55

6-
import bleak.backends.bluezdbus.defs as defs # type: ignore
7-
86
from asyncio import AbstractEventLoop, Event
9-
from dbus_next.aio import ProxyInterface, ProxyObject, MessageBus # type: ignore
10-
from dbus_next.introspection import Interface, Node # type: ignore
7+
from dbus_next.aio import ProxyInterface, MessageBus # type: ignore
118
from select import poll, POLLHUP, POLLERR, POLLNVAL
129
from socket import socket, socketpair, AF_UNIX, SOCK_SEQPACKET
1310
from typing import Callable, Coroutine, Optional, Union
@@ -39,6 +36,14 @@ def __init__(
3936

4037
self._tx: Optional[socket] = None
4138
self._device: Optional[ProxyInterface] = None
39+
self._address: Optional[str] = None
40+
41+
@property
42+
def address(self) -> str:
43+
if self._address is None:
44+
raise Exception("NotifySession not started. Address property not obtained")
45+
46+
return self._address
4247

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

6570
async def start(self) -> int:
66-
67-
# Query the device object
68-
node: Node = Node.default(name=self.device_path)
69-
device_iface: Interface = Device1().introspect()
70-
node.interfaces.append(device_iface)
71-
72-
object: ProxyObject = self.bus.get_proxy_object(
73-
"org.bluez", self.device_path, node
74-
)
75-
self._device = object.get_interface(defs.DEVICE_INTERFACE)
71+
self._device = Device1.get_device(self.bus, self.device_path)
72+
self._address = await self.get_device_address()
7673

7774
# create a bluetooth socket pair
7875
self._tx, rx = socketpair(AF_UNIX, SOCK_SEQPACKET)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import Dict, Optional, cast
2+
from ..request import BlessGATTRequest
3+
4+
5+
class BlessGATTRequestBlueZ(BlessGATTRequest):
6+
7+
@property
8+
def options(self) -> Dict:
9+
return cast(Dict, self.obj)
10+
11+
@property
12+
def central_id(self) -> str:
13+
"""
14+
Note, that the device returned within the options on
15+
BlueZ is a DBus path to the device object. It is the
16+
receving calls responsibility to use the DBus to resolve
17+
the device address and populate this field
18+
"""
19+
return self.obj["central_id"]
20+
21+
@property
22+
def mtu(self) -> int:
23+
return self.obj["mtu"]
24+
25+
@property
26+
def offset(self) -> int:
27+
return self.obj["offset"]
28+
29+
@property
30+
def response_requested(self) -> Optional[bool]:
31+
return None if "type" not in self.obj else (self.obj["type"] == "request")

0 commit comments

Comments
 (0)