Compare commits

...

42 Commits

Author SHA1 Message Date
Gilles Boccon-Gibod
1a05eebfdb fix legacy pairing with oob 2025-06-23 07:05:21 +02:00
zxzxwu
ebaa720e74 Merge pull request #712 from zxzxwu/avrcp
Fix AVRCP errors
2025-06-17 13:42:19 +08:00
Josh Wu
a505badffc Fix AVRCP errors 2025-06-16 22:51:22 +08:00
zxzxwu
45d938c901 Merge pull request #710 from zxzxwu/hci
Dataclass-based HCI packets
2025-06-16 15:06:25 +08:00
Josh Wu
a0498af626 Dataclass-based HCI packets 2025-06-14 08:03:48 +08:00
zxzxwu
bf027cf38f Merge pull request #709 from zxzxwu/hci3
Move return_parameters_fields to member
2025-06-13 13:32:00 +08:00
Gilles Boccon-Gibod
f2d7faa9af Merge pull request #708 from google/gbg/delegated-passkey
add passkey delegate support for pairing
2025-06-12 13:51:22 -04:00
zxzxwu
a0248a1cdf Move return_parameters_fields to member 2025-06-12 17:53:15 +08:00
Gilles Boccon-Gibod
1e95e19f16 tighten type bounds 2025-06-10 13:31:56 -04:00
Gilles Boccon-Gibod
8137caf37b use cancel_on_disconnection helper 2025-06-10 13:28:08 -04:00
zxzxwu
630243e243 Merge pull request #686 from zxzxwu/hci
HCI: Avoid patching __init__
2025-06-10 12:28:01 +08:00
Gilles Boccon-Gibod
39518c89f5 fix imports 2025-06-09 12:24:09 -04:00
Gilles Boccon-Gibod
d631156f6c add passkey delegate 2025-06-09 12:20:06 -04:00
Josh Wu
60e31884c8 HCI: Avoid patching __init__ 2025-06-09 22:08:18 +08:00
zxzxwu
8614e075b6 Merge pull request #707 from zxzxwu/typing
Replace pre-3.9 typing aliases
2025-06-09 14:39:29 +08:00
Josh Wu
8a0cd5d0d1 Replace deprecated typing aliases 2025-06-07 23:39:35 +08:00
zxzxwu
3a64772cc5 Merge pull request #688 from zxzxwu/hci2
Dataclass-based ASCS Packets
2025-06-05 20:59:06 +08:00
zxzxwu
1ecfb78d94 Dataclass-based packets 2025-06-05 20:44:31 +08:00
William Escande
9ad276a757 Implement optional synchronization for has (#704)
HAS can be run in synchronized mode, requiring a server to be able to
communicate with another CL.
This CL is doing a quick implementation of such functionality for preset
selection.
2025-06-02 14:57:59 -07:00
Gilles Boccon-Gibod
4c4f8c8225 Merge pull request #703 from google/gbg/hotfix
hotfix
2025-05-29 17:26:32 -04:00
Gilles Boccon-Gibod
a00b2bd707 hotfix 2025-05-29 17:24:32 -04:00
Gilles Boccon-Gibod
b8a055de45 Merge pull request #701 from google/gbg/speaker-app-opus
speaker app: enable opus, enable more options
2025-05-29 16:55:05 -04:00
Gilles Boccon-Gibod
4d07726acf Merge pull request #702 from google/gbg/bt-bench-advertise-scan
add advertise and scan options to the bench app
2025-05-28 09:35:20 -04:00
Gilles Boccon-Gibod
2e523b6f49 enable opus, enable more options 2025-05-27 11:49:44 -04:00
zxzxwu
8f9f12f1ee Merge pull request #700 from zxzxwu/iso
Fix wrong remove iso data path parameter
2025-05-22 23:52:57 +08:00
Josh Wu
a875aa4055 Fix wrong remove_data_path arguments 2025-05-21 16:01:29 +08:00
Gilles Boccon-Gibod
775b2d5d7f add advertise and scan options to the bench app 2025-05-20 17:15:43 -04:00
zxzxwu
3b399ea1a2 Merge pull request #696 from zxzxwu/att
Replace Optional[Connection] AttributeValue parameter type
2025-05-19 21:17:11 +08:00
zxzxwu
84f7cad678 Replace Optional[Connection] att parameter type 2025-05-18 07:55:39 +00:00
Gilles Boccon-Gibod
778f439e1c Merge pull request #695 from dsseng/intel-garfield-peak
intel: add Garfield Peak GfP2 and AX211 firmware loading
2025-05-17 11:02:04 -07:00
Dmitrii Sharshakov
1b95d4e1df intel: add Garfield Peak GfP2 and AX211 firmware loading
Tested with AX211 on an MSI board, using ibt-1040-0041.sfi from linux-firmware.
Firmware loads (after reboot to bootloader), then the controller functions well (ISO fails, but that might be another topic).

Signed-off-by: Dmitrii Sharshakov <d3dx12.xx@gmail.com>
2025-05-17 16:01:16 +02:00
zxzxwu
512f6d4ee1 Merge pull request #694 from zxzxwu/crypto
Fix wrong smp_test parameters
2025-05-14 23:53:43 +08:00
zxzxwu
c52b614abb Fix wrong smp_test parameters 2025-05-14 08:27:11 +00:00
zxzxwu
7b7afc7179 Merge pull request #691 from zxzxwu/crypto
Remove empty crypto.py
2025-05-13 23:40:44 +08:00
zxzxwu
b1c6044533 Remove empty crypto.py 2025-05-13 06:20:58 +00:00
zxzxwu
38499dfe3c Merge pull request #689 from zxzxwu/link_key
Fix: Missing EVENT_LINK_KEY
2025-05-13 12:09:27 +08:00
zxzxwu
b58c29202a Merge pull request #683 from zxzxwu/crypto
Implement builtin cryptography primitives
2025-05-12 20:52:28 +08:00
Josh Wu
ca759ca967 Implement builtin cryptography primitives 2025-05-12 06:23:45 +00:00
zxzxwu
3858bf80c1 Fix missing EVENT_LINK_KEY 2025-05-10 14:58:04 +00:00
Slvr
a88a034ce2 cryptography: bump version to 44.0.3 to fix python parsing (#684)
Bug: 404336381
2025-05-08 08:28:33 -07:00
zxzxwu
6b2cd1147d Merge pull request #682 from zxzxwu/linkkey
Move connection.link_key_type to keystore
2025-05-08 11:23:28 +08:00
Josh Wu
bb8dcaf63e Move connection.link_key_type to keystore 2025-05-06 02:11:25 +08:00
91 changed files with 3255 additions and 2240 deletions

View File

@@ -29,9 +29,7 @@ from typing import (
Any,
AsyncGenerator,
Coroutine,
Deque,
Optional,
Tuple,
)
import click
@@ -131,7 +129,7 @@ class BroadcastScanner(bumble.utils.EventEmitter):
basic_audio_announcement: Optional[bap.BasicAudioAnnouncement] = None
appearance: Optional[core.Appearance] = None
biginfo: Optional[bumble.device.BIGInfoAdvertisement] = None
manufacturer_data: Optional[Tuple[str, bytes]] = None
manufacturer_data: Optional[tuple[str, bytes]] = None
def __post_init__(self) -> None:
super().__init__()
@@ -748,7 +746,9 @@ async def run_receive(
sample_rate_hz=sampling_frequency.hz,
num_channels=num_bis,
)
lc3_queues: list[Deque[bytes]] = [collections.deque() for i in range(num_bis)]
lc3_queues: list[collections.deque[bytes]] = [
collections.deque() for i in range(num_bis)
]
packet_stats = [0, 0]
audio_output = await audio_io.create_audio_output(output)
@@ -764,7 +764,7 @@ async def run_receive(
)
)
def sink(queue: Deque[bytes], packet: hci.HCI_IsoDataPacket):
def sink(queue: collections.deque[bytes], packet: hci.HCI_IsoDataPacket):
# TODO: re-assemble fragments and detect errors
queue.append(packet.iso_sdu_fragment)

View File

@@ -23,6 +23,7 @@ import os
import statistics
import struct
import time
from typing import Optional
import click
@@ -75,6 +76,7 @@ DEFAULT_CENTRAL_ADDRESS = 'F0:F0:F0:F0:F0:F0'
DEFAULT_CENTRAL_NAME = 'Speed Central'
DEFAULT_PERIPHERAL_ADDRESS = 'F1:F1:F1:F1:F1:F1'
DEFAULT_PERIPHERAL_NAME = 'Speed Peripheral'
DEFAULT_ADVERTISING_INTERVAL = 100
SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5'
SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
@@ -197,6 +199,51 @@ async def switch_roles(connection, role):
logging.info(f'{color("### Role switch failed:", "red")} {error}')
async def pre_power_on(device: Device, classic: bool) -> None:
device.classic_enabled = classic
# Set up a pairing config factory with minimal requirements.
device.config.keystore = "JsonKeyStore"
device.pairing_config_factory = lambda _: PairingConfig(
sc=False, mitm=False, bonding=False
)
async def post_power_on(
device: Device,
le_scan: Optional[tuple[int, int]],
le_advertise: Optional[int],
classic_page_scan: bool,
classic_inquiry_scan: bool,
) -> None:
if classic_page_scan:
logging.info(color("*** Enabling page scan", "blue"))
await device.set_connectable(True)
if classic_inquiry_scan:
logging.info(color("*** Enabling inquiry scan", "blue"))
await device.set_discoverable(True)
if le_scan:
scan_window, scan_interval = le_scan
logging.info(
color(
f"*** Starting LE scanning [{scan_window}ms/{scan_interval}ms]",
"blue",
)
)
await device.start_scanning(
scan_interval=scan_interval, scan_window=scan_window
)
if le_advertise:
logging.info(color(f"*** Starting LE advertising [{le_advertise}ms]", "blue"))
await device.start_advertising(
advertising_interval_min=le_advertise,
advertising_interval_max=le_advertise,
auto_restart=True,
)
# -----------------------------------------------------------------------------
# Packet
# -----------------------------------------------------------------------------
@@ -1194,6 +1241,10 @@ class Central(Connection.Listener):
encrypt,
extended_data_length,
role_switch,
le_scan,
le_advertise,
classic_page_scan,
classic_inquiry_scan,
):
super().__init__()
self.transport = transport
@@ -1205,6 +1256,10 @@ class Central(Connection.Listener):
self.encrypt = encrypt or authenticate
self.extended_data_length = extended_data_length
self.role_switch = role_switch
self.le_scan = le_scan
self.le_advertise = le_advertise
self.classic_page_scan = classic_page_scan
self.classic_inquiry_scan = classic_inquiry_scan
self.device = None
self.connection = None
@@ -1253,19 +1308,16 @@ class Central(Connection.Listener):
)
mode = self.mode_factory(self.device)
scenario = self.scenario_factory(mode)
self.device.classic_enabled = self.classic
# Set up a pairing config factory with minimal requirements.
self.device.config.keystore = "JsonKeyStore"
self.device.pairing_config_factory = lambda _: PairingConfig(
sc=False, mitm=False, bonding=False
)
await pre_power_on(self.device, self.classic)
await self.device.power_on()
if self.classic:
await self.device.set_discoverable(False)
await self.device.set_connectable(False)
await post_power_on(
self.device,
self.le_scan,
self.le_advertise,
self.classic_page_scan,
self.classic_inquiry_scan,
)
logging.info(
color(f'### Connecting to {self.peripheral_address}...', 'cyan')
@@ -1378,6 +1430,10 @@ class Peripheral(Device.Listener, Connection.Listener):
classic,
extended_data_length,
role_switch,
le_scan,
le_advertise,
classic_page_scan,
classic_inquiry_scan,
):
self.transport = transport
self.classic = classic
@@ -1385,12 +1441,20 @@ class Peripheral(Device.Listener, Connection.Listener):
self.mode_factory = mode_factory
self.extended_data_length = extended_data_length
self.role_switch = role_switch
self.le_scan = le_scan
self.classic_page_scan = classic_page_scan
self.classic_inquiry_scan = classic_inquiry_scan
self.scenario = None
self.mode = None
self.device = None
self.connection = None
self.connected = asyncio.Event()
if le_advertise:
self.le_advertise = le_advertise
else:
self.le_advertise = 0 if classic else DEFAULT_ADVERTISING_INTERVAL
async def run(self):
logging.info(color('>>> Connecting to HCI...', 'green'))
async with await open_transport_or_link(self.transport) as (
@@ -1406,21 +1470,16 @@ class Peripheral(Device.Listener, Connection.Listener):
self.device.listener = self
self.mode = self.mode_factory(self.device)
self.scenario = self.scenario_factory(self.mode)
self.device.classic_enabled = self.classic
# Set up a pairing config factory with minimal requirements.
self.device.config.keystore = "JsonKeyStore"
self.device.pairing_config_factory = lambda _: PairingConfig(
sc=False, mitm=False, bonding=False
)
await pre_power_on(self.device, self.classic)
await self.device.power_on()
if self.classic:
await self.device.set_discoverable(True)
await self.device.set_connectable(True)
else:
await self.device.start_advertising(auto_restart=True)
await post_power_on(
self.device,
self.le_scan,
self.le_advertise,
self.classic or self.classic_page_scan,
self.classic or self.classic_inquiry_scan,
)
if self.classic:
logging.info(
@@ -1451,10 +1510,14 @@ class Peripheral(Device.Listener, Connection.Listener):
self.connection = connection
self.connected.set()
# Stop being discoverable and connectable
# Stop being discoverable and connectable if possible
if self.classic:
AsyncRunner.spawn(self.device.set_discoverable(False))
AsyncRunner.spawn(self.device.set_connectable(False))
if not self.classic_inquiry_scan:
logging.info(color("*** Stopping inquiry scan", "blue"))
AsyncRunner.spawn(self.device.set_discoverable(False))
if not self.classic_page_scan:
logging.info(color("*** Stopping page scan", "blue"))
AsyncRunner.spawn(self.device.set_connectable(False))
# Request a new data length if needed
if not self.classic and self.extended_data_length:
@@ -1475,7 +1538,9 @@ class Peripheral(Device.Listener, Connection.Listener):
self.scenario.reset()
if self.classic:
logging.info(color("*** Enabling inquiry scan", "blue"))
AsyncRunner.spawn(self.device.set_discoverable(True))
logging.info(color("*** Enabling page scan", "blue"))
AsyncRunner.spawn(self.device.set_connectable(True))
def on_connection_parameters_update(self):
@@ -1621,6 +1686,7 @@ def create_scenario_factory(ctx, default_scenario):
)
@click.option(
'--extended-data-length',
metavar='<TX-OCTETS>/<TX-TIME>',
help='Request a data length upon connection, specified as tx_octets/tx_time',
)
@click.option(
@@ -1628,6 +1694,26 @@ def create_scenario_factory(ctx, default_scenario):
type=click.Choice(['central', 'peripheral']),
help='Request role switch upon connection (central or peripheral)',
)
@click.option(
'--le-scan',
metavar='<WINDOW>/<INTERVAL>',
help='Perform an LE scan with a given window and interval (milliseconds)',
)
@click.option(
'--le-advertise',
metavar='<INTERVAL>',
help='Advertise with a given interval (milliseconds)',
)
@click.option(
'--classic-page-scan',
is_flag=True,
help='Enable Classic page scanning',
)
@click.option(
'--classic-inquiry-scan',
is_flag=True,
help='Enable Classic enquiry scanning',
)
@click.option(
'--rfcomm-channel',
type=int,
@@ -1753,6 +1839,10 @@ def bench(
att_mtu,
extended_data_length,
role_switch,
le_scan,
le_advertise,
classic_page_scan,
classic_inquiry_scan,
packet_size,
packet_count,
start_delay,
@@ -1801,6 +1891,10 @@ def bench(
else None
)
ctx.obj['role_switch'] = role_switch
ctx.obj['le_scan'] = [float(x) for x in le_scan.split('/')] if le_scan else None
ctx.obj['le_advertise'] = float(le_advertise) if le_advertise else None
ctx.obj['classic_page_scan'] = classic_page_scan
ctx.obj['classic_inquiry_scan'] = classic_inquiry_scan
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
@@ -1845,6 +1939,10 @@ def central(
encrypt or authenticate,
ctx.obj['extended_data_length'],
ctx.obj['role_switch'],
ctx.obj['le_scan'],
ctx.obj['le_advertise'],
ctx.obj['classic_page_scan'],
ctx.obj['classic_inquiry_scan'],
).run()
asyncio.run(run_central())
@@ -1866,6 +1964,10 @@ def peripheral(ctx, transport):
ctx.obj['classic'],
ctx.obj['extended_data_length'],
ctx.obj['role_switch'],
ctx.obj['le_scan'],
ctx.obj['le_advertise'],
ctx.obj['classic_page_scan'],
ctx.obj['classic_inquiry_scan'],
).run()
asyncio.run(run_peripheral())

View File

@@ -55,7 +55,7 @@ from prompt_toolkit.layout import (
from bumble import __version__
import bumble.core
from bumble import colors
from bumble.core import UUID, AdvertisingData, PhysicalTransport
from bumble.core import UUID, AdvertisingData
from bumble.device import (
ConnectionParametersPreferences,
ConnectionPHY,

View File

@@ -402,14 +402,19 @@ async def pair(
# Create an OOB context if needed
if oob:
our_oob_context = OobContext()
shared_data = (
None
if oob == '-'
else OobData.from_ad(
if oob == '-':
shared_data = None
legacy_context = OobLegacyContext()
else:
oob_data = OobData.from_ad(
AdvertisingData.from_bytes(bytes.fromhex(oob))
).shared_data
)
legacy_context = OobLegacyContext()
)
shared_data = oob_data.shared_data
legacy_context = oob_data.legacy_context
if legacy_context is None and not sc:
print(color('OOB pairing in legacy mode requires TK', 'red'))
return
oob_contexts = PairingConfig.OobConfig(
our_context=our_oob_context,
peer_data=shared_data,
@@ -419,7 +424,9 @@ async def pair(
print(color('@@@ OOB Data:', 'yellow'))
if shared_data is None:
oob_data = OobData(
address=device.random_address, shared_data=our_oob_context.share()
address=device.random_address,
shared_data=our_oob_context.share(),
legacy_context=(None if sc else legacy_context),
)
print(
color(
@@ -427,7 +434,8 @@ async def pair(
'yellow',
)
)
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
if legacy_context:
print(color(f'@@@ TK={legacy_context.tk.hex()}', 'yellow'))
print(color('@@@-----------------------------------', 'yellow'))
else:
oob_contexts = None

View File

@@ -4,7 +4,7 @@ import logging
import json
from bumble.pandora import PandoraDevice, Config, serve
from typing import Dict, Any
from typing import Any
BUMBLE_SERVER_GRPC_PORT = 7999
ROOTCANAL_PORT_CUTTLEFISH = 7300
@@ -39,7 +39,7 @@ def main(grpc_port: int, rootcanal_port: int, transport: str, config: str) -> No
asyncio.run(serve(device, config=server_config, port=grpc_port))
def retrieve_config(config: str) -> Dict[str, Any]:
def retrieve_config(config: str) -> dict[str, Any]:
if not config:
return {}

View File

@@ -16,6 +16,7 @@
# Imports
# -----------------------------------------------------------------------------
import datetime
import importlib
import logging
import os
import struct
@@ -154,9 +155,10 @@ class Printer:
def main(format, vendor, filename):
for vendor_name in vendor:
if vendor_name == 'android':
import bumble.vendor.android.hci
# Prevent being deleted by linter.
importlib.import_module('bumble.vendor.android.hci')
elif vendor_name == 'zephyr':
import bumble.vendor.zephyr.hci
importlib.import_module('bumble.vendor.zephyr.hci')
input = open(filename, 'rb')
if format == 'h4':

View File

@@ -15,6 +15,7 @@
<tr><td>Codec</td><td><span id="codecText"></span></td></tr>
<tr><td>Packets</td><td><span id="packetsReceivedText"></span></td></tr>
<tr><td>Bytes</td><td><span id="bytesReceivedText"></span></td></tr>
<tr><td>Bitrate</td><td><span id="bitrate"></span></td></tr>
</table>
</td>
<td>

View File

@@ -7,17 +7,19 @@ let connectionText;
let codecText;
let packetsReceivedText;
let bytesReceivedText;
let bitrateText;
let streamStateText;
let connectionStateText;
let controlsDiv;
let audioOnButton;
let mediaSource;
let sourceBuffer;
let audioElement;
let audioDecoder;
let audioCodec;
let audioContext;
let audioAnalyzer;
let audioFrequencyBinCount;
let audioFrequencyData;
let nextAudioStartPosition = 0;
let audioStartTime = 0;
let packetsReceived = 0;
let bytesReceived = 0;
let audioState = "stopped";
@@ -29,20 +31,17 @@ let bandwidthCanvas;
let bandwidthCanvasContext;
let bandwidthBinCount;
let bandwidthBins = [];
let bitrateSamples = [];
const FFT_WIDTH = 800;
const FFT_HEIGHT = 256;
const BANDWIDTH_WIDTH = 500;
const BANDWIDTH_HEIGHT = 100;
function hexToBytes(hex) {
return Uint8Array.from(hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
}
const BITRATE_WINDOW = 30;
function init() {
initUI();
initMediaSource();
initAudioElement();
initAudioContext();
initAnalyzer();
connect();
@@ -56,6 +55,7 @@ function initUI() {
codecText = document.getElementById("codecText");
packetsReceivedText = document.getElementById("packetsReceivedText");
bytesReceivedText = document.getElementById("bytesReceivedText");
bitrateText = document.getElementById("bitrate");
streamStateText = document.getElementById("streamStateText");
connectionStateText = document.getElementById("connectionStateText");
audioSupportMessageText = document.getElementById("audioSupportMessageText");
@@ -67,17 +67,9 @@ function initUI() {
requestAnimationFrame(onAnimationFrame);
}
function initMediaSource() {
mediaSource = new MediaSource();
mediaSource.onsourceopen = onMediaSourceOpen;
mediaSource.onsourceclose = onMediaSourceClose;
mediaSource.onsourceended = onMediaSourceEnd;
}
function initAudioElement() {
audioElement = document.getElementById("audio");
audioElement.src = URL.createObjectURL(mediaSource);
// audioElement.controls = true;
function initAudioContext() {
audioContext = new AudioContext();
audioContext.onstatechange = () => console.log("AudioContext state:", audioContext.state);
}
function initAnalyzer() {
@@ -94,24 +86,16 @@ function initAnalyzer() {
bandwidthCanvasContext = bandwidthCanvas.getContext('2d');
bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)";
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
}
function startAnalyzer() {
// FFT
if (audioElement.captureStream !== undefined) {
audioContext = new AudioContext();
audioAnalyzer = audioContext.createAnalyser();
audioAnalyzer.fftSize = 128;
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
const stream = audioElement.captureStream();
const source = audioContext.createMediaStreamSource(stream);
source.connect(audioAnalyzer);
}
// Bandwidth
bandwidthBinCount = BANDWIDTH_WIDTH / 2;
bandwidthBins = [];
bitrateSamples = [];
audioAnalyzer = audioContext.createAnalyser();
audioAnalyzer.fftSize = 128;
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
audioAnalyzer.connect(audioContext.destination)
}
function setConnectionText(message) {
@@ -148,7 +132,8 @@ function onAnimationFrame() {
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`;
for (let t = 0; t < bandwidthBins.length; t++) {
const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT;
const bytesReceived = bandwidthBins[t]
const lineHeight = (bytesReceived / 1000) * BANDWIDTH_HEIGHT;
bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight);
}
@@ -156,28 +141,14 @@ function onAnimationFrame() {
requestAnimationFrame(onAnimationFrame);
}
function onMediaSourceOpen() {
console.log(this.readyState);
sourceBuffer = mediaSource.addSourceBuffer("audio/aac");
}
function onMediaSourceClose() {
console.log(this.readyState);
}
function onMediaSourceEnd() {
console.log(this.readyState);
}
async function startAudio() {
try {
console.log("starting audio...");
audioOnButton.disabled = true;
audioState = "starting";
await audioElement.play();
audioContext.resume();
console.log("audio started");
audioState = "playing";
startAnalyzer();
} catch(error) {
console.error(`play failed: ${error}`);
audioState = "stopped";
@@ -185,12 +156,47 @@ async function startAudio() {
}
}
function onAudioPacket(packet) {
if (audioState != "stopped") {
// Queue the audio packet.
sourceBuffer.appendBuffer(packet);
function onDecodedAudio(audioData) {
const bufferSource = audioContext.createBufferSource()
const now = audioContext.currentTime;
let nextAudioStartTime = audioStartTime + (nextAudioStartPosition / audioData.sampleRate);
if (nextAudioStartTime < now) {
console.log("starting new audio time base")
audioStartTime = now;
nextAudioStartTime = now;
nextAudioStartPosition = 0;
} else {
console.log(`audio buffer scheduled in ${nextAudioStartTime - now}`)
}
const audioBuffer = audioContext.createBuffer(
audioData.numberOfChannels,
audioData.numberOfFrames,
audioData.sampleRate
);
for (let channel = 0; channel < audioData.numberOfChannels; channel++) {
audioData.copyTo(
audioBuffer.getChannelData(channel),
{
planeIndex: channel,
format: "f32-planar"
}
)
}
bufferSource.buffer = audioBuffer;
bufferSource.connect(audioAnalyzer)
bufferSource.start(nextAudioStartTime);
nextAudioStartPosition += audioData.numberOfFrames;
}
function onCodecError(error) {
console.log("Codec error:", error)
}
async function onAudioPacket(packet) {
packetsReceived += 1;
packetsReceivedText.innerText = packetsReceived;
bytesReceived += packet.byteLength;
@@ -200,6 +206,48 @@ function onAudioPacket(packet) {
if (bandwidthBins.length > bandwidthBinCount) {
bandwidthBins.shift();
}
bitrateSamples[bitrateSamples.length] = {ts: Date.now(), bytes: packet.byteLength}
if (bitrateSamples.length > BITRATE_WINDOW) {
bitrateSamples.shift();
}
if (bitrateSamples.length >= 2) {
const windowBytes = bitrateSamples.reduce((accumulator, x) => accumulator + x.bytes, 0) - bitrateSamples[0].bytes;
const elapsed = bitrateSamples[bitrateSamples.length-1].ts - bitrateSamples[0].ts;
const bitrate = Math.floor(8 * windowBytes / elapsed)
bitrateText.innerText = `${bitrate} kb/s`
}
if (audioState == "stopped") {
return;
}
if (audioDecoder === undefined) {
let audioConfig;
if (audioCodec == 'aac') {
audioConfig = {
codec: 'mp4a.40.2',
sampleRate: 44100, // ignored
numberOfChannels: 2, // ignored
}
} else if (audioCodec == 'opus') {
audioConfig = {
codec: 'opus',
sampleRate: 48000, // ignored
numberOfChannels: 2, // ignored
}
}
audioDecoder = new AudioDecoder({ output: onDecodedAudio, error: onCodecError });
audioDecoder.configure(audioConfig)
}
const encodedAudio = new EncodedAudioChunk({
type: "key",
data: packet,
timestamp: 0,
transfer: [packet],
});
audioDecoder.decode(encodedAudio);
}
function onChannelOpen() {
@@ -249,16 +297,19 @@ function onChannelMessage(message) {
}
}
function onHelloMessage(params) {
async function onHelloMessage(params) {
codecText.innerText = params.codec;
if (params.codec != "aac") {
audioOnButton.disabled = true;
audioSupportMessageText.innerText = "Only AAC can be played, audio will be disabled";
audioSupportMessageText.style.display = "inline-block";
} else {
if (params.codec == "aac" || params.codec == "opus") {
audioCodec = params.codec
audioSupportMessageText.innerText = "";
audioSupportMessageText.style.display = "none";
} else {
audioOnButton.disabled = true;
audioSupportMessageText.innerText = "Only AAC and Opus can be played, audio will be disabled";
audioSupportMessageText.style.display = "inline-block";
}
if (params.streamState) {
setStreamState(params.streamState);
}

View File

@@ -25,7 +25,7 @@ import os
import logging
import pathlib
import subprocess
from typing import Dict, List, Optional
from typing import Optional
import weakref
import click
@@ -50,8 +50,10 @@ from bumble.a2dp import (
make_audio_sink_service_sdp_records,
A2DP_SBC_CODEC_TYPE,
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
A2DP_NON_A2DP_CODEC_TYPE,
SbcMediaCodecInformation,
AacMediaCodecInformation,
OpusMediaCodecInformation,
)
from bumble.utils import AsyncRunner
from bumble.codecs import AacAudioRtpPacket
@@ -78,6 +80,8 @@ class AudioExtractor:
return AacAudioExtractor()
if codec == 'sbc':
return SbcAudioExtractor()
if codec == 'opus':
return OpusAudioExtractor()
def extract_audio(self, packet: MediaPacket) -> bytes:
raise NotImplementedError()
@@ -102,6 +106,13 @@ class SbcAudioExtractor:
return packet.payload[1:]
# -----------------------------------------------------------------------------
class OpusAudioExtractor:
def extract_audio(self, packet: MediaPacket) -> bytes:
# TODO: parse fields
return packet.payload[1:]
# -----------------------------------------------------------------------------
class Output:
async def start(self) -> None:
@@ -235,7 +246,7 @@ class FfplayOutput(QueuedOutput):
await super().start()
self.subprocess = await asyncio.create_subprocess_shell(
f'ffplay -f {self.codec} pipe:0',
f'ffplay -probesize 32 -f {self.codec} pipe:0',
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
@@ -399,10 +410,24 @@ class Speaker:
STARTED = 2
SUSPENDED = 3
def __init__(self, device_config, transport, codec, discover, outputs, ui_port):
def __init__(
self,
device_config,
transport,
codec,
sampling_frequencies,
bitrate,
vbr,
discover,
outputs,
ui_port,
):
self.device_config = device_config
self.transport = transport
self.codec = codec
self.sampling_frequencies = sampling_frequencies
self.bitrate = bitrate
self.vbr = vbr
self.discover = discover
self.ui_port = ui_port
self.device = None
@@ -423,7 +448,7 @@ class Speaker:
# Create an HTTP server for the UI
self.ui_server = UiServer(speaker=self, port=ui_port)
def sdp_records(self) -> Dict[int, List[ServiceAttribute]]:
def sdp_records(self) -> dict[int, list[ServiceAttribute]]:
service_record_handle = 0x00010001
return {
service_record_handle: make_audio_sink_service_sdp_records(
@@ -438,32 +463,56 @@ class Speaker:
if self.codec == 'sbc':
return self.sbc_codec_capabilities()
if self.codec == 'opus':
return self.opus_codec_capabilities()
raise RuntimeError('unsupported codec')
def aac_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = AacMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [
8000,
11025,
12000,
16000,
22050,
24000,
32000,
44100,
48000,
]:
supported_sampling_frequencies |= (
AacMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
media_codec_information=AacMediaCodecInformation(
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
sampling_frequency=supported_sampling_frequencies,
channels=AacMediaCodecInformation.Channels.MONO
| AacMediaCodecInformation.Channels.STEREO,
vbr=1,
bitrate=256000,
vbr=1 if self.vbr else 0,
bitrate=self.bitrate or 256000,
),
)
def sbc_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = SbcMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [
16000,
32000,
44100,
48000,
]:
supported_sampling_frequencies |= (
SbcMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_SBC_CODEC_TYPE,
media_codec_information=SbcMediaCodecInformation(
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
sampling_frequency=supported_sampling_frequencies,
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
| SbcMediaCodecInformation.ChannelMode.STEREO
@@ -481,6 +530,25 @@ class Speaker:
),
)
def opus_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = OpusMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [48000]:
supported_sampling_frequencies |= (
OpusMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_NON_A2DP_CODEC_TYPE,
media_codec_information=OpusMediaCodecInformation(
frame_size=OpusMediaCodecInformation.FrameSize.FS_10MS
| OpusMediaCodecInformation.FrameSize.FS_20MS,
channel_mode=OpusMediaCodecInformation.ChannelMode.MONO
| OpusMediaCodecInformation.ChannelMode.STEREO
| OpusMediaCodecInformation.ChannelMode.DUAL_MONO,
sampling_frequency=supported_sampling_frequencies,
),
)
async def dispatch_to_outputs(self, function):
for output in self.outputs:
await function(output)
@@ -675,7 +743,26 @@ def speaker_cli(ctx, device_config):
@click.command()
@click.option(
'--codec', type=click.Choice(['sbc', 'aac']), default='aac', show_default=True
'--codec',
type=click.Choice(['sbc', 'aac', 'opus']),
default='aac',
show_default=True,
)
@click.option(
'--sampling-frequency',
metavar='SAMPLING-FREQUENCY',
type=int,
multiple=True,
help='Enable a sampling frequency (may be specified more than once)',
)
@click.option(
'--bitrate',
metavar='BITRATE',
type=int,
help='Supported bitrate (AAC only)',
)
@click.option(
'--vbr/--no-vbr', is_flag=True, default=True, help='Enable VBR (AAC only)'
)
@click.option(
'--discover', is_flag=True, help='Discover remote endpoints once connected'
@@ -706,7 +793,16 @@ def speaker_cli(ctx, device_config):
@click.option('--device-config', metavar='FILENAME', help='Device configuration file')
@click.argument('transport')
def speaker(
transport, codec, connect_address, discover, output, ui_port, device_config
transport,
codec,
sampling_frequency,
bitrate,
vbr,
connect_address,
discover,
output,
ui_port,
device_config,
):
"""Run the speaker."""
@@ -721,15 +817,27 @@ def speaker(
output = list(filter(lambda x: x != '@ffplay', output))
asyncio.run(
Speaker(device_config, transport, codec, discover, output, ui_port).run(
connect_address
)
Speaker(
device_config,
transport,
codec,
sampling_frequency,
bitrate,
vbr,
discover,
output,
ui_port,
).run(connect_address)
)
# -----------------------------------------------------------------------------
def main():
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
logging.basicConfig(
level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
speaker()

View File

@@ -479,6 +479,12 @@ class OpusMediaCodecInformation(VendorSpecificMediaCodecInformation):
class SamplingFrequency(enum.IntFlag):
SF_48000 = 1 << 0
@classmethod
def from_int(cls, sampling_frequency: int) -> Self:
if sampling_frequency != 48000:
raise ValueError("no such sampling frequency")
return cls(1)
VENDOR_ID: ClassVar[int] = 0x000000E0
CODEC_ID: ClassVar[int] = 0x0001

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import List, Union
from typing import Union
from bumble import core
@@ -21,7 +21,7 @@ class AtParsingError(core.InvalidPacketError):
"""Error raised when parsing AT commands fails."""
def tokenize_parameters(buffer: bytes) -> List[bytes]:
def tokenize_parameters(buffer: bytes) -> list[bytes]:
"""Split input parameters into tokens.
Removes space characters outside of double quote blocks:
T-rec-V-25 - 5.2.1 Command line general format: "Space characters (IA5 2/0)
@@ -63,12 +63,12 @@ def tokenize_parameters(buffer: bytes) -> List[bytes]:
return [bytes(token) for token in tokens if len(token) > 0]
def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
def parse_parameters(buffer: bytes) -> list[Union[bytes, list]]:
"""Parse the parameters using the comma and parenthesis separators.
Raises AtParsingError in case of invalid input string."""
tokens = tokenize_parameters(buffer)
accumulator: List[list] = [[]]
accumulator: list[list] = [[]]
current: Union[bytes, list] = bytes()
for token in tokens:

View File

@@ -32,10 +32,6 @@ from typing import (
Awaitable,
Callable,
Generic,
Dict,
List,
Optional,
Type,
TypeVar,
Union,
TYPE_CHECKING,
@@ -251,7 +247,7 @@ class ATT_PDU:
See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU
'''
pdu_classes: Dict[int, Type[ATT_PDU]] = {}
pdu_classes: dict[int, type[ATT_PDU]] = {}
op_code = 0
name: str
@@ -770,27 +766,25 @@ class AttributeValue(Generic[_T]):
def __init__(
self,
read: Union[
Callable[[Optional[Connection]], _T],
Callable[[Optional[Connection]], Awaitable[_T]],
Callable[[Connection], _T],
Callable[[Connection], Awaitable[_T]],
None,
] = None,
write: Union[
Callable[[Optional[Connection], _T], None],
Callable[[Optional[Connection], _T], Awaitable[None]],
Callable[[Connection, _T], None],
Callable[[Connection, _T], Awaitable[None]],
None,
] = None,
):
self._read = read
self._write = write
def read(self, connection: Optional[Connection]) -> Union[_T, Awaitable[_T]]:
def read(self, connection: Connection) -> Union[_T, Awaitable[_T]]:
if self._read is None:
raise InvalidOperationError('AttributeValue has no read function')
return self._read(connection)
def write(
self, connection: Optional[Connection], value: _T
) -> Union[Awaitable[None], None]:
def write(self, connection: Connection, value: _T) -> Union[Awaitable[None], None]:
if self._write is None:
raise InvalidOperationError('AttributeValue has no write function')
return self._write(connection, value)
@@ -820,7 +814,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
# The check for `p.name is not None` here is needed because for InFlag
# enums, the .name property can be None, when the enum value is 0,
# so the type hint for .name is Optional[str].
enum_list: List[str] = [p.name for p in cls if p.name is not None]
enum_list: list[str] = [p.name for p in cls if p.name is not None]
enum_list_str = ",".join(enum_list)
raise TypeError(
f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {enum_list_str}\nGot: {permissions_str}"
@@ -871,7 +865,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
def decode_value(self, value: bytes) -> _T:
return value # type: ignore
async def read_value(self, connection: Optional[Connection]) -> bytes:
async def read_value(self, connection: Connection) -> bytes:
if (
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
and connection is not None
@@ -913,7 +907,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
return b'' if value is None else self.encode_value(value)
async def write_value(self, connection: Optional[Connection], value: bytes) -> None:
async def write_value(self, connection: Connection, value: bytes) -> None:
if (
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
and connection is not None

View File

@@ -18,7 +18,7 @@
from __future__ import annotations
import enum
import struct
from typing import Dict, Type, Union, Tuple
from typing import Union
from bumble import core
from bumble import utils
@@ -213,11 +213,11 @@ class CommandFrame(Frame):
NOTIFY = 0x03
GENERAL_INQUIRY = 0x04
subclasses: Dict[Frame.OperationCode, Type[CommandFrame]] = {}
subclasses: dict[Frame.OperationCode, type[CommandFrame]] = {}
ctype: CommandType
@staticmethod
def parse_operands(operands: bytes) -> Tuple:
def parse_operands(operands: bytes) -> tuple:
raise NotImplementedError
def __init__(
@@ -251,11 +251,11 @@ class ResponseFrame(Frame):
CHANGED = 0x0D
INTERIM = 0x0F
subclasses: Dict[Frame.OperationCode, Type[ResponseFrame]] = {}
subclasses: dict[Frame.OperationCode, type[ResponseFrame]] = {}
response: ResponseCode
@staticmethod
def parse_operands(operands: bytes) -> Tuple:
def parse_operands(operands: bytes) -> tuple:
raise NotImplementedError
def __init__(
@@ -282,7 +282,7 @@ class VendorDependentFrame:
vendor_dependent_data: bytes
@staticmethod
def parse_operands(operands: bytes) -> Tuple:
def parse_operands(operands: bytes) -> tuple:
return (
struct.unpack(">I", b"\x00" + operands[:3])[0],
operands[3:],
@@ -432,7 +432,7 @@ class PassThroughFrame:
operation_data: bytes
@staticmethod
def parse_operands(operands: bytes) -> Tuple:
def parse_operands(operands: bytes) -> tuple:
return (
PassThroughFrame.StateFlag(operands[0] >> 7),
PassThroughFrame.OperationId(operands[0] & 0x7F),

View File

@@ -19,7 +19,7 @@ from __future__ import annotations
from enum import IntEnum
import logging
import struct
from typing import Callable, cast, Dict, Optional
from typing import Callable, cast, Optional
from bumble.colors import color
from bumble import avc
@@ -146,9 +146,9 @@ class MessageAssembler:
# -----------------------------------------------------------------------------
class Protocol:
CommandHandler = Callable[[int, avc.CommandFrame], None]
command_handlers: Dict[int, CommandHandler] # Command handlers, by PID
command_handlers: dict[int, CommandHandler] # Command handlers, by PID
ResponseHandler = Callable[[int, Optional[avc.ResponseFrame]], None]
response_handlers: Dict[int, ResponseHandler] # Response handlers, by PID
response_handlers: dict[int, ResponseHandler] # Response handlers, by PID
next_transaction_label: int
message_assembler: MessageAssembler

View File

@@ -24,12 +24,8 @@ import warnings
from typing import (
Any,
Awaitable,
Dict,
Type,
Tuple,
Optional,
Callable,
List,
AsyncGenerator,
Iterable,
Union,
@@ -227,7 +223,7 @@ AVDTP_STATE_NAMES = {
# -----------------------------------------------------------------------------
async def find_avdtp_service_with_sdp_client(
sdp_client: sdp.Client,
) -> Optional[Tuple[int, int]]:
) -> Optional[tuple[int, int]]:
'''
Find an AVDTP service, using a connected SDP client, and return its version,
or None if none is found
@@ -257,7 +253,7 @@ async def find_avdtp_service_with_sdp_client(
# -----------------------------------------------------------------------------
async def find_avdtp_service_with_connection(
connection: device.Connection,
) -> Optional[Tuple[int, int]]:
) -> Optional[tuple[int, int]]:
'''
Find an AVDTP service, for a connection, and return its version,
or None if none is found
@@ -451,7 +447,7 @@ class ServiceCapabilities:
service_category: int, service_capabilities_bytes: bytes
) -> ServiceCapabilities:
# Select the appropriate subclass
cls: Type[ServiceCapabilities]
cls: type[ServiceCapabilities]
if service_category == AVDTP_MEDIA_CODEC_SERVICE_CATEGORY:
cls = MediaCodecCapabilities
else:
@@ -466,7 +462,7 @@ class ServiceCapabilities:
return instance
@staticmethod
def parse_capabilities(payload: bytes) -> List[ServiceCapabilities]:
def parse_capabilities(payload: bytes) -> list[ServiceCapabilities]:
capabilities = []
while payload:
service_category = payload[0]
@@ -499,7 +495,7 @@ class ServiceCapabilities:
self.service_category = service_category
self.service_capabilities_bytes = service_capabilities_bytes
def to_string(self, details: Optional[List[str]] = None) -> str:
def to_string(self, details: Optional[list[str]] = None) -> str:
attributes = ','.join(
[name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)]
+ (details or [])
@@ -612,7 +608,7 @@ class Message: # pylint:disable=attribute-defined-outside-init
RESPONSE_REJECT = 3
# Subclasses, by signal identifier and message type
subclasses: Dict[int, Dict[int, Type[Message]]] = {}
subclasses: dict[int, dict[int, type[Message]]] = {}
message_type: MessageType
signal_identifier: int
@@ -757,7 +753,7 @@ class Discover_Response(Message):
See Bluetooth AVDTP spec - 8.6.2 Stream End Point Discovery Response
'''
endpoints: List[EndPointInfo]
endpoints: list[EndPointInfo]
def init_from_payload(self):
self.endpoints = []
@@ -1202,10 +1198,10 @@ class DelayReport_Reject(Simple_Reject):
# -----------------------------------------------------------------------------
class Protocol(utils.EventEmitter):
local_endpoints: List[LocalStreamEndPoint]
remote_endpoints: Dict[int, DiscoveredStreamEndPoint]
streams: Dict[int, Stream]
transaction_results: List[Optional[asyncio.Future[Message]]]
local_endpoints: list[LocalStreamEndPoint]
remote_endpoints: dict[int, DiscoveredStreamEndPoint]
streams: dict[int, Stream]
transaction_results: list[Optional[asyncio.Future[Message]]]
channel_connector: Callable[[], Awaitable[l2cap.ClassicChannel]]
EVENT_OPEN = "open"
@@ -1223,7 +1219,7 @@ class Protocol(utils.EventEmitter):
@staticmethod
async def connect(
connection: device.Connection, version: Tuple[int, int] = (1, 3)
connection: device.Connection, version: tuple[int, int] = (1, 3)
) -> Protocol:
channel = await connection.create_l2cap_channel(
spec=l2cap.ClassicChannelSpec(psm=AVDTP_PSM)
@@ -1233,7 +1229,7 @@ class Protocol(utils.EventEmitter):
return protocol
def __init__(
self, l2cap_channel: l2cap.ClassicChannel, version: Tuple[int, int] = (1, 3)
self, l2cap_channel: l2cap.ClassicChannel, version: tuple[int, int] = (1, 3)
) -> None:
super().__init__()
self.l2cap_channel = l2cap_channel
@@ -1502,7 +1498,7 @@ class Protocol(utils.EventEmitter):
return response
async def start_transaction(self) -> Tuple[int, asyncio.Future[Message]]:
async def start_transaction(self) -> tuple[int, asyncio.Future[Message]]:
# Wait until we can start a new transaction
await self.transaction_semaphore.acquire()
@@ -1703,7 +1699,7 @@ class Protocol(utils.EventEmitter):
# -----------------------------------------------------------------------------
class Listener(utils.EventEmitter):
servers: Dict[int, Protocol]
servers: dict[int, Protocol]
EVENT_CONNECTION = "connection"
@@ -1735,7 +1731,7 @@ class Listener(utils.EventEmitter):
@classmethod
def for_device(
cls, device: device.Device, version: Tuple[int, int] = (1, 3)
cls, device: device.Device, version: tuple[int, int] = (1, 3)
) -> Listener:
listener = Listener(registrar=None, version=version)
l2cap_server = device.create_l2cap_server(

View File

@@ -26,14 +26,11 @@ from typing import (
Awaitable,
Callable,
cast,
Dict,
Iterable,
List,
Optional,
Sequence,
SupportsBytes,
Tuple,
Type,
TypeVar,
Union,
)
@@ -53,19 +50,10 @@ from bumble.sdp import (
ServiceAttribute,
)
from bumble import utils
from bumble.core import (
InvalidArgumentError,
ProtocolError,
BT_L2CAP_PROTOCOL_ID,
BT_AVCTP_PROTOCOL_ID,
BT_AV_REMOTE_CONTROL_SERVICE,
BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE,
BT_AV_REMOTE_CONTROL_TARGET_SERVICE,
)
from bumble import core
from bumble import l2cap
from bumble import avc
from bumble import avctp
from bumble import utils
# -----------------------------------------------------------------------------
@@ -84,10 +72,10 @@ AVRCP_BLUETOOTH_SIG_COMPANY_ID = 0x001958
# -----------------------------------------------------------------------------
def make_controller_service_sdp_records(
service_record_handle: int,
avctp_version: Tuple[int, int] = (1, 4),
avrcp_version: Tuple[int, int] = (1, 6),
avctp_version: tuple[int, int] = (1, 4),
avrcp_version: tuple[int, int] = (1, 6),
supported_features: int = 1,
) -> List[ServiceAttribute]:
) -> list[ServiceAttribute]:
# TODO: support a way to compute the supported features from a feature list
avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
@@ -105,8 +93,8 @@ def make_controller_service_sdp_records(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement.sequence(
[
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.uuid(BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
]
),
),
@@ -116,13 +104,13 @@ def make_controller_service_sdp_records(
[
DataElement.sequence(
[
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
]
),
DataElement.sequence(
[
DataElement.uuid(BT_AVCTP_PROTOCOL_ID),
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
DataElement.unsigned_integer_16(avctp_version_int),
]
),
@@ -135,7 +123,7 @@ def make_controller_service_sdp_records(
[
DataElement.sequence(
[
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.unsigned_integer_16(avrcp_version_int),
]
),
@@ -152,10 +140,10 @@ def make_controller_service_sdp_records(
# -----------------------------------------------------------------------------
def make_target_service_sdp_records(
service_record_handle: int,
avctp_version: Tuple[int, int] = (1, 4),
avrcp_version: Tuple[int, int] = (1, 6),
avctp_version: tuple[int, int] = (1, 4),
avrcp_version: tuple[int, int] = (1, 6),
supported_features: int = 0x23,
) -> List[ServiceAttribute]:
) -> list[ServiceAttribute]:
# TODO: support a way to compute the supported features from a feature list
avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
@@ -173,7 +161,7 @@ def make_target_service_sdp_records(
SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
DataElement.sequence(
[
DataElement.uuid(BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
]
),
),
@@ -183,13 +171,13 @@ def make_target_service_sdp_records(
[
DataElement.sequence(
[
DataElement.uuid(BT_L2CAP_PROTOCOL_ID),
DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
]
),
DataElement.sequence(
[
DataElement.uuid(BT_AVCTP_PROTOCOL_ID),
DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
DataElement.unsigned_integer_16(avctp_version_int),
]
),
@@ -202,7 +190,7 @@ def make_target_service_sdp_records(
[
DataElement.sequence(
[
DataElement.uuid(BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
DataElement.unsigned_integer_16(avrcp_version_int),
]
),
@@ -291,7 +279,7 @@ class Command:
pdu_id: Protocol.PduId
parameter: bytes
def to_string(self, properties: Dict[str, str]) -> str:
def to_string(self, properties: dict[str, str]) -> str:
properties_str = ",".join(
[f"{name}={value}" for name, value in properties.items()]
)
@@ -337,7 +325,7 @@ class GetPlayStatusCommand(Command):
# -----------------------------------------------------------------------------
class GetElementAttributesCommand(Command):
identifier: int
attribute_ids: List[MediaAttributeId]
attribute_ids: list[MediaAttributeId]
@classmethod
def from_bytes(cls, pdu: bytes) -> GetElementAttributesCommand:
@@ -409,7 +397,7 @@ class Response:
pdu_id: Protocol.PduId
parameter: bytes
def to_string(self, properties: Dict[str, str]) -> str:
def to_string(self, properties: dict[str, str]) -> str:
properties_str = ",".join(
[f"{name}={value}" for name, value in properties.items()]
)
@@ -454,7 +442,7 @@ class NotImplementedResponse(Response):
# -----------------------------------------------------------------------------
class GetCapabilitiesResponse(Response):
capability_id: GetCapabilitiesCommand.CapabilityId
capabilities: List[Union[SupportsBytes, bytes]]
capabilities: list[Union[SupportsBytes, bytes]]
@classmethod
def from_bytes(cls, pdu: bytes) -> GetCapabilitiesResponse:
@@ -467,7 +455,7 @@ class GetCapabilitiesResponse(Response):
capability_id = GetCapabilitiesCommand.CapabilityId(pdu[0])
capability_count = pdu[1]
capabilities: List[Union[SupportsBytes, bytes]]
capabilities: list[Union[SupportsBytes, bytes]]
if capability_id == GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED:
capabilities = [EventId(pdu[2 + x]) for x in range(capability_count)]
else:
@@ -540,13 +528,13 @@ class GetPlayStatusResponse(Response):
# -----------------------------------------------------------------------------
class GetElementAttributesResponse(Response):
attributes: List[MediaAttribute]
attributes: list[MediaAttribute]
@classmethod
def from_bytes(cls, pdu: bytes) -> GetElementAttributesResponse:
num_attributes = pdu[0]
offset = 1
attributes: List[MediaAttribute] = []
attributes: list[MediaAttribute] = []
for _ in range(num_attributes):
(
attribute_id_int,
@@ -817,7 +805,7 @@ class PlayerApplicationSettingChangedEvent(Event):
attribute_id: ApplicationSetting.AttributeId
value_id: utils.OpenIntEnum
player_application_settings: List[Setting]
player_application_settings: list[Setting]
@classmethod
def from_bytes(cls, pdu: bytes) -> PlayerApplicationSettingChangedEvent:
@@ -939,7 +927,7 @@ class VolumeChangedEvent(Event):
# -----------------------------------------------------------------------------
EVENT_SUBCLASSES: Dict[EventId, Type[Event]] = {
EVENT_SUBCLASSES: dict[EventId, type[Event]] = {
EventId.PLAYBACK_STATUS_CHANGED: PlaybackStatusChangedEvent,
EventId.PLAYBACK_POS_CHANGED: PlaybackPositionChangedEvent,
EventId.TRACK_CHANGED: TrackChangedEvent,
@@ -967,14 +955,14 @@ class Delegate:
def __init__(self, status_code: Protocol.StatusCode) -> None:
self.status_code = status_code
supported_events: List[EventId]
supported_events: list[EventId]
volume: int
def __init__(self, supported_events: Iterable[EventId] = ()) -> None:
self.supported_events = list(supported_events)
self.volume = 0
async def get_supported_events(self) -> List[EventId]:
async def get_supported_events(self) -> list[EventId]:
return self.supported_events
async def set_absolute_volume(self, volume: int) -> None:
@@ -1124,8 +1112,8 @@ class Protocol(utils.EventEmitter):
receive_response_state: Optional[ReceiveResponseState]
avctp_protocol: Optional[avctp.Protocol]
free_commands: asyncio.Queue
pending_commands: Dict[int, PendingCommand] # Pending commands, by label
notification_listeners: Dict[EventId, NotificationListener]
pending_commands: dict[int, PendingCommand] # Pending commands, by label
notification_listeners: dict[EventId, NotificationListener]
@staticmethod
def _check_vendor_dependent_frame(
@@ -1190,7 +1178,7 @@ class Protocol(utils.EventEmitter):
@staticmethod
def _check_response(
response_context: ResponseContext, expected_type: Type[_R]
response_context: ResponseContext, expected_type: type[_R]
) -> _R:
if isinstance(response_context, Protocol.FinalResponse):
if (
@@ -1211,7 +1199,7 @@ class Protocol(utils.EventEmitter):
def _delegate_command(
self, transaction_label: int, command: Command, method: Awaitable
) -> None:
async def call():
async def call() -> None:
try:
await method
except Delegate.Error as error:
@@ -1230,7 +1218,7 @@ class Protocol(utils.EventEmitter):
utils.AsyncRunner.spawn(call())
async def get_supported_events(self) -> List[EventId]:
async def get_supported_events(self) -> list[EventId]:
"""Get the list of events supported by the connected peer."""
response_context = await self.send_avrcp_command(
avc.CommandFrame.CommandType.STATUS,
@@ -1253,7 +1241,7 @@ class Protocol(utils.EventEmitter):
async def get_element_attributes(
self, element_identifier: int, attribute_ids: Sequence[MediaAttributeId]
) -> List[MediaAttribute]:
) -> list[MediaAttribute]:
"""Get element attributes from the connected peer."""
response_context = await self.send_avrcp_command(
avc.CommandFrame.CommandType.STATUS,
@@ -1335,7 +1323,7 @@ class Protocol(utils.EventEmitter):
async def monitor_player_application_settings(
self,
) -> AsyncIterator[List[PlayerApplicationSettingChangedEvent.Setting]]:
) -> AsyncIterator[list[PlayerApplicationSettingChangedEvent.Setting]]:
"""Monitor Player Application Setting changes from the connected peer."""
async for event in self.monitor_events(
EventId.PLAYER_APPLICATION_SETTING_CHANGED, 0
@@ -1415,7 +1403,7 @@ class Protocol(utils.EventEmitter):
def notify_track_changed(self, identifier: bytes) -> None:
"""Notify the connected peer of a Track change."""
if len(identifier) != 8:
raise InvalidArgumentError("identifier must be 8 bytes")
raise core.InvalidArgumentError("identifier must be 8 bytes")
self.notify_event(TrackChangedEvent(identifier))
def notify_playback_position_changed(self, position: int) -> None:
@@ -1682,7 +1670,7 @@ class Protocol(utils.EventEmitter):
else:
logger.debug("unexpected PDU ID")
pending_command.response.set_exception(
ProtocolError(
core.ProtocolError(
error_code=None,
error_namespace="avrcp",
details="unexpected PDU ID",
@@ -1691,7 +1679,7 @@ class Protocol(utils.EventEmitter):
else:
logger.debug("unexpected response code")
pending_command.response.set_exception(
ProtocolError(
core.ProtocolError(
error_code=None,
error_namespace="avrcp",
details="unexpected response code",
@@ -1869,12 +1857,12 @@ class Protocol(utils.EventEmitter):
) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}")
async def get_supported_events():
async def get_supported_events() -> None:
if (
command.capability_id
!= GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
):
raise Protocol.InvalidParameterError
raise core.InvalidArgumentError()
supported_events = await self.delegate.get_supported_events()
self.send_avrcp_response(
@@ -1890,7 +1878,7 @@ class Protocol(utils.EventEmitter):
) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}")
async def set_absolute_volume():
async def set_absolute_volume() -> None:
await self.delegate.set_absolute_volume(command.volume)
effective_volume = await self.delegate.get_absolute_volume()
self.send_avrcp_response(
@@ -1906,7 +1894,7 @@ class Protocol(utils.EventEmitter):
) -> None:
logger.debug(f"<<< AVRCP command PDU: {command}")
async def register_notification():
async def register_notification() -> None:
# Check if the event is supported.
supported_events = await self.delegate.get_supported_events()
if command.event_id not in supported_events:

View File

@@ -13,7 +13,7 @@
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from functools import partial
from typing import List, Optional, Union
from typing import Optional, Union
class ColorError(ValueError):
@@ -65,7 +65,7 @@ def color(
bg: Optional[ColorSpec] = None,
style: Optional[str] = None,
) -> str:
codes: List[ColorSpec] = []
codes: list[ColorSpec] = []
if fg:
codes.append(_color_code(fg, 30))

View File

@@ -63,7 +63,7 @@ from bumble.hci import (
HCI_Packet,
HCI_Role_Change_Event,
)
from typing import Optional, Union, Dict, Any, TYPE_CHECKING
from typing import Optional, Union, Any, TYPE_CHECKING
if TYPE_CHECKING:
from bumble.link import LocalLink
@@ -108,7 +108,9 @@ class Connection:
def on_hci_acl_data_packet(self, packet):
self.assembler.feed_packet(packet)
self.controller.send_hci_packet(
HCI_Number_Of_Completed_Packets_Event([(self.handle, 1)])
HCI_Number_Of_Completed_Packets_Event(
connection_handles=[self.handle], num_completed_packets=[1]
)
)
def on_acl_pdu(self, data):
@@ -132,17 +134,17 @@ class Controller:
self.hci_sink = None
self.link = link
self.central_connections: Dict[Address, Connection] = (
self.central_connections: dict[Address, Connection] = (
{}
) # Connections where this controller is the central
self.peripheral_connections: Dict[Address, Connection] = (
self.peripheral_connections: dict[Address, Connection] = (
{}
) # Connections where this controller is the peripheral
self.classic_connections: Dict[Address, Connection] = (
self.classic_connections: dict[Address, Connection] = (
{}
) # Connections in BR/EDR
self.central_cis_links: Dict[int, CisLink] = {} # CIS links by handle
self.peripheral_cis_links: Dict[int, CisLink] = {} # CIS links by handle
self.central_cis_links: dict[int, CisLink] = {} # CIS links by handle
self.peripheral_cis_links: dict[int, CisLink] = {} # CIS links by handle
self.hci_version = HCI_VERSION_BLUETOOTH_CORE_5_0
self.hci_revision = 0
@@ -542,15 +544,14 @@ class Controller:
acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data)
self.send_hci_packet(acl_packet)
def on_link_advertising_data(self, sender_address, data):
def on_link_advertising_data(self, sender_address: Address, data: bytes):
# Ignore if we're not scanning
if self.le_scan_enable == 0:
return
# Send a scan report
report = HCI_LE_Advertising_Report_Event.Report(
HCI_LE_Advertising_Report_Event.Report.FIELDS,
event_type=HCI_LE_Advertising_Report_Event.ADV_IND,
event_type=HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
address_type=sender_address.address_type,
address=sender_address,
data=data,
@@ -560,8 +561,7 @@ class Controller:
# Simulate a scan response
report = HCI_LE_Advertising_Report_Event.Report(
HCI_LE_Advertising_Report_Event.Report.FIELDS,
event_type=HCI_LE_Advertising_Report_Event.SCAN_RSP,
event_type=HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP,
address_type=sender_address.address_type,
address=sender_address,
data=data,

View File

@@ -1,6 +1,6 @@
# Copyright 2021-2022 Google LLC
# Copyright 2021-2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# 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
#
@@ -12,12 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------------------------------------------
# Crypto support
#
# See Bluetooth spec Vol 3, Part H - 2.2 CRYPTOGRAPHIC TOOLBOX
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
@@ -25,19 +19,15 @@ from __future__ import annotations
import logging
import operator
import secrets
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.asymmetric.ec import (
generate_private_key,
ECDH,
EllipticCurvePrivateKey,
EllipticCurvePublicNumbers,
EllipticCurvePrivateNumbers,
SECP256R1,
)
from cryptography.hazmat.primitives import cmac
from typing import Tuple
try:
from bumble.crypto.cryptography import EccKey, e, aes_cmac
except ImportError:
logging.getLogger(__name__).debug(
"Unable to import cryptography, use built-in primitives."
)
from bumble.crypto.builtin import EccKey, e, aes_cmac # type: ignore[assignment]
# -----------------------------------------------------------------------------
@@ -46,55 +36,6 @@ from typing import Tuple
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
class EccKey:
def __init__(self, private_key: EllipticCurvePrivateKey) -> None:
self.private_key = private_key
@classmethod
def generate(cls) -> EccKey:
private_key = generate_private_key(SECP256R1())
return cls(private_key)
@classmethod
def from_private_key_bytes(
cls, d_bytes: bytes, x_bytes: bytes, y_bytes: bytes
) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
x = int.from_bytes(x_bytes, byteorder='big', signed=False)
y = int.from_bytes(y_bytes, byteorder='big', signed=False)
private_key = EllipticCurvePrivateNumbers(
d, EllipticCurvePublicNumbers(x, y, SECP256R1())
).private_key()
return cls(private_key)
@property
def x(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.x.to_bytes(32, byteorder='big')
)
@property
def y(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.y.to_bytes(32, byteorder='big')
)
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
public_key = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key()
shared_key = self.private_key.exchange(ECDH(), public_key)
return shared_key
# -----------------------------------------------------------------------------
# Functions
# -----------------------------------------------------------------------------
@@ -132,19 +73,6 @@ def r() -> bytes:
return secrets.token_bytes(16)
# -----------------------------------------------------------------------------
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
cipher = Cipher(algorithms.AES(reverse(key)), modes.ECB())
encryptor = cipher.encryptor()
return reverse(encryptor.update(reverse(data)))
# -----------------------------------------------------------------------------
def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name
'''
@@ -187,18 +115,6 @@ def s1(k: bytes, r1: bytes, r2: bytes) -> bytes:
return e(k, r2[0:8] + r1[0:8])
# -----------------------------------------------------------------------------
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
mac = cmac.CMAC(algorithms.AES(k))
mac.update(m)
return mac.finalize()
# -----------------------------------------------------------------------------
def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
'''
@@ -209,7 +125,7 @@ def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
# -----------------------------------------------------------------------------
def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> Tuple[bytes, bytes]:
def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> tuple[bytes, bytes]:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation
Function f5

652
bumble/crypto/builtin.py Normal file
View File

@@ -0,0 +1,652 @@
# Copyright 2021-2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# The implementation is modified from:
# * AES - https://github.com/ricmoo/pyaes by Richard Moore under MIT License
# * CMAC - https://github.com/pycrypto/pycrypto by contributors under pycrypto License.
# -----------------------------------------------------------------------------
# Built-in implementation of cryptography primitives.
#
# Note: It's very dangerous to use this library in the real world.
# -----------------------------------------------------------------------------
from __future__ import annotations
import dataclasses
import functools
import copy
import secrets
import struct
from typing import Optional
from bumble import core
def _compact_word(word: bytes) -> int:
return int.from_bytes(word, "big")
def _shift_bytes(bs: bytes, xor_lsb: int = 0) -> bytes:
return ((int.from_bytes(bs, "big") << 1) ^ xor_lsb).to_bytes(len(bs) + 1, "big")[1:]
def _xor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
# Based *largely* on the Rijndael implementation
# See: http://csrc.nist.gov/publications/FIPS/FIPS197/FIPS-197.pdf
class _AES:
'''Encapsulates the AES block cipher.
You generally should not need this. Use the AESModeOfOperation classes
below instead.'''
# fmt: off
# Number of rounds by key size
_NUMBER_OF_ROUNDS = {16: 10, 24: 12, 32: 14}
# Round constant words
_RCON = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ]
# S-box and Inverse S-box (S is for Substitution)
_S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]
_S_INV =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ]
# Transformations for encryption
_T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ]
_T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ]
_T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ]
_T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ]
# Transformations for decryption
_T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ]
_T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ]
_T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ]
_T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ]
# Transformations for decryption key expansion
_U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ]
_U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ]
_U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ]
_U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ]
# fmt: on
def __init__(self, key: bytes) -> None:
if len(key) not in (16, 24, 32):
raise core.InvalidArgumentError(f'Invalid key size {len(key)}')
rounds = self._NUMBER_OF_ROUNDS[len(key)]
# Encryption round keys
self._ke = [[0] * 4 for i in range(rounds + 1)]
# Decryption round keys
self._kd = [[0] * 4 for i in range(rounds + 1)]
round_key_count = (rounds + 1) * 4
kc = len(key) // 4
# Convert the key into ints
tk = [struct.unpack('>i', key[i : i + 4])[0] for i in range(0, len(key), 4)]
# Copy values into round key arrays
for i in range(0, kc):
self._ke[i // 4][i % 4] = tk[i]
self._kd[rounds - (i // 4)][i % 4] = tk[i]
# Key expansion (FIPS-197 section 5.2)
r_con_pointer = 0
t = kc
while t < round_key_count:
tt = tk[kc - 1]
tk[0] ^= (
(self._S[(tt >> 16) & 0xFF] << 24)
^ (self._S[(tt >> 8) & 0xFF] << 16)
^ (self._S[tt & 0xFF] << 8)
^ self._S[(tt >> 24) & 0xFF]
^ (self._RCON[r_con_pointer] << 24)
)
r_con_pointer += 1
if kc != 8:
for i in range(1, kc):
tk[i] ^= tk[i - 1]
# Key expansion for 256-bit keys is "slightly different" (FIPS-197)
else:
for i in range(1, kc // 2):
tk[i] ^= tk[i - 1]
tt = tk[kc // 2 - 1]
tk[kc // 2] ^= (
self._S[tt & 0xFF]
^ (self._S[(tt >> 8) & 0xFF] << 8)
^ (self._S[(tt >> 16) & 0xFF] << 16)
^ (self._S[(tt >> 24) & 0xFF] << 24)
)
for i in range(kc // 2 + 1, kc):
tk[i] ^= tk[i - 1]
# Copy values into round key arrays
j = 0
while j < kc and t < round_key_count:
self._ke[t // 4][t % 4] = tk[j]
self._kd[rounds - (t // 4)][t % 4] = tk[j]
j += 1
t += 1
# Inverse-Cipher-ify the decryption round key (FIPS-197 section 5.3)
for r in range(1, rounds):
for j in range(0, 4):
tt = self._kd[r][j]
self._kd[r][j] = (
self._U1[(tt >> 24) & 0xFF]
^ self._U2[(tt >> 16) & 0xFF]
^ self._U3[(tt >> 8) & 0xFF]
^ self._U4[tt & 0xFF]
)
def encrypt(self, plaintext: bytes) -> bytes:
"""Encrypt a block of plain text using the AES block cipher."""
if len(plaintext) != 16:
raise core.InvalidArgumentError(f'wrong block length {len(plaintext)}')
rounds = len(self._ke) - 1
(s1, s2, s3) = [1, 2, 3]
a = [0, 0, 0, 0]
# Convert plaintext to (ints ^ key)
t = [
(_compact_word(plaintext[4 * i : 4 * i + 4]) ^ self._ke[0][i])
for i in range(0, 4)
]
# Apply round transforms
for r in range(1, rounds):
for i in range(0, 4):
a[i] = (
self._T1[(t[i] >> 24) & 0xFF]
^ self._T2[(t[(i + s1) % 4] >> 16) & 0xFF]
^ self._T3[(t[(i + s2) % 4] >> 8) & 0xFF]
^ self._T4[t[(i + s3) % 4] & 0xFF]
^ self._ke[r][i]
)
t = copy.copy(a)
# The last round is special
result = []
for i in range(0, 4):
tt = self._ke[rounds][i]
result.append((self._S[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append((self._S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
result.append((self._S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
result.append((self._S[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
return bytes(result)
def decrypt(self, cipher_text: bytes) -> bytes:
"""Decrypt a block of cipher text using the AES block cipher."""
if len(cipher_text) != 16:
raise core.InvalidArgumentError(f'wrong block length {len(cipher_text)}')
rounds = len(self._kd) - 1
(s1, s2, s3) = [3, 2, 1]
a = [0, 0, 0, 0]
# Convert ciphertext to (ints ^ key)
t = [
(_compact_word(cipher_text[4 * i : 4 * i + 4]) ^ self._kd[0][i])
for i in range(0, 4)
]
# Apply round transforms
for r in range(1, rounds):
for i in range(0, 4):
a[i] = (
self._T5[(t[i] >> 24) & 0xFF]
^ self._T6[(t[(i + s1) % 4] >> 16) & 0xFF]
^ self._T7[(t[(i + s2) % 4] >> 8) & 0xFF]
^ self._T8[t[(i + s3) % 4] & 0xFF]
^ self._kd[r][i]
)
t = copy.copy(a)
# The last round is special
result = []
for i in range(0, 4):
tt = self._kd[rounds][i]
result.append((self._S_INV[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append(
(self._S_INV[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF
)
result.append(
(self._S_INV[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF
)
result.append((self._S_INV[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
return bytes(result)
class _ECB:
def __init__(self, key: bytes):
self._aes = _AES(key)
def encrypt(self, plaintext: bytes) -> bytes:
return b"".join(
[
self._aes.encrypt(
plaintext[offset : offset + 16].ljust(16, b"\x00") # Pad 0.
)
for offset in range(0, len(plaintext), 16)
]
)
def decrypt(self, cipher_text: bytes) -> bytes:
return b"".join(
[
self._aes.encrypt(cipher_text[offset : offset + 16])
for offset in range(0, len(cipher_text), 16)
]
)
class _CBC:
def __init__(self, key: bytes, iv: bytes = bytes(16)) -> None:
if len(iv) != 16:
raise core.InvalidArgumentError(
f'initialization vector must be 16 bytes, get {len(iv)}'
)
else:
self._last_cipher_block = iv
self._aes = _AES(key)
def encrypt(self, plaintext: bytes) -> bytes:
cipher_text = b""
for offset in range(0, len(plaintext), 16):
pre_cipher_block = _xor(
plaintext[offset : offset + 16], self._last_cipher_block
)
self._last_cipher_block = self._aes.encrypt(pre_cipher_block)
cipher_text += self._last_cipher_block
return cipher_text
def decrypt(self, cipher_text: bytes) -> bytes:
plaintext = b""
for offset in range(0, len(cipher_text), 16):
plaintext += _xor(
self._aes.decrypt(cipher_text[offset : offset + 16]),
self._last_cipher_block,
)
self._last_cipher_block = cipher_text[offset : offset + 16]
return plaintext
class _CMAC:
def __init__(
self,
key: bytes,
msg: bytes = bytes(16),
mac_len: int = 16,
update_after_digest: bool = False,
) -> None:
self.digest_size = mac_len
self._key = key
self._block_size = bs = 16
self._mac_tag: Optional[bytes] = None
self._update_after_digest = update_after_digest
# Section 5.3 of NIST SP 800 38B and Appendix B
if bs == 8:
const_Rb = 0x1B
self._max_size = 8 * (2**21)
elif bs == 16:
const_Rb = 0x87
self._max_size = 16 * (2**48)
else:
raise core.InvalidArgumentError(
f"CMAC requires a cipher with a block size of 8 or 16 bytes, not {bs}"
)
# Compute sub-keys
zero_block = bytes(bs)
self._ecb = _ECB(key)
L = self._ecb.encrypt(zero_block)
if L[0] & 0x80:
self._k1 = _shift_bytes(L, const_Rb)
else:
self._k1 = _shift_bytes(L)
if self._k1[0] & 0x80:
self._k2 = _shift_bytes(self._k1, const_Rb)
else:
self._k2 = _shift_bytes(self._k1)
# Initialize CBC cipher with zero IV
self._cbc = _CBC(key, zero_block)
# Cache for outstanding data to authenticate
self._cache = bytearray(bs)
self._cache_n = 0
# Last piece of cipher text produced
self._last_ct = zero_block
# Last block that was encrypted with AES
self._last_pt: Optional[bytes] = None
# Counter for total message size
self._data_size = 0
if msg:
self.update(msg)
def update(self, msg: bytes) -> _CMAC:
"""Authenticate the next chunk of message.
Args:
data (byte string/byte array/memoryview): The next chunk of data
"""
if self._mac_tag is not None and not self._update_after_digest:
raise core.InvalidStateError(
"update() cannot be called after digest() or verify()"
)
self._data_size += len(msg)
bs = self._block_size
if self._cache_n > 0:
filler = min(bs - self._cache_n, len(msg))
self._cache[self._cache_n : self._cache_n + filler] = msg[:filler]
self._cache_n += filler
if self._cache_n < bs:
return self
msg = msg[filler:]
self._update(self._cache)
self._cache_n = 0
remain = len(msg) % bs
if remain > 0:
self._update(msg[:-remain])
self._cache[:remain] = msg[-remain:]
else:
self._update(msg)
self._cache_n = remain
return self
def _update(self, data_block: bytes) -> None:
"""Update a block aligned to the block boundary"""
bs = self._block_size
assert len(data_block) % bs == 0
if len(data_block) == 0:
return
ct = self._cbc.encrypt(data_block)
if len(data_block) == bs:
second_last = self._last_ct
else:
second_last = ct[-bs * 2 : -bs]
self._last_ct = ct[-bs:]
self._last_pt = _xor(second_last, data_block[-bs:])
def digest(self) -> bytes:
bs = self._block_size
if self._mac_tag is not None and not self._update_after_digest:
return self._mac_tag
if self._data_size > self._max_size:
raise core.InvalidArgumentError("MAC is unsafe for this message")
if self._cache_n == 0 and self._data_size > 0 and self._last_pt:
# Last block was full
pt = _xor(self._last_pt, self._k1)
else:
# Last block is partial (or message length is zero)
partial = self._cache[:]
partial[self._cache_n :] = b'\x80' + b'\x00' * (bs - self._cache_n - 1)
pt = _xor(_xor(self._last_ct, partial), self._k2)
self._mac_tag = self._ecb.encrypt(pt)[: self.digest_size]
return self._mac_tag
# Define the original Point class for clarity and conversion purposes
@dataclasses.dataclass
class _Point:
"""Represents a point on the elliptic curve in affine coordinates."""
curve: _EllipticCurve
x: int = 0
y: int = 0
infinite: bool = False
@dataclasses.dataclass(frozen=True)
class _JacobianPoint:
"""Represents a point on the elliptic curve in Jacobian coordinates."""
curve: _EllipticCurve
x: int = 1 # For point at infinity (1:1:0)
y: int = 1
z: int = 0 # z = 0 indicates point at infinity
@classmethod
def point_at_infinity(cls, curve: _EllipticCurve) -> _JacobianPoint:
return _JacobianPoint(curve=curve, x=1, y=1, z=0)
@classmethod
def from_affine(cls, affine_point: _Point) -> _JacobianPoint:
if affine_point.infinite:
return _JacobianPoint.point_at_infinity(affine_point.curve)
# A simple conversion is (x, y, 1)
return _JacobianPoint(
curve=affine_point.curve, x=affine_point.x, y=affine_point.y, z=1
)
def to_affine(self) -> _Point:
if self.z == 0:
return _Point(infinite=True, curve=self.curve)
p = self.curve.p
inv_z = pow(self.z, -1, p)
affine_x = (self.x * inv_z**2) % p
affine_y = (self.y * inv_z**3) % p
return _Point(curve=self.curve, x=affine_x, y=affine_y, infinite=False)
def double(self) -> _JacobianPoint:
if self.z == 0 or self.y == 0:
return _JacobianPoint.point_at_infinity(self.curve)
s = 4 * self.x * self.y**2
m = 3 * self.x**2 + self.curve.a * self.z**4
x2 = m**2 - 2 * s
y2 = m * (s - x2) - 8 * self.y**4
z2 = 2 * self.y * self.z
p = self.curve.p
return _JacobianPoint(curve=self.curve, x=x2 % p, y=y2 % p, z=z2 % p)
def __add__(self, other: _JacobianPoint) -> _JacobianPoint:
if self.z == 0 and other.z == 0:
return _JacobianPoint.point_at_infinity(self.curve)
elif self.z == 0:
return other
elif other.z == 0:
return self
x1 = self.x
y1 = self.y
z1 = self.z
x2 = other.x
y2 = other.y
z2 = other.z
p = self.curve.p
u1 = (x1 * z2**2) % p
u2 = (x2 * z1**2) % p
s1 = (y1 * z2**3) % p
s2 = (y2 * z1**3) % p
if u1 == u2:
if s1 != s2:
return _JacobianPoint.point_at_infinity(self.curve)
else:
return self.double()
else:
h = u2 - u1
r = s2 - s1
h3 = h**3 % p
u1h2 = (u1 * h**2) % p
x3 = r**2 - h3 - 2 * u1h2
y3 = r * (u1h2 - x3) - s1 * h3
z3 = h * z1 * z2
return _JacobianPoint(self.curve, x3 % p, y3 % p, z3 % p)
def __mul__(self, k: int) -> _JacobianPoint:
addend = self
result = _JacobianPoint.point_at_infinity(self.curve)
while k > 0:
if k % 2 != 0:
result = result + addend
addend = addend.double()
k = k >> 1
return result
def __rmul__(self, k: int) -> _JacobianPoint:
return self * k
@dataclasses.dataclass
class _EllipticCurve:
p: int
a: int
b: int
n: int
g_x: int
g_y: int
_generator_jacobian: _JacobianPoint = dataclasses.field(init=False)
def __post_init__(self):
self._generator_jacobian = _JacobianPoint(
curve=self, x=self.g_x, y=self.g_y, z=1
)
@dataclasses.dataclass
class PrivateKey:
key: int
curve: _EllipticCurve
def generate_private_key(self) -> PrivateKey:
"""Generates a random private key."""
return self.PrivateKey(key=secrets.randbelow(self.n), curve=self)
def generate_public_key(self, private_key: int) -> _Point:
"""Generates a public key from a private key using Jacobian coordinates for scalar multiplication."""
public_key_jacobian = self._generator_jacobian * private_key
return public_key_jacobian.to_affine()
def ecdh_shared_secret(self, private_key: int, other_public_key: _Point) -> bytes:
"""Computes the shared secret using ECDH."""
other_public_key_jacobian = _JacobianPoint.from_affine(other_public_key)
shared_point_jacobian = other_public_key_jacobian * private_key
shared_point_affine = shared_point_jacobian.to_affine()
if shared_point_affine.infinite:
raise core.InvalidPacketError(
"Shared secret calculation resulted in the point at infinite"
)
return shared_point_affine.x.to_bytes(32, 'big')
@classmethod
def SECP256R1(cls) -> _EllipticCurve:
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 # Curve order
g_x = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
g_y = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
return _EllipticCurve(p=p, a=a, b=b, n=n, g_x=g_x, g_y=g_y)
class EccKey:
def __init__(self, private_key: _EllipticCurve.PrivateKey) -> None:
self.private_key = private_key
@functools.cached_property
def x(self) -> bytes:
return self.private_key.curve.generate_public_key(
self.private_key.key
).x.to_bytes(32, byteorder='big')
@functools.cached_property
def y(self) -> bytes:
return self.private_key.curve.generate_public_key(
self.private_key.key
).y.to_bytes(32, byteorder='big')
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
return self.private_key.curve.ecdh_shared_secret(
self.private_key.key,
_Point(x=x, y=y, curve=self.private_key.curve),
)
@classmethod
def generate(cls) -> EccKey:
return EccKey(_EllipticCurve.SECP256R1().generate_private_key())
@classmethod
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
return EccKey(_EllipticCurve.PrivateKey(d, _EllipticCurve.SECP256R1()))
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
return _ECB(key[::-1]).encrypt(data[::-1])[::-1]
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
return _CMAC(key=k, msg=m).digest()

View File

@@ -0,0 +1,84 @@
# Copyright 2021-2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import functools
from cryptography.hazmat.primitives import ciphers
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import cmac
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
cipher = ciphers.Cipher(algorithms.AES(key[::-1]), modes.ECB())
encryptor = cipher.encryptor()
return encryptor.update(data[::-1])[::-1]
class EccKey:
def __init__(self, private_key: ec.EllipticCurvePrivateKey) -> None:
self.private_key = private_key
@classmethod
def generate(cls) -> EccKey:
return EccKey(ec.generate_private_key(ec.SECP256R1()))
@classmethod
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
return EccKey(ec.derive_private_key(d, ec.SECP256R1()))
@functools.cached_property
def x(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.x.to_bytes(32, byteorder='big')
)
@functools.cached_property
def y(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.y.to_bytes(32, byteorder='big')
)
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
return self.private_key.exchange(
ec.ECDH(),
ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key(),
)
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
mac = cmac.CMAC(algorithms.AES(k))
mac.update(m)
return mac.finalize()

View File

@@ -35,12 +35,10 @@ import secrets
import sys
from typing import (
Any,
Awaitable,
Callable,
ClassVar,
Deque,
Dict,
Optional,
Type,
TypeVar,
Union,
cast,
@@ -87,6 +85,7 @@ from bumble.profiles import gatt_service
if TYPE_CHECKING:
from bumble.transport.common import TransportSource, TransportSink
_T = TypeVar('_T')
# -----------------------------------------------------------------------------
# Logging
@@ -99,9 +98,9 @@ logger = logging.getLogger(__name__)
# fmt: off
# pylint: disable=line-too-long
DEVICE_MIN_SCAN_INTERVAL = 25
DEVICE_MIN_SCAN_INTERVAL = 2.5
DEVICE_MAX_SCAN_INTERVAL = 10240
DEVICE_MIN_SCAN_WINDOW = 25
DEVICE_MIN_SCAN_WINDOW = 2.5
DEVICE_MAX_SCAN_WINDOW = 10240
DEVICE_MIN_LE_RSSI = -127
DEVICE_MAX_LE_RSSI = 20
@@ -202,25 +201,35 @@ class Advertisement:
# -----------------------------------------------------------------------------
class LegacyAdvertisement(Advertisement):
@classmethod
def from_advertising_report(cls, report):
def from_advertising_report(
cls, report: hci.HCI_LE_Advertising_Report_Event.Report
) -> Self:
return cls(
address=report.address,
rssi=report.rssi,
is_legacy=True,
is_connectable=report.event_type
in (
hci.HCI_LE_Advertising_Report_Event.ADV_IND,
hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
is_connectable=(
report.event_type
in (
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND,
)
),
is_directed=report.event_type
== hci.HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND,
is_scannable=report.event_type
in (
hci.HCI_LE_Advertising_Report_Event.ADV_IND,
hci.HCI_LE_Advertising_Report_Event.ADV_SCAN_IND,
is_directed=(
report.event_type
== hci.HCI_LE_Advertising_Report_Event.EventType.ADV_DIRECT_IND
),
is_scannable=(
report.event_type
in (
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
hci.HCI_LE_Advertising_Report_Event.EventType.ADV_SCAN_IND,
)
),
is_scan_response=(
report.event_type
== hci.HCI_LE_Advertising_Report_Event.EventType.SCAN_RSP
),
is_scan_response=report.event_type
== hci.HCI_LE_Advertising_Report_Event.SCAN_RSP,
data_bytes=report.data,
)
@@ -228,18 +237,20 @@ class LegacyAdvertisement(Advertisement):
# -----------------------------------------------------------------------------
class ExtendedAdvertisement(Advertisement):
@classmethod
def from_advertising_report(cls, report):
def from_advertising_report(
cls, report: hci.HCI_LE_Extended_Advertising_Report_Event.Report
) -> Self:
# fmt: off
# pylint: disable=line-too-long
return cls(
address = report.address,
rssi = report.rssi,
is_legacy = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.LEGACY_ADVERTISING_PDU_USED) != 0,
is_legacy = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.LEGACY_ADVERTISING_PDU_USED) != 0,
is_anonymous = report.address.address_type == hci.HCI_LE_Extended_Advertising_Report_Event.ANONYMOUS_ADDRESS_TYPE,
is_connectable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.CONNECTABLE_ADVERTISING) != 0,
is_directed = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.DIRECTED_ADVERTISING) != 0,
is_scannable = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCANNABLE_ADVERTISING) != 0,
is_scan_response = report.event_type & (1 << hci.HCI_LE_Extended_Advertising_Report_Event.SCAN_RESPONSE) != 0,
is_connectable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING) != 0,
is_directed = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.DIRECTED_ADVERTISING) != 0,
is_scannable = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCANNABLE_ADVERTISING) != 0,
is_scan_response = (report.event_type & hci.HCI_LE_Extended_Advertising_Report_Event.EventType.SCAN_RESPONSE) != 0,
is_complete = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_COMPLETE,
is_truncated = (report.event_type >> 5 & 3) == hci.HCI_LE_Extended_Advertising_Report_Event.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME,
primary_phy = report.primary_phy,
@@ -436,7 +447,7 @@ class AdvertisingEventProperties:
@classmethod
def from_advertising_type(
cls: Type[AdvertisingEventProperties],
cls: type[AdvertisingEventProperties],
advertising_type: AdvertisingType,
) -> AdvertisingEventProperties:
return cls(
@@ -1343,7 +1354,7 @@ class Peer:
return self.gatt_client.get_characteristics_by_uuid(uuid, service)
def create_service_proxy(
self, proxy_class: Type[_PROXY_CLASS]
self, proxy_class: type[_PROXY_CLASS]
) -> Optional[_PROXY_CLASS]:
if proxy := proxy_class.from_client(self.gatt_client):
return cast(_PROXY_CLASS, proxy)
@@ -1351,7 +1362,7 @@ class Peer:
return None
async def discover_service_and_create_proxy(
self, proxy_class: Type[_PROXY_CLASS]
self, proxy_class: type[_PROXY_CLASS]
) -> Optional[_PROXY_CLASS]:
# Discover the first matching service and its characteristics
services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
@@ -1464,7 +1475,7 @@ class _IsoLink:
check_result=True,
)
async def remove_data_path(self, direction: _IsoLink.Direction) -> int:
async def remove_data_path(self, directions: Iterable[_IsoLink.Direction]) -> int:
"""Remove a data path with controller on given direction.
Args:
@@ -1476,7 +1487,9 @@ class _IsoLink:
response = await self.device.send_command(
hci.HCI_LE_Remove_ISO_Data_Path_Command(
connection_handle=self.handle,
data_path_direction=direction,
data_path_direction=sum(
1 << direction for direction in set(directions)
),
),
check_result=False,
)
@@ -1545,7 +1558,7 @@ class IsoPacketStream:
self.iso_link = iso_link
self.data_packet_queue = iso_link.data_packet_queue
self.data_packet_queue.on('flow', self._on_flow)
self._thresholds: Deque[int] = collections.deque()
self._thresholds: collections.deque[int] = collections.deque()
self._semaphore = asyncio.Semaphore(max_queue_size)
def _on_flow(self) -> None:
@@ -1589,7 +1602,6 @@ class Connection(utils.CompositeEventEmitter):
encryption_key_size: int
authenticated: bool
sc: bool
link_key_type: Optional[int] # [Classic only]
gatt_client: gatt_client.Client
pairing_peer_io_capability: Optional[int]
pairing_peer_authentication_requirements: Optional[int]
@@ -1629,6 +1641,7 @@ class Connection(utils.CompositeEventEmitter):
EVENT_PAIRING = "pairing"
EVENT_PAIRING_FAILURE = "pairing_failure"
EVENT_SECURITY_REQUEST = "security_request"
EVENT_LINK_KEY = "link_key"
@utils.composite_listener
class Listener:
@@ -1692,7 +1705,6 @@ class Connection(utils.CompositeEventEmitter):
self.encryption_key_size = 0
self.authenticated = False
self.sc = False
self.link_key_type = None
self.att_mtu = ATT_DEFAULT_MTU
self.data_length = DEVICE_DEFAULT_DATA_LENGTH
self.gatt_client = gatt_client.Client(self) # Per-connection client
@@ -1885,6 +1897,12 @@ class Connection(utils.CompositeEventEmitter):
def data_packet_queue(self) -> DataPacketQueue | None:
return self.device.host.get_data_packet_queue(self.handle)
def cancel_on_disconnection(self, awaitable: Awaitable[_T]) -> Awaitable[_T]:
"""
Helper method to call `utils.cancel_on_event` for the 'disconnection' event
"""
return utils.cancel_on_event(self, self.EVENT_DISCONNECTION, awaitable)
async def __aenter__(self):
return self
@@ -1955,9 +1973,9 @@ class DeviceConfiguration:
gatt_service_enabled: bool = True
def __post_init__(self) -> None:
self.gatt_services: list[Dict[str, Any]] = []
self.gatt_services: list[dict[str, Any]] = []
def load_from_dict(self, config: Dict[str, Any]) -> None:
def load_from_dict(self, config: dict[str, Any]) -> None:
config = copy.deepcopy(config)
# Load simple properties
@@ -2017,13 +2035,13 @@ class DeviceConfiguration:
self.load_from_dict(json.load(file))
@classmethod
def from_file(cls: Type[Self], filename: str) -> Self:
def from_file(cls: type[Self], filename: str) -> Self:
config = cls()
config.load_from_file(filename)
return config
@classmethod
def from_dict(cls: Type[Self], config: Dict[str, Any]) -> Self:
def from_dict(cls: type[Self], config: dict[str, Any]) -> Self:
device_config = cls()
device_config.load_from_dict(config)
return device_config
@@ -2120,22 +2138,22 @@ class Device(utils.CompositeEventEmitter):
advertising_data: bytes
scan_response_data: bytes
cs_capabilities: ChannelSoundingCapabilities | None = None
connections: Dict[int, Connection]
pending_connections: Dict[hci.Address, Connection]
classic_pending_accepts: Dict[
connections: dict[int, Connection]
pending_connections: dict[hci.Address, Connection]
classic_pending_accepts: dict[
hci.Address,
list[asyncio.Future[Union[Connection, tuple[hci.Address, int, int]]]],
]
advertisement_accumulators: Dict[hci.Address, AdvertisementDataAccumulator]
advertisement_accumulators: dict[hci.Address, AdvertisementDataAccumulator]
periodic_advertising_syncs: list[PeriodicAdvertisingSync]
config: DeviceConfiguration
legacy_advertiser: Optional[LegacyAdvertiser]
sco_links: Dict[int, ScoLink]
cis_links: Dict[int, CisLink]
sco_links: dict[int, ScoLink]
cis_links: dict[int, CisLink]
bigs: dict[int, Big]
bis_links: dict[int, BisLink]
big_syncs: dict[int, BigSync]
_pending_cis: Dict[int, tuple[int, int]]
_pending_cis: dict[int, tuple[int, int]]
gatt_service: gatt_service.GenericAttributeProfileService | None = None
EVENT_ADVERTISEMENT = "advertisement"
@@ -2295,8 +2313,8 @@ class Device(utils.CompositeEventEmitter):
self.address_generation_offload = config.address_generation_offload
# Extended advertising.
self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {}
self.extended_advertising_sets: dict[int, AdvertisingSet] = {}
self.connecting_extended_advertising_sets: dict[int, AdvertisingSet] = {}
# Legacy advertising.
# The advertising and scan response data, as well as the advertising interval
@@ -4272,11 +4290,11 @@ class Device(utils.CompositeEventEmitter):
self.smp_manager.pairing_config_factory = pairing_config_factory
@property
def smp_session_proxy(self) -> Type[smp.Session]:
def smp_session_proxy(self) -> type[smp.Session]:
return self.smp_manager.session_proxy
@smp_session_proxy.setter
def smp_session_proxy(self, session_proxy: Type[smp.Session]) -> None:
def smp_session_proxy(self, session_proxy: type[smp.Session]) -> None:
self.smp_manager.session_proxy = session_proxy
async def pair(self, connection):
@@ -4360,9 +4378,7 @@ class Device(utils.CompositeEventEmitter):
raise hci.HCI_StatusError(result)
# Wait for the authentication to complete
await utils.cancel_on_event(
connection, Connection.EVENT_DISCONNECTION, pending_authentication
)
await connection.cancel_on_disconnection(pending_authentication)
finally:
connection.remove_listener(
connection.EVENT_CONNECTION_AUTHENTICATION, on_authentication
@@ -4449,9 +4465,7 @@ class Device(utils.CompositeEventEmitter):
raise hci.HCI_StatusError(result)
# Wait for the result
await utils.cancel_on_event(
connection, Connection.EVENT_DISCONNECTION, pending_encryption
)
await connection.cancel_on_disconnection(pending_encryption)
finally:
connection.remove_listener(
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE, on_encryption_change
@@ -4495,9 +4509,7 @@ class Device(utils.CompositeEventEmitter):
f'{hci.HCI_Constant.error_name(result.status)}'
)
raise hci.HCI_StatusError(result)
await utils.cancel_on_event(
connection, Connection.EVENT_DISCONNECTION, pending_role_change
)
await connection.cancel_on_disconnection(pending_role_change)
finally:
connection.remove_listener(connection.EVENT_ROLE_CHANGE, on_role_change)
connection.remove_listener(
@@ -5068,16 +5080,16 @@ class Device(utils.CompositeEventEmitter):
# [Classic only]
@host_event_handler
def on_link_key(self, bd_addr, link_key, key_type):
def on_link_key(self, bd_addr: hci.Address, link_key: bytes, key_type: int) -> None:
# Store the keys in the key store
if self.keystore:
authenticated = key_type in (
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
)
pairing_keys = PairingKeys()
pairing_keys.link_key = PairingKeys.Key(
value=link_key, authenticated=authenticated
pairing_keys = PairingKeys(
link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
link_key_type=key_type,
)
utils.cancel_on_event(
@@ -5087,7 +5099,6 @@ class Device(utils.CompositeEventEmitter):
if connection := self.find_connection_by_bd_addr(
bd_addr, transport=PhysicalTransport.BR_EDR
):
connection.link_key_type = key_type
connection.emit(connection.EVENT_LINK_KEY)
def add_service(self, service):
@@ -5730,9 +5741,7 @@ class Device(utils.CompositeEventEmitter):
async def reply() -> None:
try:
if await utils.cancel_on_event(
connection, Connection.EVENT_DISCONNECTION, method()
):
if await connection.cancel_on_disconnection(method()):
await self.host.send_command(
hci.HCI_User_Confirmation_Request_Reply_Command(
bd_addr=connection.peer_address
@@ -5759,10 +5768,8 @@ class Device(utils.CompositeEventEmitter):
async def reply() -> None:
try:
number = await utils.cancel_on_event(
connection,
Connection.EVENT_DISCONNECTION,
pairing_config.delegate.get_number(),
number = await connection.cancel_on_disconnection(
pairing_config.delegate.get_number()
)
if number is not None:
await self.host.send_command(
@@ -5795,10 +5802,8 @@ class Device(utils.CompositeEventEmitter):
if io_capability == hci.HCI_KEYBOARD_ONLY_IO_CAPABILITY:
# Ask the user to enter a string
async def get_pin_code():
pin_code = await utils.cancel_on_event(
connection,
Connection.EVENT_DISCONNECTION,
pairing_config.delegate.get_string(16),
pin_code = await connection.cancel_on_disconnection(
pairing_config.delegate.get_string(16)
)
if pin_code is not None:
@@ -5836,10 +5841,8 @@ class Device(utils.CompositeEventEmitter):
pairing_config = self.pairing_config_factory(connection)
# Show the passkey to the user
utils.cancel_on_event(
connection,
Connection.EVENT_DISCONNECTION,
pairing_config.delegate.display_number(passkey, digits=6),
connection.cancel_on_disconnection(
pairing_config.delegate.display_number(passkey, digits=6)
)
# [Classic only]

View File

@@ -23,7 +23,7 @@ from __future__ import annotations
import logging
import pathlib
import platform
from typing import Dict, Iterable, Optional, Type, TYPE_CHECKING
from typing import Iterable, Optional, TYPE_CHECKING
from bumble.drivers import rtk, intel
from bumble.drivers.common import Driver
@@ -45,7 +45,7 @@ async def get_driver_for_host(host: Host) -> Optional[Driver]:
found.
If a "driver" HCI metadata entry is present, only that driver class will be probed.
"""
driver_classes: Dict[str, Type[Driver]] = {"rtk": rtk.Driver, "intel": intel.Driver}
driver_classes: dict[str, type[Driver]] = {"rtk": rtk.Driver, "intel": intel.Driver}
probe_list: Iterable[str]
if driver_name := host.hci_metadata.get("driver"):
# Only probe a single driver

View File

@@ -20,8 +20,6 @@ Common types for drivers.
# -----------------------------------------------------------------------------
import abc
from bumble import core
# -----------------------------------------------------------------------------
# Classes

View File

@@ -28,7 +28,7 @@ import os
import pathlib
import platform
import struct
from typing import Any, Deque, Optional, TYPE_CHECKING
from typing import Any, Optional, TYPE_CHECKING
from bumble import core
from bumble.drivers import common
@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
INTEL_USB_PRODUCTS = {
(0x8087, 0x0032), # AX210
(0x8087, 0x0033), # AX211
(0x8087, 0x0036), # BE200
}
@@ -93,23 +94,23 @@ hci.HCI_Command.register_commands(globals())
fields=[
("param0", 1),
],
return_parameters_fields=[
("status", hci.STATUS_SPEC),
("tlv", "*"),
],
)
class HCI_Intel_Read_Version_Command(hci.HCI_Command):
pass
return_parameters_fields = [
("status", hci.STATUS_SPEC),
("tlv", "*"),
]
@hci.HCI_Command.command(
fields=[("data_type", 1), ("data", "*")],
return_parameters_fields=[
("status", 1),
],
)
class Hci_Intel_Secure_Send_Command(hci.HCI_Command):
pass
return_parameters_fields = [
("status", 1),
]
@hci.HCI_Command.command(
@@ -120,23 +121,22 @@ class Hci_Intel_Secure_Send_Command(hci.HCI_Command):
("boot_option", 1),
("boot_address", 4),
],
return_parameters_fields=[
("data", "*"),
],
)
class HCI_Intel_Reset_Command(hci.HCI_Command):
pass
return_parameters_fields = [
("data", "*"),
]
@hci.HCI_Command.command(
fields=[("data", "*")],
return_parameters_fields=[
("status", hci.STATUS_SPEC),
("params", "*"),
],
)
class Hci_Intel_Write_Device_Config_Command(hci.HCI_Command):
pass
return_parameters_fields = [
("status", hci.STATUS_SPEC),
("params", "*"),
]
# -----------------------------------------------------------------------------
@@ -293,6 +293,7 @@ class HardwareVariant(utils.OpenIntEnum):
# This is a just a partial list.
# Add other constants here as new hardware is encountered and tested.
TYPHOON_PEAK = 0x17
GARFIELD_PEAK = 0x19
GALE_PEAK = 0x1C
@@ -346,7 +347,7 @@ class Driver(common.Driver):
def __init__(self, host: Host) -> None:
self.host = host
self.max_in_flight_firmware_load_commands = 1
self.pending_firmware_load_commands: Deque[hci.HCI_Command] = (
self.pending_firmware_load_commands: collections.deque[hci.HCI_Command] = (
collections.deque()
)
self.can_send_firmware_load_command = asyncio.Event()
@@ -471,6 +472,7 @@ class Driver(common.Driver):
raise DriverError("hardware platform not supported")
if hardware_info.variant not in (
HardwareVariant.TYPHOON_PEAK,
HardwareVariant.GARFIELD_PEAK,
HardwareVariant.GALE_PEAK,
):
raise DriverError("hardware variant not supported")

View File

@@ -29,7 +29,6 @@ import os
import pathlib
import platform
import struct
from typing import Tuple
import weakref
@@ -189,17 +188,16 @@ HCI_RTK_DROP_FIRMWARE_COMMAND = hci_vendor_command_op_code(0x66)
HCI_Command.register_commands(globals())
@HCI_Command.command(return_parameters_fields=[("status", STATUS_SPEC), ("version", 1)])
@HCI_Command.command()
class HCI_RTK_Read_ROM_Version_Command(HCI_Command):
pass
return_parameters_fields = [("status", STATUS_SPEC), ("version", 1)]
@HCI_Command.command(
fields=[("index", 1), ("payload", RTK_FRAGMENT_LENGTH)],
return_parameters_fields=[("status", STATUS_SPEC), ("index", 1)],
)
class HCI_RTK_Download_Command(HCI_Command):
pass
return_parameters_fields = [("status", STATUS_SPEC), ("index", 1)]
@HCI_Command.command()
@@ -294,7 +292,7 @@ class Driver(common.Driver):
@dataclass
class DriverInfo:
rom: int
hci: Tuple[int, int]
hci: tuple[int, int]
config_needed: bool
has_rom_version: bool
has_msft_ext: bool = False

View File

@@ -27,7 +27,7 @@ import enum
import functools
import logging
import struct
from typing import Iterable, List, Optional, Sequence, TypeVar, Union
from typing import Iterable, Optional, Sequence, TypeVar, Union
from bumble.colors import color
from bumble.core import BaseBumbleError, UUID
@@ -350,8 +350,8 @@ class Service(Attribute):
'''
uuid: UUID
characteristics: List[Characteristic]
included_services: List[Service]
characteristics: list[Characteristic]
included_services: list[Service]
def __init__(
self,
@@ -474,7 +474,7 @@ class Characteristic(Attribute[_T]):
# The check for `p.name is not None` here is needed because for InFlag
# enums, the .name property can be None, when the enum value is 0,
# so the type hint for .name is Optional[str].
enum_list: List[str] = [p.name for p in cls if p.name is not None]
enum_list: list[str] = [p.name for p in cls if p.name is not None]
enum_list_str = ",".join(enum_list)
raise TypeError(
f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by , or |: {enum_list_str}\nGot: {properties_str}"
@@ -579,11 +579,7 @@ class Descriptor(Attribute):
if isinstance(self.value, bytes):
value_str = self.value.hex()
elif isinstance(self.value, CharacteristicValue):
value = self.value.read(None)
if isinstance(value, bytes):
value_str = value.hex()
else:
value_str = '<async>'
value_str = '<dynamic>'
else:
value_str = '<...>'
return (

View File

@@ -28,7 +28,6 @@ from typing import (
Iterable,
Literal,
Optional,
Type,
TypeVar,
)
@@ -270,7 +269,7 @@ class SerializableCharacteristicAdapter(CharacteristicAdapter[_T2]):
`to_bytes` and `__bytes__` methods, respectively.
'''
def __init__(self, characteristic: Characteristic, cls: Type[_T2]) -> None:
def __init__(self, characteristic: Characteristic, cls: type[_T2]) -> None:
super().__init__(characteristic)
self.cls = cls
@@ -289,7 +288,7 @@ class SerializableCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T2]):
'''
def __init__(
self, characteristic_proxy: CharacteristicProxy, cls: Type[_T2]
self, characteristic_proxy: CharacteristicProxy, cls: type[_T2]
) -> None:
super().__init__(characteristic_proxy)
self.cls = cls
@@ -311,7 +310,7 @@ class EnumCharacteristicAdapter(CharacteristicAdapter[_T3]):
def __init__(
self,
characteristic: Characteristic,
cls: Type[_T3],
cls: type[_T3],
length: int,
byteorder: Literal['little', 'big'] = 'little',
):
@@ -347,7 +346,7 @@ class EnumCharacteristicProxyAdapter(CharacteristicProxyAdapter[_T3]):
def __init__(
self,
characteristic_proxy: CharacteristicProxy,
cls: Type[_T3],
cls: type[_T3],
length: int,
byteorder: Literal['little', 'big'] = 'little',
):

View File

@@ -31,15 +31,10 @@ from datetime import datetime
from typing import (
Any,
Callable,
Dict,
Generic,
Iterable,
List,
Optional,
Set,
Tuple,
Union,
Type,
TypeVar,
TYPE_CHECKING,
)
@@ -149,8 +144,8 @@ class AttributeProxy(utils.EventEmitter, Generic[_T]):
class ServiceProxy(AttributeProxy):
uuid: UUID
characteristics: List[CharacteristicProxy[bytes]]
included_services: List[ServiceProxy]
characteristics: list[CharacteristicProxy[bytes]]
included_services: list[ServiceProxy]
@staticmethod
def from_client(service_class, client: Client, service_uuid: UUID):
@@ -199,8 +194,8 @@ class ServiceProxy(AttributeProxy):
class CharacteristicProxy(AttributeProxy[_T]):
properties: Characteristic.Properties
descriptors: List[DescriptorProxy]
subscribers: Dict[Any, Callable[[_T], Any]]
descriptors: list[DescriptorProxy]
subscribers: dict[Any, Callable[[_T], Any]]
EVENT_UPDATE = "update"
@@ -277,7 +272,7 @@ class ProfileServiceProxy:
Base class for profile-specific service proxies
'''
SERVICE_CLASS: Type[TemplateService]
SERVICE_CLASS: type[TemplateService]
@classmethod
def from_client(cls, client: Client) -> Optional[ProfileServiceProxy]:
@@ -288,13 +283,13 @@ class ProfileServiceProxy:
# GATT Client
# -----------------------------------------------------------------------------
class Client:
services: List[ServiceProxy]
cached_values: Dict[int, Tuple[datetime, bytes]]
notification_subscribers: Dict[
int, Set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
services: list[ServiceProxy]
cached_values: dict[int, tuple[datetime, bytes]]
notification_subscribers: dict[
int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
]
indication_subscribers: Dict[
int, Set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
indication_subscribers: dict[
int, set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
]
pending_response: Optional[asyncio.futures.Future[ATT_PDU]]
pending_request: Optional[ATT_PDU]
@@ -379,12 +374,12 @@ class Client:
return self.connection.att_mtu
def get_services_by_uuid(self, uuid: UUID) -> List[ServiceProxy]:
def get_services_by_uuid(self, uuid: UUID) -> list[ServiceProxy]:
return [service for service in self.services if service.uuid == uuid]
def get_characteristics_by_uuid(
self, uuid: UUID, service: Optional[ServiceProxy] = None
) -> List[CharacteristicProxy[bytes]]:
) -> list[CharacteristicProxy[bytes]]:
services = [service] if service else self.services
return [
c
@@ -395,8 +390,8 @@ class Client:
def get_attribute_grouping(self, attribute_handle: int) -> Optional[
Union[
ServiceProxy,
Tuple[ServiceProxy, CharacteristicProxy],
Tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
tuple[ServiceProxy, CharacteristicProxy],
tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
]
]:
"""
@@ -429,7 +424,7 @@ class Client:
if not already_known:
self.services.append(service)
async def discover_services(self, uuids: Iterable[UUID] = ()) -> List[ServiceProxy]:
async def discover_services(self, uuids: Iterable[UUID] = ()) -> list[ServiceProxy]:
'''
See Vol 3, Part G - 4.4.1 Discover All Primary Services
'''
@@ -501,7 +496,7 @@ class Client:
return services
async def discover_service(self, uuid: Union[str, UUID]) -> List[ServiceProxy]:
async def discover_service(self, uuid: Union[str, UUID]) -> list[ServiceProxy]:
'''
See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID
'''
@@ -572,7 +567,7 @@ class Client:
async def discover_included_services(
self, service: ServiceProxy
) -> List[ServiceProxy]:
) -> list[ServiceProxy]:
'''
See Vol 3, Part G - 4.5.1 Find Included Services
'''
@@ -580,7 +575,7 @@ class Client:
starting_handle = service.handle
ending_handle = service.end_group_handle
included_services: List[ServiceProxy] = []
included_services: list[ServiceProxy] = []
while starting_handle <= ending_handle:
response = await self.send_request(
ATT_Read_By_Type_Request(
@@ -636,7 +631,7 @@ class Client:
async def discover_characteristics(
self, uuids, service: Optional[ServiceProxy]
) -> List[CharacteristicProxy[bytes]]:
) -> list[CharacteristicProxy[bytes]]:
'''
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
Discover Characteristics by UUID
@@ -649,12 +644,12 @@ class Client:
services = [service] if service else self.services
# Perform characteristic discovery for each service
discovered_characteristics: List[CharacteristicProxy[bytes]] = []
discovered_characteristics: list[CharacteristicProxy[bytes]] = []
for service in services:
starting_handle = service.handle
ending_handle = service.end_group_handle
characteristics: List[CharacteristicProxy[bytes]] = []
characteristics: list[CharacteristicProxy[bytes]] = []
while starting_handle <= ending_handle:
response = await self.send_request(
ATT_Read_By_Type_Request(
@@ -725,7 +720,7 @@ class Client:
characteristic: Optional[CharacteristicProxy] = None,
start_handle: Optional[int] = None,
end_handle: Optional[int] = None,
) -> List[DescriptorProxy]:
) -> list[DescriptorProxy]:
'''
See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
'''
@@ -738,7 +733,7 @@ class Client:
else:
return []
descriptors: List[DescriptorProxy] = []
descriptors: list[DescriptorProxy] = []
while starting_handle <= ending_handle:
response = await self.send_request(
ATT_Find_Information_Request(
@@ -787,7 +782,7 @@ class Client:
return descriptors
async def discover_attributes(self) -> List[AttributeProxy[bytes]]:
async def discover_attributes(self) -> list[AttributeProxy[bytes]]:
'''
Discover all attributes, regardless of type
'''
@@ -1002,7 +997,7 @@ class Client:
async def read_characteristics_by_uuid(
self, uuid: UUID, service: Optional[ServiceProxy]
) -> List[bytes]:
) -> list[bytes]:
'''
See Vol 3, Part G - 4.8.2 Read Using Characteristic UUID
'''

View File

@@ -29,13 +29,9 @@ import logging
from collections import defaultdict
import struct
from typing import (
Dict,
Iterable,
List,
Optional,
Tuple,
TypeVar,
Type,
TYPE_CHECKING,
)
@@ -103,10 +99,10 @@ GATT_SERVER_DEFAULT_MAX_MTU = 517
# GATT Server
# -----------------------------------------------------------------------------
class Server(utils.EventEmitter):
attributes: List[Attribute]
services: List[Service]
attributes_by_handle: Dict[int, Attribute]
subscribers: Dict[int, Dict[int, bytes]]
attributes: list[Attribute]
services: list[Service]
attributes_by_handle: dict[int, Attribute]
subscribers: dict[int, dict[int, bytes]]
indication_semaphores: defaultdict[int, asyncio.Semaphore]
pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
@@ -136,7 +132,7 @@ class Server(utils.EventEmitter):
def next_handle(self) -> int:
return 1 + len(self.attributes)
def get_advertising_service_data(self) -> Dict[Attribute, bytes]:
def get_advertising_service_data(self) -> dict[Attribute, bytes]:
return {
attribute: data
for attribute in self.attributes
@@ -160,7 +156,7 @@ class Server(utils.EventEmitter):
AttributeGroupType = TypeVar('AttributeGroupType', Service, Characteristic)
def get_attribute_group(
self, handle: int, group_type: Type[AttributeGroupType]
self, handle: int, group_type: type[AttributeGroupType]
) -> Optional[AttributeGroupType]:
return next(
(
@@ -186,7 +182,7 @@ class Server(utils.EventEmitter):
def get_characteristic_attributes(
self, service_uuid: UUID, characteristic_uuid: UUID
) -> Optional[Tuple[CharacteristicDeclaration, Characteristic]]:
) -> Optional[tuple[CharacteristicDeclaration, Characteristic]]:
service_handle = self.get_service_attribute(service_uuid)
if not service_handle:
return None
@@ -315,11 +311,8 @@ class Server(utils.EventEmitter):
self.add_service(service)
def read_cccd(
self, connection: Optional[Connection], characteristic: Characteristic
self, connection: Connection, characteristic: Characteristic
) -> bytes:
if connection is None:
return bytes([0, 0])
subscribers = self.subscribers.get(connection.handle)
cccd = None
if subscribers:

File diff suppressed because it is too large Load Diff

View File

@@ -26,14 +26,9 @@ import enum
import traceback
import re
from typing import (
Dict,
List,
Union,
Set,
Any,
Optional,
Type,
Tuple,
ClassVar,
Iterable,
TYPE_CHECKING,
@@ -375,7 +370,7 @@ class CallLineIdentification:
cli_validity: Optional[int] = None
@classmethod
def parse_from(cls: Type[Self], parameters: List[bytes]) -> Self:
def parse_from(cls, parameters: list[bytes]) -> Self:
return cls(
number=parameters[0].decode(),
type=int(parameters[1]),
@@ -505,9 +500,9 @@ STATUS_CODES = {
@dataclasses.dataclass
class HfConfiguration:
supported_hf_features: List[HfFeature]
supported_hf_indicators: List[HfIndicator]
supported_audio_codecs: List[AudioCodec]
supported_hf_features: list[HfFeature]
supported_hf_indicators: list[HfIndicator]
supported_audio_codecs: list[AudioCodec]
@dataclasses.dataclass
@@ -535,7 +530,7 @@ class AtResponse:
parameters: list
@classmethod
def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
def parse_from(cls: type[Self], buffer: bytearray) -> Self:
code_and_parameters = buffer.split(b':')
parameters = (
code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
@@ -563,7 +558,7 @@ class AtCommand:
)
@classmethod
def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
def parse_from(cls: type[Self], buffer: bytearray) -> Self:
if not (match := cls._PARSE_PATTERN.fullmatch(buffer.decode())):
if buffer.startswith(b'ATA'):
return cls(code='A', sub_code=AtCommand.SubCode.NONE, parameters=[])
@@ -598,7 +593,7 @@ class AgIndicatorState:
"""
indicator: AgIndicator
supported_values: Set[int]
supported_values: set[int]
current_status: int
index: Optional[int] = None
enabled: bool = True
@@ -616,14 +611,14 @@ class AgIndicatorState:
return f'(\"{self.indicator.value}\",{supported_values_text})'
@classmethod
def call(cls: Type[Self]) -> Self:
def call(cls: type[Self]) -> Self:
"""Default call indicator state."""
return cls(
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
)
@classmethod
def callsetup(cls: Type[Self]) -> Self:
def callsetup(cls: type[Self]) -> Self:
"""Default callsetup indicator state."""
return cls(
indicator=AgIndicator.CALL_SETUP,
@@ -632,7 +627,7 @@ class AgIndicatorState:
)
@classmethod
def callheld(cls: Type[Self]) -> Self:
def callheld(cls: type[Self]) -> Self:
"""Default call indicator state."""
return cls(
indicator=AgIndicator.CALL_HELD,
@@ -641,14 +636,14 @@ class AgIndicatorState:
)
@classmethod
def service(cls: Type[Self]) -> Self:
def service(cls: type[Self]) -> Self:
"""Default service indicator state."""
return cls(
indicator=AgIndicator.SERVICE, supported_values={0, 1}, current_status=0
)
@classmethod
def signal(cls: Type[Self]) -> Self:
def signal(cls: type[Self]) -> Self:
"""Default signal indicator state."""
return cls(
indicator=AgIndicator.SIGNAL,
@@ -657,14 +652,14 @@ class AgIndicatorState:
)
@classmethod
def roam(cls: Type[Self]) -> Self:
def roam(cls: type[Self]) -> Self:
"""Default roam indicator state."""
return cls(
indicator=AgIndicator.CALL, supported_values={0, 1}, current_status=0
)
@classmethod
def battchg(cls: Type[Self]) -> Self:
def battchg(cls: type[Self]) -> Self:
"""Default battery charge indicator state."""
return cls(
indicator=AgIndicator.BATTERY_CHARGE,
@@ -732,13 +727,13 @@ class HfProtocol(utils.EventEmitter):
"""Termination signal for run() loop."""
supported_hf_features: int
supported_audio_codecs: List[AudioCodec]
supported_audio_codecs: list[AudioCodec]
supported_ag_features: int
supported_ag_call_hold_operations: List[CallHoldOperation]
supported_ag_call_hold_operations: list[CallHoldOperation]
ag_indicators: List[AgIndicatorState]
hf_indicators: Dict[HfIndicator, HfIndicatorState]
ag_indicators: list[AgIndicatorState]
hf_indicators: dict[HfIndicator, HfIndicatorState]
dlc: rfcomm.DLC
command_lock: asyncio.Lock
@@ -836,7 +831,7 @@ class HfProtocol(utils.EventEmitter):
cmd: str,
timeout: float = 1.0,
response_type: AtResponseType = AtResponseType.NONE,
) -> Union[None, AtResponse, List[AtResponse]]:
) -> Union[None, AtResponse, list[AtResponse]]:
"""
Sends an AT command and wait for the peer response.
Wait for the AT responses sent by the peer, to the status code.
@@ -853,7 +848,7 @@ class HfProtocol(utils.EventEmitter):
async with self.command_lock:
logger.debug(f">>> {cmd}")
self.dlc.write(cmd + '\r')
responses: List[AtResponse] = []
responses: list[AtResponse] = []
while True:
result = await asyncio.wait_for(
@@ -1073,7 +1068,7 @@ class HfProtocol(utils.EventEmitter):
# code, with the value indicating (call=0).
await self.execute_command("AT+CHUP")
async def query_current_calls(self) -> List[CallInfo]:
async def query_current_calls(self) -> list[CallInfo]:
"""4.32.1 Query List of Current Calls in AG.
Return:
@@ -1204,27 +1199,27 @@ class AgProtocol(utils.EventEmitter):
EVENT_MICROPHONE_VOLUME = "microphone_volume"
supported_hf_features: int
supported_hf_indicators: Set[HfIndicator]
supported_audio_codecs: List[AudioCodec]
supported_hf_indicators: set[HfIndicator]
supported_audio_codecs: list[AudioCodec]
supported_ag_features: int
supported_ag_call_hold_operations: List[CallHoldOperation]
supported_ag_call_hold_operations: list[CallHoldOperation]
ag_indicators: List[AgIndicatorState]
ag_indicators: list[AgIndicatorState]
hf_indicators: collections.OrderedDict[HfIndicator, HfIndicatorState]
dlc: rfcomm.DLC
read_buffer: bytearray
active_codec: AudioCodec
calls: List[CallInfo]
calls: list[CallInfo]
indicator_report_enabled: bool
inband_ringtone_enabled: bool
cme_error_enabled: bool
cli_notification_enabled: bool
call_waiting_enabled: bool
_remained_slc_setup_features: Set[HfFeature]
_remained_slc_setup_features: set[HfFeature]
def __init__(self, dlc: rfcomm.DLC, configuration: AgConfiguration) -> None:
super().__init__()
@@ -1694,7 +1689,7 @@ def make_hf_sdp_records(
rfcomm_channel: int,
configuration: HfConfiguration,
version: ProfileVersion = ProfileVersion.V1_8,
) -> List[sdp.ServiceAttribute]:
) -> list[sdp.ServiceAttribute]:
"""
Generates the SDP record for HFP Hands-Free support.
@@ -1780,7 +1775,7 @@ def make_ag_sdp_records(
rfcomm_channel: int,
configuration: AgConfiguration,
version: ProfileVersion = ProfileVersion.V1_8,
) -> List[sdp.ServiceAttribute]:
) -> list[sdp.ServiceAttribute]:
"""
Generates the SDP record for HFP Audio-Gateway support.
@@ -1860,7 +1855,7 @@ def make_ag_sdp_records(
async def find_hf_sdp_record(
connection: device.Connection,
) -> Optional[Tuple[int, ProfileVersion, HfSdpFeature]]:
) -> Optional[tuple[int, ProfileVersion, HfSdpFeature]]:
"""Searches a Hands-Free SDP record from remote device.
Args:
@@ -1912,7 +1907,7 @@ async def find_hf_sdp_record(
async def find_ag_sdp_record(
connection: device.Connection,
) -> Optional[Tuple[int, ProfileVersion, AgSdpFeature]]:
) -> Optional[tuple[int, ProfileVersion, AgSdpFeature]]:
"""Searches an Audio-Gateway SDP record from remote device.
Args:
@@ -2010,7 +2005,7 @@ class EscoParameters:
transmit_codec_frame_size: int = 60
receive_codec_frame_size: int = 60
def asdict(self) -> Dict[str, Any]:
def asdict(self) -> dict[str, Any]:
# dataclasses.asdict() will recursively deep-copy the entire object,
# which is expensive and breaks CodingFormat object, so let it simply copy here.
return self.__dict__

View File

@@ -26,10 +26,7 @@ from typing import (
Any,
Awaitable,
Callable,
Deque,
Dict,
Optional,
Set,
cast,
TYPE_CHECKING,
)
@@ -89,7 +86,9 @@ class DataPacketQueue(utils.EventEmitter):
int
) # Number of packets in flight per connection
self._send = send
self._packets: Deque[tuple[hci.HCI_Packet, int]] = collections.deque()
self._packets: collections.deque[tuple[hci.HCI_Packet, int]] = (
collections.deque()
)
self._queued = 0
self._completed = 0
@@ -234,16 +233,16 @@ class IsoLink:
# -----------------------------------------------------------------------------
class Host(utils.EventEmitter):
connections: Dict[int, Connection]
cis_links: Dict[int, IsoLink]
bis_links: Dict[int, IsoLink]
sco_links: Dict[int, ScoLink]
connections: dict[int, Connection]
cis_links: dict[int, IsoLink]
bis_links: dict[int, IsoLink]
sco_links: dict[int, ScoLink]
bigs: dict[int, set[int]]
acl_packet_queue: Optional[DataPacketQueue] = None
le_acl_packet_queue: Optional[DataPacketQueue] = None
iso_packet_queue: Optional[DataPacketQueue] = None
hci_sink: Optional[TransportSink] = None
hci_metadata: Dict[str, Any]
hci_metadata: dict[str, Any]
long_term_key_provider: Optional[
Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
]
@@ -813,7 +812,7 @@ class Host(utils.EventEmitter):
) != 0
@property
def supported_commands(self) -> Set[int]:
def supported_commands(self) -> set[int]:
return set(
op_code
for op_code, mask in hci.HCI_SUPPORTED_COMMANDS_MASKS.items()
@@ -1127,11 +1126,19 @@ class Host(utils.EventEmitter):
else:
self.emit('connection_phy_update_failure', connection.handle, event.status)
def on_hci_le_advertising_report_event(self, event):
def on_hci_le_advertising_report_event(
self,
event: (
hci.HCI_LE_Advertising_Report_Event
| hci.HCI_LE_Extended_Advertising_Report_Event
),
):
for report in event.reports:
self.emit('advertising_report', report)
def on_hci_le_extended_advertising_report_event(self, event):
def on_hci_le_extended_advertising_report_event(
self, event: hci.HCI_LE_Extended_Advertising_Report_Event
):
self.on_hci_le_advertising_report_event(event)
def on_hci_le_advertising_set_terminated_event(self, event):
@@ -1520,13 +1527,15 @@ class Host(utils.EventEmitter):
self.emit('inquiry_complete')
def on_hci_inquiry_result_with_rssi_event(self, event):
for response in event.responses:
for bd_addr, class_of_device, rssi in zip(
event.bd_addr, event.class_of_device, event.rssi
):
self.emit(
'inquiry_result',
response.bd_addr,
response.class_of_device,
bd_addr,
class_of_device,
b'',
response.rssi,
rssi,
)
def on_hci_extended_inquiry_result_event(self, event):

View File

@@ -22,14 +22,15 @@
# -----------------------------------------------------------------------------
from __future__ import annotations
import asyncio
import dataclasses
import logging
import os
import json
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
from typing import TYPE_CHECKING, Optional, Any
from typing_extensions import Self
from bumble.colors import color
from bumble.hci import Address
from bumble import hci
if TYPE_CHECKING:
from bumble.device import Device
@@ -42,16 +43,17 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
@dataclasses.dataclass
class PairingKeys:
@dataclasses.dataclass
class Key:
def __init__(self, value, authenticated=False, ediv=None, rand=None):
self.value = value
self.authenticated = authenticated
self.ediv = ediv
self.rand = rand
value: bytes
authenticated: bool = False
ediv: Optional[int] = None
rand: Optional[bytes] = None
@classmethod
def from_dict(cls, key_dict):
def from_dict(cls, key_dict: dict[str, Any]) -> PairingKeys.Key:
value = bytes.fromhex(key_dict['value'])
authenticated = key_dict.get('authenticated', False)
ediv = key_dict.get('ediv')
@@ -61,7 +63,7 @@ class PairingKeys:
return cls(value, authenticated, ediv, rand)
def to_dict(self):
def to_dict(self) -> dict[str, Any]:
key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated}
if self.ediv is not None:
key_dict['ediv'] = self.ediv
@@ -70,39 +72,42 @@ class PairingKeys:
return key_dict
def __init__(self):
self.address_type = None
self.ltk = None
self.ltk_central = None
self.ltk_peripheral = None
self.irk = None
self.csrk = None
self.link_key = None # Classic
address_type: Optional[hci.AddressType] = None
ltk: Optional[Key] = None
ltk_central: Optional[Key] = None
ltk_peripheral: Optional[Key] = None
irk: Optional[Key] = None
csrk: Optional[Key] = None
link_key: Optional[Key] = None # Classic
link_key_type: Optional[int] = None # Classic
@staticmethod
def key_from_dict(keys_dict, key_name):
@classmethod
def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Optional[Key]:
key_dict = keys_dict.get(key_name)
if key_dict is None:
return None
return PairingKeys.Key.from_dict(key_dict)
@staticmethod
def from_dict(keys_dict):
keys = PairingKeys()
@classmethod
def from_dict(cls, keys_dict: dict[str, Any]) -> PairingKeys:
return PairingKeys(
address_type=(
hci.AddressType(t)
if (t := keys_dict.get('address_type')) is not None
else None
),
ltk=PairingKeys.key_from_dict(keys_dict, 'ltk'),
ltk_central=PairingKeys.key_from_dict(keys_dict, 'ltk_central'),
ltk_peripheral=PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral'),
irk=PairingKeys.key_from_dict(keys_dict, 'irk'),
csrk=PairingKeys.key_from_dict(keys_dict, 'csrk'),
link_key=PairingKeys.key_from_dict(keys_dict, 'link_key'),
link_key_type=keys_dict.get('link_key_type'),
)
keys.address_type = keys_dict.get('address_type')
keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk')
keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central')
keys.ltk_peripheral = PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral')
keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk')
keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk')
keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key')
return keys
def to_dict(self):
keys = {}
def to_dict(self) -> dict[str, Any]:
keys: dict[str, Any] = {}
if self.address_type is not None:
keys['address_type'] = self.address_type
@@ -125,9 +130,12 @@ class PairingKeys:
if self.link_key is not None:
keys['link_key'] = self.link_key.to_dict()
if self.link_key_type is not None:
keys['link_key_type'] = self.link_key_type
return keys
def print(self, prefix=''):
def print(self, prefix: str = '') -> None:
keys_dict = self.to_dict()
for container_property, value in keys_dict.items():
if isinstance(value, dict):
@@ -149,27 +157,35 @@ class KeyStore:
async def get(self, _name: str) -> Optional[PairingKeys]:
return None
async def get_all(self) -> List[Tuple[str, PairingKeys]]:
async def get_all(self) -> list[tuple[str, PairingKeys]]:
return []
async def delete_all(self) -> None:
all_keys = await self.get_all()
await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
async def get_resolving_keys(self):
async def get_resolving_keys(self) -> list[tuple[bytes, hci.Address]]:
all_keys = await self.get_all()
resolving_keys = []
for name, keys in all_keys:
if keys.irk is not None:
if keys.address_type is None:
address_type = Address.RANDOM_DEVICE_ADDRESS
else:
address_type = keys.address_type
resolving_keys.append((keys.irk.value, Address(name, address_type)))
resolving_keys.append(
(
keys.irk.value,
hci.Address(
name,
(
keys.address_type
if keys.address_type is not None
else hci.Address.RANDOM_DEVICE_ADDRESS
),
),
)
)
return resolving_keys
async def print(self, prefix=''):
async def print(self, prefix: str = '') -> None:
entries = await self.get_all()
separator = ''
for name, keys in entries:
@@ -177,8 +193,8 @@ class KeyStore:
keys.print(prefix=prefix + ' ')
separator = '\n'
@staticmethod
def create_for_device(device: Device) -> KeyStore:
@classmethod
def create_for_device(cls, device: Device) -> KeyStore:
if device.config.keystore is None:
return MemoryKeyStore()
@@ -256,7 +272,7 @@ class JsonKeyStore(KeyStore):
@classmethod
def from_device(
cls: Type[Self], device: Device, filename: Optional[str] = None
cls: type[Self], device: Device, filename: Optional[str] = None
) -> Self:
if not filename:
# Extract the filename from the config if there is one
@@ -266,9 +282,9 @@ class JsonKeyStore(KeyStore):
filename = params[0]
# Use a namespace based on the device address
if device.public_address not in (Address.ANY, Address.ANY_RANDOM):
if device.public_address not in (hci.Address.ANY, hci.Address.ANY_RANDOM):
namespace = str(device.public_address)
elif device.random_address != Address.ANY_RANDOM:
elif device.random_address != hci.Address.ANY_RANDOM:
namespace = str(device.random_address)
else:
namespace = JsonKeyStore.DEFAULT_NAMESPACE
@@ -340,7 +356,7 @@ class JsonKeyStore(KeyStore):
# -----------------------------------------------------------------------------
class MemoryKeyStore(KeyStore):
all_keys: Dict[str, PairingKeys]
all_keys: dict[str, PairingKeys]
def __init__(self) -> None:
self.all_keys = {}
@@ -355,5 +371,5 @@ class MemoryKeyStore(KeyStore):
async def get(self, name: str) -> Optional[PairingKeys]:
return self.all_keys.get(name)
async def get_all(self) -> List[Tuple[str, PairingKeys]]:
async def get_all(self) -> list[tuple[str, PairingKeys]]:
return list(self.all_keys.items())

View File

@@ -24,15 +24,10 @@ import struct
from collections import deque
from typing import (
Dict,
Type,
List,
Optional,
Tuple,
Callable,
Any,
Union,
Deque,
Iterable,
SupportsBytes,
TYPE_CHECKING,
@@ -242,7 +237,7 @@ class L2CAP_Control_Frame:
See Bluetooth spec @ Vol 3, Part A - 4 SIGNALING PACKET FORMATS
'''
classes: Dict[int, Type[L2CAP_Control_Frame]] = {}
classes: dict[int, type[L2CAP_Control_Frame]] = {}
code = 0
name: str
@@ -276,7 +271,7 @@ class L2CAP_Control_Frame:
return name_or_number(L2CAP_CONTROL_FRAME_NAMES, code)
@staticmethod
def decode_configuration_options(data: bytes) -> List[Tuple[int, bytes]]:
def decode_configuration_options(data: bytes) -> list[tuple[int, bytes]]:
options = []
while len(data) >= 2:
value_type = data[0]
@@ -288,7 +283,7 @@ class L2CAP_Control_Frame:
return options
@staticmethod
def encode_configuration_options(options: List[Tuple[int, bytes]]) -> bytes:
def encode_configuration_options(options: list[tuple[int, bytes]]) -> bytes:
return b''.join(
[bytes([option[0], len(option[1])]) + option[1] for option in options]
)
@@ -400,7 +395,7 @@ class L2CAP_Connection_Request(L2CAP_Control_Frame):
source_cid: int
@staticmethod
def parse_psm(data: bytes, offset: int = 0) -> Tuple[int, int]:
def parse_psm(data: bytes, offset: int = 0) -> tuple[int, int]:
psm_length = 2
psm = data[offset] | data[offset + 1] << 8
@@ -823,9 +818,7 @@ class ClassicChannel(utils.EventEmitter):
# Wait for the connection to succeed or fail
try:
return await utils.cancel_on_event(
self.connection, 'disconnection', self.connection_result
)
return await self.connection.cancel_on_disconnection(self.connection_result)
finally:
self.connection_result = None
@@ -1041,7 +1034,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
DISCONNECTED = 4
CONNECTION_ERROR = 5
out_queue: Deque[bytes]
out_queue: deque[bytes]
connection_result: Optional[asyncio.Future[LeCreditBasedChannel]]
disconnection_result: Optional[asyncio.Future[None]]
in_sdu: Optional[bytes]
@@ -1445,13 +1438,13 @@ class LeCreditBasedChannelServer(utils.EventEmitter):
# -----------------------------------------------------------------------------
class ChannelManager:
identifiers: Dict[int, int]
channels: Dict[int, Dict[int, Union[ClassicChannel, LeCreditBasedChannel]]]
servers: Dict[int, ClassicChannelServer]
le_coc_channels: Dict[int, Dict[int, LeCreditBasedChannel]]
le_coc_servers: Dict[int, LeCreditBasedChannelServer]
le_coc_requests: Dict[int, L2CAP_LE_Credit_Based_Connection_Request]
fixed_channels: Dict[int, Optional[Callable[[int, bytes], Any]]]
identifiers: dict[int, int]
channels: dict[int, dict[int, Union[ClassicChannel, LeCreditBasedChannel]]]
servers: dict[int, ClassicChannelServer]
le_coc_channels: dict[int, dict[int, LeCreditBasedChannel]]
le_coc_servers: dict[int, LeCreditBasedChannelServer]
le_coc_requests: dict[int, L2CAP_LE_Credit_Based_Connection_Request]
fixed_channels: dict[int, Optional[Callable[[int, bytes], Any]]]
_host: Optional[Host]
connection_parameters_update_response: Optional[asyncio.Future[int]]

View File

@@ -36,7 +36,7 @@ from bumble.hci import (
)
from bumble import controller
from typing import Optional, Set
from typing import Optional
# -----------------------------------------------------------------------------
# Logging
@@ -65,7 +65,7 @@ class LocalLink:
Link bus for controllers to communicate with each other
'''
controllers: Set[controller.Controller]
controllers: set[controller.Controller]
def __init__(self):
self.controllers = set()

View File

@@ -18,7 +18,8 @@
from __future__ import annotations
import enum
from dataclasses import dataclass
from typing import Optional, Tuple
import secrets
from typing import Optional
from bumble.hci import (
Address,
@@ -205,7 +206,7 @@ class PairingDelegate:
# [LE only]
async def key_distribution_response(
self, peer_initiator_key_distribution: int, peer_responder_key_distribution: int
) -> Tuple[int, int]:
) -> tuple[int, int]:
"""
Return the key distribution response in an SMP protocol context.
@@ -222,6 +223,14 @@ class PairingDelegate:
),
)
async def generate_passkey(self) -> int:
"""
Return a passkey value between 0 and 999999 (inclusive).
"""
# By default, generate a random passkey.
return secrets.randbelow(1000000)
# -----------------------------------------------------------------------------
class PairingConfig:

View File

@@ -45,7 +45,7 @@ __all__ = [
# Add servicers hooks.
_SERVICERS_HOOKS: List[Callable[[PandoraDevice, Config, grpc.aio.Server], None]] = []
_SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]] = []
def register_servicer_hook(

View File

@@ -15,7 +15,7 @@
from __future__ import annotations
from bumble.pairing import PairingConfig, PairingDelegate
from dataclasses import dataclass
from typing import Any, Dict
from typing import Any
@dataclass
@@ -32,7 +32,7 @@ class Config:
PairingDelegate.DEFAULT_KEY_DISTRIBUTION
)
def load_from_dict(self, config: Dict[str, Any]) -> None:
def load_from_dict(self, config: dict[str, Any]) -> None:
io_capability_name: str = config.get(
'io_capability', 'no_output_no_input'
).upper()

View File

@@ -32,7 +32,7 @@ from bumble.sdp import (
DataElement,
ServiceAttribute,
)
from typing import Any, Dict, List, Optional
from typing import Any, Optional
# Default rootcanal HCI TCP address
@@ -49,13 +49,13 @@ class PandoraDevice:
# Bumble device instance & configuration.
device: Device
config: Dict[str, Any]
config: dict[str, Any]
# HCI transport name & instance.
_hci_name: str
_hci: Optional[transport.Transport] # type: ignore[name-defined]
def __init__(self, config: Dict[str, Any]) -> None:
def __init__(self, config: dict[str, Any]) -> None:
self.config = config
self.device = _make_device(config)
self._hci_name = config.get(
@@ -95,14 +95,14 @@ class PandoraDevice:
await self.close()
await self.open()
def info(self) -> Optional[Dict[str, str]]:
def info(self) -> Optional[dict[str, str]]:
return {
'public_bd_address': str(self.device.public_address),
'random_address': str(self.device.random_address),
}
def _make_device(config: Dict[str, Any]) -> Device:
def _make_device(config: dict[str, Any]) -> Device:
"""Initialize an idle Bumble device instance."""
# initialize bumble device.
@@ -117,7 +117,7 @@ def _make_device(config: Dict[str, Any]) -> Device:
# TODO(b/267540823): remove when Pandora A2dp is supported
def _make_sdp_records(rfcomm_channel: int) -> Dict[int, List[ServiceAttribute]]:
def _make_sdp_records(rfcomm_channel: int) -> dict[int, list[ServiceAttribute]]:
return {
0x00010001: [
ServiceAttribute(

View File

@@ -73,7 +73,6 @@ from pandora.host_pb2 import (
ConnectResponse,
DataTypes,
DisconnectRequest,
DiscoverabilityMode,
InquiryResponse,
PrimaryPhy,
ReadLocalAddressResponse,
@@ -86,9 +85,9 @@ from pandora.host_pb2 import (
WaitConnectionResponse,
WaitDisconnectionRequest,
)
from typing import AsyncGenerator, Dict, List, Optional, Set, Tuple, cast
from typing import AsyncGenerator, Optional, cast
PRIMARY_PHY_MAP: Dict[int, PrimaryPhy] = {
PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
# Default value reported by Bumble for legacy Advertising reports.
# FIXME(uael): `None` might be a better value, but Bumble need to change accordingly.
0: PRIMARY_1M,
@@ -96,26 +95,26 @@ PRIMARY_PHY_MAP: Dict[int, PrimaryPhy] = {
3: PRIMARY_CODED,
}
SECONDARY_PHY_MAP: Dict[int, SecondaryPhy] = {
SECONDARY_PHY_MAP: dict[int, SecondaryPhy] = {
0: SECONDARY_NONE,
1: SECONDARY_1M,
2: SECONDARY_2M,
3: SECONDARY_CODED,
}
PRIMARY_PHY_TO_BUMBLE_PHY_MAP: Dict[PrimaryPhy, Phy] = {
PRIMARY_PHY_TO_BUMBLE_PHY_MAP: dict[PrimaryPhy, Phy] = {
PRIMARY_1M: Phy.LE_1M,
PRIMARY_CODED: Phy.LE_CODED,
}
SECONDARY_PHY_TO_BUMBLE_PHY_MAP: Dict[SecondaryPhy, Phy] = {
SECONDARY_PHY_TO_BUMBLE_PHY_MAP: dict[SecondaryPhy, Phy] = {
SECONDARY_NONE: Phy.LE_1M,
SECONDARY_1M: Phy.LE_1M,
SECONDARY_2M: Phy.LE_2M,
SECONDARY_CODED: Phy.LE_CODED,
}
OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
OWN_ADDRESS_MAP: dict[host_pb2.OwnAddressType, OwnAddressType] = {
host_pb2.PUBLIC: OwnAddressType.PUBLIC,
host_pb2.RANDOM: OwnAddressType.RANDOM,
host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC,
@@ -124,7 +123,7 @@ OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
class HostService(HostServicer):
waited_connections: Set[int]
waited_connections: set[int]
def __init__(
self, grpc_server: grpc.aio.Server, device: Device, config: Config
@@ -618,7 +617,7 @@ class HostService(HostServicer):
self.log.debug('Inquiry')
inquiry_queue: asyncio.Queue[
Optional[Tuple[Address, int, AdvertisingData, int]]
Optional[tuple[Address, int, AdvertisingData, int]]
] = asyncio.Queue()
complete_handler = self.device.on(
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
@@ -670,10 +669,10 @@ class HostService(HostServicer):
return empty_pb2.Empty()
def unpack_data_types(self, dt: DataTypes) -> AdvertisingData:
ad_structures: List[Tuple[int, bytes]] = []
ad_structures: list[tuple[int, bytes]] = []
uuids: List[str]
datas: Dict[str, bytes]
uuids: list[str]
datas: dict[str, bytes]
def uuid128_from_str(uuid: str) -> bytes:
"""Decode a 128-bit uuid encoded as XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
@@ -887,50 +886,50 @@ class HostService(HostServicer):
def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
dt = DataTypes()
uuids: List[UUID]
uuids: list[UUID]
s: str
i: int
ij: Tuple[int, int]
uuid_data: Tuple[UUID, bytes]
ij: tuple[int, int]
uuid_data: tuple[UUID, bytes]
data: bytes
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
):
dt.incomplete_service_class_uuids16.extend(
list(map(lambda x: x.to_hex_str('-'), uuids))
)
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
):
dt.complete_service_class_uuids16.extend(
list(map(lambda x: x.to_hex_str('-'), uuids))
)
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
):
dt.incomplete_service_class_uuids32.extend(
list(map(lambda x: x.to_hex_str('-'), uuids))
)
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
):
dt.complete_service_class_uuids32.extend(
list(map(lambda x: x.to_hex_str('-'), uuids))
)
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
):
dt.incomplete_service_class_uuids128.extend(
list(map(lambda x: x.to_hex_str('-'), uuids))
)
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
):
dt.complete_service_class_uuids128.extend(
@@ -945,42 +944,42 @@ class HostService(HostServicer):
if i := cast(int, ad.get(AdvertisingData.CLASS_OF_DEVICE)):
dt.class_of_device = i
if ij := cast(
Tuple[int, int],
tuple[int, int],
ad.get(AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE),
):
dt.peripheral_connection_interval_min = ij[0]
dt.peripheral_connection_interval_max = ij[1]
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS),
):
dt.service_solicitation_uuids16.extend(
list(map(lambda x: x.to_hex_str('-'), uuids))
)
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS),
):
dt.service_solicitation_uuids32.extend(
list(map(lambda x: x.to_hex_str('-'), uuids))
)
if uuids := cast(
List[UUID],
list[UUID],
ad.get(AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS),
):
dt.service_solicitation_uuids128.extend(
list(map(lambda x: x.to_hex_str('-'), uuids))
)
if uuid_data := cast(
Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
):
dt.service_data_uuid16[uuid_data[0].to_hex_str('-')] = uuid_data[1]
if uuid_data := cast(
Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
):
dt.service_data_uuid32[uuid_data[0].to_hex_str('-')] = uuid_data[1]
if uuid_data := cast(
Tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
):
dt.service_data_uuid128[uuid_data[0].to_hex_str('-')] = uuid_data[1]
if data := cast(bytes, ad.get(AdvertisingData.PUBLIC_TARGET_ADDRESS, raw=True)):

View File

@@ -51,7 +51,7 @@ from pandora.l2cap_pb2 import ( # pytype: disable=pyi-error
WaitDisconnectionRequest,
WaitDisconnectionResponse,
)
from typing import AsyncGenerator, Dict, Optional, Union
from typing import AsyncGenerator, Optional, Union
from dataclasses import dataclass
L2capChannel = Union[ClassicChannel, LeCreditBasedChannel]
@@ -70,7 +70,7 @@ class L2CAPService(L2CAPServicer):
)
self.device = device
self.config = config
self.channels: Dict[bytes, ChannelContext] = {}
self.channels: dict[bytes, ChannelContext] = {}
def register_event(self, l2cap_channel: L2capChannel) -> ChannelContext:
close_future = asyncio.get_running_loop().create_future()

View File

@@ -15,6 +15,7 @@
from __future__ import annotations
import asyncio
import contextlib
from collections.abc import Awaitable
import grpc
import logging
@@ -24,6 +25,7 @@ from bumble import hci
from bumble.core import (
PhysicalTransport,
ProtocolError,
InvalidArgumentError,
)
import bumble.utils
from bumble.device import Connection as BumbleConnection, Device
@@ -55,7 +57,7 @@ from pandora.security_pb2 import (
WaitSecurityRequest,
WaitSecurityResponse,
)
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Dict, Optional, Union
from typing import Any, AsyncGenerator, AsyncIterator, Callable, Optional, Union
class PairingDelegate(BasePairingDelegate):
@@ -188,35 +190,6 @@ class PairingDelegate(BasePairingDelegate):
self.service.event_queue.put_nowait(event)
BR_LEVEL_REACHED: Dict[SecurityLevel, Callable[[BumbleConnection], bool]] = {
LEVEL0: lambda connection: True,
LEVEL1: lambda connection: connection.encryption == 0 or connection.authenticated,
LEVEL2: lambda connection: connection.encryption != 0 and connection.authenticated,
LEVEL3: lambda connection: connection.encryption != 0
and connection.authenticated
and connection.link_key_type
in (
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
),
LEVEL4: lambda connection: connection.encryption
== hci.HCI_Encryption_Change_Event.AES_CCM
and connection.authenticated
and connection.link_key_type
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
}
LE_LEVEL_REACHED: Dict[LESecurityLevel, Callable[[BumbleConnection], bool]] = {
LE_LEVEL1: lambda connection: True,
LE_LEVEL2: lambda connection: connection.encryption != 0,
LE_LEVEL3: lambda connection: connection.encryption != 0
and connection.authenticated,
LE_LEVEL4: lambda connection: connection.encryption != 0
and connection.authenticated
and connection.sc,
}
class SecurityService(SecurityServicer):
def __init__(self, device: Device, config: Config) -> None:
self.log = utils.BumbleServerLoggerAdapter(
@@ -248,6 +221,59 @@ class SecurityService(SecurityServicer):
self.device.pairing_config_factory = pairing_config_factory
async def _classic_level_reached(
self, level: SecurityLevel, connection: BumbleConnection
) -> bool:
if level == LEVEL0:
return True
if level == LEVEL1:
return connection.encryption == 0 or connection.authenticated
if level == LEVEL2:
return connection.encryption != 0 and connection.authenticated
link_key_type: Optional[int] = None
if (keystore := connection.device.keystore) and (
keys := await keystore.get(str(connection.peer_address))
):
link_key_type = keys.link_key_type
self.log.debug("link_key_type: %d", link_key_type)
if level == LEVEL3:
return (
connection.encryption != 0
and connection.authenticated
and link_key_type
in (
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
)
)
if level == LEVEL4:
return (
connection.encryption == hci.HCI_Encryption_Change_Event.AES_CCM
and connection.authenticated
and link_key_type
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
)
raise InvalidArgumentError(f"Unexpected level {level}")
def _le_level_reached(
self, level: LESecurityLevel, connection: BumbleConnection
) -> bool:
if level == LE_LEVEL1:
return True
if level == LE_LEVEL2:
return connection.encryption != 0
if level == LE_LEVEL3:
return connection.encryption != 0 and connection.authenticated
if level == LE_LEVEL4:
return (
connection.encryption != 0
and connection.authenticated
and connection.sc
)
raise InvalidArgumentError(f"Unexpected level {level}")
@utils.rpc
async def OnPairing(
self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext
@@ -290,7 +316,7 @@ class SecurityService(SecurityServicer):
] == oneof
# security level already reached
if self.reached_security_level(connection, level):
if await self.reached_security_level(connection, level):
return SecureResponse(success=empty_pb2.Empty())
# trigger pairing if needed
@@ -361,7 +387,7 @@ class SecurityService(SecurityServicer):
return SecureResponse(encryption_failure=empty_pb2.Empty())
# security level has been reached ?
if self.reached_security_level(connection, level):
if await self.reached_security_level(connection, level):
return SecureResponse(success=empty_pb2.Empty())
return SecureResponse(not_reached=empty_pb2.Empty())
@@ -388,13 +414,10 @@ class SecurityService(SecurityServicer):
pair_task: Optional[asyncio.Future[None]] = None
async def authenticate() -> None:
assert connection
if (encryption := connection.encryption) != 0:
self.log.debug('Disable encryption...')
try:
with contextlib.suppress(Exception):
await connection.encrypt(enable=False)
except:
pass
self.log.debug('Disable encryption: done')
self.log.debug('Authenticate...')
@@ -413,15 +436,13 @@ class SecurityService(SecurityServicer):
return wrapper
def try_set_success(*_: Any) -> None:
assert connection
if self.reached_security_level(connection, level):
async def try_set_success(*_: Any) -> None:
if await self.reached_security_level(connection, level):
self.log.debug('Wait for security: done')
wait_for_security.set_result('success')
def on_encryption_change(*_: Any) -> None:
assert connection
if self.reached_security_level(connection, level):
async def on_encryption_change(*_: Any) -> None:
if await self.reached_security_level(connection, level):
self.log.debug('Wait for security: done')
wait_for_security.set_result('success')
elif (
@@ -436,7 +457,7 @@ class SecurityService(SecurityServicer):
if self.need_pairing(connection, level):
pair_task = asyncio.create_task(connection.pair())
listeners: Dict[str, Callable[..., None]] = {
listeners: dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
'disconnection': set_failure('connection_died'),
'pairing_failure': set_failure('pairing_failure'),
'connection_authentication_failure': set_failure('authentication_failure'),
@@ -455,7 +476,7 @@ class SecurityService(SecurityServicer):
watcher.on(connection, event, listener)
# security level already reached
if self.reached_security_level(connection, level):
if await self.reached_security_level(connection, level):
return WaitSecurityResponse(success=empty_pb2.Empty())
self.log.debug('Wait for security...')
@@ -465,24 +486,20 @@ class SecurityService(SecurityServicer):
# wait for `authenticate` to finish if any
if authenticate_task is not None:
self.log.debug('Wait for authentication...')
try:
with contextlib.suppress(Exception):
await authenticate_task # type: ignore
except:
pass
self.log.debug('Authenticated')
# wait for `pair` to finish if any
if pair_task is not None:
self.log.debug('Wait for authentication...')
try:
with contextlib.suppress(Exception):
await pair_task # type: ignore
except:
pass
self.log.debug('paired')
return WaitSecurityResponse(**kwargs)
def reached_security_level(
async def reached_security_level(
self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
) -> bool:
self.log.debug(
@@ -492,15 +509,14 @@ class SecurityService(SecurityServicer):
'encryption': connection.encryption,
'authenticated': connection.authenticated,
'sc': connection.sc,
'link_key_type': connection.link_key_type,
}
)
)
if isinstance(level, LESecurityLevel):
return LE_LEVEL_REACHED[level](connection)
return self._le_level_reached(level, connection)
return BR_LEVEL_REACHED[level](connection)
return await self._classic_level_reached(level, connection)
def need_pairing(self, connection: BumbleConnection, level: int) -> bool:
if connection.transport == PhysicalTransport.LE:

View File

@@ -22,9 +22,9 @@ import logging
from bumble.device import Device
from bumble.hci import Address, AddressType
from google.protobuf.message import Message # pytype: disable=pyi-error
from typing import Any, Dict, Generator, MutableMapping, Optional, Tuple
from typing import Any, Generator, MutableMapping, Optional
ADDRESS_TYPES: Dict[str, AddressType] = {
ADDRESS_TYPES: dict[str, AddressType] = {
"public": Address.PUBLIC_DEVICE_ADDRESS,
"random": Address.RANDOM_DEVICE_ADDRESS,
"public_identity": Address.PUBLIC_IDENTITY_ADDRESS,
@@ -43,7 +43,7 @@ class BumbleServerLoggerAdapter(logging.LoggerAdapter): # type: ignore
def process(
self, msg: str, kwargs: MutableMapping[str, Any]
) -> Tuple[str, MutableMapping[str, Any]]:
) -> tuple[str, MutableMapping[str, Any]]:
assert self.extra
service_name = self.extra['service_name']
assert isinstance(service_name, str)

View File

@@ -198,8 +198,7 @@ class AudioInputControlPoint:
audio_input_state: AudioInputState
gain_settings_properties: GainSettingsProperties
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
assert connection
async def on_write(self, connection: Connection, value: bytes) -> None:
opcode = AudioInputControlPointOpCode(value[0])
@@ -320,11 +319,10 @@ class AudioInputDescription:
audio_input_description: str = "Bluetooth"
attribute: Optional[Attribute] = None
def on_read(self, _connection: Optional[Connection]) -> str:
def on_read(self, _connection: Connection) -> str:
return self.audio_input_description
async def on_write(self, connection: Optional[Connection], value: str) -> None:
assert connection
async def on_write(self, connection: Connection, value: str) -> None:
assert self.attribute
self.audio_input_description = value

View File

@@ -18,10 +18,13 @@
# -----------------------------------------------------------------------------
from __future__ import annotations
from dataclasses import dataclass, field
import enum
import functools
import logging
import struct
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
from typing import Any, Optional, Union, TypeVar
from collections.abc import Sequence
from bumble import utils
from bumble import colors
@@ -48,11 +51,11 @@ class ASE_Operation:
See Audio Stream Control Service - 5 ASE Control operations.
'''
classes: Dict[int, Type[ASE_Operation]] = {}
op_code: int
classes: dict[int, type[ASE_Operation]] = {}
op_code: Opcode
name: str
fields: Optional[Sequence[Any]] = None
ase_id: List[int]
ase_id: Sequence[int]
class Opcode(enum.IntEnum):
# fmt: off
@@ -65,51 +68,30 @@ class ASE_Operation:
UPDATE_METADATA = 0x07
RELEASE = 0x08
@staticmethod
def from_bytes(pdu: bytes) -> ASE_Operation:
@classmethod
def from_bytes(cls, pdu: bytes) -> ASE_Operation:
op_code = pdu[0]
cls = ASE_Operation.classes.get(op_code)
if cls is None:
instance = ASE_Operation(pdu)
instance.name = ASE_Operation.Opcode(op_code).name
instance.op_code = op_code
return instance
self = cls.__new__(cls)
ASE_Operation.__init__(self, pdu)
if self.fields is not None:
self.init_from_bytes(pdu, 1)
return self
clazz = ASE_Operation.classes[op_code]
return clazz(
**hci.HCI_Object.dict_from_bytes(pdu, offset=1, fields=clazz.fields)
)
@staticmethod
def subclass(fields):
def inner(cls: Type[ASE_Operation]):
try:
operation = ASE_Operation.Opcode[cls.__name__[4:].upper()]
cls.name = operation.name
cls.op_code = operation
except:
raise KeyError(f'PDU name {cls.name} not found in Ase_Operation.Opcode')
cls.fields = fields
_OP = TypeVar("_OP", bound="ASE_Operation")
# Register a factory for this class
ASE_Operation.classes[cls.op_code] = cls
@classmethod
def subclass(cls, clazz: type[_OP]) -> type[_OP]:
clazz.name = f"ASE_{clazz.op_code.name.upper()}"
clazz.fields = hci.HCI_Object.fields_from_dataclass(clazz)
# Register a factory for this class
ASE_Operation.classes[clazz.op_code] = clazz
return clazz
return cls
return inner
def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
if self.fields is not None and kwargs:
hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
if pdu is None:
pdu = bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
kwargs, self.fields
)
self.pdu = pdu
def init_from_bytes(self, pdu: bytes, offset: int):
return hci.HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
@functools.cached_property
def pdu(self) -> bytes:
return bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
self.__dict__, self.fields
)
def __bytes__(self) -> bytes:
return self.pdu
@@ -124,105 +106,128 @@ class ASE_Operation:
return result
@ASE_Operation.subclass(
[
[
('ase_id', 1),
('target_latency', 1),
('target_phy', 1),
('codec_id', hci.CodingFormat.parse_from_bytes),
('codec_specific_configuration', 'v'),
],
]
)
@ASE_Operation.subclass
@dataclass
class ASE_Config_Codec(ASE_Operation):
'''
See Audio Stream Control Service 5.1 - Config Codec Operation
'''
target_latency: List[int]
target_phy: List[int]
codec_id: List[hci.CodingFormat]
codec_specific_configuration: List[bytes]
op_code = ASE_Operation.Opcode.CONFIG_CODEC
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
target_latency: Sequence[int] = field(metadata=hci.metadata(1))
target_phy: Sequence[int] = field(metadata=hci.metadata(1))
codec_id: Sequence[hci.CodingFormat] = field(
metadata=hci.metadata(hci.CodingFormat.parse_from_bytes)
)
codec_specific_configuration: Sequence[bytes] = field(
metadata=hci.metadata('v', list_end=True)
)
@ASE_Operation.subclass(
[
[
('ase_id', 1),
('cig_id', 1),
('cis_id', 1),
('sdu_interval', 3),
('framing', 1),
('phy', 1),
('max_sdu', 2),
('retransmission_number', 1),
('max_transport_latency', 2),
('presentation_delay', 3),
],
]
)
@ASE_Operation.subclass
@dataclass
class ASE_Config_QOS(ASE_Operation):
'''
See Audio Stream Control Service 5.2 - Config Qos Operation
'''
cig_id: List[int]
cis_id: List[int]
sdu_interval: List[int]
framing: List[int]
phy: List[int]
max_sdu: List[int]
retransmission_number: List[int]
max_transport_latency: List[int]
presentation_delay: List[int]
op_code = ASE_Operation.Opcode.CONFIG_QOS
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
cig_id: Sequence[int] = field(metadata=hci.metadata(1))
cis_id: Sequence[int] = field(metadata=hci.metadata(1))
sdu_interval: Sequence[int] = field(metadata=hci.metadata(3))
framing: Sequence[int] = field(metadata=hci.metadata(1))
phy: Sequence[int] = field(metadata=hci.metadata(1))
max_sdu: Sequence[int] = field(metadata=hci.metadata(2))
retransmission_number: Sequence[int] = field(metadata=hci.metadata(1))
max_transport_latency: Sequence[int] = field(metadata=hci.metadata(2))
presentation_delay: Sequence[int] = field(metadata=hci.metadata(3, list_end=True))
@ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
@ASE_Operation.subclass
@dataclass
class ASE_Enable(ASE_Operation):
'''
See Audio Stream Control Service 5.3 - Enable Operation
'''
metadata: bytes
op_code = ASE_Operation.Opcode.ENABLE
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
@ASE_Operation.subclass([[('ase_id', 1)]])
@ASE_Operation.subclass
@dataclass
class ASE_Receiver_Start_Ready(ASE_Operation):
'''
See Audio Stream Control Service 5.4 - Receiver Start Ready Operation
'''
op_code = ASE_Operation.Opcode.RECEIVER_START_READY
@ASE_Operation.subclass([[('ase_id', 1)]])
ase_id: Sequence[int] = field(
metadata=hci.metadata(1, list_begin=True, list_end=True)
)
@ASE_Operation.subclass
@dataclass
class ASE_Disable(ASE_Operation):
'''
See Audio Stream Control Service 5.5 - Disable Operation
'''
op_code = ASE_Operation.Opcode.DISABLE
@ASE_Operation.subclass([[('ase_id', 1)]])
ase_id: Sequence[int] = field(
metadata=hci.metadata(1, list_begin=True, list_end=True)
)
@ASE_Operation.subclass
@dataclass
class ASE_Receiver_Stop_Ready(ASE_Operation):
'''
See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation
'''
op_code = ASE_Operation.Opcode.RECEIVER_STOP_READY
@ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
ase_id: Sequence[int] = field(
metadata=hci.metadata(1, list_begin=True, list_end=True)
)
@ASE_Operation.subclass
@dataclass
class ASE_Update_Metadata(ASE_Operation):
'''
See Audio Stream Control Service 5.7 - Update Metadata Operation
'''
metadata: List[bytes]
op_code = ASE_Operation.Opcode.UPDATE_METADATA
ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
@ASE_Operation.subclass([[('ase_id', 1)]])
@ASE_Operation.subclass
@dataclass
class ASE_Release(ASE_Operation):
'''
See Audio Stream Control Service 5.8 - Release Operation
'''
op_code = ASE_Operation.Opcode.RELEASE
ase_id: Sequence[int] = field(
metadata=hci.metadata(1, list_begin=True, list_end=True)
)
class AseResponseCode(enum.IntEnum):
# fmt: off
@@ -384,7 +389,7 @@ class AseStateMachine(gatt.Characteristic):
target_phy: int,
codec_id: hci.CodingFormat,
codec_specific_configuration: bytes,
) -> Tuple[AseResponseCode, AseReasonCode]:
) -> tuple[AseResponseCode, AseReasonCode]:
if self.state not in (
self.State.IDLE,
self.State.CODEC_CONFIGURED,
@@ -420,7 +425,7 @@ class AseStateMachine(gatt.Characteristic):
retransmission_number: int,
max_transport_latency: int,
presentation_delay: int,
) -> Tuple[AseResponseCode, AseReasonCode]:
) -> tuple[AseResponseCode, AseReasonCode]:
if self.state not in (
AseStateMachine.State.CODEC_CONFIGURED,
AseStateMachine.State.QOS_CONFIGURED,
@@ -444,7 +449,7 @@ class AseStateMachine(gatt.Characteristic):
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_enable(self, metadata: bytes) -> Tuple[AseResponseCode, AseReasonCode]:
def on_enable(self, metadata: bytes) -> tuple[AseResponseCode, AseReasonCode]:
if self.state != AseStateMachine.State.QOS_CONFIGURED:
return (
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -456,7 +461,7 @@ class AseStateMachine(gatt.Characteristic):
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_receiver_start_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
def on_receiver_start_ready(self) -> tuple[AseResponseCode, AseReasonCode]:
if self.state != AseStateMachine.State.ENABLING:
return (
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -465,7 +470,7 @@ class AseStateMachine(gatt.Characteristic):
self.state = self.State.STREAMING
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_disable(self) -> Tuple[AseResponseCode, AseReasonCode]:
def on_disable(self) -> tuple[AseResponseCode, AseReasonCode]:
if self.state not in (
AseStateMachine.State.ENABLING,
AseStateMachine.State.STREAMING,
@@ -480,7 +485,7 @@ class AseStateMachine(gatt.Characteristic):
self.state = self.State.DISABLING
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_receiver_stop_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
def on_receiver_stop_ready(self) -> tuple[AseResponseCode, AseReasonCode]:
if (
self.role != AudioRole.SOURCE
or self.state != AseStateMachine.State.DISABLING
@@ -494,7 +499,7 @@ class AseStateMachine(gatt.Characteristic):
def on_update_metadata(
self, metadata: bytes
) -> Tuple[AseResponseCode, AseReasonCode]:
) -> tuple[AseResponseCode, AseReasonCode]:
if self.state not in (
AseStateMachine.State.ENABLING,
AseStateMachine.State.STREAMING,
@@ -506,7 +511,7 @@ class AseStateMachine(gatt.Characteristic):
self.metadata = le_audio.Metadata.from_bytes(metadata)
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
def on_release(self) -> Tuple[AseResponseCode, AseReasonCode]:
def on_release(self) -> tuple[AseResponseCode, AseReasonCode]:
if self.state == AseStateMachine.State.IDLE:
return (
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -516,7 +521,7 @@ class AseStateMachine(gatt.Characteristic):
async def remove_cis_async():
if self.cis_link:
await self.cis_link.remove_data_path(self.role)
await self.cis_link.remove_data_path([self.role])
self.state = self.State.IDLE
await self.service.device.notify_subscribers(self, self.value)
@@ -590,7 +595,7 @@ class AseStateMachine(gatt.Characteristic):
# Readonly. Do nothing in the setter.
pass
def on_read(self, _: Optional[device.Connection]) -> bytes:
def on_read(self, _: device.Connection) -> bytes:
return self.value
def __str__(self) -> str:
@@ -604,7 +609,7 @@ class AseStateMachine(gatt.Characteristic):
class AudioStreamControlService(gatt.TemplateService):
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
ase_state_machines: Dict[int, AseStateMachine]
ase_state_machines: dict[int, AseStateMachine]
ase_control_point: gatt.Characteristic[bytes]
_active_client: Optional[device.Connection] = None
@@ -649,7 +654,9 @@ class AudioStreamControlService(gatt.TemplateService):
ase.state = AseStateMachine.State.IDLE
self._active_client = None
def on_write_ase_control_point(self, connection, data):
def on_write_ase_control_point(
self, connection: device.Connection, data: bytes
) -> None:
if not self._active_client and connection:
self._active_client = connection
connection.once('disconnection', self._on_client_disconnected)
@@ -658,7 +665,7 @@ class AudioStreamControlService(gatt.TemplateService):
responses = []
logger.debug(f'*** ASCS Write {operation} ***')
if operation.op_code == ASE_Operation.Opcode.CONFIG_CODEC:
if isinstance(operation, ASE_Config_Codec):
for ase_id, *args in zip(
operation.ase_id,
operation.target_latency,
@@ -667,7 +674,7 @@ class AudioStreamControlService(gatt.TemplateService):
operation.codec_specific_configuration,
):
responses.append(self.on_operation(operation.op_code, ase_id, args))
elif operation.op_code == ASE_Operation.Opcode.CONFIG_QOS:
elif isinstance(operation, ASE_Config_QOS):
for ase_id, *args in zip(
operation.ase_id,
operation.cig_id,
@@ -681,20 +688,20 @@ class AudioStreamControlService(gatt.TemplateService):
operation.presentation_delay,
):
responses.append(self.on_operation(operation.op_code, ase_id, args))
elif operation.op_code in (
ASE_Operation.Opcode.ENABLE,
ASE_Operation.Opcode.UPDATE_METADATA,
):
elif isinstance(operation, (ASE_Enable, ASE_Update_Metadata)):
for ase_id, *args in zip(
operation.ase_id,
operation.metadata,
):
responses.append(self.on_operation(operation.op_code, ase_id, args))
elif operation.op_code in (
ASE_Operation.Opcode.RECEIVER_START_READY,
ASE_Operation.Opcode.DISABLE,
ASE_Operation.Opcode.RECEIVER_STOP_READY,
ASE_Operation.Opcode.RELEASE,
elif isinstance(
operation,
(
ASE_Receiver_Start_Ready,
ASE_Disable,
ASE_Receiver_Stop_Ready,
ASE_Release,
),
):
for ase_id in operation.ase_id:
responses.append(self.on_operation(operation.op_code, ase_id, []))
@@ -723,8 +730,8 @@ class AudioStreamControlService(gatt.TemplateService):
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
SERVICE_CLASS = AudioStreamControlService
sink_ase: List[gatt_client.CharacteristicProxy[bytes]]
source_ase: List[gatt_client.CharacteristicProxy[bytes]]
sink_ase: list[gatt_client.CharacteristicProxy[bytes]]
source_ase: list[gatt_client.CharacteristicProxy[bytes]]
ase_control_point: gatt_client.CharacteristicProxy[bytes]
def __init__(self, service_proxy: gatt_client.ServiceProxy):

View File

@@ -19,7 +19,7 @@
import enum
import struct
import logging
from typing import List, Optional, Callable, Union, Any
from typing import Optional, Callable, Union, Any
from bumble import l2cap
from bumble import utils
@@ -103,7 +103,7 @@ class AshaService(gatt.TemplateService):
def __init__(
self,
capability: int,
hisyncid: Union[List[int], bytes],
hisyncid: Union[list[int], bytes],
device: Device,
psm: int = 0,
audio_sink: Optional[Callable[[bytes], Any]] = None,
@@ -200,7 +200,7 @@ class AshaService(gatt.TemplateService):
# Handler for audio control commands
async def _on_audio_control_point_write(
self, connection: Optional[Connection], value: bytes
self, connection: Connection, value: bytes
) -> None:
_logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
opcode = value[0]
@@ -247,7 +247,7 @@ class AshaService(gatt.TemplateService):
)
# Handler for volume control
def _on_volume_write(self, connection: Optional[Connection], value: bytes) -> None:
def _on_volume_write(self, connection: Connection, value: bytes) -> None:
_logger.debug(f'--- VOLUME Write:{value[0]}')
self.volume = value[0]
self.emit(self.EVENT_VOLUME_CHANGED)

View File

@@ -24,7 +24,6 @@ import enum
import struct
import functools
import logging
from typing import List
from typing_extensions import Self
from bumble import core
@@ -282,7 +281,7 @@ class UnicastServerAdvertisingData:
# -----------------------------------------------------------------------------
def bits_to_channel_counts(data: int) -> List[int]:
def bits_to_channel_counts(data: int) -> list[int]:
pos = 0
counts = []
while data != 0:
@@ -527,7 +526,7 @@ class BasicAudioAnnouncement:
codec_id: hci.CodingFormat
codec_specific_configuration: CodecSpecificConfiguration
metadata: le_audio.Metadata
bis: List[BasicAudioAnnouncement.BIS]
bis: list[BasicAudioAnnouncement.BIS]
def __bytes__(self) -> bytes:
metadata_bytes = bytes(self.metadata)
@@ -545,7 +544,7 @@ class BasicAudioAnnouncement:
)
presentation_delay: int
subgroups: List[BasicAudioAnnouncement.Subgroup]
subgroups: list[BasicAudioAnnouncement.Subgroup]
@classmethod
def from_bytes(cls, data: bytes) -> Self:

View File

@@ -19,7 +19,7 @@
from __future__ import annotations
import enum
import struct
from typing import Optional, Tuple
from typing import Optional
from bumble import core
from bumble import crypto
@@ -164,12 +164,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
super().__init__(characteristics)
async def on_sirk_read(self, connection: Optional[device.Connection]) -> bytes:
async def on_sirk_read(self, connection: device.Connection) -> bytes:
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
sirk_bytes = self.set_identity_resolving_key
else:
assert connection
if connection.transport == core.PhysicalTransport.LE:
key = await connection.device.get_long_term_key(
connection_handle=connection.handle, rand=b'', ediv=0
@@ -230,7 +228,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
):
self.set_member_rank = characteristics[0]
async def read_set_identity_resolving_key(self) -> Tuple[SirkType, bytes]:
async def read_set_identity_resolving_key(self) -> tuple[SirkType, bytes]:
'''Reads SIRK and decrypts if encrypted.'''
response = await self.set_identity_resolving_key.read_value()
if len(response) != SET_IDENTITY_RESOLVING_KEY_LENGTH + 1:

View File

@@ -17,7 +17,7 @@
# Imports
# -----------------------------------------------------------------------------
import struct
from typing import Optional, Tuple
from typing import Optional
from bumble.gatt import (
GATT_DEVICE_INFORMATION_SERVICE,
@@ -60,7 +60,7 @@ class DeviceInformationService(TemplateService):
hardware_revision: Optional[str] = None,
firmware_revision: Optional[str] = None,
software_revision: Optional[str] = None,
system_id: Optional[Tuple[int, int]] = None, # (OUI, Manufacturer ID)
system_id: Optional[tuple[int, int]] = None, # (OUI, Manufacturer ID)
ieee_regulatory_certification_data_list: Optional[bytes] = None,
# TODO: pnp_id
):

View File

@@ -19,7 +19,7 @@
# -----------------------------------------------------------------------------
import logging
import struct
from typing import Optional, Tuple, Union
from typing import Optional, Union
from bumble.core import Appearance
from bumble.gatt import (
@@ -54,7 +54,7 @@ class GenericAccessService(TemplateService):
appearance_characteristic: Characteristic[bytes]
def __init__(
self, device_name: str, appearance: Union[Appearance, Tuple[int, int], int] = 0
self, device_name: str, appearance: Union[Appearance, tuple[int, int], int] = 0
):
if isinstance(appearance, int):
appearance_int = appearance

View File

@@ -127,9 +127,7 @@ class GenericAttributeProfileService(gatt.TemplateService):
return b''
def get_database_hash(self, connection: device.Connection | None) -> bytes:
assert connection
def get_database_hash(self, connection: device.Connection) -> bytes:
m = b''.join(
[
self.get_attribute_data(attribute)

View File

@@ -20,7 +20,7 @@ import asyncio
import functools
from dataclasses import dataclass, field
import logging
from typing import Any, Dict, List, Optional, Set, Union
from typing import Any, Optional, Union
from bumble import att, gatt, gatt_adapters, gatt_client
from bumble.core import InvalidArgumentError, InvalidStateError
@@ -228,23 +228,25 @@ class HearingAccessService(gatt.TemplateService):
hearing_aid_preset_control_point: gatt.Characteristic[bytes]
active_preset_index_characteristic: gatt.Characteristic[bytes]
active_preset_index: int
active_preset_index_per_device: Dict[Address, int]
active_preset_index_per_device: dict[Address, int]
device: Device
server_features: HearingAidFeatures
preset_records: Dict[int, PresetRecord] # key is the preset index
preset_records: dict[int, PresetRecord] # key is the preset index
read_presets_request_in_progress: bool
preset_changed_operations_history_per_device: Dict[
Address, List[PresetChangedOperation]
other_server_in_binaural_set: Optional[HearingAccessService] = None
preset_changed_operations_history_per_device: dict[
Address, list[PresetChangedOperation]
]
# Keep an updated list of connected client to send notification to
currently_connected_clients: Set[Connection]
currently_connected_clients: set[Connection]
def __init__(
self, device: Device, features: HearingAidFeatures, presets: List[PresetRecord]
self, device: Device, features: HearingAidFeatures, presets: list[PresetRecord]
) -> None:
self.active_preset_index_per_device = {}
self.read_presets_request_in_progress = False
@@ -333,11 +335,10 @@ class HearingAccessService(gatt.TemplateService):
# Update the active preset index if needed
await self.notify_active_preset_for_connection(connection)
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
connection.cancel_on_disconnection(on_connection_async())
def _on_read_active_preset_index(
self, __connection__: Optional[Connection]
) -> bytes:
def _on_read_active_preset_index(self, connection: Connection) -> bytes:
del connection # Unused
return bytes([self.active_preset_index])
# TODO this need to be triggered when device is unbonded
@@ -345,18 +346,13 @@ class HearingAccessService(gatt.TemplateService):
self.preset_changed_operations_history_per_device.pop(addr)
async def _on_write_hearing_aid_preset_control_point(
self, connection: Optional[Connection], value: bytes
self, connection: Connection, value: bytes
):
assert connection
opcode = HearingAidPresetControlPointOpcode(value[0])
handler = getattr(self, '_on_' + opcode.name.lower())
await handler(connection, value)
async def _on_read_presets_request(
self, connection: Optional[Connection], value: bytes
):
assert connection
async def _on_read_presets_request(self, connection: Connection, value: bytes):
if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
logging.warning(f'HAS require MTU >= 49: {connection}')
@@ -385,7 +381,7 @@ class HearingAccessService(gatt.TemplateService):
utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
async def _read_preset_response(
self, connection: Connection, presets: List[PresetRecord]
self, connection: Connection, presets: list[PresetRecord]
):
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects.
try:
@@ -471,10 +467,7 @@ class HearingAccessService(gatt.TemplateService):
for connection in self.currently_connected_clients:
await self._preset_changed_operation(connection)
async def _on_write_preset_name(
self, connection: Optional[Connection], value: bytes
):
assert connection
async def _on_write_preset_name(self, connection: Connection, value: bytes):
if self.read_presets_request_in_progress:
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
@@ -522,10 +515,7 @@ class HearingAccessService(gatt.TemplateService):
for connection in self.currently_connected_clients:
await self.notify_active_preset_for_connection(connection)
async def set_active_preset(
self, connection: Optional[Connection], value: bytes
) -> None:
assert connection
async def set_active_preset(self, value: bytes) -> None:
index = value[1]
preset = self.preset_records.get(index, None)
if (
@@ -542,16 +532,11 @@ class HearingAccessService(gatt.TemplateService):
self.active_preset_index = index
await self.notify_active_preset()
async def _on_set_active_preset(
self, connection: Optional[Connection], value: bytes
):
await self.set_active_preset(connection, value)
async def _on_set_active_preset(self, _: Connection, value: bytes):
await self.set_active_preset(value)
async def set_next_or_previous_preset(
self, connection: Optional[Connection], is_previous
):
async def set_next_or_previous_preset(self, is_previous):
'''Set the next or the previous preset as active'''
assert connection
if self.active_preset_index == 0x00:
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
@@ -580,48 +565,47 @@ class HearingAccessService(gatt.TemplateService):
self.active_preset_index = first_preset.index
await self.notify_active_preset()
async def _on_set_next_preset(
self, connection: Optional[Connection], __value__: bytes
) -> None:
await self.set_next_or_previous_preset(connection, False)
async def _on_set_next_preset(self, _: Connection, __value__: bytes) -> None:
await self.set_next_or_previous_preset(False)
async def _on_set_previous_preset(
self, connection: Optional[Connection], __value__: bytes
) -> None:
await self.set_next_or_previous_preset(connection, True)
async def _on_set_previous_preset(self, _: Connection, __value__: bytes) -> None:
await self.set_next_or_previous_preset(True)
async def _on_set_active_preset_synchronized_locally(
self, connection: Optional[Connection], value: bytes
self, _: Connection, value: bytes
):
if (
self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
):
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
await self.set_active_preset(connection, value)
# TODO (low priority) inform other server of the change
await self.set_active_preset(value)
if self.other_server_in_binaural_set:
await self.other_server_in_binaural_set.set_active_preset(value)
async def _on_set_next_preset_synchronized_locally(
self, connection: Optional[Connection], __value__: bytes
self, _: Connection, __value__: bytes
):
if (
self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
):
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
await self.set_next_or_previous_preset(connection, False)
# TODO (low priority) inform other server of the change
await self.set_next_or_previous_preset(False)
if self.other_server_in_binaural_set:
await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
async def _on_set_previous_preset_synchronized_locally(
self, connection: Optional[Connection], __value__: bytes
self, _: Connection, __value__: bytes
):
if (
self.server_features.preset_synchronization_support
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
):
raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
await self.set_next_or_previous_preset(connection, True)
# TODO (low priority) inform other server of the change
await self.set_next_or_previous_preset(True)
if self.other_server_in_binaural_set:
await self.other_server_in_binaural_set.set_next_or_previous_preset(True)
# -----------------------------------------------------------------------------

View File

@@ -19,7 +19,7 @@ from __future__ import annotations
import dataclasses
import enum
import struct
from typing import Any, List, Type
from typing import Any
from typing_extensions import Self
from bumble.profiles import bap
@@ -108,13 +108,13 @@ class Metadata:
return self.data
@classmethod
def from_bytes(cls: Type[Self], data: bytes) -> Self:
def from_bytes(cls: type[Self], data: bytes) -> Self:
return cls(tag=Metadata.Tag(data[0]), data=data[1:])
def __bytes__(self) -> bytes:
return bytes([len(self.data) + 1, self.tag]) + self.data
entries: List[Entry] = dataclasses.field(default_factory=list)
entries: list[Entry] = dataclasses.field(default_factory=list)
def pretty_print(self, indent: str) -> str:
"""Convenience method to generate a string with one key-value pair per line."""
@@ -140,7 +140,7 @@ class Metadata:
)
@classmethod
def from_bytes(cls: Type[Self], data: bytes) -> Self:
def from_bytes(cls: type[Self], data: bytes) -> Self:
entries = []
offset = 0
length = len(data)

View File

@@ -29,7 +29,7 @@ from bumble import gatt
from bumble import gatt_client
from bumble import utils
from typing import Type, Optional, ClassVar, Dict, TYPE_CHECKING
from typing import Optional, ClassVar, TYPE_CHECKING
from typing_extensions import Self
# -----------------------------------------------------------------------------
@@ -167,7 +167,7 @@ class ObjectId(int):
'''See Media Control Service 4.4.2. Object ID field.'''
@classmethod
def create_from_bytes(cls: Type[Self], data: bytes) -> Self:
def create_from_bytes(cls: type[Self], data: bytes) -> Self:
return cls(int.from_bytes(data, byteorder='little', signed=False))
def __bytes__(self) -> bytes:
@@ -182,7 +182,7 @@ class GroupObjectType:
object_id: ObjectId
@classmethod
def from_bytes(cls: Type[Self], data: bytes) -> Self:
def from_bytes(cls: type[Self], data: bytes) -> Self:
return cls(
object_type=ObjectType(data[0]),
object_id=ObjectId.create_from_bytes(data[1:]),
@@ -287,11 +287,8 @@ class MediaControlService(gatt.TemplateService):
)
async def on_media_control_point(
self, connection: Optional[device.Connection], data: bytes
self, connection: device.Connection, data: bytes
) -> None:
if not connection:
raise core.InvalidStateError()
opcode = MediaControlPointOpcode(data[0])
await connection.device.notify_subscriber(
@@ -313,7 +310,7 @@ class MediaControlServiceProxy(
):
SERVICE_CLASS = MediaControlService
_CHARACTERISTICS: ClassVar[Dict[str, core.UUID]] = {
_CHARACTERISTICS: ClassVar[dict[str, core.UUID]] = {
'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC,
'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC,

View File

@@ -20,7 +20,7 @@ from __future__ import annotations
import dataclasses
import enum
from typing import Optional, Sequence
from typing import Sequence
from bumble import att
from bumble import utils
@@ -146,14 +146,12 @@ class VolumeControlService(gatt.TemplateService):
included_services=list(included_services),
)
def _on_read_volume_state(self, _connection: Optional[device.Connection]) -> bytes:
def _on_read_volume_state(self, _connection: device.Connection) -> bytes:
return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
def _on_write_volume_control_point(
self, connection: Optional[device.Connection], value: bytes
self, connection: device.Connection, value: bytes
) -> None:
assert connection
opcode = VolumeControlPointOpcode(value[0])
change_counter = value[1]
@@ -163,10 +161,8 @@ class VolumeControlService(gatt.TemplateService):
handler = getattr(self, '_on_' + opcode.name.lower())
if handler(*value[2:]):
self.change_counter = (self.change_counter + 1) % 256
utils.cancel_on_event(
connection,
'disconnection',
connection.device.notify_subscribers(attribute=self.volume_state),
connection.cancel_on_disconnection(
connection.device.notify_subscribers(attribute=self.volume_state)
)
self.emit(self.EVENT_VOLUME_STATE_CHANGE)

View File

@@ -86,7 +86,7 @@ class VolumeOffsetState:
assert self.attribute is not None
await connection.device.notify_subscribers(attribute=self.attribute)
def on_read(self, _connection: Optional[Connection]) -> bytes:
def on_read(self, _connection: Connection) -> bytes:
return bytes(self)
@@ -103,11 +103,10 @@ class VocsAudioLocation:
audio_location = AudioLocation(struct.unpack('<I', data)[0])
return cls(audio_location)
def on_read(self, _connection: Optional[Connection]) -> bytes:
def on_read(self, _connection: Connection) -> bytes:
return bytes(self)
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
assert connection
async def on_write(self, connection: Connection, value: bytes) -> None:
assert self.attribute
self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
@@ -118,8 +117,7 @@ class VocsAudioLocation:
class VolumeOffsetControlPoint:
volume_offset_state: VolumeOffsetState
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
assert connection
async def on_write(self, connection: Connection, value: bytes) -> None:
opcode = value[0]
if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
@@ -159,11 +157,10 @@ class AudioOutputDescription:
def __bytes__(self) -> bytes:
return self.audio_output_description.encode('utf-8')
def on_read(self, _connection: Optional[Connection]) -> bytes:
def on_read(self, _connection: Connection) -> bytes:
return bytes(self)
async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
assert connection
async def on_write(self, connection: Connection, value: bytes) -> None:
assert self.attribute
self.audio_output_description = value.decode('utf-8')

View File

@@ -22,7 +22,7 @@ import asyncio
import collections
import dataclasses
import enum
from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
from typing import Callable, Optional, Union, TYPE_CHECKING
from typing_extensions import Self
@@ -123,7 +123,7 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
# -----------------------------------------------------------------------------
def make_service_sdp_records(
service_record_handle: int, channel: int, uuid: Optional[UUID] = None
) -> List[sdp.ServiceAttribute]:
) -> list[sdp.ServiceAttribute]:
"""
Create SDP records for an RFComm service given a channel number and an
optional UUID. A Service Class Attribute is included only if the UUID is not None.
@@ -169,7 +169,7 @@ def make_service_sdp_records(
# -----------------------------------------------------------------------------
async def find_rfcomm_channels(connection: Connection) -> Dict[int, List[UUID]]:
async def find_rfcomm_channels(connection: Connection) -> dict[int, list[UUID]]:
"""Searches all RFCOMM channels and their associated UUID from SDP service records.
Args:
@@ -188,7 +188,7 @@ async def find_rfcomm_channels(connection: Connection) -> Dict[int, List[UUID]]:
],
)
for attribute_lists in search_result:
service_classes: List[UUID] = []
service_classes: list[UUID] = []
channel: Optional[int] = None
for attribute in attribute_lists:
# The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
@@ -275,7 +275,7 @@ class RFCOMM_Frame:
self.fcs = compute_fcs(bytes([self.address, self.control]) + self.length)
@staticmethod
def parse_mcc(data) -> Tuple[int, bool, bytes]:
def parse_mcc(data) -> tuple[int, bool, bytes]:
mcc_type = data[0] >> 2
c_r = bool((data[0] >> 1) & 1)
length = data[1]
@@ -771,8 +771,8 @@ class Multiplexer(utils.EventEmitter):
connection_result: Optional[asyncio.Future]
disconnection_result: Optional[asyncio.Future]
open_result: Optional[asyncio.Future]
acceptor: Optional[Callable[[int], Optional[Tuple[int, int]]]]
dlcs: Dict[int, DLC]
acceptor: Optional[Callable[[int], Optional[tuple[int, int]]]]
dlcs: dict[int, DLC]
def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
super().__init__()
@@ -1088,8 +1088,8 @@ class Server(utils.EventEmitter):
) -> None:
super().__init__()
self.device = device
self.acceptors: Dict[int, Callable[[DLC], None]] = {}
self.dlc_configs: Dict[int, Tuple[int, int]] = {}
self.acceptors: dict[int, Callable[[DLC], None]] = {}
self.dlc_configs: dict[int, tuple[int, int]] = {}
# Register ourselves with the L2CAP channel manager
self.l2cap_server = device.create_l2cap_server(
@@ -1144,7 +1144,7 @@ class Server(utils.EventEmitter):
# Notify
self.emit(self.EVENT_START, multiplexer)
def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
def accept_dlc(self, channel_number: int) -> Optional[tuple[int, int]]:
return self.dlc_configs.get(channel_number)
def on_dlc(self, dlc: DLC) -> None:

View File

@@ -17,7 +17,6 @@
# -----------------------------------------------------------------------------
from __future__ import annotations
import struct
from typing import List
# -----------------------------------------------------------------------------
@@ -60,7 +59,7 @@ class MediaPacket:
sequence_number: int,
timestamp: int,
ssrc: int,
csrc_list: List[int],
csrc_list: list[int],
payload_type: int,
payload: bytes,
) -> None:

View File

@@ -19,7 +19,7 @@ from __future__ import annotations
import asyncio
import logging
import struct
from typing import Iterable, NewType, Optional, Union, Sequence, Type, TYPE_CHECKING
from typing import Iterable, NewType, Optional, Union, Sequence, TYPE_CHECKING
from typing_extensions import Self
from bumble import core, l2cap
@@ -547,7 +547,7 @@ class SDP_PDU:
SDP_SERVICE_ATTRIBUTE_REQUEST: SDP_SERVICE_ATTRIBUTE_RESPONSE,
SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE,
}
sdp_pdu_classes: dict[int, Type[SDP_PDU]] = {}
sdp_pdu_classes: dict[int, type[SDP_PDU]] = {}
name = None
pdu_id = 0

View File

@@ -26,18 +26,13 @@ from __future__ import annotations
import logging
import asyncio
import enum
import secrets
from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
cast,
)
@@ -210,7 +205,7 @@ class SMP_Command:
See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL
'''
smp_classes: Dict[int, Type[SMP_Command]] = {}
smp_classes: dict[int, type[SMP_Command]] = {}
fields: Any
code = 0
name = ''
@@ -254,7 +249,7 @@ class SMP_Command:
@staticmethod
def key_distribution_str(value: int) -> str:
key_types: List[str] = []
key_types: list[str] = []
if value & SMP_ENC_KEY_DISTRIBUTION_FLAG:
key_types.append('ENC')
if value & SMP_ID_KEY_DISTRIBUTION_FLAG:
@@ -706,7 +701,7 @@ class Session:
self.peer_identity_resolving_key = None
self.peer_bd_addr: Optional[Address] = None
self.peer_signature_key = None
self.peer_expected_distributions: List[Type[SMP_Command]] = []
self.peer_expected_distributions: list[type[SMP_Command]] = []
self.dh_key = b''
self.confirm_value = None
self.passkey: Optional[int] = None
@@ -767,7 +762,9 @@ class Session:
# OOB
self.oob_data_flag = (
1 if pairing_config.oob and pairing_config.oob.peer_data else 0
1
if pairing_config.oob and (not self.sc or pairing_config.oob.peer_data)
else 0
)
# Set up addresses
@@ -814,7 +811,7 @@ class Session:
self.tk = bytes(16)
@property
def pkx(self) -> Tuple[bytes, bytes]:
def pkx(self) -> tuple[bytes, bytes]:
return (self.ecc_key.x[::-1], self.peer_public_key_x)
@property
@@ -826,7 +823,7 @@ class Session:
return self.pkx[0 if self.is_responder else 1]
@property
def nx(self) -> Tuple[bytes, bytes]:
def nx(self) -> tuple[bytes, bytes]:
assert self.peer_random_value
return (self.r, self.peer_random_value)
@@ -900,7 +897,7 @@ class Session:
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
utils.cancel_on_event(self.connection, 'disconnection', prompt())
self.connection.cancel_on_disconnection(prompt())
def prompt_user_for_numeric_comparison(
self, code: int, next_steps: Callable[[], None]
@@ -919,7 +916,7 @@ class Session:
self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
utils.cancel_on_event(self.connection, 'disconnection', prompt())
self.connection.cancel_on_disconnection(prompt())
def prompt_user_for_number(self, next_steps: Callable[[int], None]) -> None:
async def prompt() -> None:
@@ -936,12 +933,11 @@ class Session:
logger.warning(f'exception while prompting: {error}')
self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
utils.cancel_on_event(self.connection, 'disconnection', prompt())
self.connection.cancel_on_disconnection(prompt())
def display_passkey(self) -> None:
# Generate random Passkey/PIN code
self.passkey = secrets.randbelow(1000000)
assert self.passkey is not None
async def display_passkey(self) -> None:
# Get the passkey value from the delegate
self.passkey = await self.pairing_config.delegate.generate_passkey()
logger.debug(f'Pairing PIN CODE: {self.passkey:06}')
self.passkey_ready.set()
@@ -950,14 +946,7 @@ class Session:
self.tk = self.passkey.to_bytes(16, byteorder='little')
logger.debug(f'TK from passkey = {self.tk.hex()}')
try:
utils.cancel_on_event(
self.connection,
'disconnection',
self.pairing_config.delegate.display_number(self.passkey, digits=6),
)
except Exception as error:
logger.warning(f'exception while displaying number: {error}')
await self.pairing_config.delegate.display_number(self.passkey, digits=6)
def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
# Prompt the user for the passkey displayed on the peer
@@ -979,9 +968,16 @@ class Session:
self, next_steps: Optional[Callable[[], None]] = None
) -> None:
if self.passkey_display:
self.display_passkey()
if next_steps is not None:
next_steps()
async def display_passkey():
await self.display_passkey()
if next_steps is not None:
next_steps()
try:
self.connection.cancel_on_disconnection(display_passkey())
except Exception as error:
logger.warning(f'exception while displaying passkey: {error}')
else:
self.input_passkey(next_steps)
@@ -1051,7 +1047,7 @@ class Session:
)
# Perform the next steps asynchronously in case we need to wait for input
utils.cancel_on_event(self.connection, 'disconnection', next_steps())
self.connection.cancel_on_disconnection(next_steps())
else:
confirm_value = crypto.c1(
self.tk,
@@ -1174,8 +1170,8 @@ class Session:
self.connection.transport == PhysicalTransport.BR_EDR
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
):
self.ctkd_task = utils.cancel_on_event(
self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
self.ctkd_task = self.connection.cancel_on_disconnection(
self.get_link_key_and_derive_ltk()
)
elif not self.sc:
# Distribute the LTK, EDIV and RAND
@@ -1213,8 +1209,8 @@ class Session:
self.connection.transport == PhysicalTransport.BR_EDR
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
):
self.ctkd_task = utils.cancel_on_event(
self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
self.ctkd_task = self.connection.cancel_on_disconnection(
self.get_link_key_and_derive_ltk()
)
# Distribute the LTK, EDIV and RAND
elif not self.sc:
@@ -1269,7 +1265,7 @@ class Session:
f'{[c.__name__ for c in self.peer_expected_distributions]}'
)
def check_key_distribution(self, command_class: Type[SMP_Command]) -> None:
def check_key_distribution(self, command_class: type[SMP_Command]) -> None:
# First, check that the connection is encrypted
if not self.connection.is_encrypted:
logger.warning(
@@ -1306,9 +1302,7 @@ class Session:
# Wait for the pairing process to finish
assert self.pairing_result
await utils.cancel_on_event(
self.connection, 'disconnection', self.pairing_result
)
await self.connection.cancel_on_disconnection(self.pairing_result)
def on_disconnection(self, _: int) -> None:
self.connection.remove_listener(
@@ -1329,7 +1323,7 @@ class Session:
if self.is_initiator:
self.distribute_keys()
utils.cancel_on_event(self.connection, 'disconnection', self.on_pairing())
self.connection.cancel_on_disconnection(self.on_pairing())
def on_connection_encryption_change(self) -> None:
if self.connection.is_encrypted and not self.completed:
@@ -1380,8 +1374,10 @@ class Session:
ediv=self.ltk_ediv,
rand=self.ltk_rand,
)
if not self.peer_ltk:
logger.error("peer_ltk is None")
peer_ltk_key = PairingKeys.Key(
value=self.peer_ltk,
value=self.peer_ltk or b'',
authenticated=authenticated,
ediv=self.peer_ediv,
rand=self.peer_rand,
@@ -1438,10 +1434,8 @@ class Session:
def on_smp_pairing_request_command(
self, command: SMP_Pairing_Request_Command
) -> None:
utils.cancel_on_event(
self.connection,
'disconnection',
self.on_smp_pairing_request_command_async(command),
self.connection.cancel_on_disconnection(
self.on_smp_pairing_request_command_async(command)
)
async def on_smp_pairing_request_command_async(
@@ -1505,7 +1499,7 @@ class Session:
# Display a passkey if we need to
if not self.sc:
if self.pairing_method == PairingMethod.PASSKEY and self.passkey_display:
self.display_passkey()
await self.display_passkey()
# Respond
self.send_pairing_response_command()
@@ -1687,7 +1681,7 @@ class Session:
):
return
elif self.pairing_method == PairingMethod.PASSKEY:
assert self.passkey and self.confirm_value
assert self.passkey is not None and self.confirm_value is not None
# Check that the random value matches what was committed to earlier
confirm_verifier = crypto.f4(
self.pkb,
@@ -1716,7 +1710,7 @@ class Session:
):
self.send_pairing_random_command()
elif self.pairing_method == PairingMethod.PASSKEY:
assert self.passkey and self.confirm_value
assert self.passkey is not None and self.confirm_value is not None
# Check that the random value matches what was committed to earlier
confirm_verifier = crypto.f4(
self.pka,
@@ -1753,7 +1747,7 @@ class Session:
ra = bytes(16)
rb = ra
elif self.pairing_method == PairingMethod.PASSKEY:
assert self.passkey
assert self.passkey is not None
ra = self.passkey.to_bytes(16, byteorder='little')
rb = ra
elif self.pairing_method == PairingMethod.OOB:
@@ -1852,19 +1846,23 @@ class Session:
elif self.pairing_method == PairingMethod.PASSKEY:
self.send_pairing_confirm_command()
else:
def next_steps() -> None:
# Send our public key back to the initiator
self.send_public_key_command()
if self.pairing_method in (
PairingMethod.JUST_WORKS,
PairingMethod.NUMERIC_COMPARISON,
PairingMethod.OOB,
):
# We can now send the confirmation value
self.send_pairing_confirm_command()
if self.pairing_method == PairingMethod.PASSKEY:
self.display_or_input_passkey()
# Send our public key back to the initiator
self.send_public_key_command()
if self.pairing_method in (
PairingMethod.JUST_WORKS,
PairingMethod.NUMERIC_COMPARISON,
PairingMethod.OOB,
):
# We can now send the confirmation value
self.send_pairing_confirm_command()
self.display_or_input_passkey(next_steps)
else:
next_steps()
def on_smp_pairing_dhkey_check_command(
self, command: SMP_Pairing_DHKey_Check_Command
@@ -1886,7 +1884,7 @@ class Session:
self.wait_before_continuing = None
self.send_pairing_dhkey_check_command()
utils.cancel_on_event(self.connection, 'disconnection', next_steps())
self.connection.cancel_on_disconnection(next_steps())
else:
self.send_pairing_dhkey_check_command()
else:
@@ -1936,9 +1934,9 @@ class Manager(utils.EventEmitter):
'''
device: Device
sessions: Dict[int, Session]
sessions: dict[int, Session]
pairing_config_factory: Callable[[Connection], PairingConfig]
session_proxy: Type[Session]
session_proxy: type[Session]
_ecc_key: Optional[crypto.EccKey]
def __init__(

View File

@@ -22,7 +22,7 @@ import os
import pathlib
import platform
import sys
from typing import Dict, Optional
from typing import Optional
import grpc.aio
@@ -143,7 +143,7 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
# -----------------------------------------------------------------------------
async def open_android_netsim_controller_transport(
server_host: Optional[str], server_port: int, options: Dict[str, str]
server_host: Optional[str], server_port: int, options: dict[str, str]
) -> Transport:
if not server_port:
raise TransportSpecError('invalid port')
@@ -288,7 +288,7 @@ async def open_android_netsim_controller_transport(
async def open_android_netsim_host_transport_with_address(
server_host: Optional[str],
server_port: int,
options: Optional[Dict[str, str]] = None,
options: Optional[dict[str, str]] = None,
):
if server_host == '_' or not server_host:
server_host = 'localhost'
@@ -313,7 +313,7 @@ async def open_android_netsim_host_transport_with_address(
# -----------------------------------------------------------------------------
async def open_android_netsim_host_transport_with_channel(
channel, options: Optional[Dict[str, str]] = None
channel, options: Optional[dict[str, str]] = None
):
# Wrapper for I/O operations
class HciDevice:
@@ -451,7 +451,7 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
port = 0
params_offset = 0
options: Dict[str, str] = {}
options: dict[str, str] = {}
for param in params[params_offset:]:
if '=' not in param:
raise TransportSpecError('invalid parameter, expected <name>=<value>')

View File

@@ -21,7 +21,7 @@ import struct
import asyncio
import logging
import io
from typing import Any, ContextManager, Tuple, Optional, Protocol, Dict
from typing import Any, ContextManager, Optional, Protocol
from bumble import core
from bumble import hci
@@ -38,7 +38,7 @@ logger = logging.getLogger(__name__)
# Information needed to parse HCI packets with a generic parser:
# For each packet type, the info represents:
# (length-size, length-offset, unpack-type)
HCI_PACKET_INFO: Dict[int, Tuple[int, int, str]] = {
HCI_PACKET_INFO: dict[int, tuple[int, int, str]] = {
hci.HCI_COMMAND_PACKET: (1, 2, 'B'),
hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
@@ -108,8 +108,8 @@ class PacketParser:
NEED_BODY = 2
sink: Optional[TransportSink]
extended_packet_info: Dict[int, Tuple[int, int, str]]
packet_info: Optional[Tuple[int, int, str]] = None
extended_packet_info: dict[int, tuple[int, int, str]]
packet_info: Optional[tuple[int, int, str]] = None
def __init__(self, sink: Optional[TransportSink] = None) -> None:
self.sink = sink

View File

@@ -23,7 +23,7 @@ import time
import usb.core
import usb.util
from typing import Optional, Set
from typing import Optional
from usb.core import Device as UsbDevice
from usb.core import USBError
from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER
@@ -49,7 +49,7 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Global
# -----------------------------------------------------------------------------
devices_in_use: Set[int] = set()
devices_in_use: set[int] = set()
# -----------------------------------------------------------------------------

View File

@@ -27,11 +27,8 @@ from typing import (
Any,
Awaitable,
Callable,
List,
Optional,
Protocol,
Set,
Tuple,
TypeVar,
Union,
overload,
@@ -156,7 +153,7 @@ class EventWatcher:
```
'''
handlers: List[Tuple[pyee.EventEmitter, str, Callable[..., Any]]]
handlers: list[tuple[pyee.EventEmitter, str, Callable[..., Any]]]
def __init__(self) -> None:
self.handlers = []
@@ -329,7 +326,7 @@ class AsyncRunner:
default_queue = WorkQueue()
# Shared set of running tasks
running_tasks: Set[Awaitable] = set()
running_tasks: set[Awaitable] = set()
@staticmethod
def run_in_task(queue=None):

View File

@@ -16,7 +16,7 @@
# Imports
# -----------------------------------------------------------------------------
import struct
from typing import Dict, Optional, Type
from typing import Optional
from bumble.hci import (
name_or_number,
@@ -54,8 +54,13 @@ HCI_Command.register_commands(globals())
# -----------------------------------------------------------------------------
@HCI_Command.command(
return_parameters_fields=[
@HCI_Command.command()
class HCI_LE_Get_Vendor_Capabilities_Command(HCI_Command):
# pylint: disable=line-too-long
'''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#vendor-specific-capabilities
'''
return_parameters_fields = [
('status', STATUS_SPEC),
('max_advt_instances', 1),
('offloaded_resolution_of_private_address', 1),
@@ -73,12 +78,6 @@ HCI_Command.register_commands(globals())
('bluetooth_quality_report_support', 1),
('dynamic_audio_buffer_support', 4),
]
)
class HCI_LE_Get_Vendor_Capabilities_Command(HCI_Command):
# pylint: disable=line-too-long
'''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#vendor-specific-capabilities
'''
@classmethod
def parse_return_parameters(cls, parameters):
@@ -115,17 +114,6 @@ class HCI_LE_Get_Vendor_Capabilities_Command(HCI_Command):
),
('payload', '*'),
],
return_parameters_fields=[
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_LE_APCF_Command.opcode_name(x),
},
),
('payload', '*'),
],
)
class HCI_LE_APCF_Command(HCI_Command):
# pylint: disable=line-too-long
@@ -135,6 +123,17 @@ class HCI_LE_APCF_Command(HCI_Command):
NOTE: the subcommand-specific payloads are left as opaque byte arrays in this
implementation. A future enhancement may define subcommand-specific data structures.
'''
return_parameters_fields = [
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_LE_APCF_Command.opcode_name(x),
},
),
('payload', '*'),
]
# APCF Subcommands
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
@@ -170,20 +169,19 @@ class HCI_LE_APCF_Command(HCI_Command):
# -----------------------------------------------------------------------------
@HCI_Command.command(
return_parameters_fields=[
('status', STATUS_SPEC),
('total_tx_time_ms', 4),
('total_rx_time_ms', 4),
('total_idle_time_ms', 4),
('total_energy_used', 4),
],
)
@HCI_Command.command()
class HCI_Get_Controller_Activity_Energy_Info_Command(HCI_Command):
# pylint: disable=line-too-long
'''
See https://source.android.com/docs/core/connect/bluetooth/hci_requirements#le_get_controller_activity_energy_info
'''
return_parameters_fields = [
('status', STATUS_SPEC),
('total_tx_time_ms', 4),
('total_rx_time_ms', 4),
('total_idle_time_ms', 4),
('total_energy_used', 4),
]
# -----------------------------------------------------------------------------
@@ -198,17 +196,6 @@ class HCI_Get_Controller_Activity_Energy_Info_Command(HCI_Command):
),
('payload', '*'),
],
return_parameters_fields=[
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_A2DP_Hardware_Offload_Command.opcode_name(x),
},
),
('payload', '*'),
],
)
class HCI_A2DP_Hardware_Offload_Command(HCI_Command):
# pylint: disable=line-too-long
@@ -218,6 +205,17 @@ class HCI_A2DP_Hardware_Offload_Command(HCI_Command):
NOTE: the subcommand-specific payloads are left as opaque byte arrays in this
implementation. A future enhancement may define subcommand-specific data structures.
'''
return_parameters_fields = [
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_A2DP_Hardware_Offload_Command.opcode_name(x),
},
),
('payload', '*'),
]
# A2DP Hardware Offload Subcommands
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
@@ -246,17 +244,6 @@ class HCI_A2DP_Hardware_Offload_Command(HCI_Command):
),
('payload', '*'),
],
return_parameters_fields=[
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_Dynamic_Audio_Buffer_Command.opcode_name(x),
},
),
('payload', '*'),
],
)
class HCI_Dynamic_Audio_Buffer_Command(HCI_Command):
# pylint: disable=line-too-long
@@ -266,6 +253,17 @@ class HCI_Dynamic_Audio_Buffer_Command(HCI_Command):
NOTE: the subcommand-specific payloads are left as opaque byte arrays in this
implementation. A future enhancement may define subcommand-specific data structures.
'''
return_parameters_fields = [
('status', STATUS_SPEC),
(
'opcode',
{
'size': 1,
'mapper': lambda x: HCI_Dynamic_Audio_Buffer_Command.opcode_name(x),
},
),
('payload', '*'),
]
# Dynamic Audio Buffer Subcommands
# TODO: use the OpenIntEnum class (when upcoming PR is merged)
@@ -283,7 +281,7 @@ class HCI_Dynamic_Audio_Buffer_Command(HCI_Command):
# -----------------------------------------------------------------------------
class HCI_Android_Vendor_Event(HCI_Extended_Event):
event_code: int = HCI_VENDOR_EVENT
subevent_classes: Dict[int, Type[HCI_Extended_Event]] = {}
subevent_classes: dict[int, type[HCI_Extended_Event]] = {}
@classmethod
def subclass_from_parameters(

View File

@@ -51,12 +51,6 @@ class TX_Power_Level_Command:
# -----------------------------------------------------------------------------
@HCI_Command.command(
fields=[('handle_type', 1), ('connection_handle', 2), ('tx_power_level', -1)],
return_parameters_fields=[
('status', STATUS_SPEC),
('handle_type', 1),
('connection_handle', 2),
('selected_tx_power_level', -1),
],
)
class HCI_Write_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
'''
@@ -67,16 +61,17 @@ class HCI_Write_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
TX_POWER_HANDLE_TYPE_SCAN should be zero.
'''
return_parameters_fields = [
('status', STATUS_SPEC),
('handle_type', 1),
('connection_handle', 2),
('selected_tx_power_level', -1),
]
# -----------------------------------------------------------------------------
@HCI_Command.command(
fields=[('handle_type', 1), ('connection_handle', 2)],
return_parameters_fields=[
('status', STATUS_SPEC),
('handle_type', 1),
('connection_handle', 2),
('tx_power_level', -1),
],
)
class HCI_Read_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
'''
@@ -86,3 +81,10 @@ class HCI_Read_Tx_Power_Level_Command(HCI_Command, TX_Power_Level_Command):
Power level is in dB. Connection handle for TX_POWER_HANDLE_TYPE_ADV and
TX_POWER_HANDLE_TYPE_SCAN should be zero.
'''
return_parameters_fields = [
('status', STATUS_SPEC),
('handle_type', 1),
('connection_handle', 2),
('tx_power_level', -1),
]

View File

@@ -19,7 +19,7 @@ import asyncio
import sys
import os
import logging
from typing import Any, Dict
from typing import Any
from bumble.device import Device
from bumble.transport import open_transport_or_link
@@ -36,7 +36,7 @@ from bumble.a2dp import (
SbcMediaCodecInformation,
)
Context: Dict[Any, Any] = {'output': None}
Context: dict[Any, Any] = {'output': None}
# -----------------------------------------------------------------------------

View File

@@ -25,7 +25,6 @@ from bumble.device import Device, Peer
from bumble.transport import open_transport
from bumble.profiles.ancs import (
AncsClient,
AppAttribute,
AppAttributeId,
EventFlags,
EventId,

View File

@@ -75,9 +75,7 @@ async def main() -> None:
def on_cis_request(
connection: Connection, cis_handle: int, _cig_id: int, _cis_id: int
):
utils.cancel_on_event(
connection, 'disconnection', devices[0].accept_cis_request(cis_handle)
)
connection.cancel_on_disconnection(devices[0].accept_cis_request(cis_handle))
devices[0].on('cis_request', on_cis_request)

View File

@@ -25,8 +25,6 @@ from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.hci import (
Address,
OwnAddressType,
HCI_LE_Set_Extended_Advertising_Parameters_Command,
)
from bumble.profiles.cap import CommonAudioServiceService
from bumble.profiles.csip import CoordinatedSetIdentificationService, SirkType

View File

@@ -19,7 +19,6 @@ import asyncio
import sys
import os
import logging
from bumble.colors import color
from bumble.hci import Address
from bumble.device import Device
from bumble.transport import open_transport_or_link

View File

@@ -0,0 +1,111 @@
# 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.gatt import (
Service,
Characteristic,
)
from bumble.pairing import PairingConfig, PairingDelegate
# -----------------------------------------------------------------------------
class FixedPinPairingDelegate(PairingDelegate):
"""
A PairingDelegate that declares that the device only has the ability to display
a passkey but not to enter or confirm one. When asked for the passkey to use for
pairing, this delegate returns a fixed value (instead of the default, which is
to generate a random value each time). This is obviously not a secure way to do
pairing, but it used here as an illustration of how a delegate can override the
default passkey generation.
"""
def __init__(self, passkey: int) -> None:
super().__init__(io_capability=PairingDelegate.IoCapability.DISPLAY_OUTPUT_ONLY)
self.passkey = passkey
async def generate_passkey(self) -> int:
return self.passkey
# -----------------------------------------------------------------------------
async def main() -> None:
if len(sys.argv) < 3:
print(
'Usage: run_gatt_server_with_pairing_delegate.py <device-config> <transport-spec> '
)
print('example: run_gatt_server_with_pairing_delegate.py device1.json usb:0')
return
print('<<< connecting to HCI...')
async with await open_transport_or_link(sys.argv[2]) as hci_transport:
print('<<< connected')
# Create a device to manage the host
device = Device.from_config_file_with_hci(
sys.argv[1], hci_transport.source, hci_transport.sink
)
# Add a service with a single characteristic.
# The characteristic requires authentication, so reading it on a non-paired
# connection will return an error.
custom_service1 = Service(
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
[
Characteristic(
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
Characteristic.Properties.READ,
Characteristic.READABLE
| Characteristic.READ_REQUIRES_AUTHENTICATION,
bytes('hello', 'utf-8'),
),
],
)
device.add_services([custom_service1])
# Debug print
for attribute in device.gatt_server.attributes:
print(attribute)
# Setup pairing
device.pairing_config_factory = lambda connection: PairingConfig(
delegate=FixedPinPairingDelegate(123456)
)
# 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_transport.source.wait_for_termination()
# -----------------------------------------------------------------------------
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
asyncio.run(main())

View File

@@ -25,7 +25,7 @@ import os
import random
import struct
import sys
from typing import Any, List, Union
from typing import Any, Union
from bumble.device import Device, Peer
from bumble import transport
@@ -342,7 +342,7 @@ async def server(device: Device) -> None:
byteorder='big',
)
characteristics: List[gatt.Characteristic] = [
characteristics: list[gatt.Characteristic] = [
c1,
c2,
c3,

View File

@@ -22,7 +22,6 @@ import os
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble import att
from bumble.profiles.hap import (
HearingAccessService,
HearingAidFeatures,

View File

@@ -61,14 +61,12 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.HfConfiguration):
else:
raise RuntimeError("unknown active codec")
utils.cancel_on_event(
connection,
'disconnection',
connection.cancel_on_disconnection(
connection.device.send_command(
hci.HCI_Enhanced_Accept_Synchronous_Connection_Request_Command(
bd_addr=connection.peer_address, **esco_parameters.asdict()
)
),
)
)
handler = functools.partial(on_sco_request, protocol=hf_protocol)

View File

@@ -170,7 +170,7 @@ async def main() -> None:
mcp.on('track_position', on_track_position)
await mcp.subscribe_characteristics()
utils.cancel_on_event(connection, 'disconnection', on_connection_async())
connection.cancel_on_disconnection(on_connection_async())
device.on('connection', on_connection)

View File

@@ -25,8 +25,6 @@ import io
import struct
import secrets
from typing import Dict
from bumble.core import AdvertisingData
from bumble.device import Device
from bumble.hci import (
@@ -73,7 +71,7 @@ def _sink_pac_record() -> PacRecord:
)
file_outputs: Dict[AseStateMachine, io.BufferedWriter] = {}
file_outputs: dict[AseStateMachine, io.BufferedWriter] = {}
# -----------------------------------------------------------------------------

View File

@@ -13,11 +13,11 @@ dependencies = [
"aiohttp ~= 3.8; platform_system!='Emscripten'",
"appdirs >= 1.4; platform_system!='Emscripten'",
"click >= 8.1.3; platform_system!='Emscripten'",
"cryptography >= 39; platform_system!='Emscripten'",
"cryptography >= 44.0.3; platform_system!='Emscripten'",
# Pyodide bundles a version of cryptography that is built for wasm, which may not match the
# versions available on PyPI. Relax the version requirement since it's better than being
# completely unable to import the package in case of version mismatch.
"cryptography >= 39.0; platform_system=='Emscripten'",
"cryptography >= 44.0.3; platform_system=='Emscripten'",
"grpcio >= 1.62.1; platform_system!='Emscripten'",
"humanize >= 4.6.0; platform_system!='Emscripten'",
"libusb1 >= 2.0.1; platform_system!='Emscripten'",

View File

@@ -22,7 +22,6 @@ Invoke tasks
import os
import glob
import shutil
import urllib
from pathlib import Path
from invoke import task, call, Collection
from invoke.exceptions import Exit, UnexpectedExit

View File

@@ -431,12 +431,7 @@ async def test_ascs():
)
# Release
await ascs_client.ase_control_point.write_value(
ASE_Release(
ase_id=[1, 2],
metadata=[b'foo', b'bar'],
)
)
await ascs_client.ase_control_point.write_value(ASE_Release(ase_id=[1, 2]))
assert (await notifications[1].get())[:2] == bytes(
[1, AseStateMachine.State.RELEASING]
)

View File

@@ -15,7 +15,6 @@
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
from enum import IntEnum
from bumble.core import AdvertisingData, Appearance, UUID, get_dict_key_by_value

View File

@@ -19,6 +19,7 @@ import asyncio
import functools
import logging
import os
from unittest import mock
import pytest
from bumble.core import (
@@ -33,6 +34,8 @@ from bumble.device import (
PeriodicAdvertisingParameters,
)
from bumble.host import DataPacketQueue, Host
from bumble import device
from bumble import hci
from bumble.hci import (
HCI_ACCEPT_CONNECTION_REQUEST_COMMAND,
HCI_COMMAND_STATUS_PENDING,
@@ -483,8 +486,8 @@ async def test_cis():
_cig_id: int,
_cis_id: int,
):
utils.cancel_on_event(
acl_connection, 'disconnection', devices[1].accept_cis_request(cis_handle)
acl_connection.cancel_on_disconnection(
devices[1].accept_cis_request(cis_handle)
)
peripheral_cis_futures[cis_handle] = asyncio.get_running_loop().create_future()
@@ -630,6 +633,73 @@ def test_gatt_services_with_gas_and_gatt():
)
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_inquiry_result():
d = (await TwoDevices.create_with_connection())[0]
m = mock.Mock()
d.on(d.EVENT_INQUIRY_RESULT, m)
d.host.on_packet(
bytes(
hci.HCI_Extended_Inquiry_Result_Event(
num_responses=1,
bd_addr=hci.Address("00:11:22:33:44:55/P"),
page_scan_repetition_mode=2,
reserved=0,
class_of_device=3,
clock_offset=4,
rssi=5,
extended_inquiry_response=b"6789",
)
)
)
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_extended_inquiry_result():
d = (await TwoDevices.create_with_connection())[0]
m = mock.Mock()
d.on(d.EVENT_INQUIRY_RESULT, m)
d.host.on_packet(
bytes(
hci.HCI_Extended_Inquiry_Result_Event(
num_responses=1,
bd_addr=hci.Address("00:11:22:33:44:55/P"),
page_scan_repetition_mode=2,
reserved=0,
class_of_device=3,
clock_offset=4,
rssi=5,
extended_inquiry_response=b"6789",
)
)
)
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_inquiry_result_with_rssi():
d = (await TwoDevices.create_with_connection())[0]
m = mock.Mock()
d.on(d.EVENT_INQUIRY_RESULT, m)
d.host.on_packet(
bytes(
hci.HCI_Inquiry_Result_With_RSSI_Event(
bd_addr=[hci.Address("00:11:22:33:44:55/P")],
page_scan_repetition_mode=[2],
reserved=[0],
class_of_device=[3],
clock_offset=[4],
rssi=[5],
)
)
)
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
# -----------------------------------------------------------------------------
async def run_test_device():
await test_device_connect_parallel()

View File

@@ -136,9 +136,9 @@ async def test_characteristic_encoding():
Characteristic.READABLE,
123,
)
x = await c.read_value(None)
x = await c.read_value(Mock())
assert x == bytes([123])
await c.write_value(None, bytes([122]))
await c.write_value(Mock(), bytes([122]))
assert c.value == 122
class FooProxy(CharacteristicProxy):
@@ -334,7 +334,7 @@ async def test_CharacteristicAdapter() -> None:
)
v = bytes([3, 4, 5])
await c.write_value(None, v)
await c.write_value(Mock(), v)
assert c.value == v
# Simple delegated adapter
@@ -342,11 +342,11 @@ async def test_CharacteristicAdapter() -> None:
c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x))
)
delegated_value = await delegated.read_value(None)
delegated_value = await delegated.read_value(Mock())
assert delegated_value == bytes(reversed(v))
delegated_value2 = bytes([3, 4, 5])
await delegated.write_value(None, delegated_value2)
await delegated.write_value(Mock(), delegated_value2)
assert delegated.value == bytes(reversed(delegated_value2))
# Packed adapter with single element format
@@ -355,10 +355,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = packed_value_ref
packed = PackedCharacteristicAdapter(c, '>H')
packed_value_read = await packed.read_value(None)
packed_value_read = await packed.read_value(Mock())
assert packed_value_read == packed_value_bytes
c.value = b''
await packed.write_value(None, packed_value_bytes)
await packed.write_value(Mock(), packed_value_bytes)
assert packed.value == packed_value_ref
# Packed adapter with multi-element format
@@ -368,10 +368,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = (v1, v2)
packed_multi = PackedCharacteristicAdapter(c, '>HH')
packed_multi_read_value = await packed_multi.read_value(None)
packed_multi_read_value = await packed_multi.read_value(Mock())
assert packed_multi_read_value == packed_multi_value_bytes
packed_multi.value = b''
await packed_multi.write_value(None, packed_multi_value_bytes)
await packed_multi.write_value(Mock(), packed_multi_value_bytes)
assert packed_multi.value == (v1, v2)
# Mapped adapter
@@ -382,10 +382,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = mapped
packed_mapped = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2'))
packed_mapped_read_value = await packed_mapped.read_value(None)
packed_mapped_read_value = await packed_mapped.read_value(Mock())
assert packed_mapped_read_value == packed_mapped_value_bytes
c.value = b''
await packed_mapped.write_value(None, packed_mapped_value_bytes)
await packed_mapped.write_value(Mock(), packed_mapped_value_bytes)
assert packed_mapped.value == mapped
# UTF-8 adapter
@@ -394,10 +394,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = string_value
string_c = UTF8CharacteristicAdapter(c)
string_read_value = await string_c.read_value(None)
string_read_value = await string_c.read_value(Mock())
assert string_read_value == string_value_bytes
c.value = b''
await string_c.write_value(None, string_value_bytes)
await string_c.write_value(Mock(), string_value_bytes)
assert string_c.value == string_value
# Class adapter
@@ -419,10 +419,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = class_value
class_c = SerializableCharacteristicAdapter(c, BlaBla)
class_read_value = await class_c.read_value(None)
class_read_value = await class_c.read_value(Mock())
assert class_read_value == class_value_bytes
class_c.value = b''
await class_c.write_value(None, class_value_bytes)
await class_c.write_value(Mock(), class_value_bytes)
assert isinstance(class_c.value, BlaBla)
assert class_c.value.a == class_value.a
assert class_c.value.b == class_value.b
@@ -436,10 +436,10 @@ async def test_CharacteristicAdapter() -> None:
enum_value_bytes = int(enum_value).to_bytes(3, 'big')
c.value = enum_value
enum_c = EnumCharacteristicAdapter(c, MyEnum, 3, 'big')
enum_read_value = await enum_c.read_value(None)
enum_read_value = await enum_c.read_value(Mock())
assert enum_read_value == enum_value_bytes
enum_c.value = b''
await enum_c.write_value(None, enum_value_bytes)
await enum_c.write_value(Mock(), enum_value_bytes)
assert isinstance(enum_c.value, MyEnum)
assert enum_c.value == enum_value
@@ -1254,7 +1254,7 @@ Characteristic(handle=0x0005, end=0x0005, uuid=UUID-16:2A01 (Appearance), READ)
Service(handle=0x0006, end=0x000D, uuid=UUID-16:1801 (Generic Attribute))
CharacteristicDeclaration(handle=0x0007, value_handle=0x0008, uuid=UUID-16:2A05 (Service Changed), INDICATE)
Characteristic(handle=0x0008, end=0x0009, uuid=UUID-16:2A05 (Service Changed), INDICATE)
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=<dynamic>)
CharacteristicDeclaration(handle=0x000A, value_handle=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE)
Characteristic(handle=0x000B, end=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE)
CharacteristicDeclaration(handle=0x000C, value_handle=0x000D, uuid=UUID-16:2B2A (Database Hash), READ)
@@ -1262,7 +1262,7 @@ Characteristic(handle=0x000D, end=0x000D, uuid=UUID-16:2B2A (Database Hash), REA
Service(handle=0x000E, end=0x0011, uuid=3A657F47-D34F-46B3-B1EC-698E29B6B829)
CharacteristicDeclaration(handle=0x000F, value_handle=0x0010, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
Characteristic(handle=0x0010, end=0x0011, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
Descriptor(handle=0x0011, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)"""
Descriptor(handle=0x0011, type=UUID-16:2902 (Client Characteristic Configuration), value=<dynamic>)"""
)

View File

@@ -17,7 +17,6 @@
# -----------------------------------------------------------------------------
import asyncio
import pytest
import functools
import pytest_asyncio
import logging
import sys

View File

@@ -15,62 +15,12 @@
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
import inspect
import struct
from bumble.hci import (
HCI_DISCONNECT_COMMAND,
HCI_LE_1M_PHY_BIT,
HCI_LE_CODED_PHY_BIT,
HCI_LE_READ_BUFFER_SIZE_COMMAND,
HCI_RESET_COMMAND,
HCI_VENDOR_EVENT,
HCI_SUCCESS,
HCI_LE_CONNECTION_COMPLETE_EVENT,
HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
Address,
CodingFormat,
CodecID,
HCI_Command,
HCI_Command_Complete_Event,
HCI_Command_Status_Event,
HCI_CustomPacket,
HCI_Disconnect_Command,
HCI_Event,
HCI_IsoDataPacket,
HCI_LE_Add_Device_To_Filter_Accept_List_Command,
HCI_LE_Advertising_Report_Event,
HCI_LE_Channel_Selection_Algorithm_Event,
HCI_LE_Connection_Complete_Event,
HCI_LE_Connection_Update_Command,
HCI_LE_Connection_Update_Complete_Event,
HCI_LE_Create_Connection_Command,
HCI_LE_Extended_Create_Connection_Command,
HCI_LE_Read_Buffer_Size_Command,
HCI_LE_Read_Remote_Features_Command,
HCI_LE_Read_Remote_Features_Complete_Event,
HCI_LE_Remove_Device_From_Filter_Accept_List_Command,
HCI_LE_Set_Advertising_Data_Command,
HCI_LE_Set_Advertising_Parameters_Command,
HCI_LE_Set_Default_PHY_Command,
HCI_LE_Set_Event_Mask_Command,
HCI_LE_Set_Extended_Advertising_Enable_Command,
HCI_LE_Set_Extended_Scan_Parameters_Command,
HCI_LE_Set_Random_Address_Command,
HCI_LE_Set_Scan_Enable_Command,
HCI_LE_Set_Scan_Parameters_Command,
HCI_LE_Setup_ISO_Data_Path_Command,
HCI_Number_Of_Completed_Packets_Event,
HCI_Packet,
HCI_PIN_Code_Request_Reply_Command,
HCI_Read_Local_Supported_Codecs_Command,
HCI_Read_Local_Supported_Codecs_V2_Command,
HCI_Read_Local_Supported_Commands_Command,
HCI_Read_Local_Supported_Features_Command,
HCI_Read_Local_Version_Information_Command,
HCI_Reset_Command,
HCI_Set_Event_Mask_Command,
HCI_Vendor_Event,
)
import pytest
from bumble import hci
# -----------------------------------------------------------------------------
@@ -80,7 +30,7 @@ from bumble.hci import (
def basic_check(x):
packet = bytes(x)
print(packet.hex())
parsed = HCI_Packet.from_bytes(packet)
parsed = hci.HCI_Packet.from_bytes(packet)
x_str = str(x)
parsed_str = str(parsed)
print(x_str)
@@ -91,18 +41,18 @@ def basic_check(x):
# -----------------------------------------------------------------------------
def test_HCI_Event():
event = HCI_Event(0xF9)
event = hci.HCI_Event(event_code=0xF9)
basic_check(event)
event = HCI_Event(0xF8, bytes.fromhex('AABBCC'))
event = hci.HCI_Event(event_code=0xF8, parameters=bytes.fromhex('AABBCC'))
basic_check(event)
# -----------------------------------------------------------------------------
def test_HCI_LE_Connection_Complete_Event():
address = Address('00:11:22:33:44:55')
event = HCI_LE_Connection_Complete_Event(
status=HCI_SUCCESS,
address = hci.Address('00:11:22:33:44:55')
event = hci.HCI_LE_Connection_Complete_Event(
status=hci.HCI_SUCCESS,
connection_handle=1,
role=1,
peer_address_type=1,
@@ -117,25 +67,47 @@ def test_HCI_LE_Connection_Complete_Event():
# -----------------------------------------------------------------------------
def test_HCI_LE_Advertising_Report_Event():
address = Address('00:11:22:33:44:55/P')
report = HCI_LE_Advertising_Report_Event.Report(
HCI_LE_Advertising_Report_Event.Report.FIELDS,
event_type=HCI_LE_Advertising_Report_Event.ADV_IND,
address_type=Address.PUBLIC_DEVICE_ADDRESS,
address = hci.Address('00:11:22:33:44:55/P')
report = hci.HCI_LE_Advertising_Report_Event.Report(
event_type=hci.HCI_LE_Advertising_Report_Event.EventType.ADV_IND,
address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
address=address,
data=bytes.fromhex(
'0201061106ba5689a6fabfa2bd01467d6e00fbabad08160a181604659b03'
),
rssi=100,
)
event = HCI_LE_Advertising_Report_Event([report])
event = hci.HCI_LE_Advertising_Report_Event([report])
basic_check(event)
# -----------------------------------------------------------------------------
def test_HCI_LE_Extended_Advertising_Report_Event():
address = hci.Address('00:11:22:33:44:55/P')
report = hci.HCI_LE_Extended_Advertising_Report_Event.Report(
event_type=hci.HCI_LE_Extended_Advertising_Report_Event.EventType.CONNECTABLE_ADVERTISING,
address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
address=address,
data=bytes.fromhex(
'0201061106ba5689a6fabfa2bd01467d6e00fbabad08160a181604659b03'
),
rssi=100,
primary_phy=hci.HCI_LE_1M_PHY,
secondary_phy=hci.HCI_LE_CODED_PHY,
advertising_sid=0,
tx_power=10,
periodic_advertising_interval=2,
direct_address=hci.Address('00:11:22:33:44:55/P'),
direct_address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
)
event = hci.HCI_LE_Extended_Advertising_Report_Event([report])
basic_check(event)
# -----------------------------------------------------------------------------
def test_HCI_LE_Read_Remote_Features_Complete_Event():
event = HCI_LE_Read_Remote_Features_Complete_Event(
status=HCI_SUCCESS,
event = hci.HCI_LE_Read_Remote_Features_Complete_Event(
status=hci.HCI_SUCCESS,
connection_handle=0x007,
le_features=bytes.fromhex('0011223344556677'),
)
@@ -144,8 +116,8 @@ def test_HCI_LE_Read_Remote_Features_Complete_Event():
# -----------------------------------------------------------------------------
def test_HCI_LE_Connection_Update_Complete_Event():
event = HCI_LE_Connection_Update_Complete_Event(
status=HCI_SUCCESS,
event = hci.HCI_LE_Connection_Update_Complete_Event(
status=hci.HCI_SUCCESS,
connection_handle=0x007,
connection_interval=10,
peripheral_latency=3,
@@ -156,7 +128,7 @@ def test_HCI_LE_Connection_Update_Complete_Event():
# -----------------------------------------------------------------------------
def test_HCI_LE_Channel_Selection_Algorithm_Event():
event = HCI_LE_Channel_Selection_Algorithm_Event(
event = hci.HCI_LE_Channel_Selection_Algorithm_Event(
connection_handle=7, channel_selection_algorithm=1
)
basic_check(event)
@@ -165,10 +137,10 @@ def test_HCI_LE_Channel_Selection_Algorithm_Event():
# -----------------------------------------------------------------------------
def test_HCI_Command_Complete_Event():
# With a serializable object
event = HCI_Command_Complete_Event(
event = hci.HCI_Command_Complete_Event(
num_hci_command_packets=34,
command_opcode=HCI_LE_READ_BUFFER_SIZE_COMMAND,
return_parameters=HCI_LE_Read_Buffer_Size_Command.create_return_parameters(
command_opcode=hci.HCI_LE_READ_BUFFER_SIZE_COMMAND,
return_parameters=hci.HCI_LE_Read_Buffer_Size_Command.create_return_parameters(
status=0,
le_acl_data_packet_length=1234,
total_num_le_acl_data_packets=56,
@@ -177,26 +149,28 @@ def test_HCI_Command_Complete_Event():
basic_check(event)
# With an arbitrary byte array
event = HCI_Command_Complete_Event(
event = hci.HCI_Command_Complete_Event(
num_hci_command_packets=1,
command_opcode=HCI_RESET_COMMAND,
command_opcode=hci.HCI_RESET_COMMAND,
return_parameters=bytes([1, 2, 3, 4]),
)
basic_check(event)
# With a simple status as a 1-byte array
event = HCI_Command_Complete_Event(
event = hci.HCI_Command_Complete_Event(
num_hci_command_packets=1,
command_opcode=HCI_RESET_COMMAND,
command_opcode=hci.HCI_RESET_COMMAND,
return_parameters=bytes([7]),
)
basic_check(event)
event = HCI_Packet.from_bytes(bytes(event))
event = hci.HCI_Packet.from_bytes(bytes(event))
assert event.return_parameters == 7
# With a simple status as an integer status
event = HCI_Command_Complete_Event(
num_hci_command_packets=1, command_opcode=HCI_RESET_COMMAND, return_parameters=9
event = hci.HCI_Command_Complete_Event(
num_hci_command_packets=1,
command_opcode=hci.HCI_RESET_COMMAND,
return_parameters=9,
)
basic_check(event)
assert event.return_parameters == 9
@@ -204,30 +178,35 @@ def test_HCI_Command_Complete_Event():
# -----------------------------------------------------------------------------
def test_HCI_Command_Status_Event():
event = HCI_Command_Status_Event(
status=0, num_hci_command_packets=37, command_opcode=HCI_DISCONNECT_COMMAND
event = hci.HCI_Command_Status_Event(
status=0, num_hci_command_packets=37, command_opcode=hci.HCI_DISCONNECT_COMMAND
)
basic_check(event)
# -----------------------------------------------------------------------------
def test_HCI_Number_Of_Completed_Packets_Event():
event = HCI_Number_Of_Completed_Packets_Event([(1, 2), (3, 4)])
event = hci.HCI_Number_Of_Completed_Packets_Event(
connection_handles=(1, 2),
num_completed_packets=(3, 4),
)
basic_check(event)
# -----------------------------------------------------------------------------
def test_HCI_Vendor_Event():
data = bytes.fromhex('01020304')
event = HCI_Vendor_Event(data=data)
event = hci.HCI_Vendor_Event(data=data)
event_bytes = bytes(event)
parsed = HCI_Packet.from_bytes(event_bytes)
assert isinstance(parsed, HCI_Vendor_Event)
parsed = hci.HCI_Packet.from_bytes(event_bytes)
assert isinstance(parsed, hci.HCI_Vendor_Event)
assert parsed.data == data
class HCI_Custom_Event(HCI_Event):
class HCI_Custom_Event(hci.HCI_Event):
def __init__(self, blabla):
super().__init__(HCI_VENDOR_EVENT, parameters=struct.pack("<I", blabla))
super().__init__(
event_code=hci.HCI_VENDOR_EVENT, parameters=struct.pack("<I", blabla)
)
self.name = 'HCI_CUSTOM_EVENT'
self.blabla = blabla
@@ -236,41 +215,91 @@ def test_HCI_Vendor_Event():
return HCI_Custom_Event(blabla=struct.unpack('<I', payload)[0])
return None
HCI_Event.add_vendor_factory(create_event)
parsed = HCI_Packet.from_bytes(event_bytes)
hci.HCI_Event.add_vendor_factory(create_event)
parsed = hci.HCI_Packet.from_bytes(event_bytes)
assert isinstance(parsed, HCI_Custom_Event)
assert parsed.blabla == 0x04030201
event_bytes2 = event_bytes[:3] + bytes([7]) + event_bytes[4:]
parsed = HCI_Packet.from_bytes(event_bytes2)
parsed = hci.HCI_Packet.from_bytes(event_bytes2)
assert not isinstance(parsed, HCI_Custom_Event)
assert isinstance(parsed, HCI_Vendor_Event)
HCI_Event.remove_vendor_factory(create_event)
assert isinstance(parsed, hci.HCI_Vendor_Event)
hci.HCI_Event.remove_vendor_factory(create_event)
parsed = HCI_Packet.from_bytes(event_bytes)
parsed = hci.HCI_Packet.from_bytes(event_bytes)
assert not isinstance(parsed, HCI_Custom_Event)
assert isinstance(parsed, HCI_Vendor_Event)
assert isinstance(parsed, hci.HCI_Vendor_Event)
# -----------------------------------------------------------------------------
def test_HCI_Command():
command = HCI_Command(0x5566)
command = hci.HCI_Command(op_code=0x5566)
basic_check(command)
command = HCI_Command(0x5566, bytes.fromhex('AABBCC'))
command = hci.HCI_Command(op_code=0x5566, parameters=bytes.fromhex('AABBCC'))
basic_check(command)
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
"clazz,",
[
clazz[1]
for clazz in inspect.getmembers(hci)
if isinstance(clazz[1], type)
and issubclass(clazz[1], hci.HCI_Command)
and clazz[1] is not hci.HCI_Command
],
)
def test_hci_command_subclasses_op_code(clazz: type[hci.HCI_Command]):
assert clazz.op_code > 0
assert isinstance(clazz.name, str)
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
"clazz,",
[
clazz[1]
for clazz in inspect.getmembers(hci)
if isinstance(clazz[1], type)
and clazz[1] is not hci.HCI_Event
and issubclass(clazz[1], hci.HCI_Event)
and not issubclass(clazz[1], hci.HCI_Extended_Event)
],
)
def test_hci_event_subclasses_event_code(clazz: type[hci.HCI_Event]):
assert clazz.event_code > 0
assert isinstance(clazz.name, str)
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
"clazz,",
[
clazz[1]
for clazz in inspect.getmembers(hci)
if isinstance(clazz[1], type)
and issubclass(clazz[1], hci.HCI_Extended_Event)
and clazz[1] not in (hci.HCI_Extended_Event, hci.HCI_LE_Meta_Event)
],
)
def test_hci_extended_event_subclasses_event_code(clazz: type[hci.HCI_Extended_Event]):
assert clazz.event_code > 0
assert clazz.subevent_code > 0
assert isinstance(clazz.name, str)
# -----------------------------------------------------------------------------
def test_HCI_PIN_Code_Request_Reply_Command():
pin_code = b'1234'
pin_code_length = len(pin_code)
# here to make the test pass, we need to
# pad pin_code, as HCI_Object.format_fields
# pad pin_code, as hci.HCI_Object.format_fields
# does not do it for us
padded_pin_code = pin_code + bytes(16 - pin_code_length)
command = HCI_PIN_Code_Request_Reply_Command(
bd_addr=Address(
'00:11:22:33:44:55', address_type=Address.PUBLIC_DEVICE_ADDRESS
command = hci.HCI_PIN_Code_Request_Reply_Command(
bd_addr=hci.Address(
'00:11:22:33:44:55', address_type=hci.Address.PUBLIC_DEVICE_ADDRESS
),
pin_code_length=pin_code_length,
pin_code=padded_pin_code,
@@ -279,47 +308,49 @@ def test_HCI_PIN_Code_Request_Reply_Command():
def test_HCI_Reset_Command():
command = HCI_Reset_Command()
command = hci.HCI_Reset_Command()
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_Read_Local_Version_Information_Command():
command = HCI_Read_Local_Version_Information_Command()
command = hci.HCI_Read_Local_Version_Information_Command()
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_Read_Local_Supported_Commands_Command():
command = HCI_Read_Local_Supported_Commands_Command()
command = hci.HCI_Read_Local_Supported_Commands_Command()
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_Read_Local_Supported_Features_Command():
command = HCI_Read_Local_Supported_Features_Command()
command = hci.HCI_Read_Local_Supported_Features_Command()
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_Disconnect_Command():
command = HCI_Disconnect_Command(connection_handle=123, reason=0x11)
command = hci.HCI_Disconnect_Command(connection_handle=123, reason=0x11)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_Set_Event_Mask_Command():
command = HCI_Set_Event_Mask_Command(event_mask=bytes.fromhex('0011223344556677'))
command = hci.HCI_Set_Event_Mask_Command(
event_mask=bytes.fromhex('0011223344556677')
)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Event_Mask_Command():
command = HCI_LE_Set_Event_Mask_Command(
le_event_mask=HCI_LE_Set_Event_Mask_Command.mask(
command = hci.HCI_LE_Set_Event_Mask_Command(
le_event_mask=hci.HCI_LE_Set_Event_Mask_Command.mask(
[
HCI_LE_CONNECTION_COMPLETE_EVENT,
HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
hci.HCI_LE_CONNECTION_COMPLETE_EVENT,
hci.HCI_LE_ENHANCED_CONNECTION_COMPLETE_V2_EVENT,
]
)
)
@@ -329,21 +360,21 @@ def test_HCI_LE_Set_Event_Mask_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Random_Address_Command():
command = HCI_LE_Set_Random_Address_Command(
random_address=Address('00:11:22:33:44:55')
command = hci.HCI_LE_Set_Random_Address_Command(
random_address=hci.Address('00:11:22:33:44:55')
)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Advertising_Parameters_Command():
command = HCI_LE_Set_Advertising_Parameters_Command(
command = hci.HCI_LE_Set_Advertising_Parameters_Command(
advertising_interval_min=20,
advertising_interval_max=30,
advertising_type=HCI_LE_Set_Advertising_Parameters_Command.ADV_NONCONN_IND,
own_address_type=Address.PUBLIC_DEVICE_ADDRESS,
peer_address_type=Address.RANDOM_DEVICE_ADDRESS,
peer_address=Address('00:11:22:33:44:55'),
advertising_type=hci.HCI_LE_Set_Advertising_Parameters_Command.ADV_NONCONN_IND,
own_address_type=hci.Address.PUBLIC_DEVICE_ADDRESS,
peer_address_type=hci.Address.RANDOM_DEVICE_ADDRESS,
peer_address=hci.Address('00:11:22:33:44:55'),
advertising_channel_map=0x03,
advertising_filter_policy=1,
)
@@ -352,7 +383,7 @@ def test_HCI_LE_Set_Advertising_Parameters_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Advertising_Data_Command():
command = HCI_LE_Set_Advertising_Data_Command(
command = hci.HCI_LE_Set_Advertising_Data_Command(
advertising_data=bytes.fromhex('AABBCC')
)
basic_check(command)
@@ -360,7 +391,7 @@ def test_HCI_LE_Set_Advertising_Data_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Scan_Parameters_Command():
command = HCI_LE_Set_Scan_Parameters_Command(
command = hci.HCI_LE_Set_Scan_Parameters_Command(
le_scan_type=1,
le_scan_interval=20,
le_scan_window=10,
@@ -372,18 +403,18 @@ def test_HCI_LE_Set_Scan_Parameters_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Scan_Enable_Command():
command = HCI_LE_Set_Scan_Enable_Command(le_scan_enable=1, filter_duplicates=0)
command = hci.HCI_LE_Set_Scan_Enable_Command(le_scan_enable=1, filter_duplicates=0)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_LE_Create_Connection_Command():
command = HCI_LE_Create_Connection_Command(
command = hci.HCI_LE_Create_Connection_Command(
le_scan_interval=4,
le_scan_window=5,
initiator_filter_policy=1,
peer_address_type=1,
peer_address=Address('00:11:22:33:44:55'),
peer_address=hci.Address('00:11:22:33:44:55'),
own_address_type=2,
connection_interval_min=7,
connection_interval_max=8,
@@ -397,11 +428,11 @@ def test_HCI_LE_Create_Connection_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Extended_Create_Connection_Command():
command = HCI_LE_Extended_Create_Connection_Command(
command = hci.HCI_LE_Extended_Create_Connection_Command(
initiator_filter_policy=0,
own_address_type=0,
peer_address_type=1,
peer_address=Address('00:11:22:33:44:55'),
peer_address=hci.Address('00:11:22:33:44:55'),
initiating_phys=3,
scan_intervals=(10, 11),
scan_windows=(12, 13),
@@ -417,23 +448,23 @@ def test_HCI_LE_Extended_Create_Connection_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Add_Device_To_Filter_Accept_List_Command():
command = HCI_LE_Add_Device_To_Filter_Accept_List_Command(
address_type=1, address=Address('00:11:22:33:44:55')
command = hci.HCI_LE_Add_Device_To_Filter_Accept_List_Command(
address_type=1, address=hci.Address('00:11:22:33:44:55')
)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_LE_Remove_Device_From_Filter_Accept_List_Command():
command = HCI_LE_Remove_Device_From_Filter_Accept_List_Command(
address_type=1, address=Address('00:11:22:33:44:55')
command = hci.HCI_LE_Remove_Device_From_Filter_Accept_List_Command(
address_type=1, address=hci.Address('00:11:22:33:44:55')
)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_LE_Connection_Update_Command():
command = HCI_LE_Connection_Update_Command(
command = hci.HCI_LE_Connection_Update_Command(
connection_handle=0x0002,
connection_interval_min=10,
connection_interval_max=20,
@@ -447,27 +478,29 @@ def test_HCI_LE_Connection_Update_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Read_Remote_Features_Command():
command = HCI_LE_Read_Remote_Features_Command(connection_handle=0x0002)
command = hci.HCI_LE_Read_Remote_Features_Command(connection_handle=0x0002)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Default_PHY_Command():
command = HCI_LE_Set_Default_PHY_Command(all_phys=0, tx_phys=1, rx_phys=1)
command = hci.HCI_LE_Set_Default_PHY_Command(all_phys=0, tx_phys=1, rx_phys=1)
basic_check(command)
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
command = HCI_LE_Set_Extended_Scan_Parameters_Command(
own_address_type=Address.RANDOM_DEVICE_ADDRESS,
command = hci.HCI_LE_Set_Extended_Scan_Parameters_Command(
own_address_type=hci.Address.RANDOM_DEVICE_ADDRESS,
# pylint: disable-next=line-too-long
scanning_filter_policy=HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY,
scanning_phys=(1 << HCI_LE_1M_PHY_BIT | 1 << HCI_LE_CODED_PHY_BIT | 1 << 4),
scanning_filter_policy=hci.HCI_LE_Set_Extended_Scan_Parameters_Command.BASIC_FILTERED_POLICY,
scanning_phys=(
1 << hci.HCI_LE_1M_PHY_BIT | 1 << hci.HCI_LE_CODED_PHY_BIT | 1 << 4
),
scan_types=[
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING,
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.ACTIVE_SCANNING,
hci.HCI_LE_Set_Extended_Scan_Parameters_Command.PASSIVE_SCANNING,
],
scan_intervals=[1, 2, 3],
scan_windows=[4, 5, 6],
@@ -477,7 +510,7 @@ def test_HCI_LE_Set_Extended_Scan_Parameters_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
command = HCI_Packet.from_bytes(
command = hci.HCI_Packet.from_bytes(
bytes.fromhex('0139200e010301050008020600090307000a')
)
assert command.enable == 1
@@ -485,7 +518,7 @@ def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
assert command.durations == [5, 6, 7]
assert command.max_extended_advertising_events == [8, 9, 10]
command = HCI_LE_Set_Extended_Advertising_Enable_Command(
command = hci.HCI_LE_Set_Extended_Advertising_Enable_Command(
enable=1,
advertising_handles=[1, 2, 3],
durations=[5, 6, 7],
@@ -496,20 +529,22 @@ def test_HCI_LE_Set_Extended_Advertising_Enable_Command():
# -----------------------------------------------------------------------------
def test_HCI_LE_Setup_ISO_Data_Path_Command():
command = HCI_Packet.from_bytes(bytes.fromhex('016e200d60000001030000000000000000'))
command = hci.HCI_Packet.from_bytes(
bytes.fromhex('016e200d60000001030000000000000000')
)
assert command.connection_handle == 0x0060
assert command.data_path_direction == 0x00
assert command.data_path_id == 0x01
assert command.codec_id == CodingFormat(CodecID.TRANSPARENT)
assert command.codec_id == hci.CodingFormat(hci.CodecID.TRANSPARENT)
assert command.controller_delay == 0
assert command.codec_configuration == b''
command = HCI_LE_Setup_ISO_Data_Path_Command(
command = hci.HCI_LE_Setup_ISO_Data_Path_Command(
connection_handle=0x0060,
data_path_direction=0x00,
data_path_id=0x01,
codec_id=CodingFormat(CodecID.TRANSPARENT),
codec_id=hci.CodingFormat(hci.CodecID.TRANSPARENT),
controller_delay=0x00,
codec_configuration=b'',
)
@@ -519,54 +554,63 @@ def test_HCI_LE_Setup_ISO_Data_Path_Command():
# -----------------------------------------------------------------------------
def test_HCI_Read_Local_Supported_Codecs_Command_Complete():
returned_parameters = (
HCI_Read_Local_Supported_Codecs_Command.parse_return_parameters(
bytes([HCI_SUCCESS, 3, CodecID.A_LOG, CodecID.CVSD, CodecID.LINEAR_PCM, 0])
)
)
assert returned_parameters.standard_codec_ids == [
CodecID.A_LOG,
CodecID.CVSD,
CodecID.LINEAR_PCM,
]
# -----------------------------------------------------------------------------
def test_HCI_Read_Local_Supported_Codecs_V2_Command_Complete():
returned_parameters = (
HCI_Read_Local_Supported_Codecs_V2_Command.parse_return_parameters(
hci.HCI_Read_Local_Supported_Codecs_Command.parse_return_parameters(
bytes(
[
HCI_SUCCESS,
hci.HCI_SUCCESS,
3,
CodecID.A_LOG,
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
CodecID.CVSD,
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
CodecID.LINEAR_PCM,
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
hci.CodecID.A_LOG,
hci.CodecID.CVSD,
hci.CodecID.LINEAR_PCM,
0,
]
)
)
)
assert returned_parameters.standard_codec_ids == [
CodecID.A_LOG,
CodecID.CVSD,
CodecID.LINEAR_PCM,
hci.CodecID.A_LOG,
hci.CodecID.CVSD,
hci.CodecID.LINEAR_PCM,
]
# -----------------------------------------------------------------------------
def test_HCI_Read_Local_Supported_Codecs_V2_Command_Complete():
returned_parameters = (
hci.HCI_Read_Local_Supported_Codecs_V2_Command.parse_return_parameters(
bytes(
[
hci.HCI_SUCCESS,
3,
hci.CodecID.A_LOG,
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
hci.CodecID.CVSD,
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
hci.CodecID.LINEAR_PCM,
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
0,
]
)
)
)
assert returned_parameters.standard_codec_ids == [
hci.CodecID.A_LOG,
hci.CodecID.CVSD,
hci.CodecID.LINEAR_PCM,
]
assert returned_parameters.standard_codec_transports == [
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_ACL,
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.BR_EDR_SCO,
hci.HCI_Read_Local_Supported_Codecs_V2_Command.Transport.LE_CIS,
]
# -----------------------------------------------------------------------------
def test_address():
a = Address('C4:F2:17:1A:1D:BB')
a = hci.Address('C4:F2:17:1A:1D:BB')
assert not a.is_public
assert a.is_random
assert a.address_type == Address.RANDOM_DEVICE_ADDRESS
assert a.address_type == hci.Address.RANDOM_DEVICE_ADDRESS
assert not a.is_resolvable
assert not a.is_resolved
assert a.is_static
@@ -575,7 +619,7 @@ def test_address():
# -----------------------------------------------------------------------------
def test_custom():
data = bytes([0x77, 0x02, 0x01, 0x03])
packet = HCI_CustomPacket(data)
packet = hci.HCI_CustomPacket(data)
assert packet.hci_packet_type == 0x77
assert packet.payload == data
@@ -586,7 +630,7 @@ def test_iso_data_packet():
'05616044002ac9f0a193003c00e83b477b00eba8d41dc018bf1a980f0290afe1e7c37652096697'
'52b6a535a8df61e22931ef5a36281bc77ed6a3206d984bcdabee6be831c699cb50e2'
)
packet = HCI_IsoDataPacket.from_bytes(data)
packet = hci.HCI_IsoDataPacket.from_bytes(data)
assert packet.connection_handle == 0x0061
assert packet.packet_status_flag == 0
assert packet.pb_flag == 0x02

View File

@@ -21,7 +21,7 @@ import os
import pytest
import pytest_asyncio
from typing import Tuple, Optional
from typing import Optional
from .test_utils import TwoDevices
from bumble import core
@@ -194,7 +194,7 @@ async def test_slc_with_minimal_features():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_slc(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_slc(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
assert hf.supported_ag_features == ag.supported_ag_features
@@ -207,7 +207,7 @@ async def test_slc(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ag_indicator(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_ag_indicator(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
future = asyncio.get_running_loop().create_future()
@@ -222,7 +222,7 @@ async def test_ag_indicator(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtoco
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_hf_indicator(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_hf_indicator(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
future = asyncio.get_running_loop().create_future()
@@ -237,7 +237,7 @@ async def test_hf_indicator(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtoco
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_codec_negotiation(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections
@@ -254,7 +254,7 @@ async def test_codec_negotiation(
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_dial(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_dial(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
NUMBER = 'ATD123456789'
@@ -268,7 +268,7 @@ async def test_dial(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_answer(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_answer(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
future = asyncio.get_running_loop().create_future()
@@ -281,7 +281,7 @@ async def test_answer(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_reject_incoming_call(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections
@@ -294,7 +294,7 @@ async def test_reject_incoming_call(
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_terminate_call(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_terminate_call(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
future = asyncio.get_running_loop().create_future()
@@ -307,7 +307,7 @@ async def test_terminate_call(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProto
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_query_calls_without_calls(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections
@@ -317,7 +317,7 @@ async def test_query_calls_without_calls(
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_query_calls_with_calls(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections
ag.calls.append(
@@ -347,7 +347,7 @@ async def test_query_calls_with_calls(
),
)
async def test_hold_call_without_call_index(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol],
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol],
operation: hfp.CallHoldOperation,
):
hf, ag = hfp_connections
@@ -369,7 +369,7 @@ async def test_hold_call_without_call_index(
),
)
async def test_hold_call_with_call_index(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol],
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol],
operation: hfp.CallHoldOperation,
):
hf, ag = hfp_connections
@@ -393,7 +393,7 @@ async def test_hold_call_with_call_index(
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ring(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_ring(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
ring_future = asyncio.get_running_loop().create_future()
hf.on("ring", lambda: ring_future.set_result(None))
@@ -405,7 +405,7 @@ async def test_ring(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_speaker_volume(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_speaker_volume(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
speaker_volume_future = asyncio.get_running_loop().create_future()
hf.on("speaker_volume", speaker_volume_future.set_result)
@@ -418,7 +418,7 @@ async def test_speaker_volume(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProto
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_microphone_volume(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections
microphone_volume_future = asyncio.get_running_loop().create_future()
@@ -431,7 +431,7 @@ async def test_microphone_volume(
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_cli_notification(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
async def test_cli_notification(hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]):
hf, ag = hfp_connections
cli_notification_future = asyncio.get_running_loop().create_future()
hf.on("cli_notification", cli_notification_future.set_result)
@@ -448,7 +448,7 @@ async def test_cli_notification(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgPro
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_voice_recognition_from_hf(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections
voice_recognition_future = asyncio.get_running_loop().create_future()
@@ -462,7 +462,7 @@ async def test_voice_recognition_from_hf(
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_voice_recognition_from_ag(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections
voice_recognition_future = asyncio.get_running_loop().create_future()
@@ -572,7 +572,7 @@ async def test_sco_setup():
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_hf_batched_response(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections
@@ -584,7 +584,7 @@ async def test_hf_batched_response(
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ag_batched_commands(
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
hfp_connections: tuple[hfp.HfProtocol, hfp.AgProtocol]
):
hf, ag = hfp_connections

View File

@@ -17,7 +17,6 @@
# -----------------------------------------------------------------------------
import asyncio
import pytest
from typing import List
from . import test_utils
from bumble import core
@@ -73,7 +72,7 @@ async def test_connection_and_disconnection() -> None:
multiplexer = await Client(devices.connections[1]).start()
dlcs = await asyncio.gather(accept_future, multiplexer.open_dlc(channel))
queues: List[asyncio.Queue] = [asyncio.Queue(), asyncio.Queue()]
queues: list[asyncio.Queue] = [asyncio.Queue(), asyncio.Queue()]
for dlc, queue in zip(dlcs, queues):
dlc.sink = queue.put_nowait

View File

@@ -21,17 +21,30 @@ from unittest import mock
from bumble import smp
from bumble import pairing
from bumble import crypto
from bumble.crypto import EccKey, aes_cmac, ah, c1, f4, f5, f6, g2, h6, h7, s1
from bumble.pairing import OobData, OobSharedData, LeRole
from bumble.hci import Address
from bumble.core import AdvertisingData
from bumble.device import Device
from typing import Optional
from typing import Optional, Any
# -----------------------------------------------------------------------------
# pylint: disable=invalid-name
# -----------------------------------------------------------------------------
@pytest.fixture(
scope="session", params=["bumble.crypto.builtin", "bumble.crypto.cryptography"]
)
def crypto_backend(request):
backend = pytest.importorskip(request.param)
with (
mock.patch.object(crypto, "e", backend.e),
mock.patch.object(crypto, "aes_cmac", backend.aes_cmac),
mock.patch.object(crypto, "EccKey", backend.EccKey),
):
yield
# -----------------------------------------------------------------------------
@@ -40,7 +53,7 @@ def reversed_hex(hex_str: str) -> bytes:
# -----------------------------------------------------------------------------
def test_ecc():
def test_ecc(crypto_backend):
key = EccKey.generate()
x = key.x
y = key.y
@@ -69,21 +82,17 @@ def test_ecc():
)
dhkey = 'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698'
key_a = EccKey.from_private_key_bytes(
bytes.fromhex(private_A), bytes.fromhex(public_A_x), bytes.fromhex(public_A_y)
)
key_a = EccKey.from_private_key_bytes(bytes.fromhex(private_A))
shared_key = key_a.dh(bytes.fromhex(public_B_x), bytes.fromhex(public_B_y))
assert shared_key == bytes.fromhex(dhkey)
key_b = EccKey.from_private_key_bytes(
bytes.fromhex(private_B), bytes.fromhex(public_B_x), bytes.fromhex(public_B_y)
)
key_b = EccKey.from_private_key_bytes(bytes.fromhex(private_B))
shared_key = key_b.dh(bytes.fromhex(public_A_x), bytes.fromhex(public_A_y))
assert shared_key == bytes.fromhex(dhkey)
# -----------------------------------------------------------------------------
def test_c1():
def test_c1(crypto_backend):
k = bytes(16)
r = reversed_hex('5783D52156AD6F0E6388274EC6702EE0')
pres = reversed_hex('05000800000302')
@@ -97,7 +106,7 @@ def test_c1():
# -----------------------------------------------------------------------------
def test_s1():
def test_s1(crypto_backend):
k = bytes(16)
r1 = reversed_hex('000F0E0D0C0B0A091122334455667788')
r2 = reversed_hex('010203040506070899AABBCCDDEEFF00')
@@ -106,7 +115,7 @@ def test_s1():
# -----------------------------------------------------------------------------
def test_aes_cmac():
def test_aes_cmac(crypto_backend):
m = b''
k = bytes.fromhex('2b7e1516 28aed2a6 abf71588 09cf4f3c')
cmac = aes_cmac(m, k)
@@ -135,7 +144,7 @@ def test_aes_cmac():
# -----------------------------------------------------------------------------
def test_f4():
def test_f4(crypto_backend):
u = reversed_hex(
'20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6'
)
@@ -149,7 +158,7 @@ def test_f4():
# -----------------------------------------------------------------------------
def test_f5():
def test_f5(crypto_backend):
w = reversed_hex(
'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698'
)
@@ -163,7 +172,7 @@ def test_f5():
# -----------------------------------------------------------------------------
def test_f6():
def test_f6(crypto_backend):
n1 = reversed_hex('d5cb8454 d177733e ffffb2ec 712baeab')
n2 = reversed_hex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf')
mac_key = reversed_hex('2965f176 a1084a02 fd3f6a20 ce636e20')
@@ -176,7 +185,7 @@ def test_f6():
# -----------------------------------------------------------------------------
def test_g2():
def test_g2(crypto_backend):
u = reversed_hex(
'20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6'
)
@@ -190,21 +199,21 @@ def test_g2():
# -----------------------------------------------------------------------------
def test_h6():
def test_h6(crypto_backend):
KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
KEY_ID = bytes.fromhex('6c656272')
assert h6(KEY, KEY_ID) == reversed_hex('2d9ae102 e76dc91c e8d3a9e2 80b16399')
# -----------------------------------------------------------------------------
def test_h7():
def test_h7(crypto_backend):
KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
SALT = bytes.fromhex('00000000 00000000 00000000 746D7031')
assert h7(SALT, KEY) == reversed_hex('fb173597 c6a3c0ec d2998c2a 75a57011')
# -----------------------------------------------------------------------------
def test_ah():
def test_ah(crypto_backend):
irk = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
prand = reversed_hex('708194')
value = ah(irk, prand)
@@ -213,7 +222,7 @@ def test_ah():
# -----------------------------------------------------------------------------
def test_oob_data():
def test_oob_data(crypto_backend):
oob_data = OobData(
address=Address("F0:F1:F2:F3:F4:F5"),
role=LeRole.BOTH_PERIPHERAL_PREFERRED,
@@ -237,7 +246,7 @@ def test_oob_data():
(True, '287ad379 dca40253 0a39f1f4 3047b835'),
],
)
def test_ltk_to_link_key(ct2: bool, expected: str):
def test_ltk_to_link_key(ct2: bool, expected: str, crypto_backend: Any):
LTK = reversed_hex('368df9bc e3264b58 bd066c33 334fbf64')
assert smp.Session.derive_link_key(LTK, ct2) == reversed_hex(expected)
@@ -250,7 +259,7 @@ def test_ltk_to_link_key(ct2: bool, expected: str):
(True, 'e85e09eb 5eccb3e2 69418a13 3211bc79'),
],
)
def test_link_key_to_ltk(ct2: bool, expected: str):
def test_link_key_to_ltk(ct2: bool, expected: str, crypto_backend: Any):
LINK_KEY = reversed_hex('05040302 01000908 07060504 03020100')
assert smp.Session.derive_ltk(LINK_KEY, ct2) == reversed_hex(expected)
@@ -291,6 +300,7 @@ async def test_send_identity_address_command(
public_address: Address,
random_address: Address,
expected_identity_address: Address,
crypto_backend: Any,
):
device = Device()
device.public_address = public_address
@@ -304,19 +314,3 @@ async def test_send_identity_address_command(
actual_command = mock_method.call_args.args[0]
assert actual_command.addr_type == expected_identity_address.address_type
assert actual_command.bd_addr == expected_identity_address
# -----------------------------------------------------------------------------
if __name__ == '__main__':
test_ecc()
test_c1()
test_s1()
test_aes_cmac()
test_f4()
test_f5()
test_f6()
test_g2()
test_h6()
test_h7()
test_ah()
test_oob_data()

View File

@@ -16,7 +16,7 @@
# Imports
# -----------------------------------------------------------------------------
import asyncio
from typing import List, Optional, Type
from typing import Optional
from typing_extensions import Self
from bumble.controller import Controller
@@ -29,7 +29,7 @@ from bumble.hci import Address
# -----------------------------------------------------------------------------
class TwoDevices:
connections: List[Optional[Connection]]
connections: list[Optional[Connection]]
def __init__(self) -> None:
self.connections = [None, None]
@@ -83,7 +83,7 @@ class TwoDevices:
return self.devices[index]
@classmethod
async def create_with_connection(cls: Type[Self]) -> Self:
async def create_with_connection(cls: type[Self]) -> Self:
devices = cls()
await devices.setup_connection()
return devices

View File

@@ -18,7 +18,6 @@
from __future__ import annotations
import enum
import logging
from typing import Dict, List
from bumble.core import PhysicalTransport, CommandTimeoutError
from bumble.device import Device, DeviceConfiguration
@@ -101,7 +100,7 @@ class Speaker:
self.stream_state = Speaker.StreamState.IDLE
self.audio_extractor = AudioExtractor.create(codec)
def sdp_records(self) -> Dict[int, List[ServiceAttribute]]:
def sdp_records(self) -> dict[int, list[ServiceAttribute]]:
service_record_handle = 0x00010001
return {
service_record_handle: make_audio_sink_service_sdp_records(