forked from auracaster/bumble_mirror
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e4948d9ef | |||
| 32d448edf3 | |||
| 3d615b13ce | |||
| 1ad92dc759 | |||
| aacfd4328c | |||
| 6aa1f5211c | |||
| df8e454ee5 | |||
| aec50ac616 | |||
| 6a3eaa457f | |||
| 6e6b4cd4b2 | |||
| aa1d7933da | |||
| 34e0f293c2 | |||
| 85215df2c3 | |||
| f8223ca81f | |||
| 2b0b1ad726 | |||
| 58debcd8bb | |||
| 6eba81e3dd | |||
| 768bbd95cc | |||
| 502b80af0d | |||
| a25427305c | |||
| 3c47739029 | |||
| 8fc1330948 | |||
| 8a5f6a61d5 | |||
| 83c5061700 | |||
| b80b790dc1 | |||
| 21bf69592c | |||
| 7d8addb849 | |||
| d86d69d816 | |||
| bb08a1c70b | |||
| dc93f32a9a | |||
| 9838908a26 | |||
| 613519f0b3 | |||
| a943ea57ef | |||
| 14401910bb | |||
| 5d35ed471c | |||
| c720ad5fdc | |||
| f02183f95d | |||
| d903937a51 | |||
| 6381ee0ab1 | |||
| 59d99780e1 | |||
| 4bf0bc03af | |||
| 91ba2f61f1 | |||
| 116dc9b319 | |||
| 9f3d8c9b49 | |||
| 31961febe5 | |||
| dab0993cba | |||
| 6f73b736d7 | |||
| 6091e6365d | |||
| 3333ba472b | |||
| 8bda7d2212 |
@@ -49,7 +49,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
rust-version: [ "1.76.0", "stable" ]
|
||||
rust-version: [ "1.80.0", "stable" ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Check out from Git
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Check License Headers
|
||||
run: cd rust && cargo run --features dev-tools --bin file-header check-all
|
||||
- name: Rust Build
|
||||
run: cd rust && cargo build --all-targets && cargo build-all-features --all-targets
|
||||
run: cd rust && cargo build --all-targets && cargo build-all-features
|
||||
# Lints after build so what clippy needs is already built
|
||||
- name: Rust Lints
|
||||
run: cd rust && cargo fmt --check && cargo clippy --all-targets -- --deny warnings && cargo clippy --all-features --all-targets -- --deny warnings
|
||||
|
||||
Vendored
+4
-1
@@ -104,5 +104,8 @@
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": []
|
||||
"python-envs.pythonProjects": [],
|
||||
"nrf-connect.applications": [
|
||||
"${workspaceFolder}/extras/zephyr/hci_usb"
|
||||
]
|
||||
}
|
||||
|
||||
+11
-22
@@ -39,8 +39,9 @@ import bumble.device
|
||||
import bumble.logging
|
||||
import bumble.transport
|
||||
import bumble.utils
|
||||
from bumble import company_ids, core, gatt, hci
|
||||
from bumble import company_ids, core, data_types, gatt, hci
|
||||
from bumble.audio import io as audio_io
|
||||
from bumble.audio import io_asrc as audio_io_asrc
|
||||
from bumble.colors import color
|
||||
from bumble.profiles import bap, bass, le_audio, pbp
|
||||
|
||||
@@ -859,21 +860,13 @@ async def run_transmit(
|
||||
)
|
||||
broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id)
|
||||
|
||||
advertising_manufacturer_data = (
|
||||
b''
|
||||
if manufacturer_data is None
|
||||
else bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA,
|
||||
struct.pack('<H', manufacturer_data[0])
|
||||
+ manufacturer_data[1],
|
||||
)
|
||||
]
|
||||
)
|
||||
advertising_data_types: list[core.DataType] = [
|
||||
data_types.BroadcastName(broadcast_name)
|
||||
]
|
||||
if manufacturer_data is not None:
|
||||
advertising_data_types.append(
|
||||
data_types.ManufacturerSpecificData(*manufacturer_data)
|
||||
)
|
||||
)
|
||||
|
||||
advertising_set = await device.create_advertising_set(
|
||||
advertising_parameters=bumble.device.AdvertisingParameters(
|
||||
@@ -885,12 +878,7 @@ async def run_transmit(
|
||||
),
|
||||
advertising_data=(
|
||||
broadcast_audio_announcement.get_advertising_data()
|
||||
+ bytes(
|
||||
core.AdvertisingData(
|
||||
[(core.AdvertisingData.BROADCAST_NAME, broadcast_name.encode())]
|
||||
)
|
||||
)
|
||||
+ advertising_manufacturer_data
|
||||
+ bytes(core.AdvertisingData(advertising_data_types))
|
||||
),
|
||||
periodic_advertising_parameters=bumble.device.PeriodicAdvertisingParameters(
|
||||
periodic_advertising_interval_min=80,
|
||||
@@ -904,7 +892,8 @@ async def run_transmit(
|
||||
print('Start Periodic Advertising')
|
||||
await advertising_set.start_periodic()
|
||||
|
||||
audio_input = await audio_io.create_audio_input(input, input_format)
|
||||
#audio_input = await audio_io.create_audio_input(input, input_format)
|
||||
audio_input = audio_io_asrc.SoundDeviceAudioInputAsrc(input[7:], input_format)
|
||||
pcm_format = await audio_input.open()
|
||||
# This try should be replaced with contextlib.aclosing() when python 3.9 is no
|
||||
# longer needed.
|
||||
|
||||
+7
-16
@@ -37,7 +37,7 @@ import click
|
||||
|
||||
import bumble
|
||||
import bumble.logging
|
||||
from bumble import utils
|
||||
from bumble import data_types, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import AdvertisingParameters, CisLink, Device, DeviceConfiguration
|
||||
@@ -330,22 +330,13 @@ class Speaker:
|
||||
advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes(device_config.name, 'utf-8'),
|
||||
data_types.CompleteLocalName(device_config.name),
|
||||
data_types.Flags(
|
||||
AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
|
||||
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes(
|
||||
[
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(pacs.PublishedAudioCapabilitiesService.UUID),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[pacs.PublishedAudioCapabilitiesService.UUID]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
+14
-27
@@ -23,6 +23,7 @@ import struct
|
||||
import click
|
||||
from prompt_toolkit.shortcuts import PromptSession
|
||||
|
||||
from bumble import data_types
|
||||
from bumble.a2dp import make_audio_sink_service_sdp_records
|
||||
from bumble.att import (
|
||||
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
||||
@@ -34,6 +35,7 @@ from bumble.core import (
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
DataType,
|
||||
PhysicalTransport,
|
||||
ProtocolError,
|
||||
)
|
||||
@@ -506,33 +508,21 @@ async def pair(
|
||||
if mode == 'dual':
|
||||
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
||||
|
||||
ad_structs = [
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([flags]),
|
||||
),
|
||||
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
|
||||
advertising_data_types: list[DataType] = [
|
||||
data_types.Flags(flags),
|
||||
data_types.CompleteLocalName('Bumble'),
|
||||
]
|
||||
if service_uuids_16:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_16),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(service_uuids_16)
|
||||
)
|
||||
if service_uuids_32:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_32),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf32BitServiceUUIDs(service_uuids_32)
|
||||
)
|
||||
if service_uuids_128:
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
||||
b"".join(bytes(uuid) for uuid in service_uuids_128),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.IncompleteListOf128BitServiceUUIDs(service_uuids_128)
|
||||
)
|
||||
|
||||
if advertise_appearance:
|
||||
@@ -559,13 +549,10 @@ async def pair(
|
||||
advertise_appearance_int = int(
|
||||
Appearance(category_enum, subcategory_enum)
|
||||
)
|
||||
ad_structs.append(
|
||||
(
|
||||
AdvertisingData.APPEARANCE,
|
||||
struct.pack('<H', advertise_appearance_int),
|
||||
)
|
||||
advertising_data_types.append(
|
||||
data_types.Appearance(category_enum, subcategory_enum)
|
||||
)
|
||||
device.advertising_data = bytes(AdvertisingData(ad_structs))
|
||||
device.advertising_data = bytes(AdvertisingData(advertising_data_types))
|
||||
await device.start_advertising(
|
||||
auto_restart=True,
|
||||
own_address_type=(
|
||||
|
||||
+11
-1
@@ -20,6 +20,7 @@ import asyncio
|
||||
import click
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.colors import color
|
||||
from bumble.device import Advertisement, Device
|
||||
from bumble.hci import HCI_LE_1M_PHY, HCI_LE_CODED_PHY, Address, HCI_Constant
|
||||
@@ -94,13 +95,22 @@ class AdvertisementPrinter:
|
||||
else:
|
||||
phy_info = ''
|
||||
|
||||
details = separator.join(
|
||||
[
|
||||
data_type.to_string(use_label=True)
|
||||
for data_type in data_types.data_types_from_advertising_data(
|
||||
advertisement.data
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
print(
|
||||
f'>>> {color(address, address_color)} '
|
||||
f'[{color(address_type_string, type_color)}]{address_qualifier}'
|
||||
f'{resolution_qualifier}:{separator}'
|
||||
f'{phy_info}'
|
||||
f'RSSI:{advertisement.rssi:4} {rssi_bar}{separator}'
|
||||
f'{advertisement.data.to_string(separator)}\n'
|
||||
f'{details}\n'
|
||||
)
|
||||
|
||||
def on_advertisement(self, advertisement):
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
# Copyright 2025
|
||||
#
|
||||
# Drop-in replacement for `SoundDeviceAudioInput` that adds a tiny ASRC stage.
|
||||
#
|
||||
# Constraints per request:
|
||||
# - Only import io_bumble.py at module level.
|
||||
# - Reuse the ASRC functionality from asrc.py conceptually (PI control + FIFO +
|
||||
# linear/sinc resampling behavior). We implement a minimal, dependency-free
|
||||
# variant (linear interpolation with a small PI loop) so this module does not
|
||||
# import anything else at top-level.
|
||||
#
|
||||
# Notes:
|
||||
# - Input stream is captured via sounddevice (imported lazily inside methods).
|
||||
# - Input is mono float32 for simplicity; output matches the original class
|
||||
# signature: INT16, stereo, at the same nominal sample rate as requested.
|
||||
|
||||
from .io import PcmFormat, ThreadedAudioInput, logger # only top-level import
|
||||
|
||||
|
||||
class SoundDeviceAudioInputAsrc(ThreadedAudioInput):
|
||||
"""Sound device audio input with a simple ASRC stage.
|
||||
|
||||
Interface-compatible with `io_bumble.SoundDeviceAudioInput`:
|
||||
- __init__(device_name: str, pcm_format: PcmFormat)
|
||||
- _open() -> PcmFormat
|
||||
- _read(frame_size: int) -> bytes
|
||||
- _close() -> None
|
||||
|
||||
Behavior:
|
||||
- Captures mono float32 frames from the device.
|
||||
- Buffers into an internal ring buffer.
|
||||
- Produces stereo INT16 frames using a linear-interp resampler whose
|
||||
ratio is adjusted by a tiny PI loop to hold FIFO depth near a target.
|
||||
"""
|
||||
|
||||
def __init__(self, device_name: str, pcm_format: str) -> None:
|
||||
super().__init__()
|
||||
# Device & format
|
||||
self._device = int(device_name) if device_name else None
|
||||
pcm_format: PcmFormat | None
|
||||
if pcm_format == 'auto':
|
||||
pcm_format = None
|
||||
else:
|
||||
pcm_format = PcmFormat.from_str(pcm_format)
|
||||
self._pcm_format_in = pcm_format
|
||||
# We always output stereo INT16 at the same nominal sample rate.
|
||||
self._pcm_format_out = PcmFormat(
|
||||
PcmFormat.Endianness.LITTLE,
|
||||
PcmFormat.SampleType.INT16,
|
||||
pcm_format.sample_rate,
|
||||
2,
|
||||
)
|
||||
|
||||
# sounddevice stream (created in _open)
|
||||
self._stream = None # type: ignore[assignment]
|
||||
|
||||
# --- ASRC state (inspired by asrc.py) ---
|
||||
# Nominal input/output rate ratio
|
||||
self._r = 1.0
|
||||
self._integral = 0.0
|
||||
self._phi = 0.0 # fractional read position within current chunk
|
||||
|
||||
# PI gains (tiny to avoid warble)
|
||||
self._Kp = 2e-6
|
||||
self._Ki = 5e-8
|
||||
self._R0 = 1.0
|
||||
|
||||
# Target FIFO level and deadband (≈10 ms target, 0.5 ms deadband)
|
||||
fs = float(self._pcm_format_in.sample_rate)
|
||||
self._target_samples = max(1, int(0.010 * fs))
|
||||
self._deadband = max(1, int(0.0005 * fs))
|
||||
|
||||
# Ring buffer for mono float32 samples
|
||||
# Capacity ~2 seconds for headroom
|
||||
self._rb_cap = max(self._target_samples * 32, int(2 * fs))
|
||||
self._rb = None # created in _init_rb()
|
||||
self._ridx = 0
|
||||
self._size = 0
|
||||
self._lock = None # created in _init_rb()
|
||||
self._init_rb()
|
||||
|
||||
# Light logging timer
|
||||
self._last_log = 0.0
|
||||
|
||||
# Streaming resampler and internal output buffer (lazy init)
|
||||
self._rs = None # samplerate.Resampler
|
||||
self._out_buf = None # numpy.ndarray float32
|
||||
|
||||
# ---------------- Internal helpers -----------------
|
||||
def _init_rb(self) -> None:
|
||||
# Lazy import standard libs to keep only io_bumble imported at top level
|
||||
import threading
|
||||
from array import array
|
||||
|
||||
self._rb = array('f', [0.0] * self._rb_cap) # float32 ring buffer
|
||||
self._lock = threading.Lock()
|
||||
self._ridx = 0
|
||||
self._size = 0
|
||||
|
||||
def _fifo_len(self) -> int:
|
||||
with self._lock:
|
||||
return self._size
|
||||
|
||||
def _fifo_write(self, x_f32) -> None:
|
||||
# x_f32: 1-D float32-like iterable
|
||||
k = len(x_f32)
|
||||
if k <= 0:
|
||||
return
|
||||
rb = self._rb
|
||||
if rb is None:
|
||||
return
|
||||
with self._lock:
|
||||
# Trim if larger than capacity: keep last N
|
||||
if k >= self._rb_cap:
|
||||
x_f32 = x_f32[-self._rb_cap:]
|
||||
k = self._rb_cap
|
||||
# Make room on overflow (drop oldest)
|
||||
excess = max(0, self._size + k - self._rb_cap)
|
||||
if excess:
|
||||
self._ridx = (self._ridx + excess) % self._rb_cap
|
||||
self._size -= excess
|
||||
# Write at tail position
|
||||
wpos = (self._ridx + self._size) % self._rb_cap
|
||||
first = min(k, self._rb_cap - wpos)
|
||||
# Write first chunk
|
||||
from array import array as _array # lazy import
|
||||
rb[wpos:wpos + first] = _array('f', x_f32[:first])
|
||||
# Wrap if needed
|
||||
second = k - first
|
||||
if second:
|
||||
rb[0:second] = _array('f', x_f32[first:])
|
||||
self._size += k
|
||||
|
||||
def _fifo_peek_array(self, n: int):
|
||||
# Returns a Python list[float] copy of up to n samples
|
||||
rb = self._rb
|
||||
if rb is None:
|
||||
return []
|
||||
m = max(0, min(n, self._fifo_len()))
|
||||
if m <= 0:
|
||||
return []
|
||||
pos = self._ridx
|
||||
first = min(m, self._rb_cap - pos)
|
||||
# Copy out
|
||||
out = [0.0] * m
|
||||
# First chunk
|
||||
out[:first] = rb[pos:pos + first]
|
||||
# Second chunk if wrap
|
||||
second = m - first
|
||||
if second > 0:
|
||||
out[first:] = rb[0:second]
|
||||
return out
|
||||
|
||||
def _fifo_discard(self, n: int) -> None:
|
||||
with self._lock:
|
||||
d = max(0, min(n, self._size))
|
||||
self._ridx = (self._ridx + d) % self._rb_cap
|
||||
self._size -= d
|
||||
|
||||
def _update_ratio(self) -> None:
|
||||
# PI loop to hold buffer near target
|
||||
e = self._target_samples - self._fifo_len()
|
||||
if -self._deadband <= e <= self._deadband:
|
||||
e = 0.0
|
||||
cand_integral = self._integral + e
|
||||
r_unclamped = self._R0 * (1.0 + self._Kp * e + self._Ki * cand_integral)
|
||||
# Limit to ±1000 ppm vs nominal
|
||||
ppm_unclamped = 1e6 * (r_unclamped / self._R0 - 1.0)
|
||||
saturated_high = ppm_unclamped > 1000.0
|
||||
saturated_low = ppm_unclamped < -1000.0
|
||||
if saturated_high:
|
||||
self._r = self._R0 * (1 + 1000e-6)
|
||||
if e <= 0:
|
||||
self._integral = cand_integral
|
||||
self._integral *= 0.99
|
||||
elif saturated_low:
|
||||
self._r = self._R0 * (1 - 1000e-6)
|
||||
if e >= 0:
|
||||
self._integral = cand_integral
|
||||
self._integral *= 0.99
|
||||
else:
|
||||
self._integral = cand_integral
|
||||
self._r = r_unclamped
|
||||
|
||||
# Occasional log
|
||||
try:
|
||||
import time as _time
|
||||
now = _time.time()
|
||||
if now - self._last_log > 1.0:
|
||||
buf_ms = 1000.0 * self._fifo_len() / float(self._pcm_format_in.sample_rate)
|
||||
print(
|
||||
f"\nASRC buf={buf_ms:5.1f} ms r={self._r:.9f} corr={1e6 * (self._r / self._R0 - 1.0):+7.1f} ppm"
|
||||
)
|
||||
self._last_log = now
|
||||
except Exception:
|
||||
# Logging must never break audio
|
||||
pass
|
||||
|
||||
def _process(self, n_out: int) -> list[float]:
|
||||
# Accumulate at least n_out samples using samplerate.Resampler
|
||||
if n_out <= 0:
|
||||
return []
|
||||
# Lazy imports
|
||||
import numpy as np # type: ignore
|
||||
|
||||
# Lazy init output buffer
|
||||
if self._out_buf is None:
|
||||
self._out_buf = np.zeros(0, dtype=np.float32)
|
||||
|
||||
# Choose chunk so we don't take too much from FIFO each time
|
||||
max_chunk = max(256, int(np.ceil(n_out / max(1e-9, self._r))))
|
||||
safety_iters = 0
|
||||
while self._out_buf.size < n_out and safety_iters < 16:
|
||||
safety_iters += 1
|
||||
available = self._fifo_len()
|
||||
if available <= 0:
|
||||
break
|
||||
take = min(available, max_chunk)
|
||||
x = self._fifo_peek_array(take)
|
||||
self._fifo_discard(take)
|
||||
if not x:
|
||||
break
|
||||
x_arr = np.asarray(x, dtype=np.float32)
|
||||
if self._rs is not None:
|
||||
try:
|
||||
y = self._rs.process(x_arr, ratio=float(self._r), end_of_input=False)
|
||||
except Exception:
|
||||
logger.exception("ASRC resampler error")
|
||||
y = None
|
||||
else:
|
||||
y = None
|
||||
if y is not None and getattr(y, 'size', 0):
|
||||
y = y.astype(np.float32, copy=False)
|
||||
if self._out_buf.size == 0:
|
||||
self._out_buf = y
|
||||
else:
|
||||
self._out_buf = np.concatenate((self._out_buf, y))
|
||||
|
||||
if self._out_buf.size >= n_out:
|
||||
out = self._out_buf[:n_out]
|
||||
self._out_buf = self._out_buf[n_out:]
|
||||
return out.tolist()
|
||||
else:
|
||||
# Not enough data produced; pad with zeros
|
||||
out = np.zeros(n_out, dtype=np.float32)
|
||||
if self._out_buf.size:
|
||||
out[: self._out_buf.size] = self._out_buf
|
||||
self._out_buf = np.zeros(0, dtype=np.float32)
|
||||
return out.tolist()
|
||||
|
||||
def _mono_to_stereo_int16_bytes(self, mono_f32: list[float]) -> bytes:
|
||||
# Convert [-1,1] float list to stereo int16 little-endian bytes
|
||||
import struct
|
||||
ba = bytearray()
|
||||
for v in mono_f32:
|
||||
# clip
|
||||
if v > 1.0:
|
||||
v = 1.0
|
||||
elif v < -1.0:
|
||||
v = -1.0
|
||||
i16 = int(v * 32767.0)
|
||||
ba += struct.pack('<hh', i16, i16)
|
||||
return bytes(ba)
|
||||
|
||||
# ---------------- ThreadedAudioInput hooks -----------------
|
||||
def _open(self) -> PcmFormat:
|
||||
# Set up sounddevice RawInputStream (int16) and start callback producer
|
||||
import sounddevice # pylint: disable=import-error
|
||||
import math
|
||||
import samplerate as sr # type: ignore
|
||||
|
||||
# We capture mono regardless of requested channels, then output stereo.
|
||||
channels = 1
|
||||
samplerate = int(self._pcm_format_in.sample_rate)
|
||||
|
||||
def _callback(indata, frames, time_info, status): # noqa: ARG001 (signature is fixed)
|
||||
# indata: raw int16 bytes-like buffer of shape (frames, channels)
|
||||
try:
|
||||
if status:
|
||||
logger.warning("Input status: %s", status)
|
||||
if frames <= 0:
|
||||
return
|
||||
# Interpret raw bytes as little-endian int16 mono
|
||||
mv = memoryview(indata).cast('h') # len == frames * channels
|
||||
# Convert to float in [-1, 1]
|
||||
# Avoid division errors; protect NaN/Inf
|
||||
mono = []
|
||||
for i in range(frames):
|
||||
v = mv[i]
|
||||
f = float(v) / 32768.0
|
||||
if not (f == f) or math.isinf(f):
|
||||
f = 0.0
|
||||
mono.append(f)
|
||||
self._fifo_write(mono)
|
||||
except Exception: # never let callback raise
|
||||
logger.exception("Audio input callback error")
|
||||
|
||||
# Create streaming resampler (mono)
|
||||
try:
|
||||
self._rs = sr.Resampler(converter_type="sinc_fastest", channels=1)
|
||||
except Exception:
|
||||
logger.exception("Failed to create samplerate.Resampler; audio may be silent")
|
||||
self._rs = None
|
||||
|
||||
self._stream = sounddevice.RawInputStream(
|
||||
samplerate=samplerate,
|
||||
device=self._device,
|
||||
channels=channels,
|
||||
dtype='int16',
|
||||
callback=_callback,
|
||||
)
|
||||
self._stream.start()
|
||||
|
||||
return self._pcm_format_out
|
||||
|
||||
def _read(self, frame_size: int) -> bytes:
|
||||
# Produce 'frame_size' output frames (stereo INT16)
|
||||
if frame_size <= 0:
|
||||
return b''
|
||||
# Update resampling ratio based on FIFO level
|
||||
try:
|
||||
self._update_ratio()
|
||||
except Exception:
|
||||
# keep going even if update failed
|
||||
pass
|
||||
# Process mono float32
|
||||
mono = self._process(frame_size)
|
||||
# Convert to stereo int16 LE bytes
|
||||
return self._mono_to_stereo_int16_bytes(mono)
|
||||
|
||||
def _close(self) -> None:
|
||||
try:
|
||||
if self._stream is not None:
|
||||
self._stream.stop()
|
||||
self._stream.close()
|
||||
except Exception:
|
||||
logger.exception('Error closing input stream')
|
||||
finally:
|
||||
self._stream = None
|
||||
+1126
-613
File diff suppressed because it is too large
Load Diff
+43
-3
@@ -33,7 +33,6 @@ from bumble.hci import (
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
HCI_COMMAND_PACKET,
|
||||
HCI_COMMAND_STATUS_PENDING,
|
||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||
HCI_CONTROLLER_BUSY_ERROR,
|
||||
HCI_EVENT_PACKET,
|
||||
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||
@@ -88,6 +87,7 @@ class CisLink:
|
||||
cis_id: int
|
||||
cig_id: int
|
||||
acl_connection: Optional[Connection] = None
|
||||
data_paths: set[int] = dataclasses.field(default_factory=set)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -381,6 +381,11 @@ class Controller:
|
||||
return connection
|
||||
return None
|
||||
|
||||
def find_iso_link_by_handle(self, handle: int) -> Optional[CisLink]:
|
||||
return self.central_cis_links.get(handle) or self.peripheral_cis_links.get(
|
||||
handle
|
||||
)
|
||||
|
||||
def on_link_central_connected(self, central_address):
|
||||
'''
|
||||
Called when an incoming connection occurs from a central on the link
|
||||
@@ -1853,16 +1858,51 @@ class Controller:
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_setup_iso_data_path_command(self, command):
|
||||
def on_hci_le_setup_iso_data_path_command(
|
||||
self, command: hci.HCI_LE_Setup_ISO_Data_Path_Command
|
||||
) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.109 LE Setup ISO Data Path Command
|
||||
'''
|
||||
if not (iso_link := self.find_iso_link_by_handle(command.connection_handle)):
|
||||
return struct.pack(
|
||||
'<BH',
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
command.connection_handle,
|
||||
)
|
||||
if command.data_path_direction in iso_link.data_paths:
|
||||
return struct.pack(
|
||||
'<BH',
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
command.connection_handle,
|
||||
)
|
||||
iso_link.data_paths.add(command.data_path_direction)
|
||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||
|
||||
def on_hci_le_remove_iso_data_path_command(self, command):
|
||||
def on_hci_le_remove_iso_data_path_command(
|
||||
self, command: hci.HCI_LE_Remove_ISO_Data_Path_Command
|
||||
) -> bytes:
|
||||
'''
|
||||
See Bluetooth spec Vol 4, Part E - 7.8.110 LE Remove ISO Data Path Command
|
||||
'''
|
||||
if not (iso_link := self.find_iso_link_by_handle(command.connection_handle)):
|
||||
return struct.pack(
|
||||
'<BH',
|
||||
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
||||
command.connection_handle,
|
||||
)
|
||||
data_paths: set[int] = set(
|
||||
direction
|
||||
for direction in hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction
|
||||
if (1 << direction) & command.data_path_direction
|
||||
)
|
||||
if not data_paths.issubset(iso_link.data_paths):
|
||||
return struct.pack(
|
||||
'<BH',
|
||||
HCI_COMMAND_DISALLOWED_ERROR,
|
||||
command.connection_handle,
|
||||
)
|
||||
iso_link.data_paths.difference_update(data_paths)
|
||||
return struct.pack('<BH', HCI_SUCCESS, command.connection_handle)
|
||||
|
||||
def on_hci_le_set_host_feature_command(
|
||||
|
||||
+573
-254
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+285
-232
File diff suppressed because it is too large
Load Diff
+62
-13
@@ -26,7 +26,17 @@ import secrets
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import field
|
||||
from typing import Any, Callable, ClassVar, Iterable, Optional, TypeVar, Union, cast
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -111,23 +121,57 @@ def phy_list_to_bits(phys: Optional[Iterable[Phy]]) -> int:
|
||||
class SpecableEnum(utils.OpenIntEnum):
|
||||
|
||||
@classmethod
|
||||
def type_spec(cls, size: int):
|
||||
return {'size': size, 'mapper': lambda x: cls(x).name}
|
||||
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
|
||||
return {
|
||||
'serializer': lambda x: x.to_bytes(size, byteorder),
|
||||
'parser': lambda data, offset: (
|
||||
offset + size,
|
||||
cls(int.from_bytes(data[offset : offset + size], byteorder)),
|
||||
),
|
||||
'mapper': lambda x: cls(x).name,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
|
||||
return metadata(cls.type_spec(size), list_begin=list_begin, list_end=list_end)
|
||||
def type_metadata(
|
||||
cls,
|
||||
size: int,
|
||||
list_begin: bool = False,
|
||||
list_end: bool = False,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
return metadata(
|
||||
cls.type_spec(size, byteorder),
|
||||
list_begin=list_begin,
|
||||
list_end=list_end,
|
||||
)
|
||||
|
||||
|
||||
class SpecableFlag(enum.IntFlag):
|
||||
|
||||
@classmethod
|
||||
def type_spec(cls, size: int):
|
||||
return {'size': size, 'mapper': lambda x: cls(x).name}
|
||||
def type_spec(cls, size: int, byteorder: Literal['little', 'big'] = 'little'):
|
||||
return {
|
||||
'serializer': lambda x: x.to_bytes(size, byteorder),
|
||||
'parser': lambda data, offset: (
|
||||
offset + size,
|
||||
cls(int.from_bytes(data[offset : offset + size], byteorder)),
|
||||
),
|
||||
'mapper': lambda x: cls(x).name,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_metadata(cls, size: int, list_begin: bool = False, list_end: bool = False):
|
||||
return metadata(cls.type_spec(size), list_begin=list_begin, list_end=list_end)
|
||||
def type_metadata(
|
||||
cls,
|
||||
size: int,
|
||||
list_begin: bool = False,
|
||||
list_end: bool = False,
|
||||
byteorder: Literal['little', 'big'] = 'little',
|
||||
):
|
||||
return metadata(
|
||||
cls.type_spec(size, byteorder),
|
||||
list_begin=list_begin,
|
||||
list_end=list_end,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1322,7 +1366,7 @@ class LeFeature(SpecableEnum):
|
||||
MONITORING_ADVERTISERS = 64
|
||||
FRAME_SPACE_UPDATE = 65
|
||||
|
||||
class LeFeatureMask(enum.IntFlag):
|
||||
class LeFeatureMask(utils.CompatibleIntFlag):
|
||||
LE_ENCRYPTION = 1 << LeFeature.LE_ENCRYPTION
|
||||
CONNECTION_PARAMETERS_REQUEST_PROCEDURE = 1 << LeFeature.CONNECTION_PARAMETERS_REQUEST_PROCEDURE
|
||||
EXTENDED_REJECT_INDICATION = 1 << LeFeature.EXTENDED_REJECT_INDICATION
|
||||
@@ -1463,7 +1507,7 @@ class LmpFeature(SpecableEnum):
|
||||
SLOT_AVAILABILITY_MASK = 138
|
||||
TRAIN_NUDGING = 139
|
||||
|
||||
class LmpFeatureMask(enum.IntFlag):
|
||||
class LmpFeatureMask(utils.CompatibleIntFlag):
|
||||
# Page 0 (Legacy LMP features)
|
||||
LMP_3_SLOT_PACKETS = (1 << LmpFeature.LMP_3_SLOT_PACKETS)
|
||||
LMP_5_SLOT_PACKETS = (1 << LmpFeature.LMP_5_SLOT_PACKETS)
|
||||
@@ -2135,6 +2179,7 @@ class Address:
|
||||
if len(address) == 12 + 5:
|
||||
# Form with ':' separators
|
||||
address = address.replace(':', '')
|
||||
|
||||
self.address_bytes = bytes(reversed(bytes.fromhex(address)))
|
||||
|
||||
if len(self.address_bytes) != 6:
|
||||
@@ -6421,7 +6466,9 @@ class HCI_LE_Create_BIG_Complete_Event(HCI_LE_Meta_Event):
|
||||
irc: int = field(metadata=metadata(1))
|
||||
max_pdu: int = field(metadata=metadata(2))
|
||||
iso_interval: int = field(metadata=metadata(2))
|
||||
connection_handle: int = field(metadata=metadata(2, list_begin=True, list_end=True))
|
||||
connection_handle: Sequence[int] = field(
|
||||
metadata=metadata(2, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -6453,7 +6500,9 @@ class HCI_LE_BIG_Sync_Established_Event(HCI_LE_Meta_Event):
|
||||
irc: int = field(metadata=metadata(1))
|
||||
max_pdu: int = field(metadata=metadata(2))
|
||||
iso_interval: int = field(metadata=metadata(2))
|
||||
connection_handle: int = field(metadata=metadata(2, list_begin=True, list_end=True))
|
||||
connection_handle: Sequence[int] = field(
|
||||
metadata=metadata(2, list_begin=True, list_end=True)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+16
-8
@@ -217,33 +217,41 @@ class HID(ABC, utils.EventEmitter):
|
||||
self.role = role
|
||||
|
||||
# Register ourselves with the L2CAP channel manager
|
||||
device.register_l2cap_server(HID_CONTROL_PSM, self.on_l2cap_connection)
|
||||
device.register_l2cap_server(HID_INTERRUPT_PSM, self.on_l2cap_connection)
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM), self.on_l2cap_connection
|
||||
)
|
||||
device.create_l2cap_server(
|
||||
l2cap.ClassicChannelSpec(HID_INTERRUPT_PSM), self.on_l2cap_connection
|
||||
)
|
||||
|
||||
device.on(device.EVENT_CONNECTION, self.on_device_connection)
|
||||
|
||||
async def connect_control_channel(self) -> None:
|
||||
if not self.connection:
|
||||
raise InvalidStateError("Connection is not established!")
|
||||
# Create a new L2CAP connection - control channel
|
||||
try:
|
||||
channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, HID_CONTROL_PSM
|
||||
channel = await self.connection.create_l2cap_channel(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
|
||||
)
|
||||
channel.sink = self.on_ctrl_pdu
|
||||
self.l2cap_ctrl_channel = channel
|
||||
except ProtocolError:
|
||||
logging.exception(f'L2CAP connection failed.')
|
||||
logging.exception('L2CAP connection failed.')
|
||||
raise
|
||||
|
||||
async def connect_interrupt_channel(self) -> None:
|
||||
if not self.connection:
|
||||
raise InvalidStateError("Connection is not established!")
|
||||
# Create a new L2CAP connection - interrupt channel
|
||||
try:
|
||||
channel = await self.device.l2cap_channel_manager.connect(
|
||||
self.connection, HID_INTERRUPT_PSM
|
||||
channel = await self.connection.create_l2cap_channel(
|
||||
l2cap.ClassicChannelSpec(HID_CONTROL_PSM)
|
||||
)
|
||||
channel.sink = self.on_intr_pdu
|
||||
self.l2cap_intr_channel = channel
|
||||
except ProtocolError:
|
||||
logging.exception(f'L2CAP connection failed.')
|
||||
logging.exception('L2CAP connection failed.')
|
||||
raise
|
||||
|
||||
async def disconnect_interrupt_channel(self) -> None:
|
||||
|
||||
+177
-83
@@ -22,11 +22,11 @@ import collections
|
||||
import dataclasses
|
||||
import logging
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, cast
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Union, cast
|
||||
|
||||
from bumble import drivers, hci, utils
|
||||
from bumble.colors import color
|
||||
from bumble.core import ConnectionParameters, ConnectionPHY, PhysicalTransport
|
||||
from bumble.core import ConnectionPHY, InvalidStateError, PhysicalTransport
|
||||
from bumble.l2cap import L2CAP_PDU
|
||||
from bumble.snoop import Snooper
|
||||
from bumble.transport.common import TransportLostError
|
||||
@@ -550,7 +550,7 @@ class Host(utils.EventEmitter):
|
||||
logger.debug(
|
||||
'HCI LE flow control: '
|
||||
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets},'
|
||||
f'iso_data_packet_length={iso_data_packet_length},'
|
||||
f'total_num_iso_data_packets={total_num_iso_data_packets}'
|
||||
)
|
||||
@@ -902,10 +902,14 @@ class Host(utils.EventEmitter):
|
||||
def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
|
||||
self.emit('l2cap_pdu', connection.handle, cid, pdu)
|
||||
|
||||
def on_command_processed(self, event):
|
||||
def on_command_processed(
|
||||
self, event: Union[hci.HCI_Command_Complete_Event, hci.HCI_Command_Status_Event]
|
||||
):
|
||||
if self.pending_response:
|
||||
# Check that it is what we were expecting
|
||||
if self.pending_command.op_code != event.command_opcode:
|
||||
if self.pending_command is None:
|
||||
logger.warning('!!! pending_command is None ')
|
||||
elif self.pending_command.op_code != event.command_opcode:
|
||||
logger.warning(
|
||||
'!!! command result mismatch, expected '
|
||||
f'0x{self.pending_command.op_code:X} but got '
|
||||
@@ -919,10 +923,10 @@ class Host(utils.EventEmitter):
|
||||
############################################################
|
||||
# HCI handlers
|
||||
############################################################
|
||||
def on_hci_event(self, event):
|
||||
def on_hci_event(self, event: hci.HCI_Event):
|
||||
logger.warning(f'{color(f"--- Ignoring event {event}", "red")}')
|
||||
|
||||
def on_hci_command_complete_event(self, event):
|
||||
def on_hci_command_complete_event(self, event: hci.HCI_Command_Complete_Event):
|
||||
if event.command_opcode == 0:
|
||||
# This is used just for the Num_HCI_Command_Packets field, not related to
|
||||
# an actual command
|
||||
@@ -931,7 +935,7 @@ class Host(utils.EventEmitter):
|
||||
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_command_status_event(self, event):
|
||||
def on_hci_command_status_event(self, event: hci.HCI_Command_Status_Event):
|
||||
return self.on_command_processed(event)
|
||||
|
||||
def on_hci_number_of_completed_packets_event(
|
||||
@@ -951,7 +955,7 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
|
||||
# Classic only
|
||||
def on_hci_connection_request_event(self, event):
|
||||
def on_hci_connection_request_event(self, event: hci.HCI_Connection_Request_Event):
|
||||
# Notify the listeners
|
||||
self.emit(
|
||||
'connection_request',
|
||||
@@ -960,7 +964,14 @@ class Host(utils.EventEmitter):
|
||||
event.link_type,
|
||||
)
|
||||
|
||||
def on_hci_le_connection_complete_event(self, event):
|
||||
def on_hci_le_connection_complete_event(
|
||||
self,
|
||||
event: Union[
|
||||
hci.HCI_LE_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
|
||||
],
|
||||
):
|
||||
# Check if this is a cancellation
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
@@ -980,20 +991,16 @@ class Host(utils.EventEmitter):
|
||||
self.connections[event.connection_handle] = connection
|
||||
|
||||
# Notify the client
|
||||
connection_parameters = ConnectionParameters(
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
self.emit(
|
||||
'connection',
|
||||
'le_connection',
|
||||
event.connection_handle,
|
||||
PhysicalTransport.LE,
|
||||
event.peer_address,
|
||||
getattr(event, 'local_resolvable_private_address', None),
|
||||
getattr(event, 'peer_resolvable_private_address', None),
|
||||
hci.Role(event.role),
|
||||
connection_parameters,
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### CONNECTION FAILED: {event.status}')
|
||||
@@ -1006,15 +1013,25 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_event(self, event):
|
||||
def on_hci_le_enhanced_connection_complete_event(
|
||||
self,
|
||||
event: Union[
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_Event,
|
||||
hci.HCI_LE_Enhanced_Connection_Complete_V2_Event,
|
||||
],
|
||||
):
|
||||
# Just use the same implementation as for the non-enhanced event for now
|
||||
self.on_hci_le_connection_complete_event(event)
|
||||
|
||||
def on_hci_le_enhanced_connection_complete_v2_event(self, event):
|
||||
def on_hci_le_enhanced_connection_complete_v2_event(
|
||||
self, event: hci.HCI_LE_Enhanced_Connection_Complete_V2_Event
|
||||
):
|
||||
# Just use the same implementation as for the v1 event for now
|
||||
self.on_hci_le_enhanced_connection_complete_event(event)
|
||||
|
||||
def on_hci_connection_complete_event(self, event):
|
||||
def on_hci_connection_complete_event(
|
||||
self, event: hci.HCI_Connection_Complete_Event
|
||||
):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
@@ -1034,14 +1051,9 @@ class Host(utils.EventEmitter):
|
||||
|
||||
# Notify the client
|
||||
self.emit(
|
||||
'connection',
|
||||
'classic_connection',
|
||||
event.connection_handle,
|
||||
PhysicalTransport.BR_EDR,
|
||||
event.bd_addr,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
else:
|
||||
logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}')
|
||||
@@ -1054,7 +1066,9 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_disconnection_complete_event(self, event):
|
||||
def on_hci_disconnection_complete_event(
|
||||
self, event: hci.HCI_Disconnection_Complete_Event
|
||||
):
|
||||
# Find the connection
|
||||
handle = event.connection_handle
|
||||
if (
|
||||
@@ -1093,27 +1107,30 @@ class Host(utils.EventEmitter):
|
||||
# Notify the listeners
|
||||
self.emit('disconnection_failure', handle, event.status)
|
||||
|
||||
def on_hci_le_connection_update_complete_event(self, event):
|
||||
def on_hci_le_connection_update_complete_event(
|
||||
self, event: hci.HCI_LE_Connection_Update_Complete_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
connection_parameters = ConnectionParameters(
|
||||
self.emit(
|
||||
'connection_parameters_update',
|
||||
connection.handle,
|
||||
event.connection_interval,
|
||||
event.peripheral_latency,
|
||||
event.supervision_timeout,
|
||||
)
|
||||
self.emit(
|
||||
'connection_parameters_update', connection.handle, connection_parameters
|
||||
)
|
||||
else:
|
||||
self.emit(
|
||||
'connection_parameters_update_failure', connection.handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_phy_update_complete_event(self, event):
|
||||
def on_hci_le_phy_update_complete_event(
|
||||
self, event: hci.HCI_LE_PHY_Update_Complete_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle')
|
||||
return
|
||||
@@ -1143,7 +1160,9 @@ class Host(utils.EventEmitter):
|
||||
):
|
||||
self.on_hci_le_advertising_report_event(event)
|
||||
|
||||
def on_hci_le_advertising_set_terminated_event(self, event):
|
||||
def on_hci_le_advertising_set_terminated_event(
|
||||
self, event: hci.HCI_LE_Advertising_Set_Terminated_Event
|
||||
):
|
||||
self.emit(
|
||||
'advertising_set_termination',
|
||||
event.status,
|
||||
@@ -1152,7 +1171,9 @@ class Host(utils.EventEmitter):
|
||||
event.num_completed_extended_advertising_events,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_established_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_established_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Established_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_establishment',
|
||||
event.status,
|
||||
@@ -1164,16 +1185,22 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_lost_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_lost_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Lost_Event
|
||||
):
|
||||
self.emit('periodic_advertising_sync_loss', event.sync_handle)
|
||||
|
||||
def on_hci_le_periodic_advertising_report_event(self, event):
|
||||
def on_hci_le_periodic_advertising_report_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Report_Event
|
||||
):
|
||||
self.emit('periodic_advertising_report', event.sync_handle, event)
|
||||
|
||||
def on_hci_le_biginfo_advertising_report_event(self, event):
|
||||
def on_hci_le_biginfo_advertising_report_event(
|
||||
self, event: hci.HCI_LE_BIGInfo_Advertising_Report_Event
|
||||
):
|
||||
self.emit('biginfo_advertising_report', event.sync_handle, event)
|
||||
|
||||
def on_hci_le_cis_request_event(self, event):
|
||||
def on_hci_le_cis_request_event(self, event: hci.HCI_LE_CIS_Request_Event):
|
||||
self.emit(
|
||||
'cis_request',
|
||||
event.acl_connection_handle,
|
||||
@@ -1182,10 +1209,12 @@ class Host(utils.EventEmitter):
|
||||
event.cis_id,
|
||||
)
|
||||
|
||||
def on_hci_le_create_big_complete_event(self, event):
|
||||
def on_hci_le_create_big_complete_event(
|
||||
self, event: hci.HCI_LE_Create_BIG_Complete_Event
|
||||
):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
logger.warning("BIS established but ISO packets not supported")
|
||||
raise InvalidStateError("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
@@ -1208,8 +1237,13 @@ class Host(utils.EventEmitter):
|
||||
event.iso_interval,
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_established_event(self, event):
|
||||
def on_hci_le_big_sync_established_event(
|
||||
self, event: hci.HCI_LE_BIG_Sync_Established_Event
|
||||
):
|
||||
self.bigs[event.big_handle] = set(event.connection_handle)
|
||||
if self.iso_packet_queue is None:
|
||||
raise InvalidStateError("BIS established but ISO packets not supported")
|
||||
|
||||
for connection_handle in event.connection_handle:
|
||||
self.bis_links[connection_handle] = IsoLink(
|
||||
connection_handle, self.iso_packet_queue
|
||||
@@ -1229,15 +1263,19 @@ class Host(utils.EventEmitter):
|
||||
event.connection_handle,
|
||||
)
|
||||
|
||||
def on_hci_le_big_sync_lost_event(self, event):
|
||||
def on_hci_le_big_sync_lost_event(self, event: hci.HCI_LE_BIG_Sync_Lost_Event):
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_sync_lost', event.big_handle, event.reason)
|
||||
|
||||
def on_hci_le_terminate_big_complete_event(self, event):
|
||||
def on_hci_le_terminate_big_complete_event(
|
||||
self, event: hci.HCI_LE_Terminate_BIG_Complete_Event
|
||||
):
|
||||
self.remove_big(event.big_handle)
|
||||
self.emit('big_termination', event.reason, event.big_handle)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_transfer',
|
||||
event.status,
|
||||
@@ -1250,7 +1288,9 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(self, event):
|
||||
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(
|
||||
self, event: hci.HCI_LE_Periodic_Advertising_Sync_Transfer_Received_V2_Event
|
||||
):
|
||||
self.emit(
|
||||
'periodic_advertising_sync_transfer',
|
||||
event.status,
|
||||
@@ -1263,11 +1303,11 @@ class Host(utils.EventEmitter):
|
||||
event.advertiser_clock_accuracy,
|
||||
)
|
||||
|
||||
def on_hci_le_cis_established_event(self, event):
|
||||
def on_hci_le_cis_established_event(self, event: hci.HCI_LE_CIS_Established_Event):
|
||||
# The remaining parameters are unused for now.
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
if self.iso_packet_queue is None:
|
||||
logger.warning("CIS established but ISO packets not supported")
|
||||
raise InvalidStateError("CIS established but ISO packets not supported")
|
||||
self.cis_links[event.connection_handle] = IsoLink(
|
||||
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
||||
)
|
||||
@@ -1294,7 +1334,9 @@ class Host(utils.EventEmitter):
|
||||
'cis_establishment_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_le_remote_connection_parameter_request_event(self, event):
|
||||
def on_hci_le_remote_connection_parameter_request_event(
|
||||
self, event: hci.HCI_LE_Remote_Connection_Parameter_Request_Event
|
||||
):
|
||||
if event.connection_handle not in self.connections:
|
||||
logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle')
|
||||
return
|
||||
@@ -1313,7 +1355,9 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
)
|
||||
|
||||
def on_hci_le_long_term_key_request_event(self, event):
|
||||
def on_hci_le_long_term_key_request_event(
|
||||
self, event: hci.HCI_LE_Long_Term_Key_Request_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle')
|
||||
return
|
||||
@@ -1347,7 +1391,9 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_long_term_key())
|
||||
|
||||
def on_hci_synchronous_connection_complete_event(self, event):
|
||||
def on_hci_synchronous_connection_complete_event(
|
||||
self, event: hci.HCI_Synchronous_Connection_Complete_Event
|
||||
):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
# Create/update the connection
|
||||
logger.debug(
|
||||
@@ -1373,7 +1419,9 @@ class Host(utils.EventEmitter):
|
||||
# Notify the client
|
||||
self.emit('sco_connection_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_synchronous_connection_changed_event(self, event):
|
||||
def on_hci_synchronous_connection_changed_event(
|
||||
self, event: hci.HCI_Synchronous_Connection_Changed_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_mode_change_event(self, event: hci.HCI_Mode_Change_Event):
|
||||
@@ -1385,7 +1433,7 @@ class Host(utils.EventEmitter):
|
||||
event.interval,
|
||||
)
|
||||
|
||||
def on_hci_role_change_event(self, event):
|
||||
def on_hci_role_change_event(self, event: hci.HCI_Role_Change_Event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
logger.debug(
|
||||
f'role change for {event.bd_addr}: '
|
||||
@@ -1399,7 +1447,9 @@ class Host(utils.EventEmitter):
|
||||
)
|
||||
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_le_data_length_change_event(self, event):
|
||||
def on_hci_le_data_length_change_event(
|
||||
self, event: hci.HCI_LE_Data_Length_Change_Event
|
||||
):
|
||||
if (connection := self.connections.get(event.connection_handle)) is None:
|
||||
logger.warning('!!! DATA LENGTH CHANGE: unknown handle')
|
||||
return
|
||||
@@ -1413,7 +1463,9 @@ class Host(utils.EventEmitter):
|
||||
event.max_rx_time,
|
||||
)
|
||||
|
||||
def on_hci_authentication_complete_event(self, event):
|
||||
def on_hci_authentication_complete_event(
|
||||
self, event: hci.HCI_Authentication_Complete_Event
|
||||
):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit('connection_authentication', event.connection_handle)
|
||||
@@ -1454,7 +1506,9 @@ class Host(utils.EventEmitter):
|
||||
'connection_encryption_failure', event.connection_handle, event.status
|
||||
)
|
||||
|
||||
def on_hci_encryption_key_refresh_complete_event(self, event):
|
||||
def on_hci_encryption_key_refresh_complete_event(
|
||||
self, event: hci.HCI_Encryption_Key_Refresh_Complete_Event
|
||||
):
|
||||
# Notify the client
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit('connection_encryption_key_refresh', event.connection_handle)
|
||||
@@ -1465,7 +1519,7 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_qos_setup_complete_event(self, event):
|
||||
def on_hci_qos_setup_complete_event(self, event: hci.HCI_QOS_Setup_Complete_Event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'connection_qos_setup', event.connection_handle, event.service_type
|
||||
@@ -1477,23 +1531,31 @@ class Host(utils.EventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||
def on_hci_link_supervision_timeout_changed_event(
|
||||
self, event: hci.HCI_Link_Supervision_Timeout_Changed_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_max_slots_change_event(self, event):
|
||||
def on_hci_max_slots_change_event(self, event: hci.HCI_Max_Slots_Change_Event):
|
||||
pass
|
||||
|
||||
def on_hci_page_scan_repetition_mode_change_event(self, event):
|
||||
def on_hci_page_scan_repetition_mode_change_event(
|
||||
self, event: hci.HCI_Page_Scan_Repetition_Mode_Change_Event
|
||||
):
|
||||
pass
|
||||
|
||||
def on_hci_link_key_notification_event(self, event):
|
||||
def on_hci_link_key_notification_event(
|
||||
self, event: hci.HCI_Link_Key_Notification_Event
|
||||
):
|
||||
logger.debug(
|
||||
f'link key for {event.bd_addr}: {event.link_key.hex()}, '
|
||||
f'type={hci.HCI_Constant.link_key_type_name(event.key_type)}'
|
||||
)
|
||||
self.emit('link_key', event.bd_addr, event.link_key, event.key_type)
|
||||
|
||||
def on_hci_simple_pairing_complete_event(self, event):
|
||||
def on_hci_simple_pairing_complete_event(
|
||||
self, event: hci.HCI_Simple_Pairing_Complete_Event
|
||||
):
|
||||
logger.debug(
|
||||
f'simple pairing complete for {event.bd_addr}: '
|
||||
f'status={hci.HCI_Constant.status_name(event.status)}'
|
||||
@@ -1503,10 +1565,10 @@ class Host(utils.EventEmitter):
|
||||
else:
|
||||
self.emit('classic_pairing_failure', event.bd_addr, event.status)
|
||||
|
||||
def on_hci_pin_code_request_event(self, event):
|
||||
def on_hci_pin_code_request_event(self, event: hci.HCI_PIN_Code_Request_Event):
|
||||
self.emit('pin_code_request', event.bd_addr)
|
||||
|
||||
def on_hci_link_key_request_event(self, event):
|
||||
def on_hci_link_key_request_event(self, event: hci.HCI_Link_Key_Request_Event):
|
||||
async def send_link_key():
|
||||
if self.link_key_provider is None:
|
||||
logger.debug('no link key provider')
|
||||
@@ -1531,10 +1593,14 @@ class Host(utils.EventEmitter):
|
||||
|
||||
asyncio.create_task(send_link_key())
|
||||
|
||||
def on_hci_io_capability_request_event(self, event):
|
||||
def on_hci_io_capability_request_event(
|
||||
self, event: hci.HCI_IO_Capability_Request_Event
|
||||
):
|
||||
self.emit('authentication_io_capability_request', event.bd_addr)
|
||||
|
||||
def on_hci_io_capability_response_event(self, event):
|
||||
def on_hci_io_capability_response_event(
|
||||
self, event: hci.HCI_IO_Capability_Response_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_io_capability_response',
|
||||
event.bd_addr,
|
||||
@@ -1542,25 +1608,33 @@ class Host(utils.EventEmitter):
|
||||
event.authentication_requirements,
|
||||
)
|
||||
|
||||
def on_hci_user_confirmation_request_event(self, event):
|
||||
def on_hci_user_confirmation_request_event(
|
||||
self, event: hci.HCI_User_Confirmation_Request_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_user_confirmation_request',
|
||||
event.bd_addr,
|
||||
event.numeric_value,
|
||||
)
|
||||
|
||||
def on_hci_user_passkey_request_event(self, event):
|
||||
def on_hci_user_passkey_request_event(
|
||||
self, event: hci.HCI_User_Passkey_Request_Event
|
||||
):
|
||||
self.emit('authentication_user_passkey_request', event.bd_addr)
|
||||
|
||||
def on_hci_user_passkey_notification_event(self, event):
|
||||
def on_hci_user_passkey_notification_event(
|
||||
self, event: hci.HCI_User_Passkey_Notification_Event
|
||||
):
|
||||
self.emit(
|
||||
'authentication_user_passkey_notification', event.bd_addr, event.passkey
|
||||
)
|
||||
|
||||
def on_hci_inquiry_complete_event(self, _event):
|
||||
def on_hci_inquiry_complete_event(self, _event: hci.HCI_Inquiry_Complete_Event):
|
||||
self.emit('inquiry_complete')
|
||||
|
||||
def on_hci_inquiry_result_with_rssi_event(self, event):
|
||||
def on_hci_inquiry_result_with_rssi_event(
|
||||
self, event: hci.HCI_Inquiry_Result_With_RSSI_Event
|
||||
):
|
||||
for bd_addr, class_of_device, rssi in zip(
|
||||
event.bd_addr, event.class_of_device, event.rssi
|
||||
):
|
||||
@@ -1572,7 +1646,9 @@ class Host(utils.EventEmitter):
|
||||
rssi,
|
||||
)
|
||||
|
||||
def on_hci_extended_inquiry_result_event(self, event):
|
||||
def on_hci_extended_inquiry_result_event(
|
||||
self, event: hci.HCI_Extended_Inquiry_Result_Event
|
||||
):
|
||||
self.emit(
|
||||
'inquiry_result',
|
||||
event.bd_addr,
|
||||
@@ -1581,7 +1657,9 @@ class Host(utils.EventEmitter):
|
||||
event.rssi,
|
||||
)
|
||||
|
||||
def on_hci_remote_name_request_complete_event(self, event):
|
||||
def on_hci_remote_name_request_complete_event(
|
||||
self, event: hci.HCI_Remote_Name_Request_Complete_Event
|
||||
):
|
||||
if event.status != hci.HCI_SUCCESS:
|
||||
self.emit('remote_name_failure', event.bd_addr, event.status)
|
||||
else:
|
||||
@@ -1592,14 +1670,18 @@ class Host(utils.EventEmitter):
|
||||
|
||||
self.emit('remote_name', event.bd_addr, utf8_name)
|
||||
|
||||
def on_hci_remote_host_supported_features_notification_event(self, event):
|
||||
def on_hci_remote_host_supported_features_notification_event(
|
||||
self, event: hci.HCI_Remote_Host_Supported_Features_Notification_Event
|
||||
):
|
||||
self.emit(
|
||||
'remote_host_supported_features',
|
||||
event.bd_addr,
|
||||
event.host_supported_features,
|
||||
)
|
||||
|
||||
def on_hci_le_read_remote_features_complete_event(self, event):
|
||||
def on_hci_le_read_remote_features_complete_event(
|
||||
self, event: hci.HCI_LE_Read_Remote_Features_Complete_Event
|
||||
):
|
||||
if event.status != hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'le_remote_features_failure', event.connection_handle, event.status
|
||||
@@ -1611,22 +1693,34 @@ class Host(utils.EventEmitter):
|
||||
int.from_bytes(event.le_features, 'little'),
|
||||
)
|
||||
|
||||
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(self, event):
|
||||
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Read_Remote_Supported_Capabilities_Complete_Event
|
||||
):
|
||||
self.emit('cs_remote_supported_capabilities', event)
|
||||
|
||||
def on_hci_le_cs_security_enable_complete_event(self, event):
|
||||
def on_hci_le_cs_security_enable_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Security_Enable_Complete_Event
|
||||
):
|
||||
self.emit('cs_security', event)
|
||||
|
||||
def on_hci_le_cs_config_complete_event(self, event):
|
||||
def on_hci_le_cs_config_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Config_Complete_Event
|
||||
):
|
||||
self.emit('cs_config', event)
|
||||
|
||||
def on_hci_le_cs_procedure_enable_complete_event(self, event):
|
||||
def on_hci_le_cs_procedure_enable_complete_event(
|
||||
self, event: hci.HCI_LE_CS_Procedure_Enable_Complete_Event
|
||||
):
|
||||
self.emit('cs_procedure', event)
|
||||
|
||||
def on_hci_le_cs_subevent_result_event(self, event):
|
||||
def on_hci_le_cs_subevent_result_event(
|
||||
self, event: hci.HCI_LE_CS_Subevent_Result_Event
|
||||
):
|
||||
self.emit('cs_subevent_result', event)
|
||||
|
||||
def on_hci_le_cs_subevent_result_continue_event(self, event):
|
||||
def on_hci_le_cs_subevent_result_continue_event(
|
||||
self, event: hci.HCI_LE_CS_Subevent_Result_Continue_Event
|
||||
):
|
||||
self.emit('cs_subevent_result_continue', event)
|
||||
|
||||
def on_hci_le_subrate_change_event(self, event: hci.HCI_LE_Subrate_Change_Event):
|
||||
@@ -1639,5 +1733,5 @@ class Host(utils.EventEmitter):
|
||||
event.supervision_timeout,
|
||||
)
|
||||
|
||||
def on_hci_vendor_event(self, event):
|
||||
def on_hci_vendor_event(self, event: hci.HCI_Vendor_Event):
|
||||
self.emit('vendor_event', event)
|
||||
|
||||
@@ -1531,16 +1531,6 @@ class ChannelManager:
|
||||
if cid in self.fixed_channels:
|
||||
del self.fixed_channels[cid]
|
||||
|
||||
@utils.deprecated("Please use create_classic_server")
|
||||
def register_server(
|
||||
self,
|
||||
psm: int,
|
||||
server: Callable[[ClassicChannel], Any],
|
||||
) -> int:
|
||||
return self.create_classic_server(
|
||||
handler=server, spec=ClassicChannelSpec(psm=psm)
|
||||
).psm
|
||||
|
||||
def create_classic_server(
|
||||
self,
|
||||
spec: ClassicChannelSpec,
|
||||
@@ -1577,22 +1567,6 @@ class ChannelManager:
|
||||
|
||||
return self.servers[spec.psm]
|
||||
|
||||
@utils.deprecated("Please use create_le_credit_based_server()")
|
||||
def register_le_coc_server(
|
||||
self,
|
||||
psm: int,
|
||||
server: Callable[[LeCreditBasedChannel], Any],
|
||||
max_credits: int,
|
||||
mtu: int,
|
||||
mps: int,
|
||||
) -> int:
|
||||
return self.create_le_credit_based_server(
|
||||
spec=LeCreditBasedChannelSpec(
|
||||
psm=None if psm == 0 else psm, mtu=mtu, mps=mps, max_credits=max_credits
|
||||
),
|
||||
handler=server,
|
||||
).psm
|
||||
|
||||
def create_le_credit_based_server(
|
||||
self,
|
||||
spec: LeCreditBasedChannelSpec,
|
||||
@@ -2145,17 +2119,6 @@ class ChannelManager:
|
||||
if channel.source_cid in connection_channels:
|
||||
del connection_channels[channel.source_cid]
|
||||
|
||||
@utils.deprecated("Please use create_le_credit_based_channel()")
|
||||
async def open_le_coc(
|
||||
self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
|
||||
) -> LeCreditBasedChannel:
|
||||
return await self.create_le_credit_based_channel(
|
||||
connection=connection,
|
||||
spec=LeCreditBasedChannelSpec(
|
||||
psm=psm, max_credits=max_credits, mtu=mtu, mps=mps
|
||||
),
|
||||
)
|
||||
|
||||
async def create_le_credit_based_channel(
|
||||
self,
|
||||
connection: Connection,
|
||||
@@ -2202,12 +2165,6 @@ class ChannelManager:
|
||||
|
||||
return channel
|
||||
|
||||
@utils.deprecated("Please use create_classic_channel()")
|
||||
async def connect(self, connection: Connection, psm: int) -> ClassicChannel:
|
||||
return await self.create_classic_channel(
|
||||
connection=connection, spec=ClassicChannelSpec(psm=psm)
|
||||
)
|
||||
|
||||
async def create_classic_channel(
|
||||
self, connection: Connection, spec: ClassicChannelSpec
|
||||
) -> ClassicChannel:
|
||||
@@ -2244,20 +2201,3 @@ class ChannelManager:
|
||||
raise e
|
||||
|
||||
return channel
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Deprecated Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Channel(ClassicChannel):
|
||||
@utils.deprecated("Please use ClassicChannel")
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class LeConnectionOrientedChannel(LeCreditBasedChannel):
|
||||
@utils.deprecated("Please use LeCreditBasedChannel")
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -21,7 +21,7 @@ import logging
|
||||
import struct
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
from bumble import gatt, gatt_client, l2cap, utils
|
||||
from bumble import data_types, gatt, gatt_client, l2cap, utils
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Connection, Device
|
||||
|
||||
@@ -185,12 +185,11 @@ class AshaService(gatt.TemplateService):
|
||||
return bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
bytes(gatt.GATT_ASHA_SERVICE)
|
||||
+ bytes([self.protocol_version, self.capability])
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_ASHA_SERVICE,
|
||||
bytes([self.protocol_version, self.capability])
|
||||
+ self.hisyncid[:4],
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
+8
-17
@@ -27,7 +27,7 @@ from collections.abc import Sequence
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core, gatt, hci, utils
|
||||
from bumble import core, data_types, gatt, hci, utils
|
||||
from bumble.profiles import le_audio
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -257,11 +257,10 @@ class UnicastServerAdvertisingData:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE,
|
||||
struct.pack(
|
||||
'<2sBIB',
|
||||
bytes(gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE),
|
||||
'<BIB',
|
||||
self.announcement_type,
|
||||
self.available_audio_contexts,
|
||||
len(self.metadata),
|
||||
@@ -490,12 +489,8 @@ class BroadcastAudioAnnouncement:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
(
|
||||
bytes(gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE)
|
||||
+ bytes(self)
|
||||
),
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -607,12 +602,8 @@ class BasicAudioAnnouncement:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
(
|
||||
bytes(gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE)
|
||||
+ bytes(self)
|
||||
),
|
||||
data_types.ServiceData16BitUUID(
|
||||
gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE, bytes(self)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
+90
-58
@@ -18,7 +18,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional, Union
|
||||
@@ -272,14 +271,21 @@ class HearingAccessService(gatt.TemplateService):
|
||||
def on_connection(connection: Connection) -> None:
|
||||
@connection.on(connection.EVENT_DISCONNECTION)
|
||||
def on_disconnection(_reason) -> None:
|
||||
self.currently_connected_clients.remove(connection)
|
||||
self.currently_connected_clients.discard(connection)
|
||||
|
||||
@connection.on(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
|
||||
def on_mtu_update(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
@connection.on(connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
|
||||
def on_encryption_change(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
@connection.on(connection.EVENT_PAIRING)
|
||||
def on_pairing(*_: Any) -> None:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
if connection.peer_resolvable_address:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
self.hearing_aid_features_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC,
|
||||
@@ -316,9 +322,30 @@ class HearingAccessService(gatt.TemplateService):
|
||||
]
|
||||
)
|
||||
|
||||
def on_incoming_paired_connection(self, connection: Connection):
|
||||
def on_incoming_connection(self, connection: Connection):
|
||||
'''Setup initial operations to handle a remote bonded HAP device'''
|
||||
# TODO Should we filter on HAP device only ?
|
||||
|
||||
if not connection.is_encrypted:
|
||||
logging.debug(f'HAS: {connection.peer_address} is not encrypted')
|
||||
return
|
||||
|
||||
if not connection.peer_resolvable_address:
|
||||
logging.debug(f'HAS: {connection.peer_address} is not paired')
|
||||
return
|
||||
|
||||
if connection.att_mtu < 49:
|
||||
logging.debug(
|
||||
f'HAS: {connection.peer_address} invalid MTU={connection.att_mtu}'
|
||||
)
|
||||
return
|
||||
|
||||
if connection.peer_address in self.currently_connected_clients:
|
||||
logging.debug(
|
||||
f'HAS: Already connected to {connection.peer_address} nothing to do'
|
||||
)
|
||||
return
|
||||
|
||||
self.currently_connected_clients.add(connection)
|
||||
if (
|
||||
connection.peer_address
|
||||
@@ -373,8 +400,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.preset_records[key]
|
||||
for key in sorted(self.preset_records.keys())
|
||||
if self.preset_records[key].index >= start_index
|
||||
]
|
||||
del presets[num_presets:]
|
||||
][:num_presets]
|
||||
if len(presets) == 0:
|
||||
raise att.ATT_Error(att.ErrorCode.OUT_OF_RANGE)
|
||||
|
||||
@@ -383,7 +409,10 @@ class HearingAccessService(gatt.TemplateService):
|
||||
async def _read_preset_response(
|
||||
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.
|
||||
# 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:
|
||||
for i, preset in enumerate(presets):
|
||||
await connection.device.indicate_subscriber(
|
||||
@@ -404,7 +433,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
|
||||
async def generic_update(self, op: PresetChangedOperation) -> None:
|
||||
'''Server API to perform a generic update. It is the responsibility of the caller to modify the preset_records to match the PresetChangedOperation being sent'''
|
||||
await self._notifyPresetOperations(op)
|
||||
await self._notify_preset_operations(op)
|
||||
|
||||
async def delete_preset(self, index: int) -> None:
|
||||
'''Server API to delete a preset. It should not be the current active preset'''
|
||||
@@ -413,14 +442,14 @@ class HearingAccessService(gatt.TemplateService):
|
||||
raise InvalidStateError('Cannot delete active preset')
|
||||
|
||||
del self.preset_records[index]
|
||||
await self._notifyPresetOperations(PresetChangedOperationDeleted(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationDeleted(index))
|
||||
|
||||
async def available_preset(self, index: int) -> None:
|
||||
'''Server API to make a preset available'''
|
||||
|
||||
preset = self.preset_records[index]
|
||||
preset.properties.is_available = PresetRecord.Property.IsAvailable.IS_AVAILABLE
|
||||
await self._notifyPresetOperations(PresetChangedOperationAvailable(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationAvailable(index))
|
||||
|
||||
async def unavailable_preset(self, index: int) -> None:
|
||||
'''Server API to make a preset unavailable. It should not be the current active preset'''
|
||||
@@ -432,7 +461,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
preset.properties.is_available = (
|
||||
PresetRecord.Property.IsAvailable.IS_UNAVAILABLE
|
||||
)
|
||||
await self._notifyPresetOperations(PresetChangedOperationUnavailable(index))
|
||||
await self._notify_preset_operations(PresetChangedOperationUnavailable(index))
|
||||
|
||||
async def _preset_changed_operation(self, connection: Connection) -> None:
|
||||
'''Send all PresetChangedOperation saved for a given connection'''
|
||||
@@ -447,27 +476,31 @@ class HearingAccessService(gatt.TemplateService):
|
||||
return op.additional_parameters
|
||||
|
||||
op_list.sort(key=get_op_index)
|
||||
# If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Preset Changed operation aborted and shall continue the operation when the client reconnects.
|
||||
while len(op_list) > 0:
|
||||
# If the ATT bearer is terminated before all notifications or indications are
|
||||
# sent, then the server shall consider the Preset Changed operation aborted and
|
||||
# shall continue the operation when the client reconnects.
|
||||
while op_list:
|
||||
try:
|
||||
await connection.device.indicate_subscriber(
|
||||
connection,
|
||||
self.hearing_aid_preset_control_point,
|
||||
value=op_list[0].to_bytes(len(op_list) == 1),
|
||||
force=True, # TODO GATT notification subscription should be persistent
|
||||
)
|
||||
# Remove item once sent, and keep the non sent item in the list
|
||||
op_list.pop(0)
|
||||
except TimeoutError:
|
||||
break
|
||||
|
||||
async def _notifyPresetOperations(self, op: PresetChangedOperation) -> None:
|
||||
for historyList in self.preset_changed_operations_history_per_device.values():
|
||||
historyList.append(op)
|
||||
async def _notify_preset_operations(self, op: PresetChangedOperation) -> None:
|
||||
for history_list in self.preset_changed_operations_history_per_device.values():
|
||||
history_list.append(op)
|
||||
|
||||
for connection in self.currently_connected_clients:
|
||||
await self._preset_changed_operation(connection)
|
||||
|
||||
async def _on_write_preset_name(self, connection: Connection, value: bytes):
|
||||
del connection # Unused
|
||||
|
||||
if self.read_presets_request_in_progress:
|
||||
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
|
||||
@@ -532,48 +565,51 @@ class HearingAccessService(gatt.TemplateService):
|
||||
self.active_preset_index = index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_active_preset(self, _: Connection, value: bytes):
|
||||
async def _on_set_active_preset(self, connection: Connection, value: bytes):
|
||||
del connection # Unused
|
||||
await self.set_active_preset(value)
|
||||
|
||||
async def set_next_or_previous_preset(self, is_previous):
|
||||
async def set_next_or_previous_preset(self, is_previous: bool) -> None:
|
||||
'''Set the next or the previous preset as active'''
|
||||
|
||||
if self.active_preset_index == 0x00:
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
|
||||
first_preset: Optional[PresetRecord] = None # To loop to first preset
|
||||
next_preset: Optional[PresetRecord] = None
|
||||
for index, record in sorted(self.preset_records.items(), reverse=is_previous):
|
||||
if not record.is_available():
|
||||
continue
|
||||
if first_preset == None:
|
||||
first_preset = record
|
||||
if is_previous:
|
||||
if index >= self.active_preset_index:
|
||||
continue
|
||||
elif index <= self.active_preset_index:
|
||||
continue
|
||||
next_preset = record
|
||||
break
|
||||
presets = sorted(
|
||||
[
|
||||
record
|
||||
for record in self.preset_records.values()
|
||||
if record.is_available()
|
||||
],
|
||||
key=lambda record: record.index,
|
||||
)
|
||||
current_preset = self.preset_records[self.active_preset_index]
|
||||
current_preset_pos = presets.index(current_preset)
|
||||
if is_previous:
|
||||
new_preset = presets[(current_preset_pos - 1) % len(presets)]
|
||||
else:
|
||||
new_preset = presets[(current_preset_pos + 1) % len(presets)]
|
||||
|
||||
if not first_preset: # If no other preset are available
|
||||
if current_preset == new_preset: # If no other preset are available
|
||||
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
|
||||
|
||||
if next_preset:
|
||||
self.active_preset_index = next_preset.index
|
||||
else:
|
||||
self.active_preset_index = first_preset.index
|
||||
self.active_preset_index = new_preset.index
|
||||
await self.notify_active_preset()
|
||||
|
||||
async def _on_set_next_preset(self, _: Connection, __value__: bytes) -> None:
|
||||
async def _on_set_next_preset(self, connection: Connection, value: bytes) -> None:
|
||||
del connection, value # Unused.
|
||||
await self.set_next_or_previous_preset(False)
|
||||
|
||||
async def _on_set_previous_preset(self, _: Connection, __value__: bytes) -> None:
|
||||
async def _on_set_previous_preset(
|
||||
self, connection: Connection, value: bytes
|
||||
) -> None:
|
||||
del connection, value # Unused.
|
||||
await self.set_next_or_previous_preset(True)
|
||||
|
||||
async def _on_set_active_preset_synchronized_locally(
|
||||
self, _: Connection, value: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -584,8 +620,9 @@ class HearingAccessService(gatt.TemplateService):
|
||||
await self.other_server_in_binaural_set.set_active_preset(value)
|
||||
|
||||
async def _on_set_next_preset_synchronized_locally(
|
||||
self, _: Connection, __value__: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection, value # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -596,8 +633,9 @@ class HearingAccessService(gatt.TemplateService):
|
||||
await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
|
||||
|
||||
async def _on_set_previous_preset_synchronized_locally(
|
||||
self, _: Connection, __value__: bytes
|
||||
self, connection: Connection, value: bytes
|
||||
):
|
||||
del connection, value # Unused.
|
||||
if (
|
||||
self.server_features.preset_synchronization_support
|
||||
== PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
|
||||
@@ -615,11 +653,13 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
SERVICE_CLASS = HearingAccessService
|
||||
|
||||
hearing_aid_preset_control_point: gatt_client.CharacteristicProxy
|
||||
preset_control_point_indications: asyncio.Queue
|
||||
active_preset_index_notification: asyncio.Queue
|
||||
preset_control_point_indications: asyncio.Queue[bytes]
|
||||
active_preset_index_notification: asyncio.Queue[bytes]
|
||||
|
||||
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
||||
self.service_proxy = service_proxy
|
||||
self.preset_control_point_indications = asyncio.Queue()
|
||||
self.active_preset_index_notification = asyncio.Queue()
|
||||
|
||||
self.server_features = gatt_adapters.PackedCharacteristicProxyAdapter(
|
||||
service_proxy.get_characteristics_by_uuid(
|
||||
@@ -641,20 +681,12 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
||||
'B',
|
||||
)
|
||||
|
||||
async def setup_subscription(self):
|
||||
self.preset_control_point_indications = asyncio.Queue()
|
||||
self.active_preset_index_notification = asyncio.Queue()
|
||||
|
||||
def on_active_preset_index_notification(data: bytes):
|
||||
self.active_preset_index_notification.put_nowait(data)
|
||||
|
||||
def on_preset_control_point_indication(data: bytes):
|
||||
self.preset_control_point_indications.put_nowait(data)
|
||||
|
||||
async def setup_subscription(self) -> None:
|
||||
await self.hearing_aid_preset_control_point.subscribe(
|
||||
functools.partial(on_preset_control_point_indication), prefer_notify=False
|
||||
self.preset_control_point_indications.put_nowait,
|
||||
prefer_notify=False,
|
||||
)
|
||||
|
||||
await self.active_preset_index.subscribe(
|
||||
functools.partial(on_active_preset_index_notification)
|
||||
self.active_preset_index_notification.put_nowait
|
||||
)
|
||||
|
||||
@@ -12,13 +12,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from bumble import utils
|
||||
@@ -85,12 +84,14 @@ async def open_transport(name: str) -> Transport:
|
||||
scheme, *tail = name.split(':', 1)
|
||||
spec = tail[0] if tail else None
|
||||
metadata = None
|
||||
if spec:
|
||||
# Metadata may precede the spec
|
||||
if spec.startswith('['):
|
||||
metadata_str, *tail = spec[1:].split(']')
|
||||
spec = tail[0] if tail else None
|
||||
metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
|
||||
if spec and (m := re.search(r'\[(\w+=\w+(?:,\w+=\w+)*,?)\]', spec)):
|
||||
metadata_str = m.group(1)
|
||||
if m.start() == 0:
|
||||
# <metadata><spec>
|
||||
spec = spec[m.end() :]
|
||||
else:
|
||||
spec = spec[: m.start()]
|
||||
metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
|
||||
|
||||
transport = await _open_transport(scheme, spec)
|
||||
if metadata:
|
||||
|
||||
@@ -131,7 +131,11 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
|
||||
|
||||
def cleanup():
|
||||
logger.debug("removing .ini file")
|
||||
ini_file.unlink()
|
||||
try:
|
||||
ini_file.unlink()
|
||||
except OSError as error:
|
||||
# Don't log at exception level, since this may happen normally.
|
||||
logger.debug(f'failed to remove .ini file ({error})')
|
||||
|
||||
atexit.register(cleanup)
|
||||
return True
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import serial_asyncio
|
||||
|
||||
@@ -28,25 +29,56 @@ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transp
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
DEFAULT_POST_OPEN_DELAY = 0.5 # in seconds
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes and Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SerialPacketSource(StreamPacketSource):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._ready = asyncio.Event()
|
||||
|
||||
async def wait_until_ready(self) -> None:
|
||||
await self._ready.wait()
|
||||
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
logger.debug('connection made')
|
||||
self._ready.set()
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
logger.debug('connection lost')
|
||||
self.on_transport_lost()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_serial_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a serial port transport.
|
||||
The parameter string has this syntax:
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr]
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]
|
||||
When <speed> is omitted, the default value of 1000000 is used
|
||||
When "rtscts" is specified, RTS/CTS hardware flow control is enabled
|
||||
When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled
|
||||
When "delay" is specified, a short delay is added after opening the port
|
||||
|
||||
Examples:
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
/dev/tty.usbmodem0006839912172,rtscts,delay
|
||||
'''
|
||||
|
||||
speed = 1000000
|
||||
rtscts = False
|
||||
dsrdtr = False
|
||||
delay = 0.0
|
||||
if ',' in spec:
|
||||
parts = spec.split(',')
|
||||
device = parts[0]
|
||||
@@ -55,13 +87,16 @@ async def open_serial_transport(spec: str) -> Transport:
|
||||
rtscts = True
|
||||
elif part == 'dsrdtr':
|
||||
dsrdtr = True
|
||||
elif part == 'delay':
|
||||
delay = DEFAULT_POST_OPEN_DELAY
|
||||
elif part.isnumeric():
|
||||
speed = int(part)
|
||||
else:
|
||||
device = spec
|
||||
|
||||
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
||||
asyncio.get_running_loop(),
|
||||
StreamPacketSource,
|
||||
SerialPacketSource,
|
||||
device,
|
||||
baudrate=speed,
|
||||
rtscts=rtscts,
|
||||
@@ -69,4 +104,23 @@ async def open_serial_transport(spec: str) -> Transport:
|
||||
)
|
||||
packet_sink = StreamPacketSink(serial_transport)
|
||||
|
||||
logger.debug('waiting for the port to be ready')
|
||||
await packet_source.wait_until_ready()
|
||||
logger.debug('port is ready')
|
||||
|
||||
# Try to assert DTR
|
||||
assert serial_transport.serial is not None
|
||||
try:
|
||||
serial_transport.serial.dtr = True
|
||||
logger.debug(
|
||||
f"DSR={serial_transport.serial.dsr}, DTR={serial_transport.serial.dtr}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'could not assert DTR: {e}')
|
||||
|
||||
# Wait a bit after opening the port, if requested
|
||||
if delay > 0.0:
|
||||
logger.debug(f'waiting {delay} seconds after opening the port')
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
return Transport(packet_source, packet_sink)
|
||||
|
||||
@@ -500,6 +500,22 @@ class OpenIntEnum(enum.IntEnum):
|
||||
return obj
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class CompatibleIntFlag(enum.IntFlag):
|
||||
"""
|
||||
Subclass of `enum.IntFlag` with a `composite_name` property that behaves like the
|
||||
`name` property of the `enum.IntFlag` implementation for python vesions >= 3.11
|
||||
"""
|
||||
|
||||
@property
|
||||
def composite_name(self) -> str:
|
||||
return '|'.join(
|
||||
name
|
||||
for flag in self.__class__
|
||||
if self.value & flag.value and (name := flag.name) is not None
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class ByteSerializable(Protocol):
|
||||
"""
|
||||
|
||||
@@ -4,9 +4,18 @@ SERIAL TRANSPORT
|
||||
The serial transport implements sending/receiving HCI packets over a UART (a.k.a serial port).
|
||||
|
||||
## Moniker
|
||||
The moniker syntax for a serial transport is: `serial:<device-path>[,<speed>]`
|
||||
When `<speed>` is omitted, the default value of 1000000 is used
|
||||
The moniker syntax for a serial transport is:
|
||||
`<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]`
|
||||
|
||||
When `<speed>` is omitted, the default value of 1000000 is used.
|
||||
When `rtscts` is specified, RTS/CTS hardware flow control is enabled.
|
||||
When `dsrdtr` is specified, DSR/DTR hardware flow control is enabled.
|
||||
When `delay` is specified, a short delay is added after opening the port.
|
||||
|
||||
!!! example
|
||||
`serial:/dev/tty.usbmodem0006839912172,1000000`
|
||||
Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps
|
||||
```
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
/dev/tty.usbmodem0006839912172,rtscts,delay
|
||||
```
|
||||
@@ -82,3 +82,7 @@ services and characteristics.
|
||||
|
||||
# `run_scanner.py`
|
||||
Run a host application connected to a 'real' BLE controller over a UART HCI to a dev board running Zephyr in HCI mode (could be any other UART BLE controller, or BlueZ over a virtual UART), that starts scanning and prints out the scan results.
|
||||
|
||||
|
||||
# run auracast with usb stick
|
||||
bumble-auracast transmit 'serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_CC69A2912F84AE5E-if00,1000000,rtscts' --input device
|
||||
|
||||
@@ -21,6 +21,7 @@ import struct
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
from bumble.profiles.battery_service import BatteryService
|
||||
@@ -47,15 +48,14 @@ async def main() -> None:
|
||||
device.advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble Battery', 'utf-8'),
|
||||
data_types.CompleteLocalName('Bumble Battery'),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[battery_service.uuid]
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(battery_service.uuid),
|
||||
data_types.Appearance(
|
||||
data_types.Appearance.Category.WEARABLE_AUDIO_DEVICE,
|
||||
data_types.Appearance.WearableAudioDeviceSubcategory.EARBUD,
|
||||
),
|
||||
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import struct
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
from bumble.profiles.device_information_service import DeviceInformationService
|
||||
@@ -53,11 +54,11 @@ async def main() -> None:
|
||||
device.advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble Device', 'utf-8'),
|
||||
data_types.CompleteLocalName('Bumble Device'),
|
||||
data_types.Appearance(
|
||||
data_types.Appearance.Category.HEART_RATE_SENSOR,
|
||||
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
|
||||
),
|
||||
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ import sys
|
||||
import time
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
from bumble.profiles.device_information_service import DeviceInformationService
|
||||
@@ -88,15 +89,14 @@ async def main() -> None:
|
||||
device.advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble Heart', 'utf-8'),
|
||||
data_types.CompleteLocalName('Bumble Heart'),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[heart_rate_service.uuid]
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(heart_rate_service.uuid),
|
||||
data_types.Appearance(
|
||||
data_types.Appearance.Category.HEART_RATE_SENSOR,
|
||||
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
|
||||
),
|
||||
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
+11
-8
@@ -23,6 +23,7 @@ import sys
|
||||
import websockets
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.colors import color
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Connection, Device, Peer
|
||||
@@ -341,16 +342,18 @@ async def keyboard_device(device, command):
|
||||
device.advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble Keyboard', 'utf-8'),
|
||||
data_types.CompleteLocalName('Bumble Keyboard'),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[GATT_HUMAN_INTERFACE_DEVICE_SERVICE]
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(GATT_HUMAN_INTERFACE_DEVICE_SERVICE),
|
||||
data_types.Appearance(
|
||||
data_types.Appearance.Category.HUMAN_INTERFACE_DEVICE,
|
||||
data_types.Appearance.HumanInterfaceDeviceSubcategory.KEYBOARD,
|
||||
),
|
||||
data_types.Flags(
|
||||
AdvertisingData.Flags.LE_LIMITED_DISCOVERABLE_MODE
|
||||
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||
),
|
||||
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x03C1)),
|
||||
(AdvertisingData.FLAGS, bytes([0x05])),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import struct
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import AdvertisingType, Device
|
||||
from bumble.hci import Address
|
||||
@@ -60,7 +61,10 @@ async def main() -> None:
|
||||
device.scan_response_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
|
||||
data_types.Appearance(
|
||||
data_types.Appearance.Category.HEART_RATE_SENSOR,
|
||||
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ from typing import Optional
|
||||
import websockets
|
||||
|
||||
import bumble.logging
|
||||
from bumble import decoder, gatt
|
||||
from bumble import data_types, decoder, gatt
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import AdvertisingParameters, Device
|
||||
from bumble.profiles import asha
|
||||
@@ -78,14 +78,10 @@ async def main() -> None:
|
||||
bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes(device.name, 'utf-8'),
|
||||
),
|
||||
(AdvertisingData.FLAGS, bytes([0x06])),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(gatt.GATT_ASHA_SERVICE),
|
||||
data_types.CompleteLocalName(device.name),
|
||||
data_types.Flags(AdvertisingData.Flags(0x06)),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[gatt.GATT_ASHA_SERVICE]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import secrets
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
from bumble.hci import Address
|
||||
@@ -66,23 +67,14 @@ async def main() -> None:
|
||||
bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes(f'Bumble LE Audio-{i}', 'utf-8'),
|
||||
data_types.CompleteLocalName(f'Bumble LE Audio-{i}'),
|
||||
data_types.Flags(
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_HOST_FLAG
|
||||
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes(
|
||||
[
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_HOST_FLAG
|
||||
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(CoordinatedSetIdentificationService.UUID),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[CoordinatedSetIdentificationService.UUID]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import asyncio
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
from bumble.profiles.hap import (
|
||||
@@ -71,23 +72,14 @@ async def main() -> None:
|
||||
advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble HearingAccessService', 'utf-8'),
|
||||
data_types.CompleteLocalName('Bumble HearingAccessService'),
|
||||
data_types.Flags(
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_HOST_FLAG
|
||||
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes(
|
||||
[
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_HOST_FLAG
|
||||
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(HearingAccessService.UUID),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[HearingAccessService.UUID]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ from typing import Optional
|
||||
import websockets
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import (
|
||||
AdvertisingEventProperties,
|
||||
@@ -106,17 +107,10 @@ async def main() -> None:
|
||||
advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble LE Audio', 'utf-8'),
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes([AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(PublishedAudioCapabilitiesService.UUID),
|
||||
data_types.CompleteLocalName('Bumble LE Audio'),
|
||||
data_types.Flags(AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[PublishedAudioCapabilitiesService.UUID]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ import struct
|
||||
import sys
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
from bumble.hci import CodecID, CodingFormat, HCI_IsoDataPacket
|
||||
@@ -111,23 +112,14 @@ async def main() -> None:
|
||||
bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble LE Audio', 'utf-8'),
|
||||
data_types.CompleteLocalName('Bumble LE Audio'),
|
||||
data_types.Flags(
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_HOST_FLAG
|
||||
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes(
|
||||
[
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_HOST_FLAG
|
||||
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(PublishedAudioCapabilitiesService.UUID),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[PublishedAudioCapabilitiesService.UUID]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ from typing import Optional
|
||||
import websockets
|
||||
|
||||
import bumble.logging
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import AdvertisingEventProperties, AdvertisingParameters, Device
|
||||
from bumble.hci import CodecID, CodingFormat, OwnAddressType
|
||||
@@ -127,23 +128,14 @@ async def main() -> None:
|
||||
bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble LE Audio', 'utf-8'),
|
||||
data_types.CompleteLocalName('Bumble LE Audio'),
|
||||
data_types.Flags(
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_HOST_FLAG
|
||||
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
|
||||
),
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes(
|
||||
[
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_HOST_FLAG
|
||||
| AdvertisingData.BR_EDR_CONTROLLER_FLAG
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(PublishedAudioCapabilitiesService.UUID),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[PublishedAudioCapabilitiesService.UUID]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
Generated
+140
-56
@@ -61,7 +61,7 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -71,7 +71,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -249,7 +249,7 @@ dependencies = [
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex 0.2.4",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
@@ -451,7 +451,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -483,24 +483,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.3"
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -683,9 +678,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.21"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
|
||||
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -693,7 +688,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"indexmap 2.11.3",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -706,6 +701,12 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
@@ -827,7 +828,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -856,7 +867,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.2",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -891,9 +902,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libusb1-sys"
|
||||
@@ -920,9 +931,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.5"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -996,13 +1007,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.8"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1073,9 +1084,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.60"
|
||||
version = "0.10.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
|
||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"cfg-if",
|
||||
@@ -1105,9 +1116,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.96"
|
||||
version = "0.9.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
|
||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1153,7 +1164,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1542,15 +1553,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.10"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1580,7 +1591,7 @@ version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1702,12 +1713,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.3"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1773,7 +1784,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1828,9 +1839,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.32.0"
|
||||
version = "1.38.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
|
||||
checksum = "68722da18b0fc4a05fdc1120b302b82051265792a1e1b399086e9b204b10ad3d"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -1840,16 +1851,16 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.3",
|
||||
"socket2 0.5.10",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2130,7 +2141,16 @@ version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2139,13 +2159,29 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2154,42 +2190,90 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
@@ -2197,5 +2281,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ documentation = "https://docs.rs/crate/bumble"
|
||||
authors = ["Marshall Pierce <marshallpierce@google.com>"]
|
||||
keywords = ["bluetooth", "ble"]
|
||||
categories = ["api-bindings", "network-programming"]
|
||||
rust-version = "1.76.0"
|
||||
rust-version = "1.80.0"
|
||||
|
||||
# https://github.com/frewsxcv/cargo-all-features#options
|
||||
[package.metadata.cargo-all-features]
|
||||
@@ -22,7 +22,7 @@ always_include_features = ["anyhow", "pyo3-asyncio-attributes", "dev-tools", "bu
|
||||
[dependencies]
|
||||
pyo3 = { version = "0.18.3", features = ["macros"] }
|
||||
pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime"] }
|
||||
tokio = { version = "1.28.2", features = ["macros", "signal"] }
|
||||
tokio = { version = "1.38.2", features = ["macros", "signal"] }
|
||||
nom = "7.1.3"
|
||||
strum = "0.25.0"
|
||||
strum_macros = "0.25.0"
|
||||
@@ -50,7 +50,7 @@ reqwest = { version = "0.11.20", features = ["blocking"], optional = true }
|
||||
rusb = { version = "0.9.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
tokio = { version = "1.38.2", features = ["full"] }
|
||||
tempfile = "3.6.0"
|
||||
nix = "0.26.2"
|
||||
anyhow = "1.0.71"
|
||||
|
||||
+242
-84
@@ -18,14 +18,12 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import Awaitable
|
||||
|
||||
import pytest
|
||||
|
||||
from bumble.a2dp import (
|
||||
AacMediaCodecInformation,
|
||||
OpusMediaCodecInformation,
|
||||
SbcMediaCodecInformation,
|
||||
)
|
||||
from bumble import a2dp
|
||||
from bumble.avdtp import (
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
@@ -82,6 +80,24 @@ class TwoDevices:
|
||||
self.paired[which] = keys
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Data:
|
||||
pointer: int = 0
|
||||
data: bytes
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
self.data = data
|
||||
|
||||
async def read(self, length: int) -> Awaitable[bytes]:
|
||||
def generate_read():
|
||||
end = min(self.pointer + length, len(self.data))
|
||||
chunk = self.data[self.pointer : end]
|
||||
self.pointer = end
|
||||
return chunk
|
||||
|
||||
return generate_read()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_connection():
|
||||
@@ -122,12 +138,12 @@ def source_codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation(
|
||||
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||
channel_mode=SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
block_length=SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
subbands=SbcMediaCodecInformation.Subbands.S_8,
|
||||
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
media_codec_information=a2dp.SbcMediaCodecInformation(
|
||||
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||
channel_mode=a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
block_length=a2dp.SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
subbands=a2dp.SbcMediaCodecInformation.Subbands.S_8,
|
||||
allocation_method=a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
@@ -139,23 +155,23 @@ def sink_codec_capabilities():
|
||||
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,
|
||||
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
subbands=SbcMediaCodecInformation.Subbands.S_4
|
||||
| SbcMediaCodecInformation.Subbands.S_8,
|
||||
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||
| SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||
media_codec_information=a2dp.SbcMediaCodecInformation(
|
||||
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||
channel_mode=a2dp.SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
block_length=a2dp.SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
subbands=a2dp.SbcMediaCodecInformation.Subbands.S_4
|
||||
| a2dp.SbcMediaCodecInformation.Subbands.S_8,
|
||||
allocation_method=a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||
| a2dp.SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
@@ -274,52 +290,54 @@ async def test_source_sink_1():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_sbc_codec_specific_information():
|
||||
sbc_info = SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
|
||||
sbc_info = a2dp.SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
|
||||
assert (
|
||||
sbc_info.sampling_frequency
|
||||
== SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
== a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
assert (
|
||||
sbc_info.channel_mode
|
||||
== SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO
|
||||
== a2dp.SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO
|
||||
)
|
||||
assert (
|
||||
sbc_info.block_length
|
||||
== SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| SbcMediaCodecInformation.BlockLength.BL_16
|
||||
== a2dp.SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16
|
||||
)
|
||||
assert (
|
||||
sbc_info.subbands
|
||||
== SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8
|
||||
== a2dp.SbcMediaCodecInformation.Subbands.S_4
|
||||
| a2dp.SbcMediaCodecInformation.Subbands.S_8
|
||||
)
|
||||
assert (
|
||||
sbc_info.allocation_method
|
||||
== SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||
== a2dp.SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS
|
||||
)
|
||||
assert sbc_info.minimum_bitpool_value == 2
|
||||
assert sbc_info.maximum_bitpool_value == 53
|
||||
|
||||
sbc_info2 = SbcMediaCodecInformation(
|
||||
SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8,
|
||||
SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
sbc_info2 = a2dp.SbcMediaCodecInformation(
|
||||
a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
a2dp.SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
a2dp.SbcMediaCodecInformation.BlockLength.BL_4
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
|
||||
| a2dp.SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
a2dp.SbcMediaCodecInformation.Subbands.S_4
|
||||
| a2dp.SbcMediaCodecInformation.Subbands.S_8,
|
||||
a2dp.SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| a2dp.SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
2,
|
||||
53,
|
||||
)
|
||||
@@ -329,36 +347,36 @@ def test_sbc_codec_specific_information():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_aac_codec_specific_information():
|
||||
aac_info = AacMediaCodecInformation.from_bytes(bytes.fromhex("f0018c83e800"))
|
||||
aac_info = a2dp.AacMediaCodecInformation.from_bytes(bytes.fromhex("f0018c83e800"))
|
||||
assert (
|
||||
aac_info.object_type
|
||||
== AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE
|
||||
== a2dp.AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE
|
||||
)
|
||||
assert (
|
||||
aac_info.sampling_frequency
|
||||
== AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
== a2dp.AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
assert (
|
||||
aac_info.channels
|
||||
== AacMediaCodecInformation.Channels.MONO
|
||||
| AacMediaCodecInformation.Channels.STEREO
|
||||
== a2dp.AacMediaCodecInformation.Channels.MONO
|
||||
| a2dp.AacMediaCodecInformation.Channels.STEREO
|
||||
)
|
||||
assert aac_info.vbr == 1
|
||||
assert aac_info.bitrate == 256000
|
||||
|
||||
aac_info2 = AacMediaCodecInformation(
|
||||
AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||
| AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE,
|
||||
AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| AacMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
AacMediaCodecInformation.Channels.MONO
|
||||
| AacMediaCodecInformation.Channels.STEREO,
|
||||
aac_info2 = a2dp.AacMediaCodecInformation(
|
||||
a2dp.AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LC
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_LTP
|
||||
| a2dp.AacMediaCodecInformation.ObjectType.MPEG_4_AAC_SCALABLE,
|
||||
a2dp.AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| a2dp.AacMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
a2dp.AacMediaCodecInformation.Channels.MONO
|
||||
| a2dp.AacMediaCodecInformation.Channels.STEREO,
|
||||
1,
|
||||
256000,
|
||||
)
|
||||
@@ -368,25 +386,159 @@ def test_aac_codec_specific_information():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_opus_codec_specific_information():
|
||||
opus_info = OpusMediaCodecInformation.from_bytes(bytes([0x92]))
|
||||
assert opus_info.vendor_id == OpusMediaCodecInformation.VENDOR_ID
|
||||
assert opus_info.codec_id == OpusMediaCodecInformation.CODEC_ID
|
||||
assert opus_info.frame_size == OpusMediaCodecInformation.FrameSize.FS_20MS
|
||||
assert opus_info.channel_mode == OpusMediaCodecInformation.ChannelMode.STEREO
|
||||
opus_info = a2dp.OpusMediaCodecInformation.from_bytes(bytes([0x92]))
|
||||
assert opus_info.vendor_id == a2dp.OpusMediaCodecInformation.VENDOR_ID
|
||||
assert opus_info.codec_id == a2dp.OpusMediaCodecInformation.CODEC_ID
|
||||
assert opus_info.frame_size == a2dp.OpusMediaCodecInformation.FrameSize.FS_20MS
|
||||
assert opus_info.channel_mode == a2dp.OpusMediaCodecInformation.ChannelMode.STEREO
|
||||
assert (
|
||||
opus_info.sampling_frequency
|
||||
== OpusMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
== a2dp.OpusMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
|
||||
opus_info2 = OpusMediaCodecInformation(
|
||||
OpusMediaCodecInformation.ChannelMode.STEREO,
|
||||
OpusMediaCodecInformation.FrameSize.FS_20MS,
|
||||
OpusMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
opus_info2 = a2dp.OpusMediaCodecInformation(
|
||||
a2dp.OpusMediaCodecInformation.ChannelMode.STEREO,
|
||||
a2dp.OpusMediaCodecInformation.FrameSize.FS_20MS,
|
||||
a2dp.OpusMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
)
|
||||
assert opus_info2 == opus_info
|
||||
assert opus_info2.value == bytes([0x92])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_sbc_parser():
|
||||
header = b'\x9c\x80\x08\x00'
|
||||
payload = b'\x00\x00\x00\x00\x00\x00'
|
||||
data = Data(header + payload)
|
||||
|
||||
parser = a2dp.SbcParser(data.read)
|
||||
async for frame in parser.frames:
|
||||
assert frame.sampling_frequency == 44100
|
||||
assert frame.block_count == 4
|
||||
assert frame.channel_mode == 0
|
||||
assert frame.allocation_method == 0
|
||||
assert frame.subband_count == 4
|
||||
assert frame.bitpool == 8
|
||||
assert frame.payload == header + payload
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_sbc_packet_source():
|
||||
header = b'\x9c\x80\x08\x00'
|
||||
payload = b'\x00\x00\x00\x00\x00\x00'
|
||||
data = Data((header + payload) * 2)
|
||||
|
||||
packet_source = a2dp.SbcPacketSource(data.read, 23)
|
||||
async for packet in packet_source.packets:
|
||||
assert packet.sequence_number == 0
|
||||
assert packet.timestamp == 0
|
||||
assert packet.payload == b'\x01' + header + payload
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_aac_parser():
|
||||
header = b'\xff\xf0\x10\x00\x01\xa0\x00'
|
||||
payload = b'\x00\x00\x00\x00\x00\x00'
|
||||
data = Data(header + payload)
|
||||
|
||||
parser = a2dp.AacParser(data.read)
|
||||
async for frame in parser.frames:
|
||||
assert frame.profile == a2dp.AacFrame.Profile.MAIN
|
||||
assert frame.sampling_frequency == 44100
|
||||
assert frame.channel_configuration == 0
|
||||
assert frame.payload == payload
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_aac_packet_source():
|
||||
header = b'\xff\xf0\x10\x00\x01\xa0\x00'
|
||||
payload = b'\x00\x00\x00\x00\x00\x00'
|
||||
data = Data(header + payload)
|
||||
|
||||
packet_source = a2dp.AacPacketSource(data.read, 0)
|
||||
async for packet in packet_source.packets:
|
||||
assert packet.sequence_number == 0
|
||||
assert packet.timestamp == 0
|
||||
assert packet.payload == b' \x00\x12\x00\x00\x000\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_opus_parser():
|
||||
packed_header_data_revised = struct.pack(
|
||||
"<QIIIB",
|
||||
0, # granule_position
|
||||
2, # bitstream_serial_number
|
||||
2, # page_sequence_number
|
||||
0, # crc_checksum
|
||||
3, # page_segments
|
||||
)
|
||||
|
||||
first_page_header_revised = (
|
||||
b'OggS' # Capture pattern
|
||||
+ b'\x00' # Version
|
||||
+ b'\x02' # Header type
|
||||
+ packed_header_data_revised
|
||||
)
|
||||
|
||||
segment_table_revised = b'\x0a\x08\x0a'
|
||||
|
||||
opus_head_packet_data = b'OpusHead' + b'\x00' + b'\x00'
|
||||
opus_tags_packet_data = b'OpusTags'
|
||||
audio_data_packet = b'0123456789'
|
||||
|
||||
data = Data(
|
||||
first_page_header_revised
|
||||
+ segment_table_revised
|
||||
+ opus_head_packet_data
|
||||
+ opus_tags_packet_data
|
||||
+ audio_data_packet
|
||||
)
|
||||
|
||||
parser = a2dp.OpusParser(data.read)
|
||||
async for packet in parser.packets:
|
||||
assert packet.channel_mode == a2dp.OpusPacket.ChannelMode.STEREO
|
||||
assert packet.payload == audio_data_packet
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def test_opus_packet_source():
|
||||
packed_header_data_revised = struct.pack(
|
||||
"<QIIIB",
|
||||
0, # granule_position
|
||||
2, # bitstream_serial_number
|
||||
2, # page_sequence_number
|
||||
0, # crc_checksum
|
||||
3, # page_segments
|
||||
)
|
||||
|
||||
first_page_header_revised = (
|
||||
b'OggS' # Capture pattern
|
||||
+ b'\x00' # Version
|
||||
+ b'\x02' # Header type
|
||||
+ packed_header_data_revised
|
||||
)
|
||||
|
||||
segment_table_revised = b'\x0a\x08\x0a'
|
||||
|
||||
opus_head_packet_data = b'OpusHead' + b'\x00' + b'\x00'
|
||||
opus_tags_packet_data = b'OpusTags'
|
||||
audio_data_packet = b'0123456789'
|
||||
|
||||
data = Data(
|
||||
first_page_header_revised
|
||||
+ segment_table_revised
|
||||
+ opus_head_packet_data
|
||||
+ opus_tags_packet_data
|
||||
+ audio_data_packet
|
||||
)
|
||||
|
||||
parser = a2dp.OpusPacketSource(data.read, 0)
|
||||
async for packet in parser.packets:
|
||||
assert packet.sequence_number == 0
|
||||
assert packet.timestamp == 0
|
||||
assert packet.payload == b'\x01' + audio_data_packet
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main():
|
||||
test_sbc_codec_specific_information()
|
||||
@@ -394,6 +546,12 @@ async def async_main():
|
||||
test_opus_codec_specific_information()
|
||||
await test_self_connection()
|
||||
await test_source_sink_1()
|
||||
test_sbc_parser()
|
||||
test_sbc_packet_source()
|
||||
test_aac_parser()
|
||||
test_aac_packet_source()
|
||||
test_opus_parser()
|
||||
test_opus_packet_source()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
+241
-48
@@ -15,67 +15,261 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from bumble import avc, avctp, avrcp, controller, core, device, host, link
|
||||
from bumble.transport import common
|
||||
from bumble import avc, avctp, avrcp
|
||||
|
||||
from . import test_utils
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class TwoDevices:
|
||||
def __init__(self):
|
||||
self.connections = [None, None]
|
||||
|
||||
addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
|
||||
self.link = link.LocalLink()
|
||||
self.controllers = [
|
||||
controller.Controller('C1', link=self.link, public_address=addresses[0]),
|
||||
controller.Controller('C2', link=self.link, public_address=addresses[1]),
|
||||
]
|
||||
self.devices = [
|
||||
device.Device(
|
||||
address=addresses[0],
|
||||
host=host.Host(
|
||||
self.controllers[0], common.AsyncPipeSink(self.controllers[0])
|
||||
),
|
||||
),
|
||||
device.Device(
|
||||
address=addresses[1],
|
||||
host=host.Host(
|
||||
self.controllers[1], common.AsyncPipeSink(self.controllers[1])
|
||||
),
|
||||
),
|
||||
]
|
||||
self.devices[0].classic_enabled = True
|
||||
self.devices[1].classic_enabled = True
|
||||
self.connections = [None, None]
|
||||
self.protocols = [None, None]
|
||||
|
||||
def on_connection(self, which, connection):
|
||||
self.connections[which] = connection
|
||||
|
||||
async def setup_connections(self):
|
||||
await self.devices[0].power_on()
|
||||
await self.devices[1].power_on()
|
||||
|
||||
self.connections = await asyncio.gather(
|
||||
self.devices[0].connect(
|
||||
self.devices[1].public_address, core.PhysicalTransport.BR_EDR
|
||||
),
|
||||
self.devices[1].accept(self.devices[0].public_address),
|
||||
)
|
||||
class TwoDevices(test_utils.TwoDevices):
|
||||
protocols: Sequence[avrcp.Protocol] = ()
|
||||
|
||||
async def setup_avdtp_connections(self):
|
||||
self.protocols = [avrcp.Protocol(), avrcp.Protocol()]
|
||||
self.protocols[0].listen(self.devices[1])
|
||||
await self.protocols[1].connect(self.connections[0])
|
||||
|
||||
@classmethod
|
||||
async def create_with_avdtp(cls) -> TwoDevices:
|
||||
devices = await cls.create_with_connection()
|
||||
await devices.setup_avdtp_connections()
|
||||
return devices
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command,",
|
||||
[
|
||||
avrcp.GetPlayStatusCommand(),
|
||||
avrcp.GetCapabilitiesCommand(
|
||||
capability_id=avrcp.GetCapabilitiesCommand.CapabilityId.COMPANY_ID
|
||||
),
|
||||
avrcp.SetAbsoluteVolumeCommand(volume=5),
|
||||
avrcp.GetElementAttributesCommand(
|
||||
identifier=999,
|
||||
attribute_ids=[
|
||||
avrcp.MediaAttributeId.ALBUM_NAME,
|
||||
avrcp.MediaAttributeId.ARTIST_NAME,
|
||||
],
|
||||
),
|
||||
avrcp.RegisterNotificationCommand(
|
||||
event_id=avrcp.EventId.ADDRESSED_PLAYER_CHANGED, playback_interval=123
|
||||
),
|
||||
avrcp.SearchCommand(
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8, search_string="Bumble!"
|
||||
),
|
||||
avrcp.PlayItemCommand(
|
||||
scope=avrcp.Scope.MEDIA_PLAYER_LIST, uid=0, uid_counter=1
|
||||
),
|
||||
avrcp.ListPlayerApplicationSettingAttributesCommand(),
|
||||
avrcp.ListPlayerApplicationSettingValuesCommand(
|
||||
attribute=avrcp.ApplicationSetting.AttributeId.REPEAT_MODE
|
||||
),
|
||||
avrcp.GetCurrentPlayerApplicationSettingValueCommand(
|
||||
attribute=[
|
||||
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF,
|
||||
]
|
||||
),
|
||||
avrcp.SetPlayerApplicationSettingValueCommand(
|
||||
attribute=[avrcp.ApplicationSetting.AttributeId.REPEAT_MODE],
|
||||
value=[avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT],
|
||||
),
|
||||
avrcp.GetPlayerApplicationSettingAttributeTextCommand(
|
||||
attribute=[
|
||||
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF,
|
||||
]
|
||||
),
|
||||
avrcp.GetPlayerApplicationSettingValueTextCommand(
|
||||
attribute=avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
value=[
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
|
||||
],
|
||||
),
|
||||
avrcp.InformDisplayableCharacterSetCommand(
|
||||
character_set_id=[avrcp.CharacterSetId.UTF_8]
|
||||
),
|
||||
avrcp.InformBatteryStatusOfCtCommand(
|
||||
battery_status=avrcp.InformBatteryStatusOfCtCommand.BatteryStatus.NORMAL
|
||||
),
|
||||
avrcp.SetAddressedPlayerCommand(player_id=1),
|
||||
avrcp.SetBrowsedPlayerCommand(player_id=1),
|
||||
avrcp.GetFolderItemsCommand(
|
||||
scope=avrcp.Scope.NOW_PLAYING,
|
||||
start_item=0,
|
||||
end_item=1,
|
||||
attributes=[avrcp.MediaAttributeId.ARTIST_NAME],
|
||||
),
|
||||
avrcp.ChangePathCommand(
|
||||
uid_counter=1,
|
||||
direction=avrcp.ChangePathCommand.Direction.DOWN,
|
||||
folder_uid=2,
|
||||
),
|
||||
avrcp.GetItemAttributesCommand(
|
||||
scope=avrcp.Scope.NOW_PLAYING,
|
||||
uid=0,
|
||||
uid_counter=1,
|
||||
start_item=0,
|
||||
end_item=0,
|
||||
attributes=[avrcp.MediaAttributeId.DEFAULT_COVER_ART],
|
||||
),
|
||||
avrcp.GetTotalNumberOfItemsCommand(scope=avrcp.Scope.NOW_PLAYING),
|
||||
avrcp.AddToNowPlayingCommand(
|
||||
scope=avrcp.Scope.NOW_PLAYING, uid=0, uid_counter=1
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_command(command: avrcp.Command):
|
||||
assert avrcp.Command.from_bytes(command.pdu_id, bytes(command)) == command
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"event,",
|
||||
[
|
||||
avrcp.UidsChangedEvent(uid_counter=7),
|
||||
avrcp.TrackChangedEvent(identifier=b'12356'),
|
||||
avrcp.VolumeChangedEvent(volume=9),
|
||||
avrcp.PlaybackStatusChangedEvent(play_status=avrcp.PlayStatus.PLAYING),
|
||||
avrcp.AddressedPlayerChangedEvent(
|
||||
player=avrcp.AddressedPlayerChangedEvent.Player(player_id=9, uid_counter=10)
|
||||
),
|
||||
avrcp.AvailablePlayersChangedEvent(),
|
||||
avrcp.PlaybackPositionChangedEvent(playback_position=1314),
|
||||
avrcp.NowPlayingContentChangedEvent(),
|
||||
avrcp.PlayerApplicationSettingChangedEvent(
|
||||
player_application_settings=[
|
||||
avrcp.PlayerApplicationSettingChangedEvent.Setting(
|
||||
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
|
||||
)
|
||||
]
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_event(event: avrcp.Event):
|
||||
assert avrcp.Event.from_bytes(bytes(event)) == event
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"response,",
|
||||
[
|
||||
avrcp.GetPlayStatusResponse(
|
||||
song_length=1010, song_position=13, play_status=avrcp.PlayStatus.PAUSED
|
||||
),
|
||||
avrcp.GetCapabilitiesResponse(
|
||||
capability_id=avrcp.GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED,
|
||||
capabilities=[
|
||||
avrcp.EventId.ADDRESSED_PLAYER_CHANGED,
|
||||
avrcp.EventId.BATT_STATUS_CHANGED,
|
||||
],
|
||||
),
|
||||
avrcp.RegisterNotificationResponse(
|
||||
event=avrcp.PlaybackPositionChangedEvent(playback_position=38)
|
||||
),
|
||||
avrcp.SetAbsoluteVolumeResponse(volume=99),
|
||||
avrcp.GetElementAttributesResponse(
|
||||
attributes=[
|
||||
avrcp.MediaAttribute(
|
||||
attribute_id=avrcp.MediaAttributeId.ALBUM_NAME,
|
||||
attribute_value="White Album",
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8,
|
||||
)
|
||||
]
|
||||
),
|
||||
avrcp.ListPlayerApplicationSettingAttributesResponse(
|
||||
attribute=[
|
||||
avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,
|
||||
avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF,
|
||||
]
|
||||
),
|
||||
avrcp.ListPlayerApplicationSettingValuesResponse(
|
||||
value=[
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
|
||||
avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
|
||||
]
|
||||
),
|
||||
avrcp.GetCurrentPlayerApplicationSettingValueResponse(
|
||||
attribute=[avrcp.ApplicationSetting.AttributeId.REPEAT_MODE],
|
||||
value=[avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT],
|
||||
),
|
||||
avrcp.SetPlayerApplicationSettingValueResponse(),
|
||||
avrcp.GetPlayerApplicationSettingAttributeTextResponse(
|
||||
attribute=[avrcp.ApplicationSetting.AttributeId.REPEAT_MODE],
|
||||
character_set_id=[avrcp.CharacterSetId.UTF_8],
|
||||
attribute_string=["Repeat"],
|
||||
),
|
||||
avrcp.GetPlayerApplicationSettingValueTextResponse(
|
||||
value=[avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT],
|
||||
character_set_id=[avrcp.CharacterSetId.UTF_8],
|
||||
attribute_string=["All track repeat"],
|
||||
),
|
||||
avrcp.InformDisplayableCharacterSetResponse(),
|
||||
avrcp.InformBatteryStatusOfCtResponse(),
|
||||
avrcp.SetAddressedPlayerResponse(status=avrcp.StatusCode.OPERATION_COMPLETED),
|
||||
avrcp.SetBrowsedPlayerResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
uid_counter=1,
|
||||
numbers_of_items=2,
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8,
|
||||
folder_names=["folder1", "folder2"],
|
||||
),
|
||||
avrcp.GetFolderItemsResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
uid_counter=1,
|
||||
items=[
|
||||
avrcp.MediaPlayerItem(
|
||||
player_id=1,
|
||||
major_player_type=avrcp.MediaPlayerItem.MajorPlayerType.AUDIO,
|
||||
player_sub_type=avrcp.MediaPlayerItem.PlayerSubType.AUDIO_BOOK,
|
||||
play_status=avrcp.PlayStatus.FWD_SEEK,
|
||||
feature_bitmask=avrcp.MediaPlayerItem.Features.ADD_TO_NOW_PLAYING,
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8,
|
||||
displayable_name="Woo",
|
||||
)
|
||||
],
|
||||
),
|
||||
avrcp.ChangePathResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED, number_of_items=2
|
||||
),
|
||||
avrcp.GetItemAttributesResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
attribute_value_entry_list=[
|
||||
avrcp.AttributeValueEntry(
|
||||
attribute_id=avrcp.MediaAttributeId.GENRE,
|
||||
character_set_id=avrcp.CharacterSetId.UTF_8,
|
||||
attribute_value="uuddlrlrabab",
|
||||
)
|
||||
],
|
||||
),
|
||||
avrcp.GetTotalNumberOfItemsResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
uid_counter=1,
|
||||
number_of_items=2,
|
||||
),
|
||||
avrcp.SearchResponse(
|
||||
status=avrcp.StatusCode.OPERATION_COMPLETED,
|
||||
uid_counter=1,
|
||||
number_of_items=2,
|
||||
),
|
||||
avrcp.PlayItemResponse(status=avrcp.StatusCode.OPERATION_COMPLETED),
|
||||
avrcp.AddToNowPlayingResponse(status=avrcp.StatusCode.OPERATION_COMPLETED),
|
||||
],
|
||||
)
|
||||
def test_response(response: avrcp.Response):
|
||||
assert avrcp.Response.from_bytes(bytes(response), response.pdu_id) == response
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_frame_parser():
|
||||
with pytest.raises(ValueError) as error:
|
||||
with pytest.raises(ValueError):
|
||||
avc.Frame.from_bytes(bytes.fromhex("11480000"))
|
||||
|
||||
x = bytes.fromhex("014D0208")
|
||||
@@ -217,8 +411,7 @@ def test_passthrough_commands():
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_supported_events():
|
||||
two_devices = TwoDevices()
|
||||
await two_devices.setup_connections()
|
||||
two_devices = await TwoDevices.create_with_avdtp()
|
||||
|
||||
supported_events = await two_devices.protocols[0].get_supported_events()
|
||||
assert supported_events == []
|
||||
|
||||
+15
-21
@@ -310,12 +310,12 @@ async def test_pacs():
|
||||
@pytest.mark.asyncio
|
||||
async def test_ascs():
|
||||
devices = TwoDevices()
|
||||
devices[0].add_service(
|
||||
AudioStreamControlService(device=devices[0], sink_ase_id=[1, 2])
|
||||
devices[1].add_service(
|
||||
AudioStreamControlService(device=devices[1], sink_ase_id=[1, 2])
|
||||
)
|
||||
|
||||
await devices.setup_connection()
|
||||
peer = device.Peer(devices.connections[1])
|
||||
peer = device.Peer(devices.connections[0])
|
||||
ascs_client = await peer.discover_service_and_create_proxy(
|
||||
AudioStreamControlServiceProxy
|
||||
)
|
||||
@@ -369,7 +369,7 @@ async def test_ascs():
|
||||
await ascs_client.ase_control_point.write_value(
|
||||
ASE_Config_QOS(
|
||||
ase_id=[1, 2],
|
||||
cig_id=[1, 2],
|
||||
cig_id=[1, 1],
|
||||
cis_id=[3, 4],
|
||||
sdu_interval=[5, 6],
|
||||
framing=[0, 1],
|
||||
@@ -402,25 +402,19 @@ async def test_ascs():
|
||||
)
|
||||
|
||||
# CIS establishment
|
||||
devices[0].emit(
|
||||
'cis_establishment',
|
||||
device.CisLink(
|
||||
device=devices[0],
|
||||
acl_connection=devices.connections[0],
|
||||
handle=5,
|
||||
cis_id=3,
|
||||
cis_handles = await devices[0].setup_cig(
|
||||
device.CigParameters(
|
||||
cig_id=1,
|
||||
),
|
||||
cis_parameters=[
|
||||
device.CigParameters.CisParameters(cis_id=3),
|
||||
device.CigParameters.CisParameters(cis_id=4),
|
||||
],
|
||||
sdu_interval_c_to_p=0,
|
||||
sdu_interval_p_to_c=0,
|
||||
)
|
||||
)
|
||||
devices[0].emit(
|
||||
'cis_establishment',
|
||||
device.CisLink(
|
||||
device=devices[0],
|
||||
acl_connection=devices.connections[0],
|
||||
handle=6,
|
||||
cis_id=4,
|
||||
cig_id=2,
|
||||
),
|
||||
await devices[0].create_cis(
|
||||
[(cis_handle, devices.connections[0]) for cis_handle in cis_handles]
|
||||
)
|
||||
assert (await notifications[1].get())[:2] == bytes(
|
||||
[1, AseStateMachine.State.STREAMING]
|
||||
|
||||
+25
-1
@@ -16,7 +16,13 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
from bumble.core import UUID, AdvertisingData, Appearance, get_dict_key_by_value
|
||||
from bumble.core import (
|
||||
UUID,
|
||||
AdvertisingData,
|
||||
Appearance,
|
||||
ClassOfDevice,
|
||||
get_dict_key_by_value,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -93,6 +99,24 @@ def test_appearance() -> None:
|
||||
assert int(a) == 0x3333
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_class_of_device() -> None:
|
||||
c1 = ClassOfDevice(
|
||||
ClassOfDevice.MajorServiceClasses.AUDIO
|
||||
| ClassOfDevice.MajorServiceClasses.RENDERING,
|
||||
ClassOfDevice.MajorDeviceClass.AUDIO_VIDEO,
|
||||
ClassOfDevice.AudioVideoMinorDeviceClass.CAMCORDER,
|
||||
)
|
||||
assert str(c1) == "ClassOfDevice(RENDERING|AUDIO,AUDIO_VIDEO/CAMCORDER)"
|
||||
|
||||
c2 = ClassOfDevice(
|
||||
ClassOfDevice.MajorServiceClasses.AUDIO,
|
||||
ClassOfDevice.MajorDeviceClass.AUDIO_VIDEO,
|
||||
0x123,
|
||||
)
|
||||
assert str(c2) == "ClassOfDevice(AUDIO,AUDIO_VIDEO/0x123)"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
test_ad_data()
|
||||
|
||||
+41
-10
@@ -24,7 +24,7 @@ from unittest import mock
|
||||
import pytest
|
||||
|
||||
from bumble import device, gatt, hci, utils
|
||||
from bumble.core import ConnectionParameters, PhysicalTransport
|
||||
from bumble.core import PhysicalTransport
|
||||
from bumble.device import (
|
||||
AdvertisingEventProperties,
|
||||
AdvertisingParameters,
|
||||
@@ -289,14 +289,15 @@ async def test_legacy_advertising_disconnection(auto_restart):
|
||||
await device.power_on()
|
||||
peer_address = Address('F0:F1:F2:F3:F4:F5')
|
||||
await device.start_advertising(auto_restart=auto_restart)
|
||||
device.on_connection(
|
||||
device.on_le_connection(
|
||||
0x0001,
|
||||
PhysicalTransport.LE,
|
||||
peer_address,
|
||||
None,
|
||||
None,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
device.on_advertising_set_termination(
|
||||
@@ -347,14 +348,15 @@ async def test_extended_advertising_connection(own_address_type):
|
||||
advertising_set = await device.create_advertising_set(
|
||||
advertising_parameters=AdvertisingParameters(own_address_type=own_address_type)
|
||||
)
|
||||
device.on_connection(
|
||||
device.on_le_connection(
|
||||
0x0001,
|
||||
PhysicalTransport.LE,
|
||||
peer_address,
|
||||
None,
|
||||
None,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
device.on_advertising_set_termination(
|
||||
HCI_SUCCESS,
|
||||
@@ -391,14 +393,15 @@ async def test_extended_advertising_connection_out_of_order(own_address_type):
|
||||
0x0001,
|
||||
0,
|
||||
)
|
||||
device.on_connection(
|
||||
device.on_le_connection(
|
||||
0x0001,
|
||||
PhysicalTransport.LE,
|
||||
Address('F0:F1:F2:F3:F4:F5'),
|
||||
None,
|
||||
None,
|
||||
Role.PERIPHERAL,
|
||||
ConnectionParameters(0, 0, 0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
if own_address_type == OwnAddressType.PUBLIC:
|
||||
@@ -758,6 +761,34 @@ async def test_inquiry_result_with_rssi():
|
||||
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"roles",
|
||||
(
|
||||
(hci.Role.PERIPHERAL, hci.Role.CENTRAL),
|
||||
(hci.Role.CENTRAL, hci.Role.PERIPHERAL),
|
||||
),
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_accept_classic_connection(roles: tuple[hci.Role, hci.Role]):
|
||||
devices = TwoDevices()
|
||||
devices[0].classic_enabled = True
|
||||
devices[1].classic_enabled = True
|
||||
await devices[0].power_on()
|
||||
await devices[1].power_on()
|
||||
|
||||
accept_task = asyncio.create_task(devices[1].accept(role=roles[1]))
|
||||
await devices[0].connect(
|
||||
devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
await accept_task
|
||||
|
||||
assert devices.connections[0]
|
||||
assert devices.connections[0].role == roles[0]
|
||||
assert devices.connections[1]
|
||||
assert devices.connections[1].role == roles[1]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run_test_device():
|
||||
await test_device_connect_parallel()
|
||||
|
||||
+3
-1
@@ -82,7 +82,6 @@ async def hap_client():
|
||||
)
|
||||
|
||||
await devices.setup_connection()
|
||||
# TODO negotiate MTU > 49 to not truncate preset names
|
||||
|
||||
# Mock encryption.
|
||||
devices.connections[0].encryption = 1 # type: ignore
|
||||
@@ -93,6 +92,9 @@ async def hap_client():
|
||||
)
|
||||
|
||||
peer = device.Peer(devices.connections[1]) # type: ignore
|
||||
await peer.request_mtu(49)
|
||||
peer2 = device.Peer(devices.connections[0]) # type: ignore
|
||||
await peer2.request_mtu(49)
|
||||
hap_client = await peer.discover_service_and_create_proxy(
|
||||
hap.HearingAccessServiceProxy
|
||||
)
|
||||
|
||||
+32
-5
@@ -24,7 +24,7 @@ import sys
|
||||
import pytest
|
||||
|
||||
from bumble import controller, device, hci, link, transport
|
||||
from bumble.transport.common import PacketParser
|
||||
from bumble.transport import common
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -61,9 +61,9 @@ class Sink:
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_parser():
|
||||
sink1 = Sink()
|
||||
parser1 = PacketParser(sink1)
|
||||
parser1 = common.PacketParser(sink1)
|
||||
sink2 = Sink()
|
||||
parser2 = PacketParser(sink2)
|
||||
parser2 = common.PacketParser(sink2)
|
||||
|
||||
for parser in [parser1, parser2]:
|
||||
with open(
|
||||
@@ -82,7 +82,7 @@ def test_parser():
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_parser_extensions():
|
||||
sink = Sink()
|
||||
parser = PacketParser(sink)
|
||||
parser = common.PacketParser(sink)
|
||||
|
||||
# Check that an exception is thrown for an unknown type
|
||||
try:
|
||||
@@ -206,7 +206,7 @@ async def test_unix_connection_abstract():
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"address,",
|
||||
("127.0.0.1",),
|
||||
("127.0.0.1", "[::1]"),
|
||||
)
|
||||
async def test_android_netsim_connection(address):
|
||||
controller_transport = await transport.open_transport(
|
||||
@@ -222,6 +222,33 @@ async def test_android_netsim_connection(address):
|
||||
await client_device.power_on()
|
||||
|
||||
await client_transport.close()
|
||||
await controller_transport.source.grpc_server.stop(None)
|
||||
await controller_transport.close()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"spec,",
|
||||
(
|
||||
"android-netsim:[::1]:{port},mode=host[a=b,c=d]",
|
||||
"android-netsim:localhost:{port},mode=host[a=b,c=d]",
|
||||
"android-netsim:[a=b,c=d][::1]:{port},mode=host",
|
||||
"android-netsim:[a=b,c=d]localhost:{port},mode=host",
|
||||
),
|
||||
)
|
||||
async def test_open_transport_with_metadata(spec):
|
||||
controller_transport = await transport.open_transport(
|
||||
"android-netsim:_:0,mode=controller"
|
||||
)
|
||||
port = controller_transport.source.port
|
||||
_make_controller_from_transport(controller_transport)
|
||||
|
||||
client_transport = await transport.open_transport(spec.format(port=port))
|
||||
assert client_transport.source.metadata['a'] == 'b'
|
||||
assert client_transport.source.metadata['c'] == 'd'
|
||||
|
||||
await client_transport.close()
|
||||
await controller_transport.source.grpc_server.stop(None)
|
||||
await controller_transport.close()
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import struct
|
||||
|
||||
from bumble import data_types
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device
|
||||
from bumble.hci import HCI_Reset_Command
|
||||
@@ -65,24 +66,18 @@ class HeartRateMonitor:
|
||||
self.device.advertising_data = bytes(
|
||||
AdvertisingData(
|
||||
[
|
||||
(
|
||||
AdvertisingData.FLAGS,
|
||||
bytes(
|
||||
[
|
||||
AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
|
||||
| AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
|
||||
]
|
||||
),
|
||||
data_types.Flags(
|
||||
AdvertisingData.Flags.LE_GENERAL_DISCOVERABLE_MODE
|
||||
| AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
||||
),
|
||||
(
|
||||
AdvertisingData.COMPLETE_LOCAL_NAME,
|
||||
bytes('Bumble Heart', 'utf-8'),
|
||||
data_types.CompleteLocalName('Bumble Heart'),
|
||||
data_types.IncompleteListOf16BitServiceUUIDs(
|
||||
[self.heart_rate_service.uuid]
|
||||
),
|
||||
(
|
||||
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
||||
bytes(self.heart_rate_service.uuid),
|
||||
data_types.Appearance(
|
||||
data_types.Appearance.Category.HEART_RATE_SENSOR,
|
||||
data_types.Appearance.HeartRateSensorSubcategory.GENERIC_HEART_RATE_SENSOR,
|
||||
),
|
||||
(AdvertisingData.APPEARANCE, struct.pack('<H', 0x0340)),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user