add support for type adapters and framework for adding standard GATT profiles

This commit is contained in:
Gilles Boccon-Gibod
2022-07-19 19:42:21 -07:00
parent 94fc81c183
commit bd28892734
18 changed files with 896 additions and 273 deletions
+81
View File
@@ -0,0 +1,81 @@
# Copyright 2021-2022 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
# -----------------------------------------------------------------------------
import asyncio
import sys
import os
import logging
from colors import color
from bumble.device import Device, Peer
from bumble.host import Host
from bumble.transport import open_transport
from bumble import gatt
from bumble.profiles.battery_service import BatteryServiceProxy
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 3:
print('Usage: battery_client.py <transport-spec> <bluetooth-address>')
print('example: battery_client.py usb:0 E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
async with await open_transport(sys.argv[1]) as (hci_source, hci_sink):
print('<<< connected')
# Create and start a device
host = Host(controller_source=hci_source, controller_sink=hci_sink)
device = Device('Bumble', address = 'F0:F1:F2:F3:F4:F5', host = host)
await device.power_on()
# Connect to the peer
target_address = sys.argv[2]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address)
print(f'=== Connected to {connection}')
# Discover the Battery Service
peer = Peer(connection)
print('=== Discovering Battery Service')
await peer.discover_services([gatt.GATT_BATTERY_SERVICE])
# Check that the service was found
battery_services = peer.get_services_by_uuid(gatt.GATT_BATTERY_SERVICE)
if not battery_services:
print('!!! Service not found')
return
battery_service = battery_services[0]
await battery_service.discover_characteristics()
# Create a service-specific proxy to read and decode the values
battery_client = BatteryServiceProxy(battery_service)
# Subscribe to and read the battery level
if battery_client.battery_level:
await battery_client.battery_level.subscribe(
lambda value: print(f'{color("Battery Level Update:", "green")} {value}')
)
value = await battery_client.battery_level.read_value()
print(f'{color("Initial Battery Level:", "green")} {value}')
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
@@ -25,59 +25,41 @@ import struct
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.transport import open_transport_or_link
from bumble.gatt import (
Service,
Characteristic,
CharacteristicValue,
GATT_DEVICE_BATTERY_SERVICE,
GATT_BATTERY_LEVEL_CHARACTERISTIC
)
# -----------------------------------------------------------------------------
def read_battery_level(connection):
return bytes([random.randint(0, 100)])
from bumble.profiles.battery_service import BatteryService
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 3:
print('Usage: python battery_service.py <device-config> <transport-spec>')
print('example: python battery_service.py device1.json usb:0')
print('Usage: python battery_server.py <device-config> <transport-spec>')
print('example: python battery_server.py device1.json usb:0')
return
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
# Create a device to manage the host
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
# Add a Battery Service to the GATT sever
device.add_services([
Service(
GATT_DEVICE_BATTERY_SERVICE,
[
Characteristic(
GATT_BATTERY_LEVEL_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
CharacteristicValue(read=read_battery_level)
)
]
)
])
# Add a Device Information Service and Battery Service to the GATT sever
battery_service = BatteryService(lambda _: random.randint(0, 100))
device.add_service(battery_service)
# Set the advertising data
device.advertising_data = bytes(
AdvertisingData([
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble Battery', 'utf-8')),
(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, struct.pack('<H', 0x180F)),
(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, bytes(battery_service.uuid)),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340))
])
)
# Go!
await device.power_on()
await device.start_advertising()
await hci_source.wait_for_termination()
await device.start_advertising(auto_restart=True)
# Notify every 3 seconds
while True:
await asyncio.sleep(3.0)
await device.notify_subscribers(battery_service.battery_level_characteristic)
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
+89
View File
@@ -0,0 +1,89 @@
# Copyright 2021-2022 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
# -----------------------------------------------------------------------------
import asyncio
import sys
import os
import logging
from colors import color
from bumble.device import Device, Peer
from bumble.host import Host
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
from bumble.transport import open_transport
from bumble import gatt
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 3:
print('Usage: device_info_client.py <transport-spec> <bluetooth-address>')
print('example: device_info_client.py usb:0 E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
async with await open_transport(sys.argv[1]) as (hci_source, hci_sink):
print('<<< connected')
# Create and start a device
host = Host(controller_source=hci_source, controller_sink=hci_sink)
device = Device('Bumble', address = 'F0:F1:F2:F3:F4:F5', host = host)
await device.power_on()
# Connect to the peer
target_address = sys.argv[2]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address)
print(f'=== Connected to {connection}')
# Discover the Device Information service
peer = Peer(connection)
print('=== Discovering Device Information Service')
await peer.discover_services([gatt.GATT_DEVICE_INFORMATION_SERVICE])
# Check that the service was found
device_info_services = peer.get_services_by_uuid(gatt.GATT_DEVICE_INFORMATION_SERVICE)
if not device_info_services:
print('!!! Service not found')
return
device_info_service = device_info_services[0]
await device_info_service.discover_characteristics()
# Create a service-specific proxy to read and decode the values
device_info = DeviceInformationServiceProxy(device_info_service)
# Read and print the fields
if device_info.manufacturer_name is not None:
print(color('Manufacturer Name: ', 'green'), await device_info.manufacturer_name.read_value())
if device_info.model_number is not None:
print(color('Model Number: ', 'green'), await device_info.model_number.read_value())
if device_info.serial_number is not None:
print(color('Serial Number: ', 'green'), await device_info.serial_number.read_value())
if device_info.hardware_revision is not None:
print(color('Hardware Revision: ', 'green'), await device_info.hardware_revision.read_value())
if device_info.firmware_revision is not None:
print(color('Firmware Revision: ', 'green'), await device_info.firmware_revision.read_value())
if device_info.software_revision is not None:
print(color('Software Revision: ', 'green'), await device_info.software_revision.read_value())
if device_info.system_id is not None:
print(color('System ID: ', 'green'), await device_info.system_id.read_value())
if device_info.ieee_regulatory_certification_data_list is not None:
print(color('Regulatory Certification:', 'green'), (await device_info.ieee_regulatory_certification_data_list.read_value()).hex())
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+66
View File
@@ -0,0 +1,66 @@
# Copyright 2021-2022 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
# -----------------------------------------------------------------------------
import asyncio
import sys
import os
import logging
import struct
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.transport import open_transport_or_link
from bumble.profiles.device_information_service import DeviceInformationService
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 3:
print('Usage: python device_info_server.py <device-config> <transport-spec>')
print('example: python device_info_server.py device1.json usb:0')
return
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
# Add a Device Information Service to the GATT sever
device_information_service = DeviceInformationService(
manufacturer_name = 'ACME',
model_number = 'AB-102',
serial_number = '7654321',
hardware_revision = '1.1.3',
software_revision = '2.5.6',
system_id = (0x123456, 0x8877665544)
)
device.add_service(device_information_service)
# Set the advertising data
device.advertising_data = bytes(
AdvertisingData([
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble Device', 'utf-8')),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340))
])
)
# Go!
await device.power_on()
await device.start_advertising(auto_restart=True)
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
-96
View File
@@ -1,96 +0,0 @@
# Copyright 2021-2022 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
# -----------------------------------------------------------------------------
import asyncio
import sys
import os
import logging
from colors import color
from bumble.device import Device, Peer
from bumble.host import Host
from bumble.transport import open_transport
from bumble.utils import AsyncRunner
from bumble import gatt
# -----------------------------------------------------------------------------
class Listener(Device.Listener):
def __init__(self, device):
self.device = device
self.done = asyncio.get_running_loop().create_future()
@AsyncRunner.run_in_task()
async def on_connection(self, connection):
print(f'=== Connected to {connection}')
# Discover the Device Info service
peer = Peer(connection)
print('=== Discovering Device Info')
await peer.discover_services([gatt.GATT_DEVICE_INFORMATION_SERVICE])
# Check that the service was found
device_info_services = peer.get_services_by_uuid(gatt.GATT_DEVICE_INFORMATION_SERVICE)
if not device_info_services:
print('!!! Service not found')
return
# Get the characteristics we want from the (first) device info service
service = device_info_services[0]
await peer.discover_characteristics([
gatt.GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC
], service)
# Read the manufacturer name
manufacturer_name = peer.get_characteristics_by_uuid(gatt.GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC, service)
if manufacturer_name:
value = await peer.read_value(manufacturer_name[0])
print(color('Manufacturer Name:', 'green'), value.decode('utf-8'))
else:
print('>>> No manufacturer name')
self.done.set_result(None)
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 3:
print('Usage: get_peer_device_info.py <transport-spec> <bluetooth-address>')
print('example: get_peer_device_info.py usb:0 E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
packet_source, packet_sink = await open_transport(sys.argv[1])
print('<<< connected')
# Create a host using the packet source and sink as controller
host = Host(controller_source=packet_source, controller_sink=packet_sink)
# Create a device to manage the host, with a custom listener
device = Device('Bumble', address = 'F0:F1:F2:F3:F4:F5', host = host)
device.listener = Listener(device)
await device.power_on()
# Connect to a peer
target_address = sys.argv[2]
print(f'=== Connecting to {target_address}...')
await device.connect(target_address)
await device.listener.done
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+1 -2
View File
@@ -45,8 +45,7 @@ async def main():
# Create a first controller using the packet source/sink as its host interface
controller1 = Controller('C1', host_source = hci_source, host_sink = hci_sink, link = link)
print("====", sys.argv)
controller1.address = sys.argv[1]
controller1.random_address = sys.argv[1]
# Create a second controller using the same link
controller2 = Controller('C2', link = link)
+2 -2
View File
@@ -41,10 +41,10 @@ class Listener(Device.Listener):
print('=== Discovering services')
peer = Peer(connection)
await peer.discover_services()
await peer.discover_characteristics()
for service in peer.services:
await service.discover_characteristics()
for characteristic in service.characteristics:
await peer.discover_descriptors(characteristic)
await characteristic.discover_descriptors()
print('=== Services discovered')
show_services(peer.services)
+30 -40
View File
@@ -25,7 +25,6 @@ from bumble.controller import Controller
from bumble.device import Device, Peer
from bumble.host import Host
from bumble.link import LocalLink
from bumble.utils import AsyncRunner
from bumble.gatt import (
Service,
Characteristic,
@@ -37,43 +36,6 @@ from bumble.gatt import (
)
# -----------------------------------------------------------------------------
class ClientListener(Device.Listener):
def __init__(self, device):
self.device = device
@AsyncRunner.run_in_task()
async def on_connection(self, connection):
print(f'=== Client: connected to {connection}')
# Discover all services
print('=== Discovering services')
peer = Peer(connection)
await peer.discover_services()
await peer.discover_characteristics()
for service in peer.services:
for characteristic in service.characteristics:
await peer.discover_descriptors(characteristic)
print('=== Services discovered')
show_services(peer.services)
# Discover all attributes
print('=== Discovering attributes')
attributes = await peer.discover_attributes()
for attribute in attributes:
print(attribute)
print('=== Attributes discovered')
# Read all attributes
for attribute in attributes:
try:
value = await peer.read_value(attribute)
print(color(f'0x{attribute.handle:04X} = {value.hex()}', 'green'))
except ProtocolError as error:
print(color(f'cannot read {attribute.handle:04X}:', 'red'), error)
# -----------------------------------------------------------------------------
class ServerListener(Device.Listener):
def on_connection(self, connection):
@@ -90,7 +52,6 @@ async def main():
client_host = Host()
client_host.controller = client_controller
client_device = Device("client", address = 'F0:F1:F2:F3:F4:F5', host = client_host)
client_device.listener = ClientListener(client_device)
await client_device.power_on()
# Setup a stack for the server
@@ -116,7 +77,36 @@ async def main():
server_device.add_service(device_info_service)
# Connect the client to the server
await client_device.connect(server_device.address)
connection = await client_device.connect(server_device.random_address)
print(f'=== Client: connected to {connection}')
# Discover all services
print('=== Discovering services')
peer = Peer(connection)
await peer.discover_services()
for service in peer.services:
await service.discover_characteristics()
for characteristic in service.characteristics:
await characteristic.discover_descriptors()
print('=== Services discovered')
show_services(peer.services)
# Discover all attributes
print('=== Discovering attributes')
attributes = await peer.discover_attributes()
for attribute in attributes:
print(attribute)
print('=== Attributes discovered')
# Read all attributes
for attribute in attributes:
try:
value = await attribute.read_value()
print(color(f'0x{attribute.handle:04X} = {value.hex()}', 'green'))
except ProtocolError as error:
print(color(f'cannot read {attribute.handle:04X}:', 'red'), error)
await asyncio.get_running_loop().create_future()
# -----------------------------------------------------------------------------