forked from auracaster/bumble_mirror
Compare commits
32 Commits
bump_crypt
...
gbg/delega
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e95e19f16 | ||
|
|
8137caf37b | ||
|
|
39518c89f5 | ||
|
|
d631156f6c | ||
|
|
8614e075b6 | ||
|
|
8a0cd5d0d1 | ||
|
|
3a64772cc5 | ||
|
|
1ecfb78d94 | ||
|
|
9ad276a757 | ||
|
|
4c4f8c8225 | ||
|
|
a00b2bd707 | ||
|
|
b8a055de45 | ||
|
|
4d07726acf | ||
|
|
2e523b6f49 | ||
|
|
8f9f12f1ee | ||
|
|
a875aa4055 | ||
|
|
775b2d5d7f | ||
|
|
3b399ea1a2 | ||
|
|
84f7cad678 | ||
|
|
778f439e1c | ||
|
|
1b95d4e1df | ||
|
|
512f6d4ee1 | ||
|
|
c52b614abb | ||
|
|
7b7afc7179 | ||
|
|
b1c6044533 | ||
|
|
38499dfe3c | ||
|
|
b58c29202a | ||
|
|
ca759ca967 | ||
|
|
3858bf80c1 | ||
|
|
a88a034ce2 | ||
|
|
6b2cd1147d | ||
|
|
bb8dcaf63e |
@@ -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)
|
||||
|
||||
|
||||
156
apps/bench.py
156
apps/bench.py
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -26,14 +26,11 @@ from typing import (
|
||||
Awaitable,
|
||||
Callable,
|
||||
cast,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
SupportsBytes,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
@@ -84,10 +81,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]
|
||||
@@ -152,10 +149,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]
|
||||
@@ -291,7 +288,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 +334,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 +406,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 +451,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 +464,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 +537,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 +814,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 +936,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 +964,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 +1121,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 +1187,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 (
|
||||
@@ -1230,7 +1227,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 +1250,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 +1332,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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -132,17 +132,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
|
||||
|
||||
@@ -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
652
bumble/crypto/builtin.py
Normal 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()
|
||||
84
bumble/crypto/cryptography.py
Normal file
84
bumble/crypto/cryptography.py
Normal 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()
|
||||
105
bumble/device.py
105
bumble/device.py
@@ -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
|
||||
@@ -436,7 +435,7 @@ class AdvertisingEventProperties:
|
||||
|
||||
@classmethod
|
||||
def from_advertising_type(
|
||||
cls: Type[AdvertisingEventProperties],
|
||||
cls: type[AdvertisingEventProperties],
|
||||
advertising_type: AdvertisingType,
|
||||
) -> AdvertisingEventProperties:
|
||||
return cls(
|
||||
@@ -1343,7 +1342,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 +1350,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 +1463,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 +1475,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 +1546,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 +1590,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 +1629,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 +1693,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 +1885,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 +1961,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 +2023,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 +2126,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 +2301,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 +4278,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 +4366,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 +4453,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 +4497,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 +5068,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 +5087,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 +5729,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 +5756,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 +5790,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 +5829,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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,8 +20,6 @@ Common types for drivers.
|
||||
# -----------------------------------------------------------------------------
|
||||
import abc
|
||||
|
||||
from bumble import core
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -293,6 +294,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 +348,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 +473,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")
|
||||
|
||||
@@ -29,7 +29,6 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import struct
|
||||
from typing import Tuple
|
||||
import weakref
|
||||
|
||||
|
||||
@@ -294,7 +293,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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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',
|
||||
):
|
||||
|
||||
@@ -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
|
||||
'''
|
||||
|
||||
@@ -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:
|
||||
|
||||
100
bumble/hci.py
100
bumble/hci.py
@@ -23,7 +23,7 @@ import functools
|
||||
import logging
|
||||
import secrets
|
||||
import struct
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Type, Union, ClassVar
|
||||
from typing import Any, Callable, Iterable, Optional, Union, ClassVar
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import crypto
|
||||
@@ -107,6 +107,36 @@ def phy_list_to_bits(phys: Optional[Iterable[Phy]]) -> int:
|
||||
return phy_bits
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Field Metadata
|
||||
# -----------------------------------------------------------------------------
|
||||
# Field specification can be:
|
||||
# - a dict with "serializer", "parser", "size", "mapper" keys
|
||||
# - a callable that takes (packet, offset) and returns (new_offset, value) (deserialize only)
|
||||
# - a string of
|
||||
# - ">2" and ">4" for 2-byte and 4-byte big-endian integers
|
||||
# - "*" for all remaining bytes in the packet
|
||||
# - "v" for variable length bytes with a leading length byte
|
||||
# - an integer [1, 4] for 1-byte, 2-byte or 4-byte unsigned little-endian integers
|
||||
# - an integer [-2, -1] for 1-byte, 2-byte signed little-endian integers
|
||||
FieldSpec = Union[dict[str, Any], Callable[[bytes, int], tuple[int, Any]], str, int]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FieldMetadata:
|
||||
spec: FieldSpec
|
||||
list_begin: bool = False
|
||||
list_end: bool = False
|
||||
|
||||
|
||||
def metadata(
|
||||
spec: FieldSpec, list_begin: bool = False, list_end: bool = False
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"bumble.hci": FieldMetadata(spec=spec, list_begin=list_begin, list_end=list_end)
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1659,7 +1689,7 @@ class HCI_Object:
|
||||
HCI_Object.init_from_fields(hci_object, parsed.keys(), parsed.values())
|
||||
|
||||
@staticmethod
|
||||
def parse_field(data, offset, field_type):
|
||||
def parse_field(data: bytes, offset: int, field_type: FieldSpec):
|
||||
# The field_type may be a dictionary with a mapper, parser, and/or size
|
||||
if isinstance(field_type, dict):
|
||||
if 'size' in field_type:
|
||||
@@ -1741,7 +1771,7 @@ class HCI_Object:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def serialize_field(field_value, field_type):
|
||||
def serialize_field(field_value: Any, field_type: FieldSpec) -> bytes:
|
||||
# The field_type may be a dictionary with a mapper, parser, serializer,
|
||||
# and/or size
|
||||
serializer = None
|
||||
@@ -1932,6 +1962,24 @@ class HCI_Object:
|
||||
for field_name, field_value in field_strings
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def fields_from_dataclass(cls, obj: Any) -> list[Any]:
|
||||
stack: list[list[Any]] = [[]]
|
||||
for field in dataclasses.fields(obj):
|
||||
# Fields without metadata should be ignored.
|
||||
if not isinstance(
|
||||
(metadata := field.metadata.get("bumble.hci")), FieldMetadata
|
||||
):
|
||||
continue
|
||||
if metadata.list_begin:
|
||||
stack.append([])
|
||||
if metadata.spec:
|
||||
stack[-1].append((field.name, metadata.spec))
|
||||
if metadata.list_end:
|
||||
top = stack.pop()
|
||||
stack[-1].append(top)
|
||||
return stack[0]
|
||||
|
||||
def __init__(self, fields, **kwargs):
|
||||
self.fields = fields
|
||||
self.init_from_fields(self, fields, kwargs)
|
||||
@@ -2221,8 +2269,8 @@ class HCI_Command(HCI_Packet):
|
||||
'''
|
||||
|
||||
hci_packet_type = HCI_COMMAND_PACKET
|
||||
command_names: Dict[int, str] = {}
|
||||
command_classes: Dict[int, Type[HCI_Command]] = {}
|
||||
command_names: dict[int, str] = {}
|
||||
command_classes: dict[int, type[HCI_Command]] = {}
|
||||
op_code: int
|
||||
|
||||
@staticmethod
|
||||
@@ -2255,7 +2303,7 @@ class HCI_Command(HCI_Packet):
|
||||
return inner
|
||||
|
||||
@staticmethod
|
||||
def command_map(symbols: Dict[str, Any]) -> Dict[int, str]:
|
||||
def command_map(symbols: dict[str, Any]) -> dict[int, str]:
|
||||
return {
|
||||
command_code: command_name
|
||||
for (command_name, command_code) in symbols.items()
|
||||
@@ -2263,7 +2311,7 @@ class HCI_Command(HCI_Packet):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def register_commands(cls, symbols: Dict[str, Any]) -> None:
|
||||
def register_commands(cls, symbols: dict[str, Any]) -> None:
|
||||
cls.command_names.update(cls.command_map(symbols))
|
||||
|
||||
@staticmethod
|
||||
@@ -5007,13 +5055,13 @@ class HCI_LE_Set_CIG_Parameters_Command(HCI_Command):
|
||||
framing: int
|
||||
max_transport_latency_c_to_p: int
|
||||
max_transport_latency_p_to_c: int
|
||||
cis_id: List[int]
|
||||
max_sdu_c_to_p: List[int]
|
||||
max_sdu_p_to_c: List[int]
|
||||
phy_c_to_p: List[int]
|
||||
phy_p_to_c: List[int]
|
||||
rtn_c_to_p: List[int]
|
||||
rtn_p_to_c: List[int]
|
||||
cis_id: list[int]
|
||||
max_sdu_c_to_p: list[int]
|
||||
max_sdu_p_to_c: list[int]
|
||||
phy_c_to_p: list[int]
|
||||
phy_p_to_c: list[int]
|
||||
rtn_c_to_p: list[int]
|
||||
rtn_p_to_c: list[int]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -5030,8 +5078,8 @@ class HCI_LE_Create_CIS_Command(HCI_Command):
|
||||
See Bluetooth spec @ 7.8.99 LE Create CIS command
|
||||
'''
|
||||
|
||||
cis_connection_handle: List[int]
|
||||
acl_connection_handle: List[int]
|
||||
cis_connection_handle: list[int]
|
||||
acl_connection_handle: list[int]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -5150,7 +5198,7 @@ class HCI_LE_BIG_Create_Sync_Command(HCI_Command):
|
||||
broadcast_code: int
|
||||
mse: int
|
||||
big_sync_timeout: int
|
||||
bis: List[int]
|
||||
bis: list[int]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -5509,8 +5557,8 @@ class HCI_Event(HCI_Packet):
|
||||
'''
|
||||
|
||||
hci_packet_type = HCI_EVENT_PACKET
|
||||
event_names: Dict[int, str] = {}
|
||||
event_classes: Dict[int, Type[HCI_Event]] = {}
|
||||
event_names: dict[int, str] = {}
|
||||
event_classes: dict[int, type[HCI_Event]] = {}
|
||||
vendor_factories: list[Callable[[bytes], Optional[HCI_Event]]] = []
|
||||
|
||||
@staticmethod
|
||||
@@ -5540,7 +5588,7 @@ class HCI_Event(HCI_Packet):
|
||||
return inner
|
||||
|
||||
@staticmethod
|
||||
def event_map(symbols: Dict[str, Any]) -> Dict[int, str]:
|
||||
def event_map(symbols: dict[str, Any]) -> dict[int, str]:
|
||||
return {
|
||||
event_code: event_name
|
||||
for (event_name, event_code) in symbols.items()
|
||||
@@ -5554,7 +5602,7 @@ class HCI_Event(HCI_Packet):
|
||||
return name_or_number(HCI_Event.event_names, event_code)
|
||||
|
||||
@staticmethod
|
||||
def register_events(symbols: Dict[str, Any]) -> None:
|
||||
def register_events(symbols: dict[str, Any]) -> None:
|
||||
HCI_Event.event_names.update(HCI_Event.event_map(symbols))
|
||||
|
||||
@staticmethod
|
||||
@@ -5662,8 +5710,8 @@ class HCI_Extended_Event(HCI_Event):
|
||||
HCI_Event subclass for events that have a subevent code.
|
||||
'''
|
||||
|
||||
subevent_names: Dict[int, str] = {}
|
||||
subevent_classes: Dict[int, Type[HCI_Extended_Event]] = {}
|
||||
subevent_names: dict[int, str] = {}
|
||||
subevent_classes: dict[int, type[HCI_Extended_Event]] = {}
|
||||
|
||||
@classmethod
|
||||
def event(cls, fields=()):
|
||||
@@ -5702,7 +5750,7 @@ class HCI_Extended_Event(HCI_Event):
|
||||
return f'{cls.__name__.upper()}[0x{subevent_code:02X}]'
|
||||
|
||||
@staticmethod
|
||||
def subevent_map(symbols: Dict[str, Any]) -> Dict[int, str]:
|
||||
def subevent_map(symbols: dict[str, Any]) -> dict[int, str]:
|
||||
return {
|
||||
subevent_code: subevent_name
|
||||
for (subevent_name, subevent_code) in symbols.items()
|
||||
@@ -5710,7 +5758,7 @@ class HCI_Extended_Event(HCI_Event):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def register_subevents(cls, symbols: Dict[str, Any]) -> None:
|
||||
def register_subevents(cls, symbols: dict[str, Any]) -> None:
|
||||
cls.subevent_names.update(cls.subevent_map(symbols))
|
||||
|
||||
@classmethod
|
||||
@@ -5759,7 +5807,7 @@ class HCI_LE_Meta_Event(HCI_Extended_Event):
|
||||
subevent_classes = {}
|
||||
|
||||
@staticmethod
|
||||
def subevent_map(symbols: Dict[str, Any]) -> Dict[int, str]:
|
||||
def subevent_map(symbols: dict[str, Any]) -> dict[int, str]:
|
||||
return {
|
||||
subevent_code: subevent_name
|
||||
for (subevent_name, subevent_code) in symbols.items()
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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()
|
||||
|
||||
116
bumble/keys.py
116
bumble/keys.py
@@ -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())
|
||||
|
||||
@@ -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]]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
120
bumble/smp.py
120
bumble/smp.py
@@ -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
|
||||
@@ -814,7 +809,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 +821,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 +895,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 +914,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 +931,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 +944,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 +966,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 +1045,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 +1168,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 +1207,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 +1263,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 +1300,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 +1321,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 +1372,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 +1432,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 +1497,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 +1679,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 +1708,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 +1745,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 +1844,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 +1882,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 +1932,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__(
|
||||
|
||||
@@ -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>')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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):
|
||||
|
||||
4
bumble/vendor/android/hci.py
vendored
4
bumble/vendor/android/hci.py
vendored
@@ -16,7 +16,7 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
from typing import Dict, Optional, Type
|
||||
from typing import Optional
|
||||
|
||||
from bumble.hci import (
|
||||
name_or_number,
|
||||
@@ -283,7 +283,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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
111
examples/run_gatt_server_with_pairing_delegate.py
Normal file
111
examples/run_gatt_server_with_pairing_delegate.py
Normal 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())
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -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'",
|
||||
|
||||
1
tasks.py
1
tasks.py
@@ -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
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from enum import IntEnum
|
||||
|
||||
from bumble.core import AdvertisingData, Appearance, UUID, get_dict_key_by_value
|
||||
|
||||
|
||||
@@ -483,8 +483,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()
|
||||
|
||||
|
||||
@@ -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>)"""
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import pytest
|
||||
import functools
|
||||
import pytest_asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user