bumble 0.0.207__py3-none-any.whl → 0.0.208__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumble/_version.py +9 -4
- bumble/apps/auracast.py +29 -35
- bumble/apps/bench.py +13 -10
- bumble/apps/console.py +19 -12
- bumble/apps/gg_bridge.py +1 -1
- bumble/att.py +51 -37
- bumble/core.py +301 -155
- bumble/device.py +102 -56
- bumble/gatt.py +9 -228
- bumble/gatt_adapters.py +374 -0
- bumble/gatt_client.py +38 -31
- bumble/gatt_server.py +5 -5
- bumble/host.py +11 -5
- bumble/pairing.py +5 -5
- bumble/pandora/host.py +7 -12
- bumble/profiles/aics.py +33 -23
- bumble/profiles/ascs.py +2 -1
- bumble/profiles/asha.py +11 -9
- bumble/profiles/bass.py +8 -5
- bumble/profiles/battery_service.py +13 -3
- bumble/profiles/device_information_service.py +16 -14
- bumble/profiles/gap.py +12 -8
- bumble/profiles/gatt_service.py +1 -0
- bumble/profiles/gmap.py +16 -11
- bumble/profiles/hap.py +8 -6
- bumble/profiles/heart_rate_service.py +20 -4
- bumble/profiles/mcp.py +11 -9
- bumble/profiles/pacs.py +37 -24
- bumble/profiles/tmap.py +6 -4
- bumble/profiles/vcs.py +6 -5
- bumble/profiles/vocs.py +49 -41
- bumble/utils.py +10 -0
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/METADATA +3 -3
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/RECORD +38 -37
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/WHEEL +0 -0
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.207.dist-info → bumble-0.0.208.dist-info}/top_level.txt +0 -0
bumble/device.py
CHANGED
|
@@ -53,7 +53,7 @@ from pyee import EventEmitter
|
|
|
53
53
|
|
|
54
54
|
from .colors import color
|
|
55
55
|
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
|
56
|
-
from .gatt import Characteristic, Descriptor, Service
|
|
56
|
+
from .gatt import Attribute, Characteristic, Descriptor, Service
|
|
57
57
|
from .host import DataPacketQueue, Host
|
|
58
58
|
from .profiles.gap import GenericAccessService
|
|
59
59
|
from .core import (
|
|
@@ -1569,8 +1569,8 @@ class Connection(CompositeEventEmitter):
|
|
|
1569
1569
|
gatt_client: gatt_client.Client
|
|
1570
1570
|
pairing_peer_io_capability: Optional[int]
|
|
1571
1571
|
pairing_peer_authentication_requirements: Optional[int]
|
|
1572
|
-
cs_configs: dict[int, ChannelSoundingConfig]
|
|
1573
|
-
cs_procedures: dict[int, ChannelSoundingProcedure]
|
|
1572
|
+
cs_configs: dict[int, ChannelSoundingConfig] # Config ID to Configuration
|
|
1573
|
+
cs_procedures: dict[int, ChannelSoundingProcedure] # Config ID to Procedures
|
|
1574
1574
|
|
|
1575
1575
|
@composite_listener
|
|
1576
1576
|
class Listener:
|
|
@@ -1586,7 +1586,7 @@ class Connection(CompositeEventEmitter):
|
|
|
1586
1586
|
def on_connection_data_length_change(self):
|
|
1587
1587
|
pass
|
|
1588
1588
|
|
|
1589
|
-
def on_connection_phy_update(self):
|
|
1589
|
+
def on_connection_phy_update(self, phy):
|
|
1590
1590
|
pass
|
|
1591
1591
|
|
|
1592
1592
|
def on_connection_phy_update_failure(self, error):
|
|
@@ -1612,7 +1612,6 @@ class Connection(CompositeEventEmitter):
|
|
|
1612
1612
|
peer_resolvable_address,
|
|
1613
1613
|
role,
|
|
1614
1614
|
parameters,
|
|
1615
|
-
phy,
|
|
1616
1615
|
):
|
|
1617
1616
|
super().__init__()
|
|
1618
1617
|
self.device = device
|
|
@@ -1629,7 +1628,6 @@ class Connection(CompositeEventEmitter):
|
|
|
1629
1628
|
self.authenticated = False
|
|
1630
1629
|
self.sc = False
|
|
1631
1630
|
self.link_key_type = None
|
|
1632
|
-
self.phy = phy
|
|
1633
1631
|
self.att_mtu = ATT_DEFAULT_MTU
|
|
1634
1632
|
self.data_length = DEVICE_DEFAULT_DATA_LENGTH
|
|
1635
1633
|
self.gatt_client = None # Per-connection client
|
|
@@ -1639,6 +1637,8 @@ class Connection(CompositeEventEmitter):
|
|
|
1639
1637
|
self.pairing_peer_io_capability = None
|
|
1640
1638
|
self.pairing_peer_authentication_requirements = None
|
|
1641
1639
|
self.peer_le_features = None
|
|
1640
|
+
self.cs_configs = {}
|
|
1641
|
+
self.cs_procedures = {}
|
|
1642
1642
|
|
|
1643
1643
|
# [Classic only]
|
|
1644
1644
|
@classmethod
|
|
@@ -1658,7 +1658,6 @@ class Connection(CompositeEventEmitter):
|
|
|
1658
1658
|
None,
|
|
1659
1659
|
role,
|
|
1660
1660
|
None,
|
|
1661
|
-
None,
|
|
1662
1661
|
)
|
|
1663
1662
|
|
|
1664
1663
|
# [Classic only]
|
|
@@ -1774,12 +1773,12 @@ class Connection(CompositeEventEmitter):
|
|
|
1774
1773
|
async def set_phy(self, tx_phys=None, rx_phys=None, phy_options=None):
|
|
1775
1774
|
return await self.device.set_connection_phy(self, tx_phys, rx_phys, phy_options)
|
|
1776
1775
|
|
|
1776
|
+
async def get_phy(self) -> ConnectionPHY:
|
|
1777
|
+
return await self.device.get_connection_phy(self)
|
|
1778
|
+
|
|
1777
1779
|
async def get_rssi(self):
|
|
1778
1780
|
return await self.device.get_connection_rssi(self)
|
|
1779
1781
|
|
|
1780
|
-
async def get_phy(self):
|
|
1781
|
-
return await self.device.get_connection_phy(self)
|
|
1782
|
-
|
|
1783
1782
|
async def transfer_periodic_sync(
|
|
1784
1783
|
self, sync_handle: int, service_data: int = 0
|
|
1785
1784
|
) -> None:
|
|
@@ -2049,9 +2048,9 @@ class Device(CompositeEventEmitter):
|
|
|
2049
2048
|
legacy_advertiser: Optional[LegacyAdvertiser]
|
|
2050
2049
|
sco_links: Dict[int, ScoLink]
|
|
2051
2050
|
cis_links: Dict[int, CisLink]
|
|
2052
|
-
bigs
|
|
2053
|
-
bis_links
|
|
2054
|
-
big_syncs
|
|
2051
|
+
bigs: dict[int, Big]
|
|
2052
|
+
bis_links: dict[int, BisLink]
|
|
2053
|
+
big_syncs: dict[int, BigSync]
|
|
2055
2054
|
_pending_cis: Dict[int, tuple[int, int]]
|
|
2056
2055
|
gatt_service: gatt_service.GenericAttributeProfileService | None = None
|
|
2057
2056
|
|
|
@@ -2144,6 +2143,9 @@ class Device(CompositeEventEmitter):
|
|
|
2144
2143
|
self.sco_links = {} # ScoLinks, by connection handle (BR/EDR only)
|
|
2145
2144
|
self.cis_links = {} # CisLinks, by connection handle (LE only)
|
|
2146
2145
|
self._pending_cis = {} # (CIS_ID, CIG_ID), by CIS_handle
|
|
2146
|
+
self.bigs = {}
|
|
2147
|
+
self.bis_links = {}
|
|
2148
|
+
self.big_syncs = {}
|
|
2147
2149
|
self.classic_enabled = False
|
|
2148
2150
|
self.inquiry_response = None
|
|
2149
2151
|
self.address_resolver = None
|
|
@@ -2221,7 +2223,7 @@ class Device(CompositeEventEmitter):
|
|
|
2221
2223
|
permissions=descriptor["permissions"],
|
|
2222
2224
|
)
|
|
2223
2225
|
descriptors.append(new_descriptor)
|
|
2224
|
-
new_characteristic = Characteristic(
|
|
2226
|
+
new_characteristic: Characteristic[bytes] = Characteristic(
|
|
2225
2227
|
uuid=characteristic["uuid"],
|
|
2226
2228
|
properties=Characteristic.Properties.from_string(
|
|
2227
2229
|
characteristic["properties"]
|
|
@@ -3937,12 +3939,14 @@ class Device(CompositeEventEmitter):
|
|
|
3937
3939
|
)
|
|
3938
3940
|
return result.return_parameters.rssi
|
|
3939
3941
|
|
|
3940
|
-
async def get_connection_phy(self, connection):
|
|
3942
|
+
async def get_connection_phy(self, connection: Connection) -> ConnectionPHY:
|
|
3941
3943
|
result = await self.send_command(
|
|
3942
3944
|
hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle),
|
|
3943
3945
|
check_result=True,
|
|
3944
3946
|
)
|
|
3945
|
-
return (
|
|
3947
|
+
return ConnectionPHY(
|
|
3948
|
+
result.return_parameters.tx_phy, result.return_parameters.rx_phy
|
|
3949
|
+
)
|
|
3946
3950
|
|
|
3947
3951
|
async def set_connection_phy(
|
|
3948
3952
|
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
|
@@ -4006,13 +4010,12 @@ class Device(CompositeEventEmitter):
|
|
|
4006
4010
|
# Create a future to wait for an address to be found
|
|
4007
4011
|
peer_address = asyncio.get_running_loop().create_future()
|
|
4008
4012
|
|
|
4009
|
-
def on_peer_found(address, ad_data):
|
|
4010
|
-
local_name = ad_data.get(
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
if local_name
|
|
4014
|
-
|
|
4015
|
-
peer_address.set_result(address)
|
|
4013
|
+
def on_peer_found(address: hci.Address, ad_data: AdvertisingData) -> None:
|
|
4014
|
+
local_name = ad_data.get(
|
|
4015
|
+
AdvertisingData.Type.COMPLETE_LOCAL_NAME
|
|
4016
|
+
) or ad_data.get(AdvertisingData.Type.SHORTENED_LOCAL_NAME)
|
|
4017
|
+
if local_name == name:
|
|
4018
|
+
peer_address.set_result(address)
|
|
4016
4019
|
|
|
4017
4020
|
listener = None
|
|
4018
4021
|
was_scanning = self.scanning
|
|
@@ -4920,16 +4923,84 @@ class Device(CompositeEventEmitter):
|
|
|
4920
4923
|
self.gatt_service = gatt_service.GenericAttributeProfileService()
|
|
4921
4924
|
self.gatt_server.add_service(self.gatt_service)
|
|
4922
4925
|
|
|
4923
|
-
async def notify_subscriber(
|
|
4926
|
+
async def notify_subscriber(
|
|
4927
|
+
self,
|
|
4928
|
+
connection: Connection,
|
|
4929
|
+
attribute: Attribute,
|
|
4930
|
+
value: Optional[Any] = None,
|
|
4931
|
+
force: bool = False,
|
|
4932
|
+
) -> None:
|
|
4933
|
+
"""
|
|
4934
|
+
Send a notification to an attribute subscriber.
|
|
4935
|
+
|
|
4936
|
+
Args:
|
|
4937
|
+
connection:
|
|
4938
|
+
The connection of the subscriber.
|
|
4939
|
+
attribute:
|
|
4940
|
+
The attribute whose value is notified.
|
|
4941
|
+
value:
|
|
4942
|
+
The value of the attribute (if None, the value is read from the attribute)
|
|
4943
|
+
force:
|
|
4944
|
+
If True, send a notification even if there is no subscriber.
|
|
4945
|
+
"""
|
|
4924
4946
|
await self.gatt_server.notify_subscriber(connection, attribute, value, force)
|
|
4925
4947
|
|
|
4926
|
-
async def notify_subscribers(
|
|
4948
|
+
async def notify_subscribers(
|
|
4949
|
+
self, attribute: Attribute, value=None, force=False
|
|
4950
|
+
) -> None:
|
|
4951
|
+
"""
|
|
4952
|
+
Send a notification to all the subscribers of an attribute.
|
|
4953
|
+
|
|
4954
|
+
Args:
|
|
4955
|
+
attribute:
|
|
4956
|
+
The attribute whose value is notified.
|
|
4957
|
+
value:
|
|
4958
|
+
The value of the attribute (if None, the value is read from the attribute)
|
|
4959
|
+
force:
|
|
4960
|
+
If True, send a notification for every connection even if there is no
|
|
4961
|
+
subscriber.
|
|
4962
|
+
"""
|
|
4927
4963
|
await self.gatt_server.notify_subscribers(attribute, value, force)
|
|
4928
4964
|
|
|
4929
|
-
async def indicate_subscriber(
|
|
4965
|
+
async def indicate_subscriber(
|
|
4966
|
+
self,
|
|
4967
|
+
connection: Connection,
|
|
4968
|
+
attribute: Attribute,
|
|
4969
|
+
value: Optional[Any] = None,
|
|
4970
|
+
force: bool = False,
|
|
4971
|
+
):
|
|
4972
|
+
"""
|
|
4973
|
+
Send an indication to an attribute subscriber.
|
|
4974
|
+
|
|
4975
|
+
This method returns when the response to the indication has been received.
|
|
4976
|
+
|
|
4977
|
+
Args:
|
|
4978
|
+
connection:
|
|
4979
|
+
The connection of the subscriber.
|
|
4980
|
+
attribute:
|
|
4981
|
+
The attribute whose value is indicated.
|
|
4982
|
+
value:
|
|
4983
|
+
The value of the attribute (if None, the value is read from the attribute)
|
|
4984
|
+
force:
|
|
4985
|
+
If True, send an indication even if there is no subscriber.
|
|
4986
|
+
"""
|
|
4930
4987
|
await self.gatt_server.indicate_subscriber(connection, attribute, value, force)
|
|
4931
4988
|
|
|
4932
|
-
async def indicate_subscribers(
|
|
4989
|
+
async def indicate_subscribers(
|
|
4990
|
+
self, attribute: Attribute, value: Optional[Any] = None, force: bool = False
|
|
4991
|
+
):
|
|
4992
|
+
"""
|
|
4993
|
+
Send an indication to all the subscribers of an attribute.
|
|
4994
|
+
|
|
4995
|
+
Args:
|
|
4996
|
+
attribute:
|
|
4997
|
+
The attribute whose value is notified.
|
|
4998
|
+
value:
|
|
4999
|
+
The value of the attribute (if None, the value is read from the attribute)
|
|
5000
|
+
force:
|
|
5001
|
+
If True, send an indication for every connection even if there is no
|
|
5002
|
+
subscriber.
|
|
5003
|
+
"""
|
|
4933
5004
|
await self.gatt_server.indicate_subscribers(attribute, value, force)
|
|
4934
5005
|
|
|
4935
5006
|
@host_event_handler
|
|
@@ -5101,29 +5172,6 @@ class Device(CompositeEventEmitter):
|
|
|
5101
5172
|
lambda _: self.abort_on('flush', advertising_set.start()),
|
|
5102
5173
|
)
|
|
5103
5174
|
|
|
5104
|
-
self._emit_le_connection(connection)
|
|
5105
|
-
|
|
5106
|
-
def _emit_le_connection(self, connection: Connection) -> None:
|
|
5107
|
-
# If supported, read which PHY we're connected with before
|
|
5108
|
-
# notifying listeners of the new connection.
|
|
5109
|
-
if self.host.supports_command(hci.HCI_LE_READ_PHY_COMMAND):
|
|
5110
|
-
|
|
5111
|
-
async def read_phy():
|
|
5112
|
-
result = await self.send_command(
|
|
5113
|
-
hci.HCI_LE_Read_PHY_Command(connection_handle=connection.handle),
|
|
5114
|
-
check_result=True,
|
|
5115
|
-
)
|
|
5116
|
-
connection.phy = ConnectionPHY(
|
|
5117
|
-
result.return_parameters.tx_phy, result.return_parameters.rx_phy
|
|
5118
|
-
)
|
|
5119
|
-
# Emit an event to notify listeners of the new connection
|
|
5120
|
-
self.emit('connection', connection)
|
|
5121
|
-
|
|
5122
|
-
# Do so asynchronously to not block the current event handler
|
|
5123
|
-
connection.abort_on('disconnection', read_phy())
|
|
5124
|
-
|
|
5125
|
-
return
|
|
5126
|
-
|
|
5127
5175
|
self.emit('connection', connection)
|
|
5128
5176
|
|
|
5129
5177
|
@host_event_handler
|
|
@@ -5222,7 +5270,6 @@ class Device(CompositeEventEmitter):
|
|
|
5222
5270
|
peer_resolvable_address,
|
|
5223
5271
|
role,
|
|
5224
5272
|
connection_parameters,
|
|
5225
|
-
ConnectionPHY(hci.HCI_LE_1M_PHY, hci.HCI_LE_1M_PHY),
|
|
5226
5273
|
)
|
|
5227
5274
|
self.connections[connection_handle] = connection
|
|
5228
5275
|
|
|
@@ -5238,7 +5285,7 @@ class Device(CompositeEventEmitter):
|
|
|
5238
5285
|
|
|
5239
5286
|
if role == hci.HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising:
|
|
5240
5287
|
# We can emit now, we have all the info we need
|
|
5241
|
-
self.
|
|
5288
|
+
self.emit('connection', connection)
|
|
5242
5289
|
return
|
|
5243
5290
|
|
|
5244
5291
|
if role == hci.HCI_PERIPHERAL_ROLE and self.supports_le_extended_advertising:
|
|
@@ -5792,14 +5839,13 @@ class Device(CompositeEventEmitter):
|
|
|
5792
5839
|
|
|
5793
5840
|
@host_event_handler
|
|
5794
5841
|
@with_connection_from_handle
|
|
5795
|
-
def on_connection_phy_update(self, connection,
|
|
5842
|
+
def on_connection_phy_update(self, connection, phy):
|
|
5796
5843
|
logger.debug(
|
|
5797
5844
|
f'*** Connection PHY Update: [0x{connection.handle:04X}] '
|
|
5798
5845
|
f'{connection.peer_address} as {connection.role_name}, '
|
|
5799
|
-
f'{
|
|
5846
|
+
f'{phy}'
|
|
5800
5847
|
)
|
|
5801
|
-
connection.phy
|
|
5802
|
-
connection.emit('connection_phy_update')
|
|
5848
|
+
connection.emit('connection_phy_update', phy)
|
|
5803
5849
|
|
|
5804
5850
|
@host_event_handler
|
|
5805
5851
|
@with_connection_from_handle
|
bumble/gatt.py
CHANGED
|
@@ -27,28 +27,16 @@ import enum
|
|
|
27
27
|
import functools
|
|
28
28
|
import logging
|
|
29
29
|
import struct
|
|
30
|
-
from typing import
|
|
31
|
-
Any,
|
|
32
|
-
Callable,
|
|
33
|
-
Dict,
|
|
34
|
-
Iterable,
|
|
35
|
-
List,
|
|
36
|
-
Optional,
|
|
37
|
-
Sequence,
|
|
38
|
-
SupportsBytes,
|
|
39
|
-
Type,
|
|
40
|
-
Union,
|
|
41
|
-
TYPE_CHECKING,
|
|
42
|
-
)
|
|
30
|
+
from typing import Iterable, List, Optional, Sequence, TypeVar, Union
|
|
43
31
|
|
|
44
32
|
from bumble.colors import color
|
|
45
|
-
from bumble.core import BaseBumbleError,
|
|
33
|
+
from bumble.core import BaseBumbleError, UUID
|
|
46
34
|
from bumble.att import Attribute, AttributeValue
|
|
47
|
-
from bumble.utils import ByteSerializable
|
|
48
|
-
|
|
49
|
-
if TYPE_CHECKING:
|
|
50
|
-
from bumble.gatt_client import AttributeProxy
|
|
51
35
|
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
# Typing
|
|
38
|
+
# -----------------------------------------------------------------------------
|
|
39
|
+
_T = TypeVar('_T')
|
|
52
40
|
|
|
53
41
|
# -----------------------------------------------------------------------------
|
|
54
42
|
# Logging
|
|
@@ -436,7 +424,7 @@ class IncludedServiceDeclaration(Attribute):
|
|
|
436
424
|
|
|
437
425
|
|
|
438
426
|
# -----------------------------------------------------------------------------
|
|
439
|
-
class Characteristic(Attribute):
|
|
427
|
+
class Characteristic(Attribute[_T]):
|
|
440
428
|
'''
|
|
441
429
|
See Vol 3, Part G - 3.3 CHARACTERISTIC DEFINITION
|
|
442
430
|
'''
|
|
@@ -499,7 +487,7 @@ class Characteristic(Attribute):
|
|
|
499
487
|
uuid: Union[str, bytes, UUID],
|
|
500
488
|
properties: Characteristic.Properties,
|
|
501
489
|
permissions: Union[str, Attribute.Permissions],
|
|
502
|
-
value:
|
|
490
|
+
value: Union[AttributeValue[_T], _T, None] = None,
|
|
503
491
|
descriptors: Sequence[Descriptor] = (),
|
|
504
492
|
):
|
|
505
493
|
super().__init__(uuid, permissions, value)
|
|
@@ -559,217 +547,10 @@ class CharacteristicDeclaration(Attribute):
|
|
|
559
547
|
|
|
560
548
|
|
|
561
549
|
# -----------------------------------------------------------------------------
|
|
562
|
-
class CharacteristicValue(AttributeValue):
|
|
550
|
+
class CharacteristicValue(AttributeValue[_T]):
|
|
563
551
|
"""Same as AttributeValue, for backward compatibility"""
|
|
564
552
|
|
|
565
553
|
|
|
566
|
-
# -----------------------------------------------------------------------------
|
|
567
|
-
class CharacteristicAdapter:
|
|
568
|
-
'''
|
|
569
|
-
An adapter that can adapt Characteristic and AttributeProxy objects
|
|
570
|
-
by wrapping their `read_value()` and `write_value()` methods with ones that
|
|
571
|
-
return/accept encoded/decoded values.
|
|
572
|
-
|
|
573
|
-
For proxies (i.e used by a GATT client), the adaptation is one where the return
|
|
574
|
-
value of `read_value()` is decoded and the value passed to `write_value()` is
|
|
575
|
-
encoded. The `subscribe()` method, is wrapped with one where the values are decoded
|
|
576
|
-
before being passed to the subscriber.
|
|
577
|
-
|
|
578
|
-
For local values (i.e hosted by a GATT server) the adaptation is one where the
|
|
579
|
-
return value of `read_value()` is encoded and the value passed to `write_value()`
|
|
580
|
-
is decoded.
|
|
581
|
-
'''
|
|
582
|
-
|
|
583
|
-
read_value: Callable
|
|
584
|
-
write_value: Callable
|
|
585
|
-
|
|
586
|
-
def __init__(self, characteristic: Union[Characteristic, AttributeProxy]):
|
|
587
|
-
self.wrapped_characteristic = characteristic
|
|
588
|
-
self.subscribers: Dict[Callable, Callable] = (
|
|
589
|
-
{}
|
|
590
|
-
) # Map from subscriber to proxy subscriber
|
|
591
|
-
|
|
592
|
-
if isinstance(characteristic, Characteristic):
|
|
593
|
-
self.read_value = self.read_encoded_value
|
|
594
|
-
self.write_value = self.write_encoded_value
|
|
595
|
-
else:
|
|
596
|
-
self.read_value = self.read_decoded_value
|
|
597
|
-
self.write_value = self.write_decoded_value
|
|
598
|
-
self.subscribe = self.wrapped_subscribe
|
|
599
|
-
self.unsubscribe = self.wrapped_unsubscribe
|
|
600
|
-
|
|
601
|
-
def __getattr__(self, name):
|
|
602
|
-
return getattr(self.wrapped_characteristic, name)
|
|
603
|
-
|
|
604
|
-
def __setattr__(self, name, value):
|
|
605
|
-
if name in (
|
|
606
|
-
'wrapped_characteristic',
|
|
607
|
-
'subscribers',
|
|
608
|
-
'read_value',
|
|
609
|
-
'write_value',
|
|
610
|
-
'subscribe',
|
|
611
|
-
'unsubscribe',
|
|
612
|
-
):
|
|
613
|
-
super().__setattr__(name, value)
|
|
614
|
-
else:
|
|
615
|
-
setattr(self.wrapped_characteristic, name, value)
|
|
616
|
-
|
|
617
|
-
async def read_encoded_value(self, connection):
|
|
618
|
-
return self.encode_value(
|
|
619
|
-
await self.wrapped_characteristic.read_value(connection)
|
|
620
|
-
)
|
|
621
|
-
|
|
622
|
-
async def write_encoded_value(self, connection, value):
|
|
623
|
-
return await self.wrapped_characteristic.write_value(
|
|
624
|
-
connection, self.decode_value(value)
|
|
625
|
-
)
|
|
626
|
-
|
|
627
|
-
async def read_decoded_value(self):
|
|
628
|
-
return self.decode_value(await self.wrapped_characteristic.read_value())
|
|
629
|
-
|
|
630
|
-
async def write_decoded_value(self, value, with_response=False):
|
|
631
|
-
return await self.wrapped_characteristic.write_value(
|
|
632
|
-
self.encode_value(value), with_response
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
def encode_value(self, value):
|
|
636
|
-
return value
|
|
637
|
-
|
|
638
|
-
def decode_value(self, value):
|
|
639
|
-
return value
|
|
640
|
-
|
|
641
|
-
def wrapped_subscribe(self, subscriber=None):
|
|
642
|
-
if subscriber is not None:
|
|
643
|
-
if subscriber in self.subscribers:
|
|
644
|
-
# We already have a proxy subscriber
|
|
645
|
-
subscriber = self.subscribers[subscriber]
|
|
646
|
-
else:
|
|
647
|
-
# Create and register a proxy that will decode the value
|
|
648
|
-
original_subscriber = subscriber
|
|
649
|
-
|
|
650
|
-
def on_change(value):
|
|
651
|
-
original_subscriber(self.decode_value(value))
|
|
652
|
-
|
|
653
|
-
self.subscribers[subscriber] = on_change
|
|
654
|
-
subscriber = on_change
|
|
655
|
-
|
|
656
|
-
return self.wrapped_characteristic.subscribe(subscriber)
|
|
657
|
-
|
|
658
|
-
def wrapped_unsubscribe(self, subscriber=None):
|
|
659
|
-
if subscriber in self.subscribers:
|
|
660
|
-
subscriber = self.subscribers.pop(subscriber)
|
|
661
|
-
|
|
662
|
-
return self.wrapped_characteristic.unsubscribe(subscriber)
|
|
663
|
-
|
|
664
|
-
def __str__(self) -> str:
|
|
665
|
-
wrapped = str(self.wrapped_characteristic)
|
|
666
|
-
return f'{self.__class__.__name__}({wrapped})'
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
# -----------------------------------------------------------------------------
|
|
670
|
-
class DelegatedCharacteristicAdapter(CharacteristicAdapter):
|
|
671
|
-
'''
|
|
672
|
-
Adapter that converts bytes values using an encode and a decode function.
|
|
673
|
-
'''
|
|
674
|
-
|
|
675
|
-
def __init__(self, characteristic, encode=None, decode=None):
|
|
676
|
-
super().__init__(characteristic)
|
|
677
|
-
self.encode = encode
|
|
678
|
-
self.decode = decode
|
|
679
|
-
|
|
680
|
-
def encode_value(self, value):
|
|
681
|
-
if self.encode is None:
|
|
682
|
-
raise InvalidOperationError('delegated adapter does not have an encoder')
|
|
683
|
-
return self.encode(value)
|
|
684
|
-
|
|
685
|
-
def decode_value(self, value):
|
|
686
|
-
if self.decode is None:
|
|
687
|
-
raise InvalidOperationError('delegate adapter does not have a decoder')
|
|
688
|
-
return self.decode(value)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
# -----------------------------------------------------------------------------
|
|
692
|
-
class PackedCharacteristicAdapter(CharacteristicAdapter):
|
|
693
|
-
'''
|
|
694
|
-
Adapter that packs/unpacks characteristic values according to a standard
|
|
695
|
-
Python `struct` format.
|
|
696
|
-
For formats with a single value, the adapted `read_value` and `write_value`
|
|
697
|
-
methods return/accept single values. For formats with multiple values,
|
|
698
|
-
they return/accept a tuple with the same number of elements as is required for
|
|
699
|
-
the format.
|
|
700
|
-
'''
|
|
701
|
-
|
|
702
|
-
def __init__(self, characteristic, pack_format):
|
|
703
|
-
super().__init__(characteristic)
|
|
704
|
-
self.struct = struct.Struct(pack_format)
|
|
705
|
-
|
|
706
|
-
def pack(self, *values):
|
|
707
|
-
return self.struct.pack(*values)
|
|
708
|
-
|
|
709
|
-
def unpack(self, buffer):
|
|
710
|
-
return self.struct.unpack(buffer)
|
|
711
|
-
|
|
712
|
-
def encode_value(self, value):
|
|
713
|
-
return self.pack(*value if isinstance(value, tuple) else (value,))
|
|
714
|
-
|
|
715
|
-
def decode_value(self, value):
|
|
716
|
-
unpacked = self.unpack(value)
|
|
717
|
-
return unpacked[0] if len(unpacked) == 1 else unpacked
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
# -----------------------------------------------------------------------------
|
|
721
|
-
class MappedCharacteristicAdapter(PackedCharacteristicAdapter):
|
|
722
|
-
'''
|
|
723
|
-
Adapter that packs/unpacks characteristic values according to a standard
|
|
724
|
-
Python `struct` format.
|
|
725
|
-
The adapted `read_value` and `write_value` methods return/accept a dictionary which
|
|
726
|
-
is packed/unpacked according to format, with the arguments extracted from the
|
|
727
|
-
dictionary by key, in the same order as they occur in the `keys` parameter.
|
|
728
|
-
'''
|
|
729
|
-
|
|
730
|
-
def __init__(self, characteristic, pack_format, keys):
|
|
731
|
-
super().__init__(characteristic, pack_format)
|
|
732
|
-
self.keys = keys
|
|
733
|
-
|
|
734
|
-
# pylint: disable=arguments-differ
|
|
735
|
-
def pack(self, values):
|
|
736
|
-
return super().pack(*(values[key] for key in self.keys))
|
|
737
|
-
|
|
738
|
-
def unpack(self, buffer):
|
|
739
|
-
return dict(zip(self.keys, super().unpack(buffer)))
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
# -----------------------------------------------------------------------------
|
|
743
|
-
class UTF8CharacteristicAdapter(CharacteristicAdapter):
|
|
744
|
-
'''
|
|
745
|
-
Adapter that converts strings to/from bytes using UTF-8 encoding
|
|
746
|
-
'''
|
|
747
|
-
|
|
748
|
-
def encode_value(self, value: str) -> bytes:
|
|
749
|
-
return value.encode('utf-8')
|
|
750
|
-
|
|
751
|
-
def decode_value(self, value: bytes) -> str:
|
|
752
|
-
return value.decode('utf-8')
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
# -----------------------------------------------------------------------------
|
|
756
|
-
class SerializableCharacteristicAdapter(CharacteristicAdapter):
|
|
757
|
-
'''
|
|
758
|
-
Adapter that converts any class to/from bytes using the class'
|
|
759
|
-
`to_bytes` and `__bytes__` methods, respectively.
|
|
760
|
-
'''
|
|
761
|
-
|
|
762
|
-
def __init__(self, characteristic, cls: Type[ByteSerializable]):
|
|
763
|
-
super().__init__(characteristic)
|
|
764
|
-
self.cls = cls
|
|
765
|
-
|
|
766
|
-
def encode_value(self, value: SupportsBytes) -> bytes:
|
|
767
|
-
return bytes(value)
|
|
768
|
-
|
|
769
|
-
def decode_value(self, value: bytes) -> Any:
|
|
770
|
-
return self.cls.from_bytes(value)
|
|
771
|
-
|
|
772
|
-
|
|
773
554
|
# -----------------------------------------------------------------------------
|
|
774
555
|
class Descriptor(Attribute):
|
|
775
556
|
'''
|