mirror of
https://github.com/google/bumble.git
synced 2026-06-17 10:02:27 +00:00
initial import
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Bumble Speaker",
|
||||
"class_of_device": 2360324,
|
||||
"keystore": "JsonKeyStore"
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Bumble",
|
||||
"class_of_device": 2360324,
|
||||
"keystore": "JsonKeyStore"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Bumble",
|
||||
"class_of_device": 7936
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Bumble",
|
||||
"address": "F0:F1:F2:F3:F4:F5",
|
||||
"advertising_interval": 2000,
|
||||
"keystore": "JsonKeyStore",
|
||||
"irk": "865F81FF5A8B486EAAE29A27AD9F77DC"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Bumble2",
|
||||
"address": "F6:F7:F8:F9:FA:FB",
|
||||
"keystore": "JsonKeyStore",
|
||||
"irk": "43E96EC5C5DBD8D0F5204CFFDECE0096",
|
||||
"advertising_interval": 2000,
|
||||
"advertising_data": "0201061106ba5689a6fabfa2bd01467d6e00fbabad08160a181604659b03"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Bumble3",
|
||||
"address": "F1:F2:F3:F4:F5:F6"
|
||||
}
|
||||
@@ -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())
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Bumble Phone",
|
||||
"class_of_device": 6291980
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Bumble Hands-Free",
|
||||
"class_of_device": 2360324
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Bumble Keyboard",
|
||||
"address": "F0:F1:F2:F3:F4:FA",
|
||||
"advertising_interval": 200
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user