initial import

This commit is contained in:
Gilles Boccon-Gibod
2022-05-16 19:42:31 -07:00
commit 6ac91f7dec
185 changed files with 32064 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
Bumble Examples
===============
NOTE:
To run python scripts from this directory when the Bumble package isn't installed in your environment,
put .. in your PYTHONPATH: `export PYTHONPATH=..`
# `run_controller.py`
Run two virtual controllers, one connected to a soft device written in python with a simple GATT server, and the other connected to an external host.
## Running `run_controller.py` with a BlueZ host running on Linux.
In this configuration, a BlueZ stack running on a Linux host is connected to a Bumble virtual
controller, attached to a local link bus to a second, in-process, virtual controller, itself
used by a virtual device with a GATT server.
### Running with two separate hosts (ex: a mac laptop and a Linux VM)
In this setup, the virtual controllers and host run on a mac desktop, and the BlueZ stack on a Linux VM. A UDP socket communicates HCI packets between the macOS host and the Linux guest.
#### Linux setup
In a terminal, run `socat` to bridge a UDP socket to a local PTY.
The PTY is used a virtual HCI UART.
(in this example, the mac's IP address seen from the Linux VM is `172.16.104.1`, replace it with
the appropriate address for your environment. (you may also use a port number other than `22333` used here)
```
socat -d -d -x PTY,link=./hci_pty,rawer UDP-SENDTO:172.16.104.1:22333,bind=:22333
```
In the local directory, `socat` creates a symbolic link named `hci_pty` that points to the PTY.
In a second terminal, run
```
sudo btattach -P h4 -B hci_pty
```
This tells BlueZ to use the PTY as an HCI UART controller.
(optional) In a third terminal, run `sudo btmon`. This monitors the HCI traffic with BlueZ, which is great to see what's going on.
In a fourth terminal, run `sudo bluetoothctl` to interact with BlueZ as a client. From there, you can scan, advertise, connect, etc.
#### Mac setup
In a macOS terminal, run
```
python run_controller.py device1.json udp:0.0.0.0:22333,172.16.104.161:22333
```
This configures one of the virtual controllers to use a UDP socket as its HCI transport. In this example, the ip address of the Linux VM is `172.16.104.161`, replace it with the appropriate
address for your environment.
Once both the Linux and macOS processes are started, you should be able to interact with the
`bluetoothctl` tool on the Linux side and scan/connect/discover the virtual device running on
the macOS side. Relevant log output in each of the terminal consoles should show what it going on.
### Running with a single Linux host
In setup, both the BlueZ stack and tools as well as the Bumble virtual stack are running on the same
host.
In a terminal, run the example as
```
python run_controller.py device1.json pty:hci_pty
```
In the local directory, a symbolic link named `hci_pty` that points to the PTY is created.
From this point, run the same steps as in the previous example to attach the PTY to BlueZ and use
`bluetoothctl` to interact with the virtual controller.
# `run_gatt_client.py`
Run a host application connected to a 'real' BLE controller over a UART HCI to a dev board running Zephyr in HCI mode (could be any other UART BLE controller, or BlueZ over a virtual UART). The application connects to a Bluetooth peer specified as an argument.
Once connected, the application hosts a GATT client that discovers all services and all attributes of the peer and displays them.
# `run_gatt_server.py`
Run a host application connected to a 'real' BLE controller over a UART HCI to a dev board running Zephyr in HCI mode (could be any other UART BLE controller, or BlueZ over a virtual UART). The application connects to a Bluetooth peer specified as an argument.
The application hosts a simple GATT server with basic
services and characteristics.
# `run_gatt_client_and_server.py`
# `run_advertiser.py`
# `run_scanner.py`
Run a host application connected to a 'real' BLE controller over a UART HCI to a dev board running Zephyr in HCI mode (could be any other UART BLE controller, or BlueZ over a virtual UART), that starts scanning and prints out the scan results.
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Bumble Speaker",
"class_of_device": 2360324,
"keystore": "JsonKeyStore"
}
+85
View File
@@ -0,0 +1,85 @@
# 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 logging
import asyncio
import os
from bumble.utils import AsyncRunner
# -----------------------------------------------------------------------------
my_work_queue1 = AsyncRunner.WorkQueue()
my_work_queue2 = AsyncRunner.WorkQueue(create_task=False)
# -----------------------------------------------------------------------------
@AsyncRunner.run_in_task()
async def func1(x, y):
print('FUNC1: start', x, y)
await asyncio.sleep(x)
print('FUNC1: end', x, y)
# -----------------------------------------------------------------------------
@AsyncRunner.run_in_task(queue=my_work_queue1)
async def func2(x, y):
print('FUNC2: start', x, y)
await asyncio.sleep(x)
print('FUNC2: end', x, y)
# -----------------------------------------------------------------------------
@AsyncRunner.run_in_task(queue=my_work_queue2)
async def func3(x, y):
print('FUNC3: start', x, y)
await asyncio.sleep(x)
print('FUNC3: end', x, y)
# -----------------------------------------------------------------------------
@AsyncRunner.run_in_task(queue=None)
async def func4(x, y):
print('FUNC4: start', x, y)
await asyncio.sleep(x)
print('FUNC4: end', x, y)
raise ValueError('test')
# -----------------------------------------------------------------------------
async def main():
print("MAIN: start, loop=", asyncio.get_running_loop())
print("MAIN: invoke func1")
func1(1, 2)
print("MAIN: invoke func2")
func2(3, 4)
print("MAIN: invoke func3")
func3(5, 6)
print("MAIN: invoke func4")
func4(7, 8)
print("MAIN: sleeping 2 seconds")
await asyncio.sleep(2)
print("MAIN: running my_work_queue2.run")
await my_work_queue2.run()
print("MAIN: end (should never get here)")
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+84
View File
@@ -0,0 +1,84 @@
# 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 random
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)])
# -----------------------------------------------------------------------------
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')
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)
)
]
)
])
# 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.APPEARANCE, struct.pack('<H', 0x0340))
])
)
# Go!
await device.power_on()
await device.start_advertising()
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Bumble",
"class_of_device": 2360324,
"keystore": "JsonKeyStore"
}
+4
View File
@@ -0,0 +1,4 @@
{
"name": "Bumble",
"class_of_device": 7936
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "Bumble",
"address": "F0:F1:F2:F3:F4:F5",
"advertising_interval": 2000,
"keystore": "JsonKeyStore",
"irk": "865F81FF5A8B486EAAE29A27AD9F77DC"
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "Bumble2",
"address": "F6:F7:F8:F9:FA:FB",
"keystore": "JsonKeyStore",
"irk": "43E96EC5C5DBD8D0F5204CFFDECE0096",
"advertising_interval": 2000,
"advertising_data": "0201061106ba5689a6fabfa2bd01467d6e00fbabad08160a181604659b03"
}
+4
View File
@@ -0,0 +1,4 @@
{
"name": "Bumble3",
"address": "F1:F2:F3:F4:F5:F6"
}
+96
View File
@@ -0,0 +1,96 @@
# 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())
+4
View File
@@ -0,0 +1,4 @@
{
"name": "Bumble Phone",
"class_of_device": 6291980
}
+79
View File
@@ -0,0 +1,79 @@
<html>
<head>
<style>
* {
font-family: sans-serif;
}
label {
display: block;
}
input, label {
margin: .4rem 0;
}
</style>
</head>
<body>
Server Port <input id="port" type="text" value="8989"></input> <button onclick="connect()">Connect</button><br>
AT Command <input type="text" id="at_command" required size="10"> <button onclick="send_at_command()">Send</button><br>
Dial Phone Number <input type="text" id="dial_number" required size="10"> <button onclick="dial()">Dial</button><br>
<button onclick="answer()">Answer</button>
<button onclick="hangup()">Hang Up</button>
<button onclick="start_voice_assistant()">Start Voice Assistant</button>
<button onclick="stop_voice_assistant()">Stop Voice Assistant</button>
<hr>
<div id="socketState"></div>
<script>
let portInput = document.getElementById("port")
let atCommandInput = document.getElementById("at_command")
let dialNumberInput = document.getElementById("dial_number")
let socketState = document.getElementById("socketState")
let socket
function connect() {
socket = new WebSocket(`ws://localhost:${portInput.value}`);
socket.onopen = _ => {
socketState.innerText = 'OPEN'
}
socket.onclose = _ => {
socketState.innerText = 'CLOSED'
}
socket.onerror = (error) => {
socketState.innerText = 'ERROR'
console.log(`ERROR: ${error}`)
}
}
function send(message) {
if (socket && socket.readyState == WebSocket.OPEN) {
socket.send(JSON.stringify(message))
}
}
function send_at_command() {
send({ type:'at_command', command: atCommandInput.value })
}
function answer() {
send({ type:'at_command', command: 'ATA' })
}
function hangup() {
send({ type:'at_command', command: 'AT+CHUP' })
}
function dial() {
send({ type:'at_command', command: `ATD${dialNumberInput.value}` })
}
function start_voice_assistant() {
send(({ type:'at_command', command: 'AT+BVRA=1' }))
}
function stop_voice_assistant() {
send(({ type:'at_command', command: 'AT+BVRA=0' }))
}
</script>
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
{
"name": "Bumble Hands-Free",
"class_of_device": 2360324
}
+61
View File
@@ -0,0 +1,61 @@
<html>
<head>
</head>
<body>
Server Port <input id="port" type="text" value="8989"></input> <button onclick="connect()">Connect</button><br>
<div id="socketState"></div>
<div id="mouseInfo"></div>
<div id="keyInfo"></div>
<br>
<div id="frame" style="border: 2px solid; height:300"></div>
<script>
let portInput = document.getElementById("port")
let mouseInfo = document.getElementById("mouseInfo")
let ketInfo = document.getElementById("keyInfo")
let frame = document.getElementById("frame")
let socketState = document.getElementById("socketState")
let socket
frame.addEventListener('mousemove', onMouseMove)
document.addEventListener('keydown', onKeyDown)
document.addEventListener('keyup', onKeyUp)
function connect() {
socket = new WebSocket(`ws://localhost:${portInput.value}`);
socket.onopen = _ => {
socketState.innerText = 'OPEN'
}
socket.onclose = _ => {
socketState.innerText = 'CLOSED'
}
socket.onerror = (error) => {
socketState.innerText = 'ERROR'
console.log(`ERROR: ${error}`)
}
}
function send(message) {
if (socket && socket.readyState == WebSocket.OPEN) {
socket.send(JSON.stringify(message))
}
}
function onMouseMove(event) {
//console.log(event.clientX, event.clientY)
mouseInfo.innerText = `MOUSE: x=${event.clientX}, y=${event.clientY}`
send({ type:'mousemove', x: event.clientX, y: event.clientY })
}
function onKeyDown(event) {
//console.log(event)
keyInfo.innerText = `KEYDOWN: ${event.key}`
send({ type:'keydown', key: event.key })
}
function onKeyUp(event) {
//console.log(event)
keyInfo.innerText = `KEYUP: ${event.key}`
send({ type:'keyup', key: event.key })
}
</script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Bumble Keyboard",
"address": "F0:F1:F2:F3:F4:FA",
"advertising_interval": 200
}
+359
View File
@@ -0,0 +1,359 @@
# 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
import websockets
import json
from colors import color
from bumble.core import AdvertisingData
from bumble.device import Device, Connection, Peer
from bumble.utils import AsyncRunner
from bumble.transport import open_transport_or_link
from bumble.gatt import (
Descriptor,
Service,
Characteristic,
CharacteristicValue,
GATT_DEVICE_INFORMATION_SERVICE,
GATT_DEVICE_HUMAN_INTERFACE_DEVICE_SERVICE,
GATT_DEVICE_BATTERY_SERVICE,
GATT_BATTERY_LEVEL_CHARACTERISTIC,
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
GATT_REPORT_CHARACTERISTIC,
GATT_REPORT_MAP_CHARACTERISTIC,
GATT_PROTOCOL_MODE_CHARACTERISTIC,
GATT_HID_INFORMATION_CHARACTERISTIC,
GATT_HID_CONTROL_POINT_CHARACTERISTIC,
GATT_REPORT_REFERENCE_DESCRIPTOR
)
# -----------------------------------------------------------------------------
# Protocol Modes
HID_BOOT_PROTOCOL = 0x00
HID_REPORT_PROTOCOL = 0x01
# Report Types
HID_INPUT_REPORT = 0x01
HID_OUTPUT_REPORT = 0x02
HID_FEATURE_REPORT = 0x03
# Report Map
HID_KEYBOARD_REPORT_MAP = bytes([
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
0x09, 0x06, # Usage (Keyboard)
0xA1, 0x01, # Collection (Application)
0x85, 0x01, # . Report ID (1)
0x05, 0x07, # . Usage Page (Kbrd/Keypad)
0x19, 0xE0, # . Usage Minimum (0xE0)
0x29, 0xE7, # . Usage Maximum (0xE7)
0x15, 0x00, # . Logical Minimum (0)
0x25, 0x01, # . Logical Maximum (1)
0x75, 0x01, # . Report Size (1)
0x95, 0x08, # . Report Count (8)
0x81, 0x02, # . Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01, # . Report Count (1)
0x75, 0x08, # . Report Size (8)
0x81, 0x01, # . Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x06, # . Report Count (6)
0x75, 0x08, # . Report Size (8)
0x15, 0x00, # . Logical Minimum (0x00)
0x25, 0x94, # . Logical Maximum (0x94)
0x05, 0x07, # . Usage Page (Kbrd/Keypad)
0x19, 0x00, # . Usage Minimum (0x00)
0x29, 0x94, # . Usage Maximum (0x94)
0x81, 0x00, # . Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x05, # . Report Count (5)
0x75, 0x01, # . Report Size (1)
0x05, 0x08, # . Usage Page (LEDs)
0x19, 0x01, # . Usage Minimum (Num Lock)
0x29, 0x05, # . Usage Maximum (Kana)
0x91, 0x02, # . Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x95, 0x01, # . Report Count (1)
0x75, 0x03, # . Report Size (3)
0x91, 0x01, # . Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0 # End Collection
])
# -----------------------------------------------------------------------------
class ServerListener(Device.Listener, Connection.Listener):
def __init__(self, device):
self.device = device
@AsyncRunner.run_in_task()
async def on_connection(self, connection):
print(f'=== Connected to {connection}')
connection.listener = self
@AsyncRunner.run_in_task()
async def on_disconnection(self, reason):
print(f'### Disconnected, reason={reason}')
# -----------------------------------------------------------------------------
def on_hid_control_point_write(connection, value):
print(f'Control Point Write: {value}')
# -----------------------------------------------------------------------------
def on_report(characteristic, value):
print(color('Report:', 'cyan'), value.hex(), 'from', characteristic)
# -----------------------------------------------------------------------------
async def keyboard_host(device, peer_address):
await device.power_on()
connection = await device.connect(peer_address)
await connection.pair()
peer = Peer(connection)
await peer.discover_service(GATT_DEVICE_HUMAN_INTERFACE_DEVICE_SERVICE)
hid_services = peer.get_services_by_uuid(GATT_DEVICE_HUMAN_INTERFACE_DEVICE_SERVICE)
if not hid_services:
print(color('!!! No HID service', 'red'))
return
await peer.discover_characteristics()
protocol_mode_characteristics = peer.get_characteristics_by_uuid(GATT_PROTOCOL_MODE_CHARACTERISTIC)
if not protocol_mode_characteristics:
print(color('!!! No Protocol Mode characteristic', 'red'))
return
protocol_mode_characteristic = protocol_mode_characteristics[0]
hid_information_characteristics = peer.get_characteristics_by_uuid(GATT_HID_INFORMATION_CHARACTERISTIC)
if not hid_information_characteristics:
print(color('!!! No HID Information characteristic', 'red'))
return
hid_information_characteristic = hid_information_characteristics[0]
report_map_characteristics = peer.get_characteristics_by_uuid(GATT_REPORT_MAP_CHARACTERISTIC)
if not report_map_characteristics:
print(color('!!! No Report Map characteristic', 'red'))
return
report_map_characteristic = report_map_characteristics[0]
control_point_characteristics = peer.get_characteristics_by_uuid(GATT_HID_CONTROL_POINT_CHARACTERISTIC)
if not control_point_characteristics:
print(color('!!! No Control Point characteristic', 'red'))
return
# control_point_characteristic = control_point_characteristics[0]
report_characteristics = peer.get_characteristics_by_uuid(GATT_REPORT_CHARACTERISTIC)
if not report_characteristics:
print(color('!!! No Report characteristic', 'red'))
return
for i, characteristic in enumerate(report_characteristics):
print(color('REPORT:', 'yellow'), characteristic)
if characteristic.properties & Characteristic.NOTIFY:
await peer.discover_descriptors(characteristic)
report_reference_descriptor = characteristic.get_descriptor(GATT_REPORT_REFERENCE_DESCRIPTOR)
if report_reference_descriptor:
report_reference = await peer.read_value(report_reference_descriptor)
print(color(' Report Reference:', 'blue'), report_reference.hex())
else:
report_reference = bytes([0, 0])
await peer.subscribe(characteristic, lambda value, param=f'[{i}] {report_reference.hex()}': on_report(param, value))
protocol_mode = await peer.read_value(protocol_mode_characteristic)
print(f'Protocol Mode: {protocol_mode.hex()}')
hid_information = await peer.read_value(hid_information_characteristic)
print(f'HID Information: {hid_information.hex()}')
report_map = await peer.read_value(report_map_characteristic)
print(f'Report Map: {report_map.hex()}')
await asyncio.get_running_loop().create_future()
# -----------------------------------------------------------------------------
async def keyboard_device(device, command):
# Create an 'input report' characteristic to send keyboard reports to the host
input_report_characteristic = Characteristic(
GATT_REPORT_CHARACTERISTIC,
Characteristic.READ | Characteristic.WRITE | Characteristic.NOTIFY,
Characteristic.READABLE | Characteristic.WRITEABLE,
bytes([0, 0, 0, 0, 0, 0, 0, 0]),
[
Descriptor(GATT_REPORT_REFERENCE_DESCRIPTOR, Descriptor.READABLE, bytes([0x01, HID_INPUT_REPORT]))
]
)
# Create an 'output report' characteristic to receive keyboard reports from the host
output_report_characteristic = Characteristic(
GATT_REPORT_CHARACTERISTIC,
Characteristic.READ | Characteristic.WRITE | Characteristic.WRITE_WITHOUT_RESPONSE,
Characteristic.READABLE | Characteristic.WRITEABLE,
bytes([0]),
[
Descriptor(GATT_REPORT_REFERENCE_DESCRIPTOR, Descriptor.READABLE, bytes([0x01, HID_OUTPUT_REPORT]))
]
)
# Add the services to the GATT sever
device.add_services([
Service(
GATT_DEVICE_INFORMATION_SERVICE,
[
Characteristic(
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
'Bumble'
)
]
),
Service(
GATT_DEVICE_HUMAN_INTERFACE_DEVICE_SERVICE,
[
Characteristic(
GATT_PROTOCOL_MODE_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
bytes([HID_REPORT_PROTOCOL])
),
Characteristic(
GATT_HID_INFORMATION_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
bytes([0x11, 0x01, 0x00, 0x03]) # bcdHID=1.1, bCountryCode=0x00, Flags=RemoteWake|NormallyConnectable
),
Characteristic(
GATT_HID_CONTROL_POINT_CHARACTERISTIC,
Characteristic.WRITE_WITHOUT_RESPONSE,
Characteristic.WRITEABLE,
CharacteristicValue(write=on_hid_control_point_write)
),
Characteristic(
GATT_REPORT_MAP_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
HID_KEYBOARD_REPORT_MAP
),
input_report_characteristic,
output_report_characteristic
]
),
Service(
GATT_DEVICE_BATTERY_SERVICE,
[
Characteristic(
GATT_BATTERY_LEVEL_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
bytes([100])
)
]
)
])
# Debug print
for attribute in device.gatt_server.attributes:
print(attribute)
# Set the advertising data
device.advertising_data = bytes(
AdvertisingData([
(AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble Keyboard', 'utf-8')),
(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
bytes(GATT_DEVICE_HUMAN_INTERFACE_DEVICE_SERVICE)),
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x03C1)),
(AdvertisingData.FLAGS, bytes([0x05]))
])
)
# Attach a listener
device.listener = ServerListener(device)
# Go!
await device.power_on()
await device.start_advertising(auto_restart=True)
if command == 'web':
# Start a Websocket server to receive events from a web page
async def serve(websocket, path):
while True:
try:
message = await websocket.recv()
print('Received: ', str(message))
parsed = json.loads(message)
message_type = parsed['type']
if message_type == 'keydown':
# Only deal with keys a to z for now
key = parsed['key']
if len(key) == 1:
code = ord(key)
if code >= ord('a') and code <= ord('z'):
hid_code = 0x04 + code - ord('a')
input_report_characteristic.value = bytes([0, 0, hid_code, 0, 0, 0, 0, 0])
await device.notify_subscribers(input_report_characteristic)
elif message_type == 'keyup':
input_report_characteristic.value = bytes.fromhex('0000000000000000')
await device.notify_subscribers(input_report_characteristic)
except websockets.exceptions.ConnectionClosedOK:
pass
await websockets.serve(serve, 'localhost', 8989)
await asyncio.get_event_loop().create_future()
else:
message = bytes('hello', 'ascii')
while True:
for letter in message:
await asyncio.sleep(3.0)
# Keypress for the letter
keycode = 0x04 + letter - 0x61
input_report_characteristic.value = bytes([0, 0, keycode, 0, 0, 0, 0, 0])
await device.notify_subscribers(input_report_characteristic)
# Key release
input_report_characteristic.value = bytes.fromhex('0000000000000000')
await device.notify_subscribers(input_report_characteristic)
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 4:
print('Usage: python keyboard.py <device-config> <transport-spec> <command>')
print(' where <command> is one of:')
print(' connect <address> (run a keyboard host, connecting to a keyboard)')
print(' web (run a keyboard with keypress input from a web page, see keyboard.html')
print(' sim (run a keyboard simulation, emitting a canned sequence of keystrokes')
print('example: python keyboard.py keyboard.json usb:0 sim')
print('example: python keyboard.py keyboard.json usb:0 connect A0:A1:A2:A3:A4:A5')
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)
command = sys.argv[3]
if command == 'connect':
# Run as a Keyboard host
await keyboard_host(device, sys.argv[4])
elif command in {'sim', 'web'}:
# Run as a keyboard device
await keyboard_device(device, command)
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+188
View File
@@ -0,0 +1,188 @@
# 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
from bumble.transport import open_transport_or_link
from bumble.core import (
BT_BR_EDR_TRANSPORT,
BT_AVDTP_PROTOCOL_ID,
BT_AUDIO_SINK_SERVICE,
BT_L2CAP_PROTOCOL_ID
)
from bumble.avdtp import (
Protocol as AVDTP_Protocol,
find_avdtp_service_with_connection
)
from bumble.a2dp import make_audio_source_service_sdp_records
from bumble.sdp import (
Client as SDP_Client,
ServiceAttribute,
DataElement,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
)
# -----------------------------------------------------------------------------
def sdp_records():
service_record_handle = 0x00010001
return {
service_record_handle: make_audio_source_service_sdp_records(service_record_handle)
}
# -----------------------------------------------------------------------------
async def find_a2dp_service(device, connection):
# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
# Search for services with an Audio Sink service class
search_result = await sdp_client.search_attributes(
[BT_AUDIO_SINK_SERVICE],
[
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
]
)
print(color('==================================', 'blue'))
print(color('A2DP Sink Services:', 'yellow'))
service_version = None
for attribute_list in search_result:
print(color('SERVICE:', 'green'))
# Service classes
service_class_id_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
)
if service_class_id_list:
if service_class_id_list.value:
print(color(' Service Classes:', 'green'))
for service_class_id in service_class_id_list.value:
print(' ', service_class_id.value)
# Protocol info
protocol_descriptor_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
if protocol_descriptor_list:
print(color(' Protocol:', 'green'))
for protocol_descriptor in protocol_descriptor_list.value:
if protocol_descriptor.value[0].value == BT_L2CAP_PROTOCOL_ID:
if len(protocol_descriptor.value) >= 2:
psm = protocol_descriptor.value[1].value
print(f'{color(" L2CAP PSM:", "cyan")} {psm}')
elif protocol_descriptor.value[0].value == BT_AVDTP_PROTOCOL_ID:
if len(protocol_descriptor.value) >= 2:
avdtp_version_major = protocol_descriptor.value[1].value >> 8
avdtp_version_minor = protocol_descriptor.value[1].value & 0xFF
print(f'{color(" AVDTP Version:", "cyan")} {avdtp_version_major}.{avdtp_version_minor}')
service_version = (avdtp_version_major, avdtp_version_minor)
# Profile info
bluetooth_profile_descriptor_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
if bluetooth_profile_descriptor_list:
if bluetooth_profile_descriptor_list.value:
if bluetooth_profile_descriptor_list.value[0].type == DataElement.SEQUENCE:
bluetooth_profile_descriptors = bluetooth_profile_descriptor_list.value
else:
# Sometimes, instead of a list of lists, we just find a list. Fix that
bluetooth_profile_descriptors = [bluetooth_profile_descriptor_list]
print(color(' Profiles:', 'green'))
for bluetooth_profile_descriptor in bluetooth_profile_descriptors:
version_major = bluetooth_profile_descriptor.value[1].value >> 8
version_minor = bluetooth_profile_descriptor.value[1].value & 0xFF
print(f' {bluetooth_profile_descriptor.value[0].value} - version {version_major}.{version_minor}')
await sdp_client.disconnect()
return service_version
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 4:
print('Usage: run_a2dp_info.py <device-config> <transport-spec> <bt-addr>')
print('example: run_a2dp_info.py classic1.json usb:0 14:7D:DA:4E:53:A8')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
# Start the controller
await device.power_on()
# Setup the SDP to expose a SRC service, in case the remote device queries us back
device.sdp_service_records = sdp_records()
# Connect to a peer
target_address = sys.argv[3]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
print(f'=== Connected to {connection.peer_address}!')
# Request authentication
print('*** Authenticating...')
await connection.authenticate()
print('*** Authenticated')
# Enable encryption
print('*** Enabling encryption...')
await connection.encrypt()
print('*** Encryption on')
# Look for an A2DP service
avdtp_version = await find_a2dp_service(device, connection)
if not avdtp_version:
print(color('!!! no AVDTP service found'))
return
print(f'AVDTP version: {avdtp_version[0]}.{avdtp_version[1]}')
# Create a client to interact with the remote device
client = await AVDTP_Protocol.connect(connection, avdtp_version)
# Discover all endpoints on the remote device
endpoints = await client.discover_remote_endpoints()
print(f'@@@ Found {len(endpoints)} endpoints')
for endpoint in endpoints:
print('@@@', endpoint)
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+163
View File
@@ -0,0 +1,163 @@
# 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
from bumble.transport import open_transport_or_link
from bumble.core import BT_BR_EDR_TRANSPORT
from bumble.avdtp import (
AVDTP_AUDIO_MEDIA_TYPE,
Protocol,
Listener,
MediaCodecCapabilities
)
from bumble.a2dp import (
make_audio_sink_service_sdp_records,
A2DP_SBC_CODEC_TYPE,
SBC_MONO_CHANNEL_MODE,
SBC_DUAL_CHANNEL_MODE,
SBC_SNR_ALLOCATION_METHOD,
SBC_LOUDNESS_ALLOCATION_METHOD,
SBC_STEREO_CHANNEL_MODE,
SBC_JOINT_STEREO_CHANNEL_MODE,
SbcMediaCodecInformation
)
Context = {
'output': None
}
# -----------------------------------------------------------------------------
def sdp_records():
service_record_handle = 0x00010001
return {
service_record_handle: make_audio_sink_service_sdp_records(service_record_handle)
}
# -----------------------------------------------------------------------------
def codec_capabilities():
# NOTE: this shouldn't be hardcoded, but passed on the command line instead
return MediaCodecCapabilities(
media_type = AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type = A2DP_SBC_CODEC_TYPE,
media_codec_information = SbcMediaCodecInformation.from_lists(
sampling_frequencies = [48000, 44100, 32000, 16000],
channel_modes = [
SBC_MONO_CHANNEL_MODE,
SBC_DUAL_CHANNEL_MODE,
SBC_STEREO_CHANNEL_MODE,
SBC_JOINT_STEREO_CHANNEL_MODE
],
block_lengths = [4, 8, 12, 16],
subbands = [4, 8],
allocation_methods = [SBC_LOUDNESS_ALLOCATION_METHOD, SBC_SNR_ALLOCATION_METHOD],
minimum_bitpool_value = 2,
maximum_bitpool_value = 53
)
)
# -----------------------------------------------------------------------------
def on_avdtp_connection(server):
# Add a sink endpoint to the server
sink = server.add_sink(codec_capabilities())
sink.on('rtp_packet', on_rtp_packet)
# -----------------------------------------------------------------------------
def on_rtp_packet(packet):
header = packet.payload[0]
fragmented = header >> 7
start = (header >> 6) & 0x01
last = (header >> 5) & 0x01
number_of_frames = header & 0x0F
if fragmented:
print(f'RTP: fragment {number_of_frames}')
else:
print(f'RTP: {number_of_frames} frames')
Context['output'].write(packet.payload[1:])
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 4:
print('Usage: run_a2dp_sink.py <device-config> <transport-spec> <sbc-file> [<bt-addr>]')
print('example: run_a2dp_sink.py classic1.json usb:0 output.sbc')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
with open(sys.argv[3], 'wb') as sbc_file:
Context['output'] = sbc_file
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
# Setup the SDP to expose the sink service
device.sdp_service_records = sdp_records()
# Start the controller
await device.power_on()
# Create a listener to wait for AVDTP connections
listener = Listener(Listener.create_registrar(device))
listener.on('connection', on_avdtp_connection)
if len(sys.argv) >= 5:
# Connect to the source
target_address = sys.argv[4]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
print(f'=== Connected to {connection.peer_address}!')
# Request authentication
print('*** Authenticating...')
await connection.authenticate()
print('*** Authenticated')
# Enable encryption
print('*** Enabling encryption...')
await connection.encrypt()
print('*** Encryption on')
server = await Protocol.connect(connection)
listener.set_server(connection, server)
sink = server.add_sink(codec_capabilities())
sink.on('rtp_packet', on_rtp_packet)
else:
# Start being discoverable and connectable
await device.set_discoverable(True)
await device.set_connectable(True)
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+175
View File
@@ -0,0 +1,175 @@
# 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
from bumble.transport import open_transport_or_link
from bumble.core import BT_BR_EDR_TRANSPORT
from bumble.avdtp import (
find_avdtp_service_with_connection,
AVDTP_AUDIO_MEDIA_TYPE,
MediaCodecCapabilities,
MediaPacketPump,
Protocol,
Listener
)
from bumble.a2dp import (
SBC_JOINT_STEREO_CHANNEL_MODE,
SBC_LOUDNESS_ALLOCATION_METHOD,
make_audio_source_service_sdp_records,
A2DP_SBC_CODEC_TYPE,
SbcMediaCodecInformation,
SbcPacketSource
)
# -----------------------------------------------------------------------------
def sdp_records():
service_record_handle = 0x00010001
return {
service_record_handle: make_audio_source_service_sdp_records(service_record_handle)
}
# -----------------------------------------------------------------------------
def codec_capabilities():
# NOTE: this shouldn't be hardcoded, but should be inferred from the input file instead
return MediaCodecCapabilities(
media_type = AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type = A2DP_SBC_CODEC_TYPE,
media_codec_information = SbcMediaCodecInformation.from_discrete_values(
sampling_frequency = 44100,
channel_mode = SBC_JOINT_STEREO_CHANNEL_MODE,
block_length = 16,
subbands = 8,
allocation_method = SBC_LOUDNESS_ALLOCATION_METHOD,
minimum_bitpool_value = 2,
maximum_bitpool_value = 53
)
)
# -----------------------------------------------------------------------------
def on_avdtp_connection(read_function, protocol):
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.mtu, codec_capabilities())
packet_pump = MediaPacketPump(packet_source.packets)
protocol.add_source(packet_source.codec_capabilities, packet_pump)
# -----------------------------------------------------------------------------
async def stream_packets(read_function, protocol):
# Discover all endpoints on the remote device
endpoints = await protocol.discover_remote_endpoints()
for endpoint in endpoints:
print('@@@', endpoint)
# Select a sink
sink = protocol.find_remote_sink_by_codec(AVDTP_AUDIO_MEDIA_TYPE, A2DP_SBC_CODEC_TYPE)
if sink is None:
print(color('!!! no SBC sink found', 'red'))
return
print(f'### Selected sink: {sink.seid}')
# Stream the packets
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.mtu, codec_capabilities())
packet_pump = MediaPacketPump(packet_source.packets)
source = protocol.add_source(packet_source.codec_capabilities, packet_pump)
stream = await protocol.create_stream(source, sink)
await stream.start()
await asyncio.sleep(5)
await stream.stop()
await asyncio.sleep(5)
await stream.start()
await asyncio.sleep(5)
await stream.stop()
await stream.close()
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 4:
print('Usage: run_a2dp_source.py <device-config> <transport-spec> <sbc-file> [<bluetooth-address>]')
print('example: run_a2dp_source.py classic1.json usb:0 test.sbc E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
# Setup the SDP to expose the SRC service
device.sdp_service_records = sdp_records()
# Start
await device.power_on()
with open(sys.argv[3], 'rb') as sbc_file:
# NOTE: this should be using asyncio file reading, but blocking reads are good enough for testing
async def read(byte_count):
return sbc_file.read(byte_count)
if len(sys.argv) > 4:
# Connect to a peer
target_address = sys.argv[4]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
print(f'=== Connected to {connection.peer_address}!')
# Request authentication
print('*** Authenticating...')
await connection.authenticate()
print('*** Authenticated')
# Enable encryption
print('*** Enabling encryption...')
await connection.encrypt()
print('*** Encryption on')
# Look for an A2DP service
avdtp_version = await find_avdtp_service_with_connection(device, connection)
if not avdtp_version:
print(color('!!! no A2DP service found'))
return
# Create a client to interact with the remote device
protocol = await Protocol.connect(connection, avdtp_version)
# Start streaming
await stream_packets(read, protocol)
else:
# Create a listener to wait for AVDTP connections
listener = Listener(Listener.create_registrar(device), version=(1, 2))
listener.on('connection', lambda protocol: on_avdtp_connection(read, protocol))
# Become connectable and wait for a connection
await device.set_discoverable(True)
await device.set_connectable(True)
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+48
View File
@@ -0,0 +1,48 @@
# 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
from bumble.hci import *
from bumble.controller import *
from bumble.device import *
from bumble.transport import *
from bumble.host import *
from bumble.transport import open_transport_or_link
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 3:
print('Usage: run_advertiser.py <config-file> <transport-spec>')
print('example: run_advertiser.py device1.json link-relay:ws://localhost:8888/test')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
await device.power_on()
await device.start_advertising()
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+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
from bumble.transport import open_transport_or_link
from bumble.core import BT_BR_EDR_TRANSPORT, BT_L2CAP_PROTOCOL_ID
from bumble.sdp import Client as SDP_Client, SDP_PUBLIC_BROWSE_ROOT, SDP_ALL_ATTRIBUTES_RANGE
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_classic_connect.py <device-config> <transport-spec> <bluetooth-address>')
print('example: run_classic_connect.py classic1.json usb:04b4:f901 E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
await device.power_on()
# Connect to a peer
target_address = sys.argv[3]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
print(f'=== Connected to {connection.peer_address}!')
# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
# List all services in the root browse group
service_record_handles = await sdp_client.search_services([SDP_PUBLIC_BROWSE_ROOT])
print(color('\n==================================', 'blue'))
print(color('SERVICES:', 'yellow'), service_record_handles)
# For each service in the root browse group, get all its attributes
for service_record_handle in service_record_handles:
attributes = await sdp_client.get_attributes(service_record_handle, [SDP_ALL_ATTRIBUTES_RANGE])
print(color(f'SERVICE {service_record_handle:04X} attributes:', 'yellow'))
for attribute in attributes:
print(' ', attribute.to_string(color=True))
# Search for services with an L2CAP service attribute
search_result = await sdp_client.search_attributes([BT_L2CAP_PROTOCOL_ID], [SDP_ALL_ATTRIBUTES_RANGE])
print(color('\n==================================', 'blue'))
print(color('SEARCH RESULTS:', 'yellow'))
for attribute_list in search_result:
print(color('SERVICE:', 'green'))
print(' ' + '\n '.join([attribute.to_string(color=True) for attribute in attribute_list]))
await sdp_client.disconnect()
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+104
View File
@@ -0,0 +1,104 @@
# 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 bumble.device import Device
from bumble.transport import open_transport_or_link
from bumble.sdp import (
DataElement,
ServiceAttribute,
SDP_PUBLIC_BROWSE_ROOT,
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
from bumble.core import (
BT_AUDIO_SINK_SERVICE,
BT_L2CAP_PROTOCOL_ID,
BT_AVDTP_PROTOCOL_ID,
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE
)
# -----------------------------------------------------------------------------
SDP_SERVICE_RECORDS = {
0x00010001: [
ServiceAttribute(SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, DataElement.unsigned_integer_32(0x00010001)),
ServiceAttribute(SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, DataElement.sequence([
DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)
])),
ServiceAttribute(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement.sequence([DataElement.uuid(BT_AUDIO_SINK_SERVICE)])
),
ServiceAttribute(
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
DataElement.sequence([
DataElement.sequence([
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
DataElement.unsigned_integer_16(25)
]),
DataElement.sequence([
DataElement.uuid(BT_AVDTP_PROTOCOL_ID),
DataElement.unsigned_integer_16(256)
])
])
),
ServiceAttribute(
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
DataElement.sequence([
DataElement.sequence([
DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE),
DataElement.unsigned_integer_16(256)
])
])
)
]
}
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_classic_discoverable.py <device-config> <transport-spec>')
print('example: run_classic_discoverable.py classic1.json usb:04b4:f901')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
device.sdp_service_records = SDP_SERVICE_RECORDS
await device.power_on()
# Start being discoverable and connectable
await device.set_discoverable(True)
await device.set_connectable(True)
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+64
View File
@@ -0,0 +1,64 @@
# 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
from bumble.transport import open_transport_or_link
from bumble.core import DeviceClass
# -----------------------------------------------------------------------------
class DiscoveryListener(Device.Listener):
def on_inquiry_result(self, address, class_of_device, eir_data, rssi):
service_classes, major_device_class, minor_device_class = DeviceClass.split_class_of_device(class_of_device)
separator = '\n '
print(f'>>> {color(address, "yellow")}:')
print(f' Device Class (raw): {class_of_device:06X}')
print(f' Device Major Class: {DeviceClass.major_device_class_name(major_device_class)}')
print(f' Device Minor Class: {DeviceClass.minor_device_class_name(major_device_class, minor_device_class)}')
print(f' Device Services: {", ".join(DeviceClass.service_class_labels(service_classes))}')
print(f' RSSI: {rssi}')
if eir_data.ad_structures:
print(f' {eir_data.to_string(separator)}')
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 2:
print('Usage: run_classic_discovery.py <transport-spec>')
print('example: run_classic_discovery.py usb:04b4:f901')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[1]) as (hci_source, hci_sink):
print('<<< connected')
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
device.listener = DiscoveryListener()
await device.power_on()
await device.start_discovery()
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+58
View File
@@ -0,0 +1,58 @@
# 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 bumble.device import Device
from bumble.transport import open_transport_or_link
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_connect_and_encrypt.py <device-config> <transport-spec> <bluetooth-address>')
print('example: run_connect_and_encrypt.py device1.json usb:0 E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
await device.power_on()
# Connect to the peer
target_address = sys.argv[3]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address)
print('=== Connected')
print('*** Encrypting...')
try:
await connection.encrypt()
except Exception as error:
print(f'!!! Encryption failed: {error}')
return
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+88
View File
@@ -0,0 +1,88 @@
# 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 logging
import asyncio
import sys
import os
from bumble.hci import *
from bumble.controller import *
from bumble.host import *
from bumble.device import *
from bumble.transport import *
from bumble.link import *
from bumble.transport import open_transport_or_link
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 4:
print('Usage: run_controller.py <controller-address> <device-config> <transport-spec>')
print('example: run_controller.py F2:F3:F4:F5:F6:F7 device1.json udp:0.0.0.0:22333,172.16.104.161:22333')
return
print('>>> connecting to HCI...')
async with await open_transport_or_link(sys.argv[3]) as (hci_source, hci_sink):
print('>>> connected')
# Create a local link
link = LocalLink()
# 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]
# Create a second controller using the same link
controller2 = Controller('C2', link = link)
# Create a host for the second controller
host = Host()
host.controller = controller2
# Create a device to manage the host
device = Device.from_config_file(sys.argv[2])
device.host = host
# Add some basic services to the device's GATT server
descriptor = Descriptor(GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR, Descriptor.READABLE, 'My Description')
manufacturer_name_characteristic = Characteristic(
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
"Fitbit",
[descriptor]
)
device_info_service = Service(GATT_DEVICE_INFORMATION_SERVICE, [
manufacturer_name_characteristic
])
device.add_service(device_info_service)
# Debug print
for attribute in device.gatt_server.attributes:
print(attribute)
await device.power_on()
await device.start_advertising()
await device.start_scanning()
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+74
View File
@@ -0,0 +1,74 @@
# 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 logging
import asyncio
import sys
import os
from colors import color
from bumble.device import Device
from bumble.controller import Controller
from bumble.link import LocalLink
from bumble.transport import open_transport_or_link
# -----------------------------------------------------------------------------
class ScannerListener(Device.Listener):
def on_advertisement(self, address, ad_data, rssi, connectable):
address_type_string = ('P', 'R', 'PI', 'RI')[address.address_type]
address_color = 'yellow' if connectable else 'red'
if address_type_string.startswith('P'):
type_color = 'green'
else:
type_color = 'cyan'
print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]: RSSI={rssi}, {ad_data}')
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) != 2:
print('Usage: run_controller.py <transport-spec>')
print('example: run_controller_with_scanner.py serial:/dev/pts/14,1000000')
return
print('>>> connecting to HCI...')
async with await open_transport_or_link(sys.argv[1]) as (hci_source, hci_sink):
print('>>> connected')
# Create a local link
link = LocalLink()
# 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)
controller1.address = 'E0:E1:E2:E3:E4:E5'
# Create a second controller using the same link
controller2 = Controller('C2', link = link)
# Create a device with a scanner listener
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', controller2, controller2)
device.listener = ScannerListener()
await device.power_on()
await device.start_scanning()
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+98
View File
@@ -0,0 +1,98 @@
# 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.core import ProtocolError
from bumble.device import Device, Peer
from bumble.gatt import show_services
from bumble.transport import open_transport_or_link
from bumble.utils import AsyncRunner
# -----------------------------------------------------------------------------
class Listener(Device.Listener):
def __init__(self, device):
self.device = device
@AsyncRunner.run_in_task()
async def on_connection(self, connection):
print(f'=== 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)
except TimeoutError:
print(color('read timeout'))
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_gatt_client.py <device-config> <transport-spec> [<bluetooth-address>]')
print('example: run_gatt_client.py device1.json usb:0 E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device to manage the host, with a custom listener
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.listener = Listener(device)
await device.power_on()
# Connect to a peer
if len(sys.argv) > 3:
target_address = sys.argv[3]
print(f'=== Connecting to {target_address}...')
await device.connect(target_address)
else:
await device.start_advertising()
await asyncio.get_running_loop().create_future()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+124
View File
@@ -0,0 +1,124 @@
# 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 os
import logging
from colors import color
from bumble.core import ProtocolError
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,
Descriptor,
show_services,
GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR,
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
GATT_DEVICE_INFORMATION_SERVICE
)
# -----------------------------------------------------------------------------
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):
print(f'### Server: connected to {connection}')
# -----------------------------------------------------------------------------
async def main():
# Create a local link
link = LocalLink()
# Setup a stack for the client
client_controller = Controller("client controller", link = link)
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
server_controller = Controller("server controller", link = link)
server_host = Host()
server_host.controller = server_controller
server_device = Device("server", address = 'F6:F7:F8:F9:FA:FB', host = server_host)
server_device.listener = ServerListener()
await server_device.power_on()
# Add a few entries to the device's GATT server
descriptor = Descriptor(GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR, Descriptor.READABLE, 'My Description')
manufacturer_name_characteristic = Characteristic(
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
"Fitbit",
[descriptor]
)
device_info_service = Service(GATT_DEVICE_INFORMATION_SERVICE, [
manufacturer_name_characteristic
])
server_device.add_service(device_info_service)
# Connect the client to the server
await client_device.connect(server_device.address)
await asyncio.get_running_loop().create_future()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+147
View File
@@ -0,0 +1,147 @@
# 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 bumble.device import Device, Connection
from bumble.transport import open_transport_or_link
from bumble.att import (
ATT_Error,
ATT_INSUFFICIENT_ENCRYPTION_ERROR
)
from bumble.gatt import (
Service,
Characteristic,
CharacteristicValue,
Descriptor,
GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR,
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
GATT_DEVICE_INFORMATION_SERVICE
)
# -----------------------------------------------------------------------------
class Listener(Device.Listener, Connection.Listener):
def __init__(self, device):
self.device = device
def on_connection(self, connection):
print(f'=== Connected to {connection}')
connection.listener = self
def on_disconnection(self, reason):
print(f'### Disconnected, reason={reason}')
def my_custom_read(connection):
print('----- READ from', connection)
return bytes(f'Hello {connection}', 'ascii')
def my_custom_write(connection, value):
print(f'----- WRITE from {connection}: {value}')
def my_custom_read_with_error(connection):
print('----- READ from', connection, '[returning error]')
if connection.is_encrypted:
return bytes([123])
else:
raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
def my_custom_write_with_error(connection, value):
print(f'----- WRITE from {connection}: {value}', '[returning error]')
if not connection.is_encrypted:
raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_gatt_server.py <device-config> <transport-spec> [<bluetooth-address>]')
print('example: run_gatt_server.py device1.json usb:0 E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device to manage the host
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.listener = Listener(device)
# Add a few entries to the device's GATT server
descriptor = Descriptor(GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR, Descriptor.READABLE, 'My Description')
manufacturer_name_characteristic = Characteristic(
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
Characteristic.READ,
Characteristic.READABLE,
'Fitbit',
[descriptor]
)
device_info_service = Service(GATT_DEVICE_INFORMATION_SERVICE, [
manufacturer_name_characteristic
])
custom_service1 = Service(
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
[
Characteristic(
'D901B45B-4916-412E-ACCA-376ECB603B2C',
Characteristic.READ | Characteristic.WRITE,
Characteristic.READABLE | Characteristic.WRITEABLE,
CharacteristicValue(read=my_custom_read, write=my_custom_write)
),
Characteristic(
'552957FB-CF1F-4A31-9535-E78847E1A714',
Characteristic.READ | Characteristic.WRITE,
Characteristic.READABLE | Characteristic.WRITEABLE,
CharacteristicValue(read=my_custom_read_with_error, write=my_custom_write_with_error)
),
Characteristic(
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
Characteristic.READ | Characteristic.NOTIFY,
Characteristic.READABLE,
'hello'
)
]
)
device.add_services([device_info_service, custom_service1])
# Debug print
for attribute in device.gatt_server.attributes:
print(attribute)
# Get things going
await device.power_on()
# Connect to a peer
if len(sys.argv) > 3:
target_address = sys.argv[3]
print(f'=== Connecting to {target_address}...')
await device.connect(target_address)
else:
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())
+209
View File
@@ -0,0 +1,209 @@
# 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
from bumble.transport import open_transport_or_link
from bumble.core import ConnectionError, BT_BR_EDR_TRANSPORT
from bumble.rfcomm import Client
from bumble.sdp import (
Client as SDP_Client,
DataElement,
ServiceAttribute,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
from bumble.hci import (
BT_HANDSFREE_SERVICE,
BT_RFCOMM_PROTOCOL_ID
)
from bumble.hfp import HfpProtocol
# -----------------------------------------------------------------------------
async def list_rfcomm_channels(device, connection):
# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
# Search for services that support the Handsfree Profile
search_result = await sdp_client.search_attributes(
[BT_HANDSFREE_SERVICE],
[
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
]
)
print(color('==================================', 'blue'))
print(color('Handsfree Services:', 'yellow'))
rfcomm_channels = []
for attribute_list in search_result:
# Look for the RFCOMM Channel number
protocol_descriptor_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
if protocol_descriptor_list:
for protocol_descriptor in protocol_descriptor_list.value:
if len(protocol_descriptor.value) >= 2:
if protocol_descriptor.value[0].value == BT_RFCOMM_PROTOCOL_ID:
print(color('SERVICE:', 'green'))
print(color(' RFCOMM Channel:', 'cyan'), protocol_descriptor.value[1].value)
rfcomm_channels.append(protocol_descriptor.value[1].value)
# List profiles
bluetooth_profile_descriptor_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
if bluetooth_profile_descriptor_list:
if bluetooth_profile_descriptor_list.value:
if bluetooth_profile_descriptor_list.value[0].type == DataElement.SEQUENCE:
bluetooth_profile_descriptors = bluetooth_profile_descriptor_list.value
else:
# Sometimes, instead of a list of lists, we just find a list. Fix that
bluetooth_profile_descriptors = [bluetooth_profile_descriptor_list]
print(color(' Profiles:', 'green'))
for bluetooth_profile_descriptor in bluetooth_profile_descriptors:
version_major = bluetooth_profile_descriptor.value[1].value >> 8
version_minor = bluetooth_profile_descriptor.value[1].value & 0xFF
print(f' {bluetooth_profile_descriptor.value[0].value} - version {version_major}.{version_minor}')
# List service classes
service_class_id_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
)
if service_class_id_list:
if service_class_id_list.value:
print(color(' Service Classes:', 'green'))
for service_class_id in service_class_id_list.value:
print(' ', service_class_id.value)
await sdp_client.disconnect()
return rfcomm_channels
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 4:
print('Usage: run_hfp_gateway.py <device-config> <transport-spec> <bluetooth-address>')
print(' specifying a channel number, or "discover" to list all RFCOMM channels')
print('example: run_hfp_gateway.py hfp_gateway.json usb:04b4:f901 E1:CA:72:48:C4:E8')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
await device.power_on()
# Connect to a peer
target_address = sys.argv[3]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
print(f'=== Connected to {connection.peer_address}!')
# Get a list of all the Handsfree services (should only be 1)
channels = await list_rfcomm_channels(device, connection)
if len(channels) == 0:
print('!!! no service found')
return
# Pick the first one
channel = channels[0]
# Request authentication
print('*** Authenticating...')
await connection.authenticate()
print('*** Authenticated')
# Enable encryption
print('*** Enabling encryption...')
await connection.encrypt()
print('*** Encryption on')
# Create a client and start it
print('@@@ Starting to RFCOMM client...')
rfcomm_client = Client(device, connection)
rfcomm_mux = await rfcomm_client.start()
print('@@@ Started')
print(f'### Opening session for channel {channel}...')
try:
session = await rfcomm_mux.open_dlc(channel)
print('### Session open', session)
except ConnectionError as error:
print(f'### Session open failed: {error}')
await rfcomm_mux.disconnect()
print('@@@ Disconnected from RFCOMM server')
return
# Protocol loop (just for testing at this point)
protocol = HfpProtocol(session)
while True:
line = await protocol.next_line()
if line.startswith('AT+BRSF='):
protocol.send_response_line('+BRSF: 30')
protocol.send_response_line('OK')
elif line.startswith('AT+CIND=?'):
protocol.send_response_line('+CIND: ("call",(0,1)),("callsetup",(0-3)),("service",(0-1)),("signal",(0-5)),("roam",(0,1)),("battchg",(0-5)),("callheld",(0-2))')
protocol.send_response_line('OK')
elif line.startswith('AT+CIND?'):
protocol.send_response_line('+CIND: 0,0,1,4,1,5,0')
protocol.send_response_line('OK')
elif line.startswith('AT+CMER='):
protocol.send_response_line('OK')
elif line.startswith('AT+CHLD=?'):
protocol.send_response_line('+CHLD: 0')
protocol.send_response_line('OK')
elif line.startswith('AT+BTRH?'):
protocol.send_response_line('+BTRH: 0')
protocol.send_response_line('OK')
elif line.startswith('AT+CLIP='):
protocol.send_response_line('OK')
elif line.startswith('AT+VGS='):
protocol.send_response_line('OK')
elif line.startswith('AT+BIA='):
protocol.send_response_line('OK')
elif line.startswith('AT+BVRA='):
protocol.send_response_line('+BVRA: 1,1,12AA,1,1,"Message 1 from Janina"')
elif line.startswith('AT+XEVENT='):
protocol.send_response_line('OK')
elif line.startswith('AT+XAPL='):
protocol.send_response_line('OK')
else:
print(color('UNSUPPORTED AT COMMAND', 'red'))
protocol.send_response_line('ERROR')
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+165
View File
@@ -0,0 +1,165 @@
# 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 websockets
import json
from bumble.device import Device
from bumble.transport import open_transport_or_link
from bumble.rfcomm import Server as RfommServer
from bumble.sdp import (
DataElement,
ServiceAttribute,
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
from bumble.core import (
BT_GENERIC_AUDIO_SERVICE,
BT_HANDSFREE_SERVICE,
BT_L2CAP_PROTOCOL_ID,
BT_RFCOMM_PROTOCOL_ID
)
from bumble.hfp import HfpProtocol
# -----------------------------------------------------------------------------
def make_sdp_records(rfcomm_channel):
return {
0x00010001: [
ServiceAttribute(
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
DataElement.unsigned_integer_32(0x00010001)
),
ServiceAttribute(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement.sequence([
DataElement.uuid(BT_HANDSFREE_SERVICE),
DataElement.uuid(BT_GENERIC_AUDIO_SERVICE)
])
),
ServiceAttribute(
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
DataElement.sequence([
DataElement.sequence([
DataElement.uuid(BT_L2CAP_PROTOCOL_ID)
]),
DataElement.sequence([
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
DataElement.unsigned_integer_8(rfcomm_channel)
])
])
),
ServiceAttribute(
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
DataElement.sequence([
DataElement.sequence([
DataElement.uuid(BT_HANDSFREE_SERVICE),
DataElement.unsigned_integer_16(0x0105)
])
])
)
]
}
# -----------------------------------------------------------------------------
class UiServer:
protocol = None
async def start(self):
# Start a Websocket server to receive events from a web page
async def serve(websocket, path):
while True:
try:
message = await websocket.recv()
print('Received: ', str(message))
parsed = json.loads(message)
message_type = parsed['type']
if message_type == 'at_command':
if self.protocol is not None:
self.protocol.send_command_line(parsed['command'])
except websockets.exceptions.ConnectionClosedOK:
pass
await websockets.serve(serve, 'localhost', 8989)
# -----------------------------------------------------------------------------
async def protocol_loop(protocol):
await protocol.initialize_service()
while True:
await(protocol.next_line())
# -----------------------------------------------------------------------------
def on_dlc(dlc):
print('*** DLC connected', dlc)
protocol = HfpProtocol(dlc)
UiServer.protocol = protocol
asyncio.create_task(protocol_loop(protocol))
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_classic_hfp.py <device-config> <transport-spec>')
print('example: run_classic_hfp.py classic2.json usb:04b4:f901')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
# Create and register a server
rfcomm_server = RfommServer(device)
# Listen for incoming DLC connections
channel_number = rfcomm_server.listen(on_dlc)
print(f'### Listening for connection on channel {channel_number}')
# Advertise the HFP RFComm channel in the SDP
device.sdp_service_records = make_sdp_records(channel_number)
# Let's go!
await device.power_on()
# Start being discoverable and connectable
await device.set_discoverable(True)
await device.set_connectable(True)
# Start the UI websocket server to offer a few buttons and input boxes
ui_server = UiServer()
await ui_server.start()
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+120
View File
@@ -0,0 +1,120 @@
# 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 random
import logging
from bumble.device import Device, Connection
from bumble.transport import open_transport_or_link
from bumble.gatt import (
Service,
Characteristic
)
# -----------------------------------------------------------------------------
class Listener(Device.Listener, Connection.Listener):
def __init__(self, device):
self.device = device
def on_connection(self, connection):
print(f'=== Connected to {connection}')
connection.listener = self
def on_disconnection(self, reason):
print(f'### Disconnected, reason={reason}')
def on_characteristic_subscription(self, connection, characteristic, notify_enabled, indicate_enabled):
print(
f'$$$ Characteristic subscription for handle {characteristic.handle} from {connection}: '
f'notify {"enabled" if notify_enabled else "disabled"}, '
f'indicate {"enabled" if indicate_enabled else "disabled"}'
)
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_gatt_server.py <device-config> <transport-spec>')
print('example: run_gatt_server.py device1.json usb:0')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device to manage the host
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.listener = Listener(device)
# Add a few entries to the device's GATT server
characteristic1 = Characteristic(
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
Characteristic.READ | Characteristic.NOTIFY,
Characteristic.READABLE,
bytes([0x40])
)
characteristic2 = Characteristic(
'8EBDEBAE-0017-418E-8D3B-3A3809492165',
Characteristic.READ | Characteristic.INDICATE,
Characteristic.READABLE,
bytes([0x41])
)
characteristic3 = Characteristic(
'8EBDEBAE-0017-418E-8D3B-3A3809492165',
Characteristic.READ | Characteristic.NOTIFY | Characteristic.INDICATE,
Characteristic.READABLE,
bytes([0x42])
)
custom_service = Service(
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
[characteristic1, characteristic2, characteristic3]
)
device.add_services([custom_service])
# Debug print
for attribute in device.gatt_server.attributes:
print(attribute)
# Get things going
await device.power_on()
# Connect to a peer
if len(sys.argv) > 3:
target_address = sys.argv[3]
print(f'=== Connecting to {target_address}...')
await device.connect(target_address)
else:
await device.start_advertising(auto_restart=True)
while True:
await asyncio.sleep(3.0)
characteristic1.value = bytes([random.randint(0, 255)])
await device.notify_subscribers(characteristic1)
characteristic2.value = bytes([random.randint(0, 255)])
await device.indicate_subscribers(characteristic2)
characteristic3.value = bytes([random.randint(0, 255)])
await device.notify_subscribers(characteristic3)
await device.indicate_subscribers(characteristic3)
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+201
View File
@@ -0,0 +1,201 @@
# 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
from bumble.transport import open_transport_or_link
from bumble.core import ConnectionError, BT_BR_EDR_TRANSPORT
from bumble.rfcomm import Client
from bumble.sdp import (
Client as SDP_Client,
DataElement,
ServiceAttribute,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
from bumble.hci import BT_L2CAP_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID
# -----------------------------------------------------------------------------
async def list_rfcomm_channels(device, connection):
# Connect to the SDP Server
sdp_client = SDP_Client(device)
await sdp_client.connect(connection)
# Search for services with an L2CAP service attribute
search_result = await sdp_client.search_attributes(
[BT_L2CAP_PROTOCOL_ID],
[
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
]
)
print(color('==================================', 'blue'))
print(color('RFCOMM Services:', 'yellow'))
for attribute_list in search_result:
# Look for the RFCOMM Channel number
protocol_descriptor_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
if protocol_descriptor_list:
for protocol_descriptor in protocol_descriptor_list.value:
if len(protocol_descriptor.value) >= 2:
if protocol_descriptor.value[0].value == BT_RFCOMM_PROTOCOL_ID:
print(color('SERVICE:', 'green'))
print(color(' RFCOMM Channel:', 'cyan'), protocol_descriptor.value[1].value)
# List profiles
bluetooth_profile_descriptor_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
if bluetooth_profile_descriptor_list:
if bluetooth_profile_descriptor_list.value:
if bluetooth_profile_descriptor_list.value[0].type == DataElement.SEQUENCE:
bluetooth_profile_descriptors = bluetooth_profile_descriptor_list.value
else:
# Sometimes, instead of a list of lists, we just find a list. Fix that
bluetooth_profile_descriptors = [bluetooth_profile_descriptor_list]
print(color(' Profiles:', 'green'))
for bluetooth_profile_descriptor in bluetooth_profile_descriptors:
version_major = bluetooth_profile_descriptor.value[1].value >> 8
version_minor = bluetooth_profile_descriptor.value[1].value & 0xFF
print(f' {bluetooth_profile_descriptor.value[0].value} - version {version_major}.{version_minor}')
# List service classes
service_class_id_list = ServiceAttribute.find_attribute_in_list(
attribute_list,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
)
if service_class_id_list:
if service_class_id_list.value:
print(color(' Service Classes:', 'green'))
for service_class_id in service_class_id_list.value:
print(' ', service_class_id.value)
await sdp_client.disconnect()
# -----------------------------------------------------------------------------
class TcpServerProtocol(asyncio.Protocol):
def __init__(self, rfcomm_session):
self.rfcomm_session = rfcomm_session
def connection_made(self, transport):
peername = transport.get_extra_info('peername')
print(f'<<< TCP Server: connection from {peername}')
self.transport = transport
self.rfcomm_session.sink = self.rfcomm_data_received
def rfcomm_data_received(self, data):
print(f'<<< RFCOMM Data: {data.hex()}')
if self.transport:
self.transport.write(data)
else:
print('!!! no TCP connection, dropping data')
def data_received(self, data):
print(f'<<< TCP Server: data received: {len(data)} bytes - {data.hex()}')
self.rfcomm_session.write(data)
# -----------------------------------------------------------------------------
async def tcp_server(tcp_port, rfcomm_session):
print(f'$$$ Starting TCP server on port {tcp_port}')
server = await asyncio.get_running_loop().create_server(
lambda: TcpServerProtocol(rfcomm_session), '127.0.0.1', tcp_port
)
await asyncio.get_running_loop().create_future()
async with server:
await server.serve_forever()
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 5:
print('Usage: run_rfcomm_client.py <device-config> <transport-spec> <bluetooth-address> <channel>|discover [tcp-port]')
print(' specifying a channel number, or "discover" to list all RFCOMM channels')
print('example: run_rfcomm_client.py classic1.json usb:04b4:f901 E1:CA:72:48:C4:E8 8')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
await device.power_on()
# Connect to a peer
target_address = sys.argv[3]
print(f'=== Connecting to {target_address}...')
connection = await device.connect(target_address, transport=BT_BR_EDR_TRANSPORT)
print(f'=== Connected to {connection.peer_address}!')
channel = sys.argv[4]
if channel == 'discover':
await list_rfcomm_channels(device, connection)
return
# Request authentication
print('*** Authenticating...')
await connection.authenticate()
print('*** Authenticated')
# Enable encryption
print('*** Enabling encryption...')
await connection.encrypt()
print('*** Encryption on')
# Create a client and start it
print('@@@ Starting to RFCOMM client...')
rfcomm_client = Client(device, connection)
rfcomm_mux = await rfcomm_client.start()
print('@@@ Started')
channel = int(channel)
print(f'### Opening session for channel {channel}...')
try:
session = await rfcomm_mux.open_dlc(channel)
print('### Session open', session)
except ConnectionError as error:
print(f'### Session open failed: {error}')
await rfcomm_mux.disconnect()
print('@@@ Disconnected from RFCOMM server')
return
if len(sys.argv) == 6:
# A TCP port was specified, start listening
tcp_port = int(sys.argv[5])
asyncio.get_running_loop().create_task(tcp_server(tcp_port, session))
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+118
View File
@@ -0,0 +1,118 @@
# 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 bumble.device import Device
from bumble.transport import open_transport_or_link
from bumble.core import UUID
from bumble.rfcomm import Server
from bumble.sdp import (
DataElement,
ServiceAttribute,
SDP_PUBLIC_BROWSE_ROOT,
SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID
)
from bumble.hci import BT_L2CAP_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID
# -----------------------------------------------------------------------------
def sdp_records(channel):
return {
0x00010001: [
ServiceAttribute(SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, DataElement.unsigned_integer_32(0x00010001)),
ServiceAttribute(SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, DataElement.sequence([
DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)
])),
ServiceAttribute(SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, DataElement.sequence([
DataElement.uuid(UUID('E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'))
])),
ServiceAttribute(SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([
DataElement.sequence([
DataElement.uuid(BT_L2CAP_PROTOCOL_ID)
]),
DataElement.sequence([
DataElement.uuid(BT_RFCOMM_PROTOCOL_ID),
DataElement.unsigned_integer_8(channel)
])
]))
]
}
# -----------------------------------------------------------------------------
def on_dlc(dlc):
print('*** DLC connected', dlc)
dlc.sink = lambda data: on_rfcomm_data_received(dlc, data)
# -----------------------------------------------------------------------------
def on_rfcomm_data_received(dlc, data):
print(f'<<< Data received: {data.hex()}')
try:
message = data.decode('utf-8')
print(f'<<< Message = {message}')
except Exception:
pass
# Echo everything back
dlc.write(data)
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 3:
print('Usage: run_rfcomm_server.py <device-config> <transport-spec>')
print('example: run_rfcomm_server.py classic2.json usb:04b4:f901')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as (hci_source, hci_sink):
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(sys.argv[1], hci_source, hci_sink)
device.classic_enabled = True
# Create and register a server
rfcomm_server = Server(device)
# Listen for incoming DLC connections
channel_number = rfcomm_server.listen(on_dlc)
print(f'### Listening for connection on channel {channel_number}')
# Setup the SDP to advertise this channel
device.sdp_service_records = sdp_records(channel_number)
# Start the controller
await device.power_on()
# Start being discoverable and connectable
await device.set_discoverable(True)
await device.set_connectable(True)
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())
+69
View File
@@ -0,0 +1,69 @@
# 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
from bumble.transport import open_transport_or_link
# -----------------------------------------------------------------------------
async def main():
if len(sys.argv) < 2:
print('Usage: run_scanner.py <transport-spec> [filter]')
print('example: run_scanner.py usb:0')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[1]) as (hci_source, hci_sink):
print('<<< connected')
filter_duplicates = (len(sys.argv) == 3 and sys.argv[2] == 'filter')
device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink)
@device.on('advertisement')
def _(address, ad_data, rssi, connectable):
address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[address.address_type]
address_color = 'yellow' if connectable else 'red'
address_qualifier = ''
if address_type_string.startswith('P'):
type_color = 'cyan'
else:
if address.is_static:
type_color = 'green'
address_qualifier = '(static)'
elif address.is_resolvable:
type_color = 'magenta'
address_qualifier = '(resolvable)'
else:
type_color = 'white'
separator = '\n '
print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]{address_qualifier}:{separator}RSSI:{rssi}{separator}{ad_data.to_string(separator)}')
await device.power_on()
await device.start_scanning(filter_duplicates=filter_duplicates)
await hci_source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())