forked from auracaster/bumble_mirror
@@ -93,6 +93,7 @@ from bumble import smp
|
|||||||
from bumble import sdp
|
from bumble import sdp
|
||||||
from bumble import l2cap
|
from bumble import l2cap
|
||||||
from bumble import core
|
from bumble import core
|
||||||
|
from bumble.profiles import gatt_service
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .transport.common import TransportSource, TransportSink
|
from .transport.common import TransportSource, TransportSink
|
||||||
@@ -1747,6 +1748,8 @@ class DeviceConfiguration:
|
|||||||
cis_enabled: bool = False
|
cis_enabled: bool = False
|
||||||
identity_address_type: Optional[int] = None
|
identity_address_type: Optional[int] = None
|
||||||
io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
|
io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
|
||||||
|
gap_service_enabled: bool = True
|
||||||
|
gatt_service_enabled: bool = True
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
self.gatt_services: list[Dict[str, Any]] = []
|
self.gatt_services: list[Dict[str, Any]] = []
|
||||||
@@ -1929,6 +1932,7 @@ class Device(CompositeEventEmitter):
|
|||||||
bis_links = dict[int, BisLink]()
|
bis_links = dict[int, BisLink]()
|
||||||
big_syncs = dict[int, BigSync]()
|
big_syncs = dict[int, BigSync]()
|
||||||
_pending_cis: Dict[int, tuple[int, int]]
|
_pending_cis: Dict[int, tuple[int, int]]
|
||||||
|
gatt_service: gatt_service.GenericAttributeProfileService | None = None
|
||||||
|
|
||||||
@composite_listener
|
@composite_listener
|
||||||
class Listener:
|
class Listener:
|
||||||
@@ -1995,7 +1999,6 @@ class Device(CompositeEventEmitter):
|
|||||||
address: Optional[hci.Address] = None,
|
address: Optional[hci.Address] = None,
|
||||||
config: Optional[DeviceConfiguration] = None,
|
config: Optional[DeviceConfiguration] = None,
|
||||||
host: Optional[Host] = None,
|
host: Optional[Host] = None,
|
||||||
generic_access_service: bool = True,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -2142,7 +2145,10 @@ class Device(CompositeEventEmitter):
|
|||||||
# Register the SDP server with the L2CAP Channel Manager
|
# Register the SDP server with the L2CAP Channel Manager
|
||||||
self.sdp_server.register(self.l2cap_channel_manager)
|
self.sdp_server.register(self.l2cap_channel_manager)
|
||||||
|
|
||||||
self.add_default_services(generic_access_service)
|
self.add_default_services(
|
||||||
|
add_gap_service=config.gap_service_enabled,
|
||||||
|
add_gatt_service=config.gatt_service_enabled,
|
||||||
|
)
|
||||||
self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
|
self.l2cap_channel_manager.register_fixed_channel(ATT_CID, self.on_gatt_pdu)
|
||||||
|
|
||||||
# Forward some events
|
# Forward some events
|
||||||
@@ -4506,10 +4512,15 @@ class Device(CompositeEventEmitter):
|
|||||||
def add_services(self, services):
|
def add_services(self, services):
|
||||||
self.gatt_server.add_services(services)
|
self.gatt_server.add_services(services)
|
||||||
|
|
||||||
def add_default_services(self, generic_access_service=True):
|
def add_default_services(
|
||||||
|
self, add_gap_service: bool = True, add_gatt_service: bool = True
|
||||||
|
) -> None:
|
||||||
# Add a GAP Service if requested
|
# Add a GAP Service if requested
|
||||||
if generic_access_service:
|
if add_gap_service:
|
||||||
self.gatt_server.add_service(GenericAccessService(self.name))
|
self.gatt_server.add_service(GenericAccessService(self.name))
|
||||||
|
if add_gatt_service:
|
||||||
|
self.gatt_service = gatt_service.GenericAttributeProfileService()
|
||||||
|
self.gatt_server.add_service(self.gatt_service)
|
||||||
|
|
||||||
async def notify_subscriber(self, connection, attribute, value=None, force=False):
|
async def notify_subscriber(self, connection, attribute, value=None, force=False):
|
||||||
await self.gatt_server.notify_subscriber(connection, attribute, value, force)
|
await self.gatt_server.notify_subscriber(connection, attribute, value, force)
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ from bumble.utils import ByteSerializable
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bumble.gatt_client import AttributeProxy
|
from bumble.gatt_client import AttributeProxy
|
||||||
from bumble.device import Connection
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -802,3 +801,23 @@ class ClientCharacteristicConfigurationBits(enum.IntFlag):
|
|||||||
DEFAULT = 0x0000
|
DEFAULT = 0x0000
|
||||||
NOTIFICATION = 0x0001
|
NOTIFICATION = 0x0001
|
||||||
INDICATION = 0x0002
|
INDICATION = 0x0002
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class ClientSupportedFeatures(enum.IntFlag):
|
||||||
|
'''
|
||||||
|
See Vol 3, Part G - 7.2 - Table 7.6: Client Supported Features bit assignments.
|
||||||
|
'''
|
||||||
|
|
||||||
|
ROBUST_CACHING = 0x01
|
||||||
|
ENHANCED_ATT_BEARER = 0x02
|
||||||
|
MULTIPLE_HANDLE_VALUE_NOTIFICATIONS = 0x04
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class ServerSupportedFeatures(enum.IntFlag):
|
||||||
|
'''
|
||||||
|
See Vol 3, Part G - 7.4 - Table 7.11: Server Supported Features bit assignments.
|
||||||
|
'''
|
||||||
|
|
||||||
|
EATT_SUPPORTED = 0x01
|
||||||
|
|||||||
166
bumble/profiles/gatt_service.py
Normal file
166
bumble/profiles/gatt_service.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# Copyright 2021-2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from bumble import att
|
||||||
|
from bumble import gatt
|
||||||
|
from bumble import gatt_client
|
||||||
|
from bumble import crypto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bumble import device
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class GenericAttributeProfileService(gatt.TemplateService):
|
||||||
|
'''See Vol 3, Part G - 7 - DEFINED GENERIC ATTRIBUTE PROFILE SERVICE.'''
|
||||||
|
|
||||||
|
UUID = gatt.GATT_GENERIC_ATTRIBUTE_SERVICE
|
||||||
|
|
||||||
|
client_supported_features_characteristic: gatt.Characteristic | None = None
|
||||||
|
server_supported_features_characteristic: gatt.Characteristic | None = None
|
||||||
|
database_hash_characteristic: gatt.Characteristic | None = None
|
||||||
|
service_changed_characteristic: gatt.Characteristic | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
server_supported_features: gatt.ServerSupportedFeatures | None = None,
|
||||||
|
database_hash_enabled: bool = True,
|
||||||
|
service_change_enabled: bool = True,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
if server_supported_features is not None:
|
||||||
|
self.server_supported_features_characteristic = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READABLE,
|
||||||
|
value=bytes([server_supported_features]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if database_hash_enabled:
|
||||||
|
self.database_hash_characteristic = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_DATABASE_HASH_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READABLE,
|
||||||
|
value=gatt.CharacteristicValue(read=self.get_database_hash),
|
||||||
|
)
|
||||||
|
|
||||||
|
if service_change_enabled:
|
||||||
|
self.service_changed_characteristic = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_SERVICE_CHANGED_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.INDICATE,
|
||||||
|
permissions=gatt.Characteristic.Permissions(0),
|
||||||
|
value=b'',
|
||||||
|
)
|
||||||
|
|
||||||
|
if (database_hash_enabled and service_change_enabled) or (
|
||||||
|
server_supported_features
|
||||||
|
and (
|
||||||
|
server_supported_features & gatt.ServerSupportedFeatures.EATT_SUPPORTED
|
||||||
|
)
|
||||||
|
): # TODO: Support Multiple Handle Value Notifications
|
||||||
|
self.client_supported_features_characteristic = gatt.Characteristic(
|
||||||
|
uuid=gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC,
|
||||||
|
properties=(
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.WRITE
|
||||||
|
),
|
||||||
|
permissions=(
|
||||||
|
gatt.Characteristic.Permissions.READABLE
|
||||||
|
| gatt.Characteristic.Permissions.WRITEABLE
|
||||||
|
),
|
||||||
|
value=bytes(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
characteristics=[
|
||||||
|
c
|
||||||
|
for c in (
|
||||||
|
self.service_changed_characteristic,
|
||||||
|
self.client_supported_features_characteristic,
|
||||||
|
self.database_hash_characteristic,
|
||||||
|
self.server_supported_features_characteristic,
|
||||||
|
)
|
||||||
|
if c is not None
|
||||||
|
],
|
||||||
|
primary=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_attribute_data(cls, attribute: att.Attribute) -> bytes:
|
||||||
|
if attribute.type in (
|
||||||
|
gatt.GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
|
gatt.GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||||
|
gatt.GATT_INCLUDE_ATTRIBUTE_TYPE,
|
||||||
|
gatt.GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
||||||
|
gatt.GATT_CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR,
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
struct.pack("<H", attribute.handle)
|
||||||
|
+ attribute.type.to_bytes()
|
||||||
|
+ attribute.value
|
||||||
|
)
|
||||||
|
elif attribute.type in (
|
||||||
|
gatt.GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR,
|
||||||
|
gatt.GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
|
gatt.GATT_SERVER_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
|
gatt.GATT_CHARACTERISTIC_PRESENTATION_FORMAT_DESCRIPTOR,
|
||||||
|
gatt.GATT_CHARACTERISTIC_AGGREGATE_FORMAT_DESCRIPTOR,
|
||||||
|
):
|
||||||
|
return struct.pack("<H", attribute.handle) + attribute.type.to_bytes()
|
||||||
|
|
||||||
|
return b''
|
||||||
|
|
||||||
|
def get_database_hash(self, connection: device.Connection | None) -> bytes:
|
||||||
|
assert connection
|
||||||
|
|
||||||
|
m = b''.join(
|
||||||
|
[
|
||||||
|
self.get_attribute_data(attribute)
|
||||||
|
for attribute in connection.device.gatt_server.attributes
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return crypto.aes_cmac(m=m, k=bytes(16))
|
||||||
|
|
||||||
|
|
||||||
|
class GenericAttributeProfileServiceProxy(gatt_client.ProfileServiceProxy):
|
||||||
|
SERVICE_CLASS = GenericAttributeProfileService
|
||||||
|
|
||||||
|
client_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
||||||
|
None
|
||||||
|
)
|
||||||
|
server_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
||||||
|
None
|
||||||
|
)
|
||||||
|
database_hash_characteristic: gatt_client.CharacteristicProxy | None = None
|
||||||
|
service_changed_characteristic: gatt_client.CharacteristicProxy | None = None
|
||||||
|
|
||||||
|
_CHARACTERISTICS = {
|
||||||
|
gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC: 'client_supported_features_characteristic',
|
||||||
|
gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC: 'server_supported_features_characteristic',
|
||||||
|
gatt.GATT_DATABASE_HASH_CHARACTERISTIC: 'database_hash_characteristic',
|
||||||
|
gatt.GATT_SERVICE_CHANGED_CHARACTERISTIC: 'service_changed_characteristic',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||||
|
self.service_proxy = service_proxy
|
||||||
|
|
||||||
|
for uuid, attribute_name in self._CHARACTERISTICS.items():
|
||||||
|
if characteristics := self.service_proxy.get_characteristics_by_uuid(uuid):
|
||||||
|
setattr(self, attribute_name, characteristics[0])
|
||||||
@@ -50,12 +50,7 @@ from bumble.hci import (
|
|||||||
HCI_Error,
|
HCI_Error,
|
||||||
HCI_Packet,
|
HCI_Packet,
|
||||||
)
|
)
|
||||||
from bumble.gatt import (
|
from bumble import gatt
|
||||||
GATT_GENERIC_ACCESS_SERVICE,
|
|
||||||
GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
|
||||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
|
||||||
GATT_APPEARANCE_CHARACTERISTIC,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .test_utils import TwoDevices, async_barrier
|
from .test_utils import TwoDevices, async_barrier
|
||||||
|
|
||||||
@@ -592,32 +587,54 @@ async def test_power_on_default_static_address_should_not_be_any():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_gatt_services_with_gas():
|
def test_gatt_services_with_gas_and_gatt():
|
||||||
device = Device(host=Host(None, None))
|
device = Device(host=Host(None, None))
|
||||||
|
|
||||||
# there should be one service and two chars, therefore 5 attributes
|
# there should be 2 service, 5 chars, and 1 descriptors, therefore 13 attributes
|
||||||
assert len(device.gatt_server.attributes) == 5
|
assert len(device.gatt_server.attributes) == 13
|
||||||
assert device.gatt_server.attributes[0].uuid == GATT_GENERIC_ACCESS_SERVICE
|
assert device.gatt_server.attributes[0].uuid == gatt.GATT_GENERIC_ACCESS_SERVICE
|
||||||
assert device.gatt_server.attributes[1].type == GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
assert (
|
||||||
assert device.gatt_server.attributes[2].uuid == GATT_DEVICE_NAME_CHARACTERISTIC
|
device.gatt_server.attributes[1].type == gatt.GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||||
assert device.gatt_server.attributes[3].type == GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
)
|
||||||
assert device.gatt_server.attributes[4].uuid == GATT_APPEARANCE_CHARACTERISTIC
|
assert device.gatt_server.attributes[2].uuid == gatt.GATT_DEVICE_NAME_CHARACTERISTIC
|
||||||
|
assert (
|
||||||
|
device.gatt_server.attributes[3].type == gatt.GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||||
|
)
|
||||||
|
assert device.gatt_server.attributes[4].uuid == gatt.GATT_APPEARANCE_CHARACTERISTIC
|
||||||
|
|
||||||
|
assert device.gatt_server.attributes[5].uuid == gatt.GATT_GENERIC_ATTRIBUTE_SERVICE
|
||||||
# -----------------------------------------------------------------------------
|
assert (
|
||||||
def test_gatt_services_without_gas():
|
device.gatt_server.attributes[6].type == gatt.GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||||
device = Device(host=Host(None, None), generic_access_service=False)
|
)
|
||||||
|
assert (
|
||||||
# there should be no services
|
device.gatt_server.attributes[7].uuid
|
||||||
assert len(device.gatt_server.attributes) == 0
|
== gatt.GATT_SERVICE_CHANGED_CHARACTERISTIC
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
device.gatt_server.attributes[8].type
|
||||||
|
== gatt.GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
device.gatt_server.attributes[9].type == gatt.GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
device.gatt_server.attributes[10].uuid
|
||||||
|
== gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
device.gatt_server.attributes[11].type
|
||||||
|
== gatt.GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
device.gatt_server.attributes[12].uuid == gatt.GATT_DATABASE_HASH_CHARACTERISTIC
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def run_test_device():
|
async def run_test_device():
|
||||||
await test_device_connect_parallel()
|
await test_device_connect_parallel()
|
||||||
await test_flush()
|
await test_flush()
|
||||||
await test_gatt_services_with_gas()
|
await test_gatt_services_with_gas_and_gatt()
|
||||||
await test_gatt_services_without_gas()
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
140
tests/gatt_service_test.py
Normal file
140
tests/gatt_service_test.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Copyright 2021-2025 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Imports
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import test_utils
|
||||||
|
|
||||||
|
from bumble import device
|
||||||
|
from bumble import gatt
|
||||||
|
from bumble.profiles import gatt_service
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def test_database_hash():
|
||||||
|
devices = await test_utils.TwoDevices.create_with_connection()
|
||||||
|
devices[0].gatt_server.services.clear()
|
||||||
|
devices[0].gatt_server.attributes.clear()
|
||||||
|
devices[0].gatt_server.attributes_by_handle.clear()
|
||||||
|
devices[0].add_service(
|
||||||
|
gatt.Service(
|
||||||
|
gatt.GATT_GENERIC_ACCESS_SERVICE,
|
||||||
|
characteristics=[
|
||||||
|
gatt.Characteristic(
|
||||||
|
gatt.GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||||
|
(
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.WRITE
|
||||||
|
),
|
||||||
|
gatt.Characteristic.Permissions.READ_REQUIRES_AUTHENTICATION,
|
||||||
|
),
|
||||||
|
gatt.Characteristic(
|
||||||
|
gatt.GATT_APPEARANCE_CHARACTERISTIC,
|
||||||
|
gatt.Characteristic.Properties.READ,
|
||||||
|
gatt.Characteristic.Permissions.READ_REQUIRES_AUTHENTICATION,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
devices[0].add_service(
|
||||||
|
gatt_service.GenericAttributeProfileService(
|
||||||
|
server_supported_features=None,
|
||||||
|
database_hash_enabled=True,
|
||||||
|
service_change_enabled=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
devices[0].gatt_server.add_attribute(
|
||||||
|
gatt.Service(gatt.GATT_GLUCOSE_SERVICE, characteristics=[])
|
||||||
|
)
|
||||||
|
# There is a special attribute order in the spec, so we need to add attribute one by
|
||||||
|
# one here.
|
||||||
|
battery_service = gatt.Service(
|
||||||
|
gatt.GATT_BATTERY_SERVICE,
|
||||||
|
characteristics=[
|
||||||
|
gatt.Characteristic(
|
||||||
|
gatt.GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
|
properties=gatt.Characteristic.Properties.READ,
|
||||||
|
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_AUTHENTICATION,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
primary=False,
|
||||||
|
)
|
||||||
|
battery_service.handle = 0x0014
|
||||||
|
battery_service.end_group_handle = 0x0016
|
||||||
|
devices[0].gatt_server.add_attribute(
|
||||||
|
gatt.IncludedServiceDeclaration(battery_service)
|
||||||
|
)
|
||||||
|
c = gatt.Characteristic(
|
||||||
|
'2A18',
|
||||||
|
properties=(
|
||||||
|
gatt.Characteristic.Properties.READ
|
||||||
|
| gatt.Characteristic.Properties.INDICATE
|
||||||
|
| gatt.Characteristic.Properties.EXTENDED_PROPERTIES
|
||||||
|
),
|
||||||
|
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_AUTHENTICATION,
|
||||||
|
)
|
||||||
|
devices[0].gatt_server.add_attribute(
|
||||||
|
gatt.CharacteristicDeclaration(c, devices[0].gatt_server.next_handle() + 1)
|
||||||
|
)
|
||||||
|
devices[0].gatt_server.add_attribute(c)
|
||||||
|
devices[0].gatt_server.add_attribute(
|
||||||
|
gatt.Descriptor(
|
||||||
|
gatt.GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
||||||
|
gatt.Descriptor.Permissions.READ_REQUIRES_AUTHENTICATION,
|
||||||
|
b'\x02\x00',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
devices[0].gatt_server.add_attribute(
|
||||||
|
gatt.Descriptor(
|
||||||
|
gatt.GATT_CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR,
|
||||||
|
gatt.Descriptor.Permissions.READ_REQUIRES_AUTHENTICATION,
|
||||||
|
b'\x00\x00',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
devices[0].add_service(battery_service)
|
||||||
|
|
||||||
|
peer = device.Peer(devices.connections[1])
|
||||||
|
client = await peer.discover_service_and_create_proxy(
|
||||||
|
gatt_service.GenericAttributeProfileServiceProxy
|
||||||
|
)
|
||||||
|
assert client.database_hash_characteristic
|
||||||
|
assert await client.database_hash_characteristic.read_value() == bytes.fromhex(
|
||||||
|
'F1CA2D48ECF58BAC8A8830BBB9FBA990'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def test_service_changed():
|
||||||
|
devices = await test_utils.TwoDevices.create_with_connection()
|
||||||
|
assert (service := devices[0].gatt_service)
|
||||||
|
|
||||||
|
peer = device.Peer(devices.connections[1])
|
||||||
|
assert (
|
||||||
|
client := await peer.discover_service_and_create_proxy(
|
||||||
|
gatt_service.GenericAttributeProfileServiceProxy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert client.service_changed_characteristic
|
||||||
|
indications = []
|
||||||
|
await client.service_changed_characteristic.subscribe(
|
||||||
|
indications.append, prefer_notify=False
|
||||||
|
)
|
||||||
|
await devices[0].indicate_subscribers(
|
||||||
|
service.service_changed_characteristic, b'1234'
|
||||||
|
)
|
||||||
|
await test_utils.async_barrier()
|
||||||
|
assert indications[0] == b'1234'
|
||||||
@@ -957,11 +957,12 @@ async def test_discover_all():
|
|||||||
peer = Peer(connection)
|
peer = Peer(connection)
|
||||||
|
|
||||||
await peer.discover_all()
|
await peer.discover_all()
|
||||||
assert len(peer.gatt_client.services) == 3
|
assert len(peer.gatt_client.services) == 4
|
||||||
# service 1800 gets added automatically
|
# service 1800 and 1801 get added automatically
|
||||||
assert peer.gatt_client.services[0].uuid == UUID('1800')
|
assert peer.gatt_client.services[0].uuid == UUID('1800')
|
||||||
assert peer.gatt_client.services[1].uuid == service1.uuid
|
assert peer.gatt_client.services[1].uuid == UUID('1801')
|
||||||
assert peer.gatt_client.services[2].uuid == service2.uuid
|
assert peer.gatt_client.services[2].uuid == service1.uuid
|
||||||
|
assert peer.gatt_client.services[3].uuid == service2.uuid
|
||||||
s = peer.get_services_by_uuid(service1.uuid)
|
s = peer.get_services_by_uuid(service1.uuid)
|
||||||
assert len(s) == 1
|
assert len(s) == 1
|
||||||
assert len(s[0].characteristics) == 2
|
assert len(s[0].characteristics) == 2
|
||||||
@@ -1084,10 +1085,18 @@ CharacteristicDeclaration(handle=0x0002, value_handle=0x0003, uuid=UUID-16:2A00
|
|||||||
Characteristic(handle=0x0003, end=0x0003, uuid=UUID-16:2A00 (Device Name), READ)
|
Characteristic(handle=0x0003, end=0x0003, uuid=UUID-16:2A00 (Device Name), READ)
|
||||||
CharacteristicDeclaration(handle=0x0004, value_handle=0x0005, uuid=UUID-16:2A01 (Appearance), READ)
|
CharacteristicDeclaration(handle=0x0004, value_handle=0x0005, uuid=UUID-16:2A01 (Appearance), READ)
|
||||||
Characteristic(handle=0x0005, end=0x0005, uuid=UUID-16:2A01 (Appearance), READ)
|
Characteristic(handle=0x0005, end=0x0005, uuid=UUID-16:2A01 (Appearance), READ)
|
||||||
Service(handle=0x0006, end=0x0009, uuid=3A657F47-D34F-46B3-B1EC-698E29B6B829)
|
Service(handle=0x0006, end=0x000D, uuid=UUID-16:1801 (Generic Attribute))
|
||||||
CharacteristicDeclaration(handle=0x0007, value_handle=0x0008, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
|
CharacteristicDeclaration(handle=0x0007, value_handle=0x0008, uuid=UUID-16:2A05 (Service Changed), INDICATE)
|
||||||
Characteristic(handle=0x0008, end=0x0009, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
|
Characteristic(handle=0x0008, end=0x0009, uuid=UUID-16:2A05 (Service Changed), INDICATE)
|
||||||
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)"""
|
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)
|
||||||
|
CharacteristicDeclaration(handle=0x000A, value_handle=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE)
|
||||||
|
Characteristic(handle=0x000B, end=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE)
|
||||||
|
CharacteristicDeclaration(handle=0x000C, value_handle=0x000D, uuid=UUID-16:2B2A (Database Hash), READ)
|
||||||
|
Characteristic(handle=0x000D, end=0x000D, uuid=UUID-16:2B2A (Database Hash), READ)
|
||||||
|
Service(handle=0x000E, end=0x0011, uuid=3A657F47-D34F-46B3-B1EC-698E29B6B829)
|
||||||
|
CharacteristicDeclaration(handle=0x000F, value_handle=0x0010, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
|
||||||
|
Characteristic(handle=0x0010, end=0x0011, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
|
||||||
|
Descriptor(handle=0x0011, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user