forked from auracaster/bumble_mirror
Compare commits
36 Commits
gbg/bench-
...
v0.0.202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a060a70fba | ||
|
|
a06394ad4a | ||
|
|
a1414c2b5b | ||
|
|
b2864dac2d | ||
|
|
b78f895143 | ||
|
|
c4e9726828 | ||
|
|
d4b8e8348a | ||
|
|
19debaa52e | ||
|
|
73fe564321 | ||
|
|
a00abd65b3 | ||
|
|
f169ceaebb | ||
|
|
528af0d338 | ||
|
|
4b25eed869 | ||
|
|
32642c5d7c | ||
|
|
ff8b0c375d | ||
|
|
ae0228aeb8 | ||
|
|
5d2dac18c8 | ||
|
|
d03fc14cfd | ||
|
|
ad7ce79bc4 | ||
|
|
c6bf27fd2c | ||
|
|
7584daa3f9 | ||
|
|
654030e789 | ||
|
|
1de7d2cd6f | ||
|
|
68db78c833 | ||
|
|
e1714c16cc | ||
|
|
0a20f14ea9 | ||
|
|
23f46b36b3 | ||
|
|
009649abd1 | ||
|
|
855a007116 | ||
|
|
d064de35e0 | ||
|
|
dab4d13303 | ||
|
|
f5443a9826 | ||
|
|
db723a5196 | ||
|
|
e16be1a8f4 | ||
|
|
2fa8075fb0 | ||
|
|
9e663ad051 |
2
.github/workflows/code-check.yml
vendored
2
.github/workflows/code-check.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
|
||||
4
.github/workflows/python-build-test.yml
vendored
4
.github/workflows/python-build-test.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
rust-version: [ "1.76.0", "stable" ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
|
||||
608
apps/player/player.py
Normal file
608
apps/player/player.py
Normal file
@@ -0,0 +1,608 @@
|
||||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import asyncio.subprocess
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
|
||||
import click
|
||||
|
||||
from bumble.a2dp import (
|
||||
make_audio_source_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
AacFrame,
|
||||
AacParser,
|
||||
AacPacketSource,
|
||||
AacMediaCodecInformation,
|
||||
SbcFrame,
|
||||
SbcParser,
|
||||
SbcPacketSource,
|
||||
SbcMediaCodecInformation,
|
||||
OpusPacket,
|
||||
OpusParser,
|
||||
OpusPacketSource,
|
||||
OpusMediaCodecInformation,
|
||||
)
|
||||
from bumble.avrcp import Protocol as AvrcpProtocol
|
||||
from bumble.avdtp import (
|
||||
find_avdtp_service_with_connection,
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
AVDTP_DELAY_REPORTING_SERVICE_CATEGORY,
|
||||
MediaCodecCapabilities,
|
||||
MediaPacketPump,
|
||||
Protocol as AvdtpProtocol,
|
||||
)
|
||||
from bumble.colors import color
|
||||
from bumble.core import (
|
||||
AdvertisingData,
|
||||
ConnectionError as BumbleConnectionError,
|
||||
DeviceClass,
|
||||
BT_BR_EDR_TRANSPORT,
|
||||
)
|
||||
from bumble.device import Connection, Device, DeviceConfiguration
|
||||
from bumble.hci import Address, HCI_CONNECTION_ALREADY_EXISTS_ERROR, HCI_Constant
|
||||
from bumble.pairing import PairingConfig
|
||||
from bumble.transport import open_transport
|
||||
from bumble.utils import AsyncRunner
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def a2dp_source_sdp_records():
|
||||
service_record_handle = 0x00010001
|
||||
return {
|
||||
service_record_handle: make_audio_source_service_sdp_records(
|
||||
service_record_handle
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def sbc_codec_capabilities(read_function) -> MediaCodecCapabilities:
|
||||
sbc_parser = SbcParser(read_function)
|
||||
sbc_frame: SbcFrame
|
||||
async for sbc_frame in sbc_parser.frames:
|
||||
# We only need the first frame
|
||||
print(color(f"SBC format: {sbc_frame}", "cyan"))
|
||||
break
|
||||
|
||||
channel_mode = [
|
||||
SbcMediaCodecInformation.ChannelMode.MONO,
|
||||
SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL,
|
||||
SbcMediaCodecInformation.ChannelMode.STEREO,
|
||||
SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||
][sbc_frame.channel_mode]
|
||||
block_length = {
|
||||
4: SbcMediaCodecInformation.BlockLength.BL_4,
|
||||
8: SbcMediaCodecInformation.BlockLength.BL_8,
|
||||
12: SbcMediaCodecInformation.BlockLength.BL_12,
|
||||
16: SbcMediaCodecInformation.BlockLength.BL_16,
|
||||
}[sbc_frame.block_count]
|
||||
subbands = {
|
||||
4: SbcMediaCodecInformation.Subbands.S_4,
|
||||
8: SbcMediaCodecInformation.Subbands.S_8,
|
||||
}[sbc_frame.subband_count]
|
||||
allocation_method = [
|
||||
SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||
SbcMediaCodecInformation.AllocationMethod.SNR,
|
||||
][sbc_frame.allocation_method]
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation(
|
||||
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.from_int(
|
||||
sbc_frame.sampling_frequency
|
||||
),
|
||||
channel_mode=channel_mode,
|
||||
block_length=block_length,
|
||||
subbands=subbands,
|
||||
allocation_method=allocation_method,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=40,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def aac_codec_capabilities(read_function) -> MediaCodecCapabilities:
|
||||
aac_parser = AacParser(read_function)
|
||||
aac_frame: AacFrame
|
||||
async for aac_frame in aac_parser.frames:
|
||||
# We only need the first frame
|
||||
print(color(f"AAC format: {aac_frame}", "cyan"))
|
||||
break
|
||||
|
||||
sampling_frequency = AacMediaCodecInformation.SamplingFrequency.from_int(
|
||||
aac_frame.sampling_frequency
|
||||
)
|
||||
channels = (
|
||||
AacMediaCodecInformation.Channels.MONO
|
||||
if aac_frame.channel_configuration == 1
|
||||
else AacMediaCodecInformation.Channels.STEREO
|
||||
)
|
||||
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
media_codec_information=AacMediaCodecInformation(
|
||||
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||
sampling_frequency=sampling_frequency,
|
||||
channels=channels,
|
||||
vbr=1,
|
||||
bitrate=128000,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def opus_codec_capabilities(read_function) -> MediaCodecCapabilities:
|
||||
opus_parser = OpusParser(read_function)
|
||||
opus_packet: OpusPacket
|
||||
async for opus_packet in opus_parser.packets:
|
||||
# We only need the first packet
|
||||
print(color(f"Opus format: {opus_packet}", "cyan"))
|
||||
break
|
||||
|
||||
if opus_packet.channel_mode == OpusPacket.ChannelMode.MONO:
|
||||
channel_mode = OpusMediaCodecInformation.ChannelMode.MONO
|
||||
elif opus_packet.channel_mode == OpusPacket.ChannelMode.STEREO:
|
||||
channel_mode = OpusMediaCodecInformation.ChannelMode.STEREO
|
||||
else:
|
||||
channel_mode = OpusMediaCodecInformation.ChannelMode.DUAL_MONO
|
||||
|
||||
if opus_packet.duration == 10:
|
||||
frame_size = OpusMediaCodecInformation.FrameSize.FS_10MS
|
||||
else:
|
||||
frame_size = OpusMediaCodecInformation.FrameSize.FS_20MS
|
||||
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_NON_A2DP_CODEC_TYPE,
|
||||
media_codec_information=OpusMediaCodecInformation(
|
||||
channel_mode=channel_mode,
|
||||
sampling_frequency=OpusMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
frame_size=frame_size,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class Player:
|
||||
def __init__(
|
||||
self,
|
||||
transport: str,
|
||||
device_config: Optional[str],
|
||||
authenticate: bool,
|
||||
encrypt: bool,
|
||||
) -> None:
|
||||
self.transport = transport
|
||||
self.device_config = device_config
|
||||
self.authenticate = authenticate
|
||||
self.encrypt = encrypt
|
||||
self.avrcp_protocol: Optional[AvrcpProtocol] = None
|
||||
self.done: Optional[asyncio.Event]
|
||||
|
||||
async def run(self, workload) -> None:
|
||||
self.done = asyncio.Event()
|
||||
try:
|
||||
await self._run(workload)
|
||||
except Exception as error:
|
||||
print(color(f"!!! ERROR: {error}", "red"))
|
||||
|
||||
async def _run(self, workload) -> None:
|
||||
async with await open_transport(self.transport) as (hci_source, hci_sink):
|
||||
# Create a device
|
||||
device_config = DeviceConfiguration()
|
||||
if self.device_config:
|
||||
device_config.load_from_file(self.device_config)
|
||||
else:
|
||||
device_config.name = "Bumble Player"
|
||||
device_config.class_of_device = DeviceClass.pack_class_of_device(
|
||||
DeviceClass.AUDIO_SERVICE_CLASS,
|
||||
DeviceClass.AUDIO_VIDEO_MAJOR_DEVICE_CLASS,
|
||||
DeviceClass.AUDIO_VIDEO_UNCATEGORIZED_MINOR_DEVICE_CLASS,
|
||||
)
|
||||
device_config.keystore = "JsonKeyStore"
|
||||
|
||||
device_config.classic_enabled = True
|
||||
device_config.le_enabled = False
|
||||
device_config.le_simultaneous_enabled = False
|
||||
device_config.classic_sc_enabled = False
|
||||
device_config.classic_smp_enabled = False
|
||||
device = Device.from_config_with_hci(device_config, hci_source, hci_sink)
|
||||
|
||||
# Setup the SDP records to expose the SRC service
|
||||
device.sdp_service_records = a2dp_source_sdp_records()
|
||||
|
||||
# Setup AVRCP
|
||||
self.avrcp_protocol = AvrcpProtocol()
|
||||
self.avrcp_protocol.listen(device)
|
||||
|
||||
# Don't require MITM when pairing.
|
||||
device.pairing_config_factory = lambda connection: PairingConfig(mitm=False)
|
||||
|
||||
# Start the controller
|
||||
await device.power_on()
|
||||
|
||||
# Print some of the config/properties
|
||||
print(
|
||||
"Player Bluetooth Address:",
|
||||
color(
|
||||
device.public_address.to_string(with_type_qualifier=False),
|
||||
"yellow",
|
||||
),
|
||||
)
|
||||
|
||||
# Listen for connections
|
||||
device.on("connection", self.on_bluetooth_connection)
|
||||
|
||||
# Run the workload
|
||||
try:
|
||||
await workload(device)
|
||||
except BumbleConnectionError as error:
|
||||
if error.error_code == HCI_CONNECTION_ALREADY_EXISTS_ERROR:
|
||||
print(color("Connection already established", "blue"))
|
||||
else:
|
||||
print(color(f"Failed to connect: {error}", "red"))
|
||||
|
||||
# Wait until it is time to exit
|
||||
assert self.done is not None
|
||||
await asyncio.wait(
|
||||
[hci_source.terminated, asyncio.ensure_future(self.done.wait())],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
|
||||
def on_bluetooth_connection(self, connection: Connection) -> None:
|
||||
print(color(f"--- Connected: {connection}", "cyan"))
|
||||
connection.on("disconnection", self.on_bluetooth_disconnection)
|
||||
|
||||
def on_bluetooth_disconnection(self, reason) -> None:
|
||||
print(color(f"--- Disconnected: {HCI_Constant.error_name(reason)}", "cyan"))
|
||||
self.set_done()
|
||||
|
||||
async def connect(self, device: Device, address: str) -> Connection:
|
||||
print(color(f"Connecting to {address}...", "green"))
|
||||
connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||
|
||||
# Request authentication
|
||||
if self.authenticate:
|
||||
print(color("*** Authenticating...", "blue"))
|
||||
await connection.authenticate()
|
||||
print(color("*** Authenticated", "blue"))
|
||||
|
||||
# Enable encryption
|
||||
if self.encrypt:
|
||||
print(color("*** Enabling encryption...", "blue"))
|
||||
await connection.encrypt()
|
||||
print(color("*** Encryption on", "blue"))
|
||||
|
||||
return connection
|
||||
|
||||
async def create_avdtp_protocol(self, connection: Connection) -> AvdtpProtocol:
|
||||
# Look for an A2DP service
|
||||
avdtp_version = await find_avdtp_service_with_connection(connection)
|
||||
if not avdtp_version:
|
||||
raise RuntimeError("no A2DP service found")
|
||||
|
||||
print(color(f"AVDTP Version: {avdtp_version}"))
|
||||
|
||||
# Create a client to interact with the remote device
|
||||
return await AvdtpProtocol.connect(connection, avdtp_version)
|
||||
|
||||
async def stream_packets(
|
||||
self,
|
||||
protocol: AvdtpProtocol,
|
||||
codec_type: int,
|
||||
vendor_id: int,
|
||||
codec_id: int,
|
||||
packet_source: Union[SbcPacketSource, AacPacketSource, OpusPacketSource],
|
||||
codec_capabilities: MediaCodecCapabilities,
|
||||
):
|
||||
# Discover all endpoints on the remote device
|
||||
endpoints = await protocol.discover_remote_endpoints()
|
||||
for endpoint in endpoints:
|
||||
print('@@@', endpoint)
|
||||
|
||||
# Select a sink
|
||||
sink = protocol.find_remote_sink_by_codec(
|
||||
AVDTP_AUDIO_MEDIA_TYPE, codec_type, vendor_id, codec_id
|
||||
)
|
||||
if sink is None:
|
||||
print(color('!!! no compatible sink found', 'red'))
|
||||
return
|
||||
print(f'### Selected sink: {sink.seid}')
|
||||
|
||||
# Check if the sink supports delay reporting
|
||||
delay_reporting = False
|
||||
for capability in sink.capabilities:
|
||||
if capability.service_category == AVDTP_DELAY_REPORTING_SERVICE_CATEGORY:
|
||||
delay_reporting = True
|
||||
break
|
||||
|
||||
def on_delay_report(delay: int):
|
||||
print(color(f"*** DELAY REPORT: {delay}", "blue"))
|
||||
|
||||
# Adjust the codec capabilities for certain codecs
|
||||
for capability in sink.capabilities:
|
||||
if isinstance(capability, MediaCodecCapabilities):
|
||||
if isinstance(
|
||||
codec_capabilities.media_codec_information, SbcMediaCodecInformation
|
||||
) and isinstance(
|
||||
capability.media_codec_information, SbcMediaCodecInformation
|
||||
):
|
||||
codec_capabilities.media_codec_information.minimum_bitpool_value = (
|
||||
capability.media_codec_information.minimum_bitpool_value
|
||||
)
|
||||
codec_capabilities.media_codec_information.maximum_bitpool_value = (
|
||||
capability.media_codec_information.maximum_bitpool_value
|
||||
)
|
||||
print(color("Source media codec:", "green"), codec_capabilities)
|
||||
|
||||
# Stream the packets
|
||||
packet_pump = MediaPacketPump(packet_source.packets)
|
||||
source = protocol.add_source(codec_capabilities, packet_pump, delay_reporting)
|
||||
source.on("delay_report", on_delay_report)
|
||||
stream = await protocol.create_stream(source, sink)
|
||||
await stream.start()
|
||||
|
||||
await packet_pump.wait_for_completion()
|
||||
|
||||
async def discover(self, device: Device) -> None:
|
||||
@device.listens_to("inquiry_result")
|
||||
def on_inquiry_result(
|
||||
address: Address, class_of_device: int, data: AdvertisingData, rssi: int
|
||||
) -> None:
|
||||
(
|
||||
service_classes,
|
||||
major_device_class,
|
||||
minor_device_class,
|
||||
) = DeviceClass.split_class_of_device(class_of_device)
|
||||
separator = "\n "
|
||||
print(f">>> {color(address.to_string(False), 'yellow')}:")
|
||||
print(f" Device Class (raw): {class_of_device:06X}")
|
||||
major_class_name = DeviceClass.major_device_class_name(major_device_class)
|
||||
print(" Device Major Class: " f"{major_class_name}")
|
||||
minor_class_name = DeviceClass.minor_device_class_name(
|
||||
major_device_class, minor_device_class
|
||||
)
|
||||
print(" Device Minor Class: " f"{minor_class_name}")
|
||||
print(
|
||||
" Device Services: "
|
||||
f"{', '.join(DeviceClass.service_class_labels(service_classes))}"
|
||||
)
|
||||
print(f" RSSI: {rssi}")
|
||||
if data.ad_structures:
|
||||
print(f" {data.to_string(separator)}")
|
||||
|
||||
await device.start_discovery()
|
||||
|
||||
async def pair(self, device: Device, address: str) -> None:
|
||||
print(color(f"Connecting to {address}...", "green"))
|
||||
connection = await device.connect(address, transport=BT_BR_EDR_TRANSPORT)
|
||||
|
||||
print(color("Pairing...", "magenta"))
|
||||
await connection.authenticate()
|
||||
print(color("Pairing completed", "magenta"))
|
||||
self.set_done()
|
||||
|
||||
async def inquire(self, device: Device, address: str) -> None:
|
||||
connection = await self.connect(device, address)
|
||||
avdtp_protocol = await self.create_avdtp_protocol(connection)
|
||||
|
||||
# Discover the remote endpoints
|
||||
endpoints = await avdtp_protocol.discover_remote_endpoints()
|
||||
print(f'@@@ Found {len(list(endpoints))} endpoints')
|
||||
for endpoint in endpoints:
|
||||
print('@@@', endpoint)
|
||||
|
||||
self.set_done()
|
||||
|
||||
async def play(
|
||||
self,
|
||||
device: Device,
|
||||
address: Optional[str],
|
||||
audio_format: str,
|
||||
audio_file: str,
|
||||
) -> None:
|
||||
if audio_format == "auto":
|
||||
if audio_file.endswith(".sbc"):
|
||||
audio_format = "sbc"
|
||||
elif audio_file.endswith(".aac") or audio_file.endswith(".adts"):
|
||||
audio_format = "aac"
|
||||
elif audio_file.endswith(".ogg"):
|
||||
audio_format = "opus"
|
||||
else:
|
||||
raise ValueError("Unable to determine audio format from file extension")
|
||||
|
||||
device.on(
|
||||
"connection",
|
||||
lambda connection: AsyncRunner.spawn(on_connection(connection)),
|
||||
)
|
||||
|
||||
async def on_connection(connection: Connection):
|
||||
avdtp_protocol = await self.create_avdtp_protocol(connection)
|
||||
|
||||
with open(audio_file, 'rb') as input_file:
|
||||
# NOTE: this should be using asyncio file reading, but blocking reads
|
||||
# are good enough for this command line app.
|
||||
async def read_audio_data(byte_count):
|
||||
return input_file.read(byte_count)
|
||||
|
||||
# Obtain the codec capabilities from the stream
|
||||
packet_source: Union[SbcPacketSource, AacPacketSource, OpusPacketSource]
|
||||
vendor_id = 0
|
||||
codec_id = 0
|
||||
if audio_format == "sbc":
|
||||
codec_type = A2DP_SBC_CODEC_TYPE
|
||||
codec_capabilities = await sbc_codec_capabilities(read_audio_data)
|
||||
packet_source = SbcPacketSource(
|
||||
read_audio_data,
|
||||
avdtp_protocol.l2cap_channel.peer_mtu,
|
||||
)
|
||||
elif audio_format == "aac":
|
||||
codec_type = A2DP_MPEG_2_4_AAC_CODEC_TYPE
|
||||
codec_capabilities = await aac_codec_capabilities(read_audio_data)
|
||||
packet_source = AacPacketSource(
|
||||
read_audio_data,
|
||||
avdtp_protocol.l2cap_channel.peer_mtu,
|
||||
)
|
||||
else:
|
||||
codec_type = A2DP_NON_A2DP_CODEC_TYPE
|
||||
vendor_id = OpusMediaCodecInformation.VENDOR_ID
|
||||
codec_id = OpusMediaCodecInformation.CODEC_ID
|
||||
codec_capabilities = await opus_codec_capabilities(read_audio_data)
|
||||
packet_source = OpusPacketSource(
|
||||
read_audio_data,
|
||||
avdtp_protocol.l2cap_channel.peer_mtu,
|
||||
)
|
||||
|
||||
# Rewind to the start
|
||||
input_file.seek(0)
|
||||
|
||||
try:
|
||||
await self.stream_packets(
|
||||
avdtp_protocol,
|
||||
codec_type,
|
||||
vendor_id,
|
||||
codec_id,
|
||||
packet_source,
|
||||
codec_capabilities,
|
||||
)
|
||||
except Exception as error:
|
||||
print(color(f"!!! Error while streaming: {error}", "red"))
|
||||
|
||||
self.set_done()
|
||||
|
||||
if address:
|
||||
await self.connect(device, address)
|
||||
else:
|
||||
print(color("Waiting for an incoming connection...", "magenta"))
|
||||
|
||||
def set_done(self) -> None:
|
||||
if self.done:
|
||||
self.done.set()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def create_player(context) -> Player:
|
||||
return Player(
|
||||
transport=context.obj["hci_transport"],
|
||||
device_config=context.obj["device_config"],
|
||||
authenticate=context.obj["authenticate"],
|
||||
encrypt=context.obj["encrypt"],
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
@click.option("--hci-transport", metavar="TRANSPORT", required=True)
|
||||
@click.option("--device-config", metavar="FILENAME", help="Device configuration file")
|
||||
@click.option(
|
||||
"--authenticate",
|
||||
is_flag=True,
|
||||
help="Request authentication when connecting",
|
||||
default=False,
|
||||
)
|
||||
@click.option(
|
||||
"--encrypt", is_flag=True, help="Request encryption when connecting", default=True
|
||||
)
|
||||
def player_cli(ctx, hci_transport, device_config, authenticate, encrypt):
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["hci_transport"] = hci_transport
|
||||
ctx.obj["device_config"] = device_config
|
||||
ctx.obj["authenticate"] = authenticate
|
||||
ctx.obj["encrypt"] = encrypt
|
||||
|
||||
|
||||
@player_cli.command("discover")
|
||||
@click.pass_context
|
||||
def discover(context):
|
||||
"""Discover speakers or headphones"""
|
||||
player = create_player(context)
|
||||
asyncio.run(player.run(player.discover))
|
||||
|
||||
|
||||
@player_cli.command("inquire")
|
||||
@click.pass_context
|
||||
@click.argument(
|
||||
"address",
|
||||
metavar="ADDRESS",
|
||||
)
|
||||
def inquire(context, address):
|
||||
"""Connect to a speaker or headphone and inquire about their capabilities"""
|
||||
player = create_player(context)
|
||||
asyncio.run(player.run(lambda device: player.inquire(device, address)))
|
||||
|
||||
|
||||
@player_cli.command("pair")
|
||||
@click.pass_context
|
||||
@click.argument(
|
||||
"address",
|
||||
metavar="ADDRESS",
|
||||
)
|
||||
def pair(context, address):
|
||||
"""Pair with a speaker or headphone"""
|
||||
player = create_player(context)
|
||||
asyncio.run(player.run(lambda device: player.pair(device, address)))
|
||||
|
||||
|
||||
@player_cli.command("play")
|
||||
@click.pass_context
|
||||
@click.option(
|
||||
"--connect",
|
||||
"address",
|
||||
metavar="ADDRESS",
|
||||
help="Address or name to connect to",
|
||||
)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--audio-format",
|
||||
type=click.Choice(["auto", "sbc", "aac", "opus"]),
|
||||
help="Audio file format (use 'auto' to infer the format from the file extension)",
|
||||
default="auto",
|
||||
)
|
||||
@click.argument("audio_file")
|
||||
def play(context, address, audio_format, audio_file):
|
||||
"""Play and audio file"""
|
||||
player = create_player(context)
|
||||
asyncio.run(
|
||||
player.run(
|
||||
lambda device: player.play(device, address, audio_format, audio_file)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
|
||||
player_cli()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
@@ -44,25 +44,18 @@ from bumble.avdtp import (
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
Listener,
|
||||
MediaCodecCapabilities,
|
||||
MediaPacket,
|
||||
Protocol,
|
||||
)
|
||||
from bumble.a2dp import (
|
||||
MPEG_2_AAC_LC_OBJECT_TYPE,
|
||||
make_audio_sink_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
SBC_MONO_CHANNEL_MODE,
|
||||
SBC_DUAL_CHANNEL_MODE,
|
||||
SBC_SNR_ALLOCATION_METHOD,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
SBC_STEREO_CHANNEL_MODE,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
SbcMediaCodecInformation,
|
||||
AacMediaCodecInformation,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -93,7 +86,7 @@ class AudioExtractor:
|
||||
# -----------------------------------------------------------------------------
|
||||
class AacAudioExtractor:
|
||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
||||
return AacAudioRtpPacket(packet.payload).to_adts()
|
||||
return AacAudioRtpPacket.from_bytes(packet.payload).to_adts()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -451,10 +444,12 @@ class Speaker:
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
media_codec_information=AacMediaCodecInformation.from_lists(
|
||||
object_types=[MPEG_2_AAC_LC_OBJECT_TYPE],
|
||||
sampling_frequencies=[48000, 44100],
|
||||
channels=[1, 2],
|
||||
media_codec_information=AacMediaCodecInformation(
|
||||
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||
channels=AacMediaCodecInformation.Channels.MONO
|
||||
| AacMediaCodecInformation.Channels.STEREO,
|
||||
vbr=1,
|
||||
bitrate=256000,
|
||||
),
|
||||
@@ -464,20 +459,23 @@ class Speaker:
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
||||
channel_modes=[
|
||||
SBC_MONO_CHANNEL_MODE,
|
||||
SBC_DUAL_CHANNEL_MODE,
|
||||
SBC_STEREO_CHANNEL_MODE,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
],
|
||||
block_lengths=[4, 8, 12, 16],
|
||||
subbands=[4, 8],
|
||||
allocation_methods=[
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
SBC_SNR_ALLOCATION_METHOD,
|
||||
],
|
||||
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,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
|
||||
708
bumble/a2dp.py
708
bumble/a2dp.py
@@ -17,12 +17,16 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import struct
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import List, Callable, Awaitable
|
||||
import dataclasses
|
||||
import enum
|
||||
import logging
|
||||
import struct
|
||||
from typing import Awaitable, Callable
|
||||
from typing_extensions import ClassVar, Self
|
||||
|
||||
|
||||
from .codecs import AacAudioRtpPacket
|
||||
from .company_ids import COMPANY_IDENTIFIERS
|
||||
from .sdp import (
|
||||
DataElement,
|
||||
@@ -42,6 +46,7 @@ from .core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
name_or_number,
|
||||
)
|
||||
from .rtp import MediaPacket
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -103,6 +108,8 @@ SBC_ALLOCATION_METHOD_NAMES = {
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD: 'SBC_LOUDNESS_ALLOCATION_METHOD'
|
||||
}
|
||||
|
||||
SBC_MAX_FRAMES_IN_RTP_PAYLOAD = 15
|
||||
|
||||
MPEG_2_4_AAC_SAMPLING_FREQUENCIES = [
|
||||
8000,
|
||||
11025,
|
||||
@@ -130,6 +137,9 @@ MPEG_2_4_OBJECT_TYPE_NAMES = {
|
||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE'
|
||||
}
|
||||
|
||||
|
||||
OPUS_MAX_FRAMES_IN_RTP_PAYLOAD = 15
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -257,38 +267,61 @@ class SbcMediaCodecInformation:
|
||||
A2DP spec - 4.3.2 Codec Specific Information Elements
|
||||
'''
|
||||
|
||||
sampling_frequency: int
|
||||
channel_mode: int
|
||||
block_length: int
|
||||
subbands: int
|
||||
allocation_method: int
|
||||
sampling_frequency: SamplingFrequency
|
||||
channel_mode: ChannelMode
|
||||
block_length: BlockLength
|
||||
subbands: Subbands
|
||||
allocation_method: AllocationMethod
|
||||
minimum_bitpool_value: int
|
||||
maximum_bitpool_value: int
|
||||
|
||||
SAMPLING_FREQUENCY_BITS = {16000: 1 << 3, 32000: 1 << 2, 44100: 1 << 1, 48000: 1}
|
||||
CHANNEL_MODE_BITS = {
|
||||
SBC_MONO_CHANNEL_MODE: 1 << 3,
|
||||
SBC_DUAL_CHANNEL_MODE: 1 << 2,
|
||||
SBC_STEREO_CHANNEL_MODE: 1 << 1,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE: 1,
|
||||
}
|
||||
BLOCK_LENGTH_BITS = {4: 1 << 3, 8: 1 << 2, 12: 1 << 1, 16: 1}
|
||||
SUBBANDS_BITS = {4: 1 << 1, 8: 1}
|
||||
ALLOCATION_METHOD_BITS = {
|
||||
SBC_SNR_ALLOCATION_METHOD: 1 << 1,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD: 1,
|
||||
}
|
||||
class SamplingFrequency(enum.IntFlag):
|
||||
SF_16000 = 1 << 3
|
||||
SF_32000 = 1 << 2
|
||||
SF_44100 = 1 << 1
|
||||
SF_48000 = 1 << 0
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> SbcMediaCodecInformation:
|
||||
sampling_frequency = (data[0] >> 4) & 0x0F
|
||||
channel_mode = (data[0] >> 0) & 0x0F
|
||||
block_length = (data[1] >> 4) & 0x0F
|
||||
subbands = (data[1] >> 2) & 0x03
|
||||
allocation_method = (data[1] >> 0) & 0x03
|
||||
@classmethod
|
||||
def from_int(cls, sampling_frequency: int) -> Self:
|
||||
sampling_frequencies = [
|
||||
16000,
|
||||
32000,
|
||||
44100,
|
||||
48000,
|
||||
]
|
||||
index = sampling_frequencies.index(sampling_frequency)
|
||||
return cls(1 << (len(sampling_frequencies) - index - 1))
|
||||
|
||||
class ChannelMode(enum.IntFlag):
|
||||
MONO = 1 << 3
|
||||
DUAL_CHANNEL = 1 << 2
|
||||
STEREO = 1 << 1
|
||||
JOINT_STEREO = 1 << 0
|
||||
|
||||
class BlockLength(enum.IntFlag):
|
||||
BL_4 = 1 << 3
|
||||
BL_8 = 1 << 2
|
||||
BL_12 = 1 << 1
|
||||
BL_16 = 1 << 0
|
||||
|
||||
class Subbands(enum.IntFlag):
|
||||
S_4 = 1 << 1
|
||||
S_8 = 1 << 0
|
||||
|
||||
class AllocationMethod(enum.IntFlag):
|
||||
SNR = 1 << 1
|
||||
LOUDNESS = 1 << 0
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> Self:
|
||||
sampling_frequency = cls.SamplingFrequency((data[0] >> 4) & 0x0F)
|
||||
channel_mode = cls.ChannelMode((data[0] >> 0) & 0x0F)
|
||||
block_length = cls.BlockLength((data[1] >> 4) & 0x0F)
|
||||
subbands = cls.Subbands((data[1] >> 2) & 0x03)
|
||||
allocation_method = cls.AllocationMethod((data[1] >> 0) & 0x03)
|
||||
minimum_bitpool_value = (data[2] >> 0) & 0xFF
|
||||
maximum_bitpool_value = (data[3] >> 0) & 0xFF
|
||||
return SbcMediaCodecInformation(
|
||||
return cls(
|
||||
sampling_frequency,
|
||||
channel_mode,
|
||||
block_length,
|
||||
@@ -298,52 +331,6 @@ class SbcMediaCodecInformation:
|
||||
maximum_bitpool_value,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_discrete_values(
|
||||
cls,
|
||||
sampling_frequency: int,
|
||||
channel_mode: int,
|
||||
block_length: int,
|
||||
subbands: int,
|
||||
allocation_method: int,
|
||||
minimum_bitpool_value: int,
|
||||
maximum_bitpool_value: int,
|
||||
) -> SbcMediaCodecInformation:
|
||||
return SbcMediaCodecInformation(
|
||||
sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
||||
channel_mode=cls.CHANNEL_MODE_BITS[channel_mode],
|
||||
block_length=cls.BLOCK_LENGTH_BITS[block_length],
|
||||
subbands=cls.SUBBANDS_BITS[subbands],
|
||||
allocation_method=cls.ALLOCATION_METHOD_BITS[allocation_method],
|
||||
minimum_bitpool_value=minimum_bitpool_value,
|
||||
maximum_bitpool_value=maximum_bitpool_value,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_lists(
|
||||
cls,
|
||||
sampling_frequencies: List[int],
|
||||
channel_modes: List[int],
|
||||
block_lengths: List[int],
|
||||
subbands: List[int],
|
||||
allocation_methods: List[int],
|
||||
minimum_bitpool_value: int,
|
||||
maximum_bitpool_value: int,
|
||||
) -> SbcMediaCodecInformation:
|
||||
return SbcMediaCodecInformation(
|
||||
sampling_frequency=sum(
|
||||
cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
|
||||
),
|
||||
channel_mode=sum(cls.CHANNEL_MODE_BITS[x] for x in channel_modes),
|
||||
block_length=sum(cls.BLOCK_LENGTH_BITS[x] for x in block_lengths),
|
||||
subbands=sum(cls.SUBBANDS_BITS[x] for x in subbands),
|
||||
allocation_method=sum(
|
||||
cls.ALLOCATION_METHOD_BITS[x] for x in allocation_methods
|
||||
),
|
||||
minimum_bitpool_value=minimum_bitpool_value,
|
||||
maximum_bitpool_value=maximum_bitpool_value,
|
||||
)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
[
|
||||
@@ -356,23 +343,6 @@ class SbcMediaCodecInformation:
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
|
||||
allocation_methods = ['SNR', 'Loudness']
|
||||
return '\n'.join(
|
||||
# pylint: disable=line-too-long
|
||||
[
|
||||
'SbcMediaCodecInformation(',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}',
|
||||
f' channel_mode: {",".join([str(x) for x in flags_to_list(self.channel_mode, channel_modes)])}',
|
||||
f' block_length: {",".join([str(x) for x in flags_to_list(self.block_length, SBC_BLOCK_LENGTHS)])}',
|
||||
f' subbands: {",".join([str(x) for x in flags_to_list(self.subbands, SBC_SUBBANDS)])}',
|
||||
f' allocation_method: {",".join([str(x) for x in flags_to_list(self.allocation_method, allocation_methods)])}',
|
||||
f' minimum_bitpool_value: {self.minimum_bitpool_value}',
|
||||
f' maximum_bitpool_value: {self.maximum_bitpool_value}' ')',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
@@ -381,83 +351,66 @@ class AacMediaCodecInformation:
|
||||
A2DP spec - 4.5.2 Codec Specific Information Elements
|
||||
'''
|
||||
|
||||
object_type: int
|
||||
sampling_frequency: int
|
||||
channels: int
|
||||
rfa: int
|
||||
object_type: ObjectType
|
||||
sampling_frequency: SamplingFrequency
|
||||
channels: Channels
|
||||
vbr: int
|
||||
bitrate: int
|
||||
|
||||
OBJECT_TYPE_BITS = {
|
||||
MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
|
||||
MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
|
||||
MPEG_4_AAC_LTP_OBJECT_TYPE: 1 << 5,
|
||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 1 << 4,
|
||||
}
|
||||
SAMPLING_FREQUENCY_BITS = {
|
||||
8000: 1 << 11,
|
||||
11025: 1 << 10,
|
||||
12000: 1 << 9,
|
||||
16000: 1 << 8,
|
||||
22050: 1 << 7,
|
||||
24000: 1 << 6,
|
||||
32000: 1 << 5,
|
||||
44100: 1 << 4,
|
||||
48000: 1 << 3,
|
||||
64000: 1 << 2,
|
||||
88200: 1 << 1,
|
||||
96000: 1,
|
||||
}
|
||||
CHANNELS_BITS = {1: 1 << 1, 2: 1}
|
||||
class ObjectType(enum.IntFlag):
|
||||
MPEG_2_AAC_LC = 1 << 7
|
||||
MPEG_4_AAC_LC = 1 << 6
|
||||
MPEG_4_AAC_LTP = 1 << 5
|
||||
MPEG_4_AAC_SCALABLE = 1 << 4
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> AacMediaCodecInformation:
|
||||
object_type = data[0]
|
||||
sampling_frequency = (data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
||||
channels = (data[2] >> 2) & 0x03
|
||||
rfa = 0
|
||||
class SamplingFrequency(enum.IntFlag):
|
||||
SF_8000 = 1 << 11
|
||||
SF_11025 = 1 << 10
|
||||
SF_12000 = 1 << 9
|
||||
SF_16000 = 1 << 8
|
||||
SF_22050 = 1 << 7
|
||||
SF_24000 = 1 << 6
|
||||
SF_32000 = 1 << 5
|
||||
SF_44100 = 1 << 4
|
||||
SF_48000 = 1 << 3
|
||||
SF_64000 = 1 << 2
|
||||
SF_88200 = 1 << 1
|
||||
SF_96000 = 1 << 0
|
||||
|
||||
@classmethod
|
||||
def from_int(cls, sampling_frequency: int) -> Self:
|
||||
sampling_frequencies = [
|
||||
8000,
|
||||
11025,
|
||||
12000,
|
||||
16000,
|
||||
22050,
|
||||
24000,
|
||||
32000,
|
||||
44100,
|
||||
48000,
|
||||
64000,
|
||||
88200,
|
||||
96000,
|
||||
]
|
||||
index = sampling_frequencies.index(sampling_frequency)
|
||||
return cls(1 << (len(sampling_frequencies) - index - 1))
|
||||
|
||||
class Channels(enum.IntFlag):
|
||||
MONO = 1 << 1
|
||||
STEREO = 1 << 0
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> AacMediaCodecInformation:
|
||||
object_type = cls.ObjectType(data[0])
|
||||
sampling_frequency = cls.SamplingFrequency(
|
||||
(data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
||||
)
|
||||
channels = cls.Channels((data[2] >> 2) & 0x03)
|
||||
vbr = (data[3] >> 7) & 0x01
|
||||
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
||||
return AacMediaCodecInformation(
|
||||
object_type, sampling_frequency, channels, rfa, vbr, bitrate
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_discrete_values(
|
||||
cls,
|
||||
object_type: int,
|
||||
sampling_frequency: int,
|
||||
channels: int,
|
||||
vbr: int,
|
||||
bitrate: int,
|
||||
) -> AacMediaCodecInformation:
|
||||
return AacMediaCodecInformation(
|
||||
object_type=cls.OBJECT_TYPE_BITS[object_type],
|
||||
sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
|
||||
channels=cls.CHANNELS_BITS[channels],
|
||||
rfa=0,
|
||||
vbr=vbr,
|
||||
bitrate=bitrate,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_lists(
|
||||
cls,
|
||||
object_types: List[int],
|
||||
sampling_frequencies: List[int],
|
||||
channels: List[int],
|
||||
vbr: int,
|
||||
bitrate: int,
|
||||
) -> AacMediaCodecInformation:
|
||||
return AacMediaCodecInformation(
|
||||
object_type=sum(cls.OBJECT_TYPE_BITS[x] for x in object_types),
|
||||
sampling_frequency=sum(
|
||||
cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
|
||||
),
|
||||
channels=sum(cls.CHANNELS_BITS[x] for x in channels),
|
||||
rfa=0,
|
||||
vbr=vbr,
|
||||
bitrate=bitrate,
|
||||
object_type, sampling_frequency, channels, vbr, bitrate
|
||||
)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
@@ -472,30 +425,6 @@ class AacMediaCodecInformation:
|
||||
]
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
object_types = [
|
||||
'MPEG_2_AAC_LC',
|
||||
'MPEG_4_AAC_LC',
|
||||
'MPEG_4_AAC_LTP',
|
||||
'MPEG_4_AAC_SCALABLE',
|
||||
'[4]',
|
||||
'[5]',
|
||||
'[6]',
|
||||
'[7]',
|
||||
]
|
||||
channels = [1, 2]
|
||||
# pylint: disable=line-too-long
|
||||
return '\n'.join(
|
||||
[
|
||||
'AacMediaCodecInformation(',
|
||||
f' object_type: {",".join([str(x) for x in flags_to_list(self.object_type, object_types)])}',
|
||||
f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, MPEG_2_4_AAC_SAMPLING_FREQUENCIES)])}',
|
||||
f' channels: {",".join([str(x) for x in flags_to_list(self.channels, channels)])}',
|
||||
f' vbr: {self.vbr}',
|
||||
f' bitrate: {self.bitrate}' ')',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -514,7 +443,7 @@ class VendorSpecificMediaCodecInformation:
|
||||
return VendorSpecificMediaCodecInformation(vendor_id, codec_id, data[6:])
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return struct.pack('<IH', self.vendor_id, self.codec_id, self.value)
|
||||
return struct.pack('<IH', self.vendor_id, self.codec_id) + self.value
|
||||
|
||||
def __str__(self) -> str:
|
||||
# pylint: disable=line-too-long
|
||||
@@ -528,13 +457,69 @@ class VendorSpecificMediaCodecInformation:
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class OpusMediaCodecInformation(VendorSpecificMediaCodecInformation):
|
||||
vendor_id: int = dataclasses.field(init=False, repr=False)
|
||||
codec_id: int = dataclasses.field(init=False, repr=False)
|
||||
value: bytes = dataclasses.field(init=False, repr=False)
|
||||
channel_mode: ChannelMode
|
||||
frame_size: FrameSize
|
||||
sampling_frequency: SamplingFrequency
|
||||
|
||||
class ChannelMode(enum.IntFlag):
|
||||
MONO = 1 << 0
|
||||
STEREO = 1 << 1
|
||||
DUAL_MONO = 1 << 2
|
||||
|
||||
class FrameSize(enum.IntFlag):
|
||||
FS_10MS = 1 << 0
|
||||
FS_20MS = 1 << 1
|
||||
|
||||
class SamplingFrequency(enum.IntFlag):
|
||||
SF_48000 = 1 << 0
|
||||
|
||||
VENDOR_ID: ClassVar[int] = 0x000000E0
|
||||
CODEC_ID: ClassVar[int] = 0x0001
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.vendor_id = self.VENDOR_ID
|
||||
self.codec_id = self.CODEC_ID
|
||||
self.value = bytes(
|
||||
[
|
||||
self.channel_mode
|
||||
| (self.frame_size << 3)
|
||||
| (self.sampling_frequency << 7)
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> Self:
|
||||
"""Create a new instance from the `value` part of the data, not including
|
||||
the vendor id and codec id"""
|
||||
channel_mode = cls.ChannelMode(data[0] & 0x07)
|
||||
frame_size = cls.FrameSize((data[0] >> 3) & 0x03)
|
||||
sampling_frequency = cls.SamplingFrequency((data[0] >> 7) & 0x01)
|
||||
|
||||
return cls(
|
||||
channel_mode,
|
||||
frame_size,
|
||||
sampling_frequency,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class SbcFrame:
|
||||
sampling_frequency: int
|
||||
block_count: int
|
||||
channel_mode: int
|
||||
allocation_method: int
|
||||
subband_count: int
|
||||
bitpool: int
|
||||
payload: bytes
|
||||
|
||||
@property
|
||||
@@ -553,8 +538,10 @@ class SbcFrame:
|
||||
return (
|
||||
f'SBC(sf={self.sampling_frequency},'
|
||||
f'cm={self.channel_mode},'
|
||||
f'am={self.allocation_method},'
|
||||
f'br={self.bitrate},'
|
||||
f'sc={self.sample_count},'
|
||||
f'bp={self.bitpool},'
|
||||
f'size={len(self.payload)})'
|
||||
)
|
||||
|
||||
@@ -583,6 +570,7 @@ class SbcParser:
|
||||
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
||||
channel_mode = (header[1] >> 2) & 3
|
||||
channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2
|
||||
allocation_method = (header[1] >> 1) & 1
|
||||
subbands = 8 if ((header[1]) & 1) else 4
|
||||
bitpool = header[2]
|
||||
|
||||
@@ -602,7 +590,13 @@ class SbcParser:
|
||||
|
||||
# Emit the next frame
|
||||
yield SbcFrame(
|
||||
sampling_frequency, blocks, channel_mode, subbands, payload
|
||||
sampling_frequency,
|
||||
blocks,
|
||||
channel_mode,
|
||||
allocation_method,
|
||||
subbands,
|
||||
bitpool,
|
||||
payload,
|
||||
)
|
||||
|
||||
return generate_frames()
|
||||
@@ -610,21 +604,15 @@ class SbcParser:
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SbcPacketSource:
|
||||
def __init__(
|
||||
self, read: Callable[[int], Awaitable[bytes]], mtu: int, codec_capabilities
|
||||
) -> None:
|
||||
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
||||
self.read = read
|
||||
self.mtu = mtu
|
||||
self.codec_capabilities = codec_capabilities
|
||||
|
||||
@property
|
||||
def packets(self):
|
||||
async def generate_packets():
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
||||
|
||||
sequence_number = 0
|
||||
timestamp = 0
|
||||
sample_count = 0
|
||||
frames = []
|
||||
frames_size = 0
|
||||
max_rtp_payload = self.mtu - 12 - 1
|
||||
@@ -632,29 +620,29 @@ class SbcPacketSource:
|
||||
# NOTE: this doesn't support frame fragments
|
||||
sbc_parser = SbcParser(self.read)
|
||||
async for frame in sbc_parser.frames:
|
||||
print(frame)
|
||||
|
||||
if (
|
||||
frames_size + len(frame.payload) > max_rtp_payload
|
||||
or len(frames) == 16
|
||||
or len(frames) == SBC_MAX_FRAMES_IN_RTP_PAYLOAD
|
||||
):
|
||||
# Need to flush what has been accumulated so far
|
||||
logger.debug(f"yielding {len(frames)} frames")
|
||||
|
||||
# Emit a packet
|
||||
sbc_payload = bytes([len(frames)]) + b''.join(
|
||||
sbc_payload = bytes([len(frames) & 0x0F]) + b''.join(
|
||||
[frame.payload for frame in frames]
|
||||
)
|
||||
timestamp_seconds = sample_count / frame.sampling_frequency
|
||||
timestamp = int(1000 * timestamp_seconds)
|
||||
packet = MediaPacket(
|
||||
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload
|
||||
)
|
||||
packet.timestamp_seconds = timestamp / frame.sampling_frequency
|
||||
packet.timestamp_seconds = timestamp_seconds
|
||||
yield packet
|
||||
|
||||
# Prepare for next packets
|
||||
sequence_number += 1
|
||||
sequence_number &= 0xFFFF
|
||||
timestamp += sum((frame.sample_count for frame in frames))
|
||||
timestamp &= 0xFFFFFFFF
|
||||
sample_count += sum((frame.sample_count for frame in frames))
|
||||
frames = [frame]
|
||||
frames_size = len(frame.payload)
|
||||
else:
|
||||
@@ -663,3 +651,315 @@ class SbcPacketSource:
|
||||
frames_size += len(frame.payload)
|
||||
|
||||
return generate_packets()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class AacFrame:
|
||||
class Profile(enum.IntEnum):
|
||||
MAIN = 0
|
||||
LC = 1
|
||||
SSR = 2
|
||||
LTP = 3
|
||||
|
||||
profile: Profile
|
||||
sampling_frequency: int
|
||||
channel_configuration: int
|
||||
payload: bytes
|
||||
|
||||
@property
|
||||
def sample_count(self) -> int:
|
||||
return 1024
|
||||
|
||||
@property
|
||||
def duration(self) -> float:
|
||||
return self.sample_count / self.sampling_frequency
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'AAC(sf={self.sampling_frequency},'
|
||||
f'ch={self.channel_configuration},'
|
||||
f'size={len(self.payload)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
ADTS_AAC_SAMPLING_FREQUENCIES = [
|
||||
96000,
|
||||
88200,
|
||||
64000,
|
||||
48000,
|
||||
44100,
|
||||
32000,
|
||||
24000,
|
||||
22050,
|
||||
16000,
|
||||
12000,
|
||||
11025,
|
||||
8000,
|
||||
7350,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AacParser:
|
||||
"""Parser for AAC frames in an ADTS stream"""
|
||||
|
||||
def __init__(self, read: Callable[[int], Awaitable[bytes]]) -> None:
|
||||
self.read = read
|
||||
|
||||
@property
|
||||
def frames(self) -> AsyncGenerator[AacFrame, None]:
|
||||
async def generate_frames() -> AsyncGenerator[AacFrame, None]:
|
||||
while True:
|
||||
header = await self.read(7)
|
||||
if not header:
|
||||
return
|
||||
|
||||
sync_word = (header[0] << 4) | (header[1] >> 4)
|
||||
if sync_word != 0b111111111111:
|
||||
raise ValueError(f"invalid sync word ({sync_word:06x})")
|
||||
layer = (header[1] >> 1) & 0b11
|
||||
profile = AacFrame.Profile((header[2] >> 6) & 0b11)
|
||||
sampling_frequency = ADTS_AAC_SAMPLING_FREQUENCIES[
|
||||
(header[2] >> 2) & 0b1111
|
||||
]
|
||||
channel_configuration = ((header[2] & 0b1) << 2) | (header[3] >> 6)
|
||||
frame_length = (
|
||||
((header[3] & 0b11) << 11) | (header[4] << 3) | (header[5] >> 5)
|
||||
)
|
||||
|
||||
if layer != 0:
|
||||
raise ValueError("layer must be 0")
|
||||
|
||||
payload = await self.read(frame_length - 7)
|
||||
if payload:
|
||||
yield AacFrame(
|
||||
profile, sampling_frequency, channel_configuration, payload
|
||||
)
|
||||
|
||||
return generate_frames()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AacPacketSource:
|
||||
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
||||
self.read = read
|
||||
self.mtu = mtu
|
||||
|
||||
@property
|
||||
def packets(self):
|
||||
async def generate_packets():
|
||||
sequence_number = 0
|
||||
sample_count = 0
|
||||
|
||||
aac_parser = AacParser(self.read)
|
||||
async for frame in aac_parser.frames:
|
||||
logger.debug("yielding one AAC frame")
|
||||
|
||||
# Emit a packet
|
||||
aac_payload = bytes(
|
||||
AacAudioRtpPacket.for_simple_aac(
|
||||
frame.sampling_frequency,
|
||||
frame.channel_configuration,
|
||||
frame.payload,
|
||||
)
|
||||
)
|
||||
timestamp_seconds = sample_count / frame.sampling_frequency
|
||||
timestamp = int(1000 * timestamp_seconds)
|
||||
packet = MediaPacket(
|
||||
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, aac_payload
|
||||
)
|
||||
packet.timestamp_seconds = timestamp_seconds
|
||||
yield packet
|
||||
|
||||
# Prepare for next packets
|
||||
sequence_number += 1
|
||||
sequence_number &= 0xFFFF
|
||||
sample_count += frame.sample_count
|
||||
|
||||
return generate_packets()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclasses.dataclass
|
||||
class OpusPacket:
|
||||
class ChannelMode(enum.IntEnum):
|
||||
MONO = 0
|
||||
STEREO = 1
|
||||
DUAL_MONO = 2
|
||||
|
||||
channel_mode: ChannelMode
|
||||
duration: int # Duration in ms.
|
||||
sampling_frequency: int
|
||||
payload: bytes
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'Opus(ch={self.channel_mode.name}, '
|
||||
f'd={self.duration}ms, '
|
||||
f'size={len(self.payload)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class OpusParser:
|
||||
"""
|
||||
Parser for Opus packets in an Ogg stream
|
||||
|
||||
See RFC 3533
|
||||
|
||||
NOTE: this parser only supports bitstreams with a single logical stream.
|
||||
"""
|
||||
|
||||
CAPTURE_PATTERN = b'OggS'
|
||||
|
||||
class HeaderType(enum.IntFlag):
|
||||
CONTINUED = 0x01
|
||||
FIRST = 0x02
|
||||
LAST = 0x04
|
||||
|
||||
def __init__(self, read: Callable[[int], Awaitable[bytes]]) -> None:
|
||||
self.read = read
|
||||
|
||||
@property
|
||||
def packets(self) -> AsyncGenerator[OpusPacket, None]:
|
||||
async def generate_frames() -> AsyncGenerator[OpusPacket, None]:
|
||||
packet = b''
|
||||
packet_count = 0
|
||||
expected_bitstream_serial_number = None
|
||||
expected_page_sequence_number = 0
|
||||
channel_mode = OpusPacket.ChannelMode.STEREO
|
||||
|
||||
while True:
|
||||
# Parse the page header
|
||||
header = await self.read(27)
|
||||
if len(header) != 27:
|
||||
logger.debug("end of stream")
|
||||
break
|
||||
|
||||
capture_pattern = header[:4]
|
||||
if capture_pattern != self.CAPTURE_PATTERN:
|
||||
print(capture_pattern.hex())
|
||||
raise ValueError("invalid capture pattern at start of page")
|
||||
|
||||
version = header[4]
|
||||
if version != 0:
|
||||
raise ValueError(f"version {version} not supported")
|
||||
|
||||
header_type = self.HeaderType(header[5])
|
||||
(
|
||||
granule_position,
|
||||
bitstream_serial_number,
|
||||
page_sequence_number,
|
||||
crc_checksum,
|
||||
page_segments,
|
||||
) = struct.unpack_from("<QIIIB", header, 6)
|
||||
segment_table = await self.read(page_segments)
|
||||
|
||||
if header_type & self.HeaderType.FIRST:
|
||||
if expected_bitstream_serial_number is None:
|
||||
# We will only accept pages for the first encountered stream
|
||||
logger.debug("BOS")
|
||||
expected_bitstream_serial_number = bitstream_serial_number
|
||||
expected_page_sequence_number = page_sequence_number
|
||||
|
||||
if (
|
||||
expected_bitstream_serial_number is None
|
||||
or expected_bitstream_serial_number != bitstream_serial_number
|
||||
):
|
||||
logger.debug("skipping page (not the first logical bitstream)")
|
||||
for lacing_value in segment_table:
|
||||
if lacing_value:
|
||||
await self.read(lacing_value)
|
||||
continue
|
||||
|
||||
if expected_page_sequence_number != page_sequence_number:
|
||||
raise ValueError(
|
||||
f"expected page sequence number {expected_page_sequence_number}"
|
||||
f" but got {page_sequence_number}"
|
||||
)
|
||||
expected_page_sequence_number = page_sequence_number + 1
|
||||
|
||||
# Assemble the page
|
||||
if not header_type & self.HeaderType.CONTINUED:
|
||||
packet = b''
|
||||
for lacing_value in segment_table:
|
||||
if lacing_value:
|
||||
packet += await self.read(lacing_value)
|
||||
if lacing_value < 255:
|
||||
# End of packet
|
||||
packet_count += 1
|
||||
|
||||
if packet_count == 1:
|
||||
# The first packet contains the identification header
|
||||
logger.debug("first packet (header)")
|
||||
if packet[:8] != b"OpusHead":
|
||||
raise ValueError("first packet is not OpusHead")
|
||||
packet_count = (
|
||||
OpusPacket.ChannelMode.MONO
|
||||
if packet[9] == 1
|
||||
else OpusPacket.ChannelMode.STEREO
|
||||
)
|
||||
|
||||
elif packet_count == 2:
|
||||
# The second packet contains the comment header
|
||||
logger.debug("second packet (tags)")
|
||||
if packet[:8] != b"OpusTags":
|
||||
logger.warning("second packet is not OpusTags")
|
||||
else:
|
||||
yield OpusPacket(channel_mode, 20, 48000, packet)
|
||||
|
||||
packet = b''
|
||||
|
||||
if header_type & self.HeaderType.LAST:
|
||||
logger.debug("EOS")
|
||||
|
||||
return generate_frames()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class OpusPacketSource:
|
||||
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
||||
self.read = read
|
||||
self.mtu = mtu
|
||||
|
||||
@property
|
||||
def packets(self):
|
||||
async def generate_packets():
|
||||
sequence_number = 0
|
||||
elapsed_ms = 0
|
||||
|
||||
opus_parser = OpusParser(self.read)
|
||||
async for opus_packet in opus_parser.packets:
|
||||
# We only support sending one Opus frame per RTP packet
|
||||
# TODO: check the spec for the first byte value here
|
||||
opus_payload = bytes([1]) + opus_packet.payload
|
||||
elapsed_s = elapsed_ms / 1000
|
||||
timestamp = int(elapsed_s * opus_packet.sampling_frequency)
|
||||
rtp_packet = MediaPacket(
|
||||
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, opus_payload
|
||||
)
|
||||
rtp_packet.timestamp_seconds = elapsed_s
|
||||
yield rtp_packet
|
||||
|
||||
# Prepare for next packets
|
||||
sequence_number += 1
|
||||
sequence_number &= 0xFFFF
|
||||
elapsed_ms += opus_packet.duration
|
||||
|
||||
return generate_packets()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# This map should be left at the end of the file so it can refer to the classes
|
||||
# above
|
||||
# -----------------------------------------------------------------------------
|
||||
A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES = {
|
||||
OpusMediaCodecInformation.VENDOR_ID: {
|
||||
OpusMediaCodecInformation.CODEC_ID: OpusMediaCodecInformation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,7 +710,7 @@ class ATT_Prepare_Write_Response(ATT_PDU):
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ATT_PDU.subclass([])
|
||||
@ATT_PDU.subclass([("flags", 1)])
|
||||
class ATT_Execute_Write_Request(ATT_PDU):
|
||||
'''
|
||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.3 Execute Write Request
|
||||
|
||||
@@ -119,7 +119,7 @@ class Frame:
|
||||
# Not supported
|
||||
raise NotImplementedError("extended subunit types not supported")
|
||||
|
||||
if subunit_id < 5:
|
||||
if subunit_id < 5 or subunit_id == 7:
|
||||
opcode_offset = 2
|
||||
elif subunit_id == 5:
|
||||
# Extended to the next byte
|
||||
@@ -132,9 +132,10 @@ class Frame:
|
||||
else:
|
||||
subunit_id = 5 + extension
|
||||
opcode_offset = 3
|
||||
|
||||
elif subunit_id == 6:
|
||||
raise core.InvalidPacketError("reserved subunit ID")
|
||||
else:
|
||||
raise core.InvalidPacketError("invalid subunit ID")
|
||||
|
||||
opcode = Frame.OperationCode(data[opcode_offset])
|
||||
operands = data[opcode_offset + 1 :]
|
||||
|
||||
151
bumble/avdtp.py
151
bumble/avdtp.py
@@ -17,12 +17,10 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import struct
|
||||
import time
|
||||
import logging
|
||||
import enum
|
||||
import warnings
|
||||
from pyee import EventEmitter
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
@@ -39,6 +37,8 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from pyee import EventEmitter
|
||||
|
||||
from .core import (
|
||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||
InvalidStateError,
|
||||
@@ -51,13 +51,16 @@ from .a2dp import (
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
A2DP_NON_A2DP_CODEC_TYPE,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES,
|
||||
AacMediaCodecInformation,
|
||||
SbcMediaCodecInformation,
|
||||
VendorSpecificMediaCodecInformation,
|
||||
)
|
||||
from .rtp import MediaPacket
|
||||
from . import sdp, device, l2cap
|
||||
from .colors import color
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -278,95 +281,6 @@ class RealtimeClock:
|
||||
await asyncio.sleep(duration)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MediaPacket:
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> MediaPacket:
|
||||
version = (data[0] >> 6) & 0x03
|
||||
padding = (data[0] >> 5) & 0x01
|
||||
extension = (data[0] >> 4) & 0x01
|
||||
csrc_count = data[0] & 0x0F
|
||||
marker = (data[1] >> 7) & 0x01
|
||||
payload_type = data[1] & 0x7F
|
||||
sequence_number = struct.unpack_from('>H', data, 2)[0]
|
||||
timestamp = struct.unpack_from('>I', data, 4)[0]
|
||||
ssrc = struct.unpack_from('>I', data, 8)[0]
|
||||
csrc_list = [
|
||||
struct.unpack_from('>I', data, 12 + i)[0] for i in range(csrc_count)
|
||||
]
|
||||
payload = data[12 + csrc_count * 4 :]
|
||||
|
||||
return MediaPacket(
|
||||
version,
|
||||
padding,
|
||||
extension,
|
||||
marker,
|
||||
sequence_number,
|
||||
timestamp,
|
||||
ssrc,
|
||||
csrc_list,
|
||||
payload_type,
|
||||
payload,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: int,
|
||||
padding: int,
|
||||
extension: int,
|
||||
marker: int,
|
||||
sequence_number: int,
|
||||
timestamp: int,
|
||||
ssrc: int,
|
||||
csrc_list: List[int],
|
||||
payload_type: int,
|
||||
payload: bytes,
|
||||
) -> None:
|
||||
self.version = version
|
||||
self.padding = padding
|
||||
self.extension = extension
|
||||
self.marker = marker
|
||||
self.sequence_number = sequence_number & 0xFFFF
|
||||
self.timestamp = timestamp & 0xFFFFFFFF
|
||||
self.ssrc = ssrc
|
||||
self.csrc_list = csrc_list
|
||||
self.payload_type = payload_type
|
||||
self.payload = payload
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
header = bytes(
|
||||
[
|
||||
self.version << 6
|
||||
| self.padding << 5
|
||||
| self.extension << 4
|
||||
| len(self.csrc_list),
|
||||
self.marker << 7 | self.payload_type,
|
||||
]
|
||||
) + struct.pack(
|
||||
'>HII',
|
||||
self.sequence_number,
|
||||
self.timestamp,
|
||||
self.ssrc,
|
||||
)
|
||||
for csrc in self.csrc_list:
|
||||
header += struct.pack('>I', csrc)
|
||||
return header + self.payload
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'RTP(v={self.version},'
|
||||
f'p={self.padding},'
|
||||
f'x={self.extension},'
|
||||
f'm={self.marker},'
|
||||
f'pt={self.payload_type},'
|
||||
f'sn={self.sequence_number},'
|
||||
f'ts={self.timestamp},'
|
||||
f'ssrc={self.ssrc},'
|
||||
f'csrcs={self.csrc_list},'
|
||||
f'payload_size={len(self.payload)})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MediaPacketPump:
|
||||
pump_task: Optional[asyncio.Task]
|
||||
@@ -377,6 +291,7 @@ class MediaPacketPump:
|
||||
self.packets = packets
|
||||
self.clock = clock
|
||||
self.pump_task = None
|
||||
self.completed = asyncio.Event()
|
||||
|
||||
async def start(self, rtp_channel: l2cap.ClassicChannel) -> None:
|
||||
async def pump_packets():
|
||||
@@ -406,6 +321,8 @@ class MediaPacketPump:
|
||||
)
|
||||
except asyncio.exceptions.CancelledError:
|
||||
logger.debug('pump canceled')
|
||||
finally:
|
||||
self.completed.set()
|
||||
|
||||
# Pump packets
|
||||
self.pump_task = asyncio.create_task(pump_packets())
|
||||
@@ -417,6 +334,9 @@ class MediaPacketPump:
|
||||
await self.pump_task
|
||||
self.pump_task = None
|
||||
|
||||
async def wait_for_completion(self) -> None:
|
||||
await self.completed.wait()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MessageAssembler:
|
||||
@@ -615,11 +535,25 @@ class MediaCodecCapabilities(ServiceCapabilities):
|
||||
self.media_codec_information
|
||||
)
|
||||
elif self.media_codec_type == A2DP_NON_A2DP_CODEC_TYPE:
|
||||
self.media_codec_information = (
|
||||
vendor_media_codec_information = (
|
||||
VendorSpecificMediaCodecInformation.from_bytes(
|
||||
self.media_codec_information
|
||||
)
|
||||
)
|
||||
if (
|
||||
vendor_class_map := A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES.get(
|
||||
vendor_media_codec_information.vendor_id
|
||||
)
|
||||
) and (
|
||||
media_codec_information_class := vendor_class_map.get(
|
||||
vendor_media_codec_information.codec_id
|
||||
)
|
||||
):
|
||||
self.media_codec_information = media_codec_information_class.from_bytes(
|
||||
vendor_media_codec_information.value
|
||||
)
|
||||
else:
|
||||
self.media_codec_information = vendor_media_codec_information
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1316,10 +1250,20 @@ class Protocol(EventEmitter):
|
||||
return None
|
||||
|
||||
def add_source(
|
||||
self, codec_capabilities: MediaCodecCapabilities, packet_pump: MediaPacketPump
|
||||
self,
|
||||
codec_capabilities: MediaCodecCapabilities,
|
||||
packet_pump: MediaPacketPump,
|
||||
delay_reporting: bool = False,
|
||||
) -> LocalSource:
|
||||
seid = len(self.local_endpoints) + 1
|
||||
source = LocalSource(self, seid, codec_capabilities, packet_pump)
|
||||
service_capabilities = (
|
||||
[ServiceCapabilities(AVDTP_DELAY_REPORTING_SERVICE_CATEGORY)]
|
||||
if delay_reporting
|
||||
else []
|
||||
)
|
||||
source = LocalSource(
|
||||
self, seid, codec_capabilities, service_capabilities, packet_pump
|
||||
)
|
||||
self.local_endpoints.append(source)
|
||||
|
||||
return source
|
||||
@@ -1372,7 +1316,7 @@ class Protocol(EventEmitter):
|
||||
return self.remote_endpoints.values()
|
||||
|
||||
def find_remote_sink_by_codec(
|
||||
self, media_type: int, codec_type: int
|
||||
self, media_type: int, codec_type: int, vendor_id: int = 0, codec_id: int = 0
|
||||
) -> Optional[DiscoveredStreamEndPoint]:
|
||||
for endpoint in self.remote_endpoints.values():
|
||||
if (
|
||||
@@ -1397,7 +1341,19 @@ class Protocol(EventEmitter):
|
||||
codec_capabilities.media_type == AVDTP_AUDIO_MEDIA_TYPE
|
||||
and codec_capabilities.media_codec_type == codec_type
|
||||
):
|
||||
has_codec = True
|
||||
if isinstance(
|
||||
codec_capabilities.media_codec_information,
|
||||
VendorSpecificMediaCodecInformation,
|
||||
):
|
||||
if (
|
||||
codec_capabilities.media_codec_information.vendor_id
|
||||
== vendor_id
|
||||
and codec_capabilities.media_codec_information.codec_id
|
||||
== codec_id
|
||||
):
|
||||
has_codec = True
|
||||
else:
|
||||
has_codec = True
|
||||
if has_media_transport and has_codec:
|
||||
return endpoint
|
||||
|
||||
@@ -2180,12 +2136,13 @@ class LocalSource(LocalStreamEndPoint):
|
||||
protocol: Protocol,
|
||||
seid: int,
|
||||
codec_capabilities: MediaCodecCapabilities,
|
||||
other_capabilitiles: Iterable[ServiceCapabilities],
|
||||
packet_pump: MediaPacketPump,
|
||||
) -> None:
|
||||
capabilities = [
|
||||
ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY),
|
||||
codec_capabilities,
|
||||
]
|
||||
] + list(other_capabilitiles)
|
||||
super().__init__(
|
||||
protocol,
|
||||
seid,
|
||||
|
||||
@@ -1491,10 +1491,14 @@ class Protocol(pyee.EventEmitter):
|
||||
f"<<< AVCTP Command, transaction_label={transaction_label}: " f"{command}"
|
||||
)
|
||||
|
||||
# Only the PANEL subunit type with subunit ID 0 is supported in this profile.
|
||||
if (
|
||||
command.subunit_type != avc.Frame.SubunitType.PANEL
|
||||
or command.subunit_id != 0
|
||||
# Only addressing the unit, or the PANEL subunit with subunit ID 0 is supported
|
||||
# in this profile.
|
||||
if not (
|
||||
command.subunit_type == avc.Frame.SubunitType.UNIT
|
||||
and command.subunit_id == 7
|
||||
) and not (
|
||||
command.subunit_type == avc.Frame.SubunitType.PANEL
|
||||
and command.subunit_id == 0
|
||||
):
|
||||
logger.debug("subunit not supported")
|
||||
self.send_not_implemented_response(transaction_label, command)
|
||||
@@ -1528,8 +1532,8 @@ class Protocol(pyee.EventEmitter):
|
||||
# TODO: delegate
|
||||
response = avc.PassThroughResponseFrame(
|
||||
avc.ResponseFrame.ResponseCode.ACCEPTED,
|
||||
avc.Frame.SubunitType.PANEL,
|
||||
0,
|
||||
command.subunit_type,
|
||||
command.subunit_id,
|
||||
command.state_flag,
|
||||
command.operation_id,
|
||||
command.operation_data,
|
||||
@@ -1846,6 +1850,15 @@ class Protocol(pyee.EventEmitter):
|
||||
RejectedResponse(pdu_id, status_code),
|
||||
)
|
||||
|
||||
def send_not_implemented_avrcp_response(
|
||||
self, transaction_label: int, pdu_id: Protocol.PduId
|
||||
) -> None:
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.NOT_IMPLEMENTED,
|
||||
NotImplementedResponse(pdu_id, b''),
|
||||
)
|
||||
|
||||
def _on_get_capabilities_command(
|
||||
self, transaction_label: int, command: GetCapabilitiesCommand
|
||||
) -> None:
|
||||
@@ -1891,29 +1904,35 @@ class Protocol(pyee.EventEmitter):
|
||||
async def register_notification():
|
||||
# Check if the event is supported.
|
||||
supported_events = await self.delegate.get_supported_events()
|
||||
if command.event_id in supported_events:
|
||||
if command.event_id == EventId.VOLUME_CHANGED:
|
||||
volume = await self.delegate.get_absolute_volume()
|
||||
response = RegisterNotificationResponse(VolumeChangedEvent(volume))
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||
response,
|
||||
)
|
||||
self._register_notification_listener(transaction_label, command)
|
||||
return
|
||||
if command.event_id not in supported_events:
|
||||
logger.debug("event not supported")
|
||||
self.send_not_implemented_avrcp_response(
|
||||
transaction_label, self.PduId.REGISTER_NOTIFICATION
|
||||
)
|
||||
return
|
||||
|
||||
if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
|
||||
# TODO: testing only, use delegate
|
||||
response = RegisterNotificationResponse(
|
||||
PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
|
||||
)
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||
response,
|
||||
)
|
||||
self._register_notification_listener(transaction_label, command)
|
||||
return
|
||||
if command.event_id == EventId.VOLUME_CHANGED:
|
||||
volume = await self.delegate.get_absolute_volume()
|
||||
response = RegisterNotificationResponse(VolumeChangedEvent(volume))
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||
response,
|
||||
)
|
||||
self._register_notification_listener(transaction_label, command)
|
||||
return
|
||||
|
||||
if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
|
||||
# TODO: testing only, use delegate
|
||||
response = RegisterNotificationResponse(
|
||||
PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
|
||||
)
|
||||
self.send_avrcp_response(
|
||||
transaction_label,
|
||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||
response,
|
||||
)
|
||||
self._register_notification_listener(transaction_label, command)
|
||||
return
|
||||
|
||||
self._delegate_command(transaction_label, command, register_notification())
|
||||
|
||||
290
bumble/codecs.py
290
bumble/codecs.py
@@ -17,6 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import core
|
||||
|
||||
@@ -101,12 +102,40 @@ class BitReader:
|
||||
break
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class BitWriter:
|
||||
"""Simple but not optimized bit stream writer."""
|
||||
|
||||
data: int
|
||||
bit_count: int
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.data = 0
|
||||
self.bit_count = 0
|
||||
|
||||
def write(self, value: int, bit_count: int) -> None:
|
||||
self.data = (self.data << bit_count) | value
|
||||
self.bit_count += bit_count
|
||||
|
||||
def write_bytes(self, data: bytes) -> None:
|
||||
bit_count = 8 * len(data)
|
||||
self.data = (self.data << bit_count) | int.from_bytes(data, 'big')
|
||||
self.bit_count += bit_count
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return (self.data << ((8 - (self.bit_count % 8)) % 8)).to_bytes(
|
||||
(self.bit_count + 7) // 8, 'big'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AacAudioRtpPacket:
|
||||
"""AAC payload encapsulated in an RTP packet payload"""
|
||||
|
||||
audio_mux_element: AudioMuxElement
|
||||
|
||||
@staticmethod
|
||||
def latm_value(reader: BitReader) -> int:
|
||||
def read_latm_value(reader: BitReader) -> int:
|
||||
bytes_for_value = reader.read(2)
|
||||
value = 0
|
||||
for _ in range(bytes_for_value + 1):
|
||||
@@ -114,24 +143,33 @@ class AacAudioRtpPacket:
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def program_config_element(reader: BitReader):
|
||||
raise core.InvalidPacketError('program_config_element not supported')
|
||||
def read_audio_object_type(reader: BitReader):
|
||||
# GetAudioObjectType - ISO/EIC 14496-3 Table 1.16
|
||||
audio_object_type = reader.read(5)
|
||||
if audio_object_type == 31:
|
||||
audio_object_type = 32 + reader.read(6)
|
||||
|
||||
return audio_object_type
|
||||
|
||||
@dataclass
|
||||
class GASpecificConfig:
|
||||
def __init__(
|
||||
self, reader: BitReader, channel_configuration: int, audio_object_type: int
|
||||
) -> None:
|
||||
audio_object_type: int
|
||||
# NOTE: other fields not supported
|
||||
|
||||
@classmethod
|
||||
def from_bits(
|
||||
cls, reader: BitReader, channel_configuration: int, audio_object_type: int
|
||||
) -> Self:
|
||||
# GASpecificConfig - ISO/EIC 14496-3 Table 4.1
|
||||
frame_length_flag = reader.read(1)
|
||||
depends_on_core_coder = reader.read(1)
|
||||
if depends_on_core_coder:
|
||||
self.core_coder_delay = reader.read(14)
|
||||
core_coder_delay = reader.read(14)
|
||||
extension_flag = reader.read(1)
|
||||
if not channel_configuration:
|
||||
AacAudioRtpPacket.program_config_element(reader)
|
||||
raise core.InvalidPacketError('program_config_element not supported')
|
||||
if audio_object_type in (6, 20):
|
||||
self.layer_nr = reader.read(3)
|
||||
layer_nr = reader.read(3)
|
||||
if extension_flag:
|
||||
if audio_object_type == 22:
|
||||
num_of_sub_frame = reader.read(5)
|
||||
@@ -144,14 +182,13 @@ class AacAudioRtpPacket:
|
||||
if extension_flag_3 == 1:
|
||||
raise core.InvalidPacketError('extensionFlag3 == 1 not supported')
|
||||
|
||||
@staticmethod
|
||||
def audio_object_type(reader: BitReader):
|
||||
# GetAudioObjectType - ISO/EIC 14496-3 Table 1.16
|
||||
audio_object_type = reader.read(5)
|
||||
if audio_object_type == 31:
|
||||
audio_object_type = 32 + reader.read(6)
|
||||
return cls(audio_object_type)
|
||||
|
||||
return audio_object_type
|
||||
def to_bits(self, writer: BitWriter) -> None:
|
||||
assert self.audio_object_type in (1, 2)
|
||||
writer.write(0, 1) # frame_length_flag = 0
|
||||
writer.write(0, 1) # depends_on_core_coder = 0
|
||||
writer.write(0, 1) # extension_flag = 0
|
||||
|
||||
@dataclass
|
||||
class AudioSpecificConfig:
|
||||
@@ -159,6 +196,7 @@ class AacAudioRtpPacket:
|
||||
sampling_frequency_index: int
|
||||
sampling_frequency: int
|
||||
channel_configuration: int
|
||||
ga_specific_config: AacAudioRtpPacket.GASpecificConfig
|
||||
sbr_present_flag: int
|
||||
ps_present_flag: int
|
||||
extension_audio_object_type: int
|
||||
@@ -182,44 +220,73 @@ class AacAudioRtpPacket:
|
||||
7350,
|
||||
]
|
||||
|
||||
def __init__(self, reader: BitReader) -> None:
|
||||
# AudioSpecificConfig - ISO/EIC 14496-3 Table 1.15
|
||||
self.audio_object_type = AacAudioRtpPacket.audio_object_type(reader)
|
||||
self.sampling_frequency_index = reader.read(4)
|
||||
if self.sampling_frequency_index == 0xF:
|
||||
self.sampling_frequency = reader.read(24)
|
||||
else:
|
||||
self.sampling_frequency = self.SAMPLING_FREQUENCIES[
|
||||
self.sampling_frequency_index
|
||||
]
|
||||
self.channel_configuration = reader.read(4)
|
||||
self.sbr_present_flag = -1
|
||||
self.ps_present_flag = -1
|
||||
if self.audio_object_type in (5, 29):
|
||||
self.extension_audio_object_type = 5
|
||||
self.sbc_present_flag = 1
|
||||
if self.audio_object_type == 29:
|
||||
self.ps_present_flag = 1
|
||||
self.extension_sampling_frequency_index = reader.read(4)
|
||||
if self.extension_sampling_frequency_index == 0xF:
|
||||
self.extension_sampling_frequency = reader.read(24)
|
||||
else:
|
||||
self.extension_sampling_frequency = self.SAMPLING_FREQUENCIES[
|
||||
self.extension_sampling_frequency_index
|
||||
]
|
||||
self.audio_object_type = AacAudioRtpPacket.audio_object_type(reader)
|
||||
if self.audio_object_type == 22:
|
||||
self.extension_channel_configuration = reader.read(4)
|
||||
else:
|
||||
self.extension_audio_object_type = 0
|
||||
@classmethod
|
||||
def for_simple_aac(
|
||||
cls,
|
||||
audio_object_type: int,
|
||||
sampling_frequency: int,
|
||||
channel_configuration: int,
|
||||
) -> Self:
|
||||
if sampling_frequency not in cls.SAMPLING_FREQUENCIES:
|
||||
raise ValueError(f'invalid sampling frequency {sampling_frequency}')
|
||||
|
||||
if self.audio_object_type in (1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23):
|
||||
ga_specific_config = AacAudioRtpPacket.GASpecificConfig(
|
||||
reader, self.channel_configuration, self.audio_object_type
|
||||
ga_specific_config = AacAudioRtpPacket.GASpecificConfig(audio_object_type)
|
||||
|
||||
return cls(
|
||||
audio_object_type=audio_object_type,
|
||||
sampling_frequency_index=cls.SAMPLING_FREQUENCIES.index(
|
||||
sampling_frequency
|
||||
),
|
||||
sampling_frequency=sampling_frequency,
|
||||
channel_configuration=channel_configuration,
|
||||
ga_specific_config=ga_specific_config,
|
||||
sbr_present_flag=0,
|
||||
ps_present_flag=0,
|
||||
extension_audio_object_type=0,
|
||||
extension_sampling_frequency_index=0,
|
||||
extension_sampling_frequency=0,
|
||||
extension_channel_configuration=0,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_bits(cls, reader: BitReader) -> Self:
|
||||
# AudioSpecificConfig - ISO/EIC 14496-3 Table 1.15
|
||||
audio_object_type = AacAudioRtpPacket.read_audio_object_type(reader)
|
||||
sampling_frequency_index = reader.read(4)
|
||||
if sampling_frequency_index == 0xF:
|
||||
sampling_frequency = reader.read(24)
|
||||
else:
|
||||
sampling_frequency = cls.SAMPLING_FREQUENCIES[sampling_frequency_index]
|
||||
channel_configuration = reader.read(4)
|
||||
sbr_present_flag = 0
|
||||
ps_present_flag = 0
|
||||
extension_sampling_frequency_index = 0
|
||||
extension_sampling_frequency = 0
|
||||
extension_channel_configuration = 0
|
||||
extension_audio_object_type = 0
|
||||
if audio_object_type in (5, 29):
|
||||
extension_audio_object_type = 5
|
||||
sbr_present_flag = 1
|
||||
if audio_object_type == 29:
|
||||
ps_present_flag = 1
|
||||
extension_sampling_frequency_index = reader.read(4)
|
||||
if extension_sampling_frequency_index == 0xF:
|
||||
extension_sampling_frequency = reader.read(24)
|
||||
else:
|
||||
extension_sampling_frequency = cls.SAMPLING_FREQUENCIES[
|
||||
extension_sampling_frequency_index
|
||||
]
|
||||
audio_object_type = AacAudioRtpPacket.read_audio_object_type(reader)
|
||||
if audio_object_type == 22:
|
||||
extension_channel_configuration = reader.read(4)
|
||||
|
||||
if audio_object_type in (1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23):
|
||||
ga_specific_config = AacAudioRtpPacket.GASpecificConfig.from_bits(
|
||||
reader, channel_configuration, audio_object_type
|
||||
)
|
||||
else:
|
||||
raise core.InvalidPacketError(
|
||||
f'audioObjectType {self.audio_object_type} not supported'
|
||||
f'audioObjectType {audio_object_type} not supported'
|
||||
)
|
||||
|
||||
# if self.extension_audio_object_type != 5 and bits_to_decode >= 16:
|
||||
@@ -248,13 +315,44 @@ class AacAudioRtpPacket:
|
||||
# self.extension_sampling_frequency = self.SAMPLING_FREQUENCIES[self.extension_sampling_frequency_index]
|
||||
# self.extension_channel_configuration = reader.read(4)
|
||||
|
||||
return cls(
|
||||
audio_object_type,
|
||||
sampling_frequency_index,
|
||||
sampling_frequency,
|
||||
channel_configuration,
|
||||
ga_specific_config,
|
||||
sbr_present_flag,
|
||||
ps_present_flag,
|
||||
extension_audio_object_type,
|
||||
extension_sampling_frequency_index,
|
||||
extension_sampling_frequency,
|
||||
extension_channel_configuration,
|
||||
)
|
||||
|
||||
def to_bits(self, writer: BitWriter) -> None:
|
||||
if self.sampling_frequency_index >= 15:
|
||||
raise ValueError(
|
||||
f"unsupported sampling frequency index {self.sampling_frequency_index}"
|
||||
)
|
||||
|
||||
if self.audio_object_type not in (1, 2):
|
||||
raise ValueError(
|
||||
f"unsupported audio object type {self.audio_object_type} "
|
||||
)
|
||||
|
||||
writer.write(self.audio_object_type, 5)
|
||||
writer.write(self.sampling_frequency_index, 4)
|
||||
writer.write(self.channel_configuration, 4)
|
||||
self.ga_specific_config.to_bits(writer)
|
||||
|
||||
@dataclass
|
||||
class StreamMuxConfig:
|
||||
other_data_present: int
|
||||
other_data_len_bits: int
|
||||
audio_specific_config: AacAudioRtpPacket.AudioSpecificConfig
|
||||
|
||||
def __init__(self, reader: BitReader) -> None:
|
||||
@classmethod
|
||||
def from_bits(cls, reader: BitReader) -> Self:
|
||||
# StreamMuxConfig - ISO/EIC 14496-3 Table 1.42
|
||||
audio_mux_version = reader.read(1)
|
||||
if audio_mux_version == 1:
|
||||
@@ -264,7 +362,7 @@ class AacAudioRtpPacket:
|
||||
if audio_mux_version_a != 0:
|
||||
raise core.InvalidPacketError('audioMuxVersionA != 0 not supported')
|
||||
if audio_mux_version == 1:
|
||||
tara_buffer_fullness = AacAudioRtpPacket.latm_value(reader)
|
||||
tara_buffer_fullness = AacAudioRtpPacket.read_latm_value(reader)
|
||||
stream_cnt = 0
|
||||
all_streams_same_time_framing = reader.read(1)
|
||||
num_sub_frames = reader.read(6)
|
||||
@@ -275,13 +373,13 @@ class AacAudioRtpPacket:
|
||||
if num_layer != 0:
|
||||
raise core.InvalidPacketError('num_layer != 0 not supported')
|
||||
if audio_mux_version == 0:
|
||||
self.audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig(
|
||||
audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig.from_bits(
|
||||
reader
|
||||
)
|
||||
else:
|
||||
asc_len = AacAudioRtpPacket.latm_value(reader)
|
||||
asc_len = AacAudioRtpPacket.read_latm_value(reader)
|
||||
marker = reader.bit_position
|
||||
self.audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig(
|
||||
audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig.from_bits(
|
||||
reader
|
||||
)
|
||||
audio_specific_config_len = reader.bit_position - marker
|
||||
@@ -299,36 +397,49 @@ class AacAudioRtpPacket:
|
||||
f'frame_length_type {frame_length_type} not supported'
|
||||
)
|
||||
|
||||
self.other_data_present = reader.read(1)
|
||||
if self.other_data_present:
|
||||
other_data_present = reader.read(1)
|
||||
other_data_len_bits = 0
|
||||
if other_data_present:
|
||||
if audio_mux_version == 1:
|
||||
self.other_data_len_bits = AacAudioRtpPacket.latm_value(reader)
|
||||
other_data_len_bits = AacAudioRtpPacket.read_latm_value(reader)
|
||||
else:
|
||||
self.other_data_len_bits = 0
|
||||
while True:
|
||||
self.other_data_len_bits *= 256
|
||||
other_data_len_bits *= 256
|
||||
other_data_len_esc = reader.read(1)
|
||||
self.other_data_len_bits += reader.read(8)
|
||||
other_data_len_bits += reader.read(8)
|
||||
if other_data_len_esc == 0:
|
||||
break
|
||||
crc_check_present = reader.read(1)
|
||||
if crc_check_present:
|
||||
crc_checksum = reader.read(8)
|
||||
|
||||
return cls(other_data_present, other_data_len_bits, audio_specific_config)
|
||||
|
||||
def to_bits(self, writer: BitWriter) -> None:
|
||||
writer.write(0, 1) # audioMuxVersion = 0
|
||||
writer.write(1, 1) # allStreamsSameTimeFraming = 1
|
||||
writer.write(0, 6) # numSubFrames = 0
|
||||
writer.write(0, 4) # numProgram = 0
|
||||
writer.write(0, 3) # numLayer = 0
|
||||
self.audio_specific_config.to_bits(writer)
|
||||
writer.write(0, 3) # frameLengthType = 0
|
||||
writer.write(0, 8) # latmBufferFullness = 0
|
||||
writer.write(0, 1) # otherDataPresent = 0
|
||||
writer.write(0, 1) # crcCheckPresent = 0
|
||||
|
||||
@dataclass
|
||||
class AudioMuxElement:
|
||||
payload: bytes
|
||||
stream_mux_config: AacAudioRtpPacket.StreamMuxConfig
|
||||
payload: bytes
|
||||
|
||||
def __init__(self, reader: BitReader, mux_config_present: int):
|
||||
if mux_config_present == 0:
|
||||
raise core.InvalidPacketError('muxConfigPresent == 0 not supported')
|
||||
|
||||
@classmethod
|
||||
def from_bits(cls, reader: BitReader) -> Self:
|
||||
# AudioMuxElement - ISO/EIC 14496-3 Table 1.41
|
||||
# (only supports mux_config_present=1)
|
||||
use_same_stream_mux = reader.read(1)
|
||||
if use_same_stream_mux:
|
||||
raise core.InvalidPacketError('useSameStreamMux == 1 not supported')
|
||||
self.stream_mux_config = AacAudioRtpPacket.StreamMuxConfig(reader)
|
||||
stream_mux_config = AacAudioRtpPacket.StreamMuxConfig.from_bits(reader)
|
||||
|
||||
# We only support:
|
||||
# allStreamsSameTimeFraming == 1
|
||||
@@ -344,19 +455,46 @@ class AacAudioRtpPacket:
|
||||
if tmp != 255:
|
||||
break
|
||||
|
||||
self.payload = reader.read_bytes(mux_slot_length_bytes)
|
||||
payload = reader.read_bytes(mux_slot_length_bytes)
|
||||
|
||||
if self.stream_mux_config.other_data_present:
|
||||
reader.skip(self.stream_mux_config.other_data_len_bits)
|
||||
if stream_mux_config.other_data_present:
|
||||
reader.skip(stream_mux_config.other_data_len_bits)
|
||||
|
||||
# ByteAlign
|
||||
while reader.bit_position % 8:
|
||||
reader.read(1)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
return cls(stream_mux_config, payload)
|
||||
|
||||
def to_bits(self, writer: BitWriter) -> None:
|
||||
writer.write(0, 1) # useSameStreamMux = 0
|
||||
self.stream_mux_config.to_bits(writer)
|
||||
mux_slot_length_bytes = len(self.payload)
|
||||
while mux_slot_length_bytes > 255:
|
||||
writer.write(255, 8)
|
||||
mux_slot_length_bytes -= 255
|
||||
writer.write(mux_slot_length_bytes, 8)
|
||||
if mux_slot_length_bytes == 255:
|
||||
writer.write(0, 8)
|
||||
writer.write_bytes(self.payload)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> Self:
|
||||
# Parse the bit stream
|
||||
reader = BitReader(data)
|
||||
self.audio_mux_element = self.AudioMuxElement(reader, mux_config_present=1)
|
||||
return cls(cls.AudioMuxElement.from_bits(reader))
|
||||
|
||||
@classmethod
|
||||
def for_simple_aac(
|
||||
cls, sampling_frequency: int, channel_configuration: int, payload: bytes
|
||||
) -> Self:
|
||||
audio_specific_config = cls.AudioSpecificConfig.for_simple_aac(
|
||||
2, sampling_frequency, channel_configuration
|
||||
)
|
||||
stream_mux_config = cls.StreamMuxConfig(0, 0, audio_specific_config)
|
||||
audio_mux_element = cls.AudioMuxElement(stream_mux_config, payload)
|
||||
|
||||
return cls(audio_mux_element)
|
||||
|
||||
def to_adts(self):
|
||||
# pylint: disable=line-too-long
|
||||
@@ -383,3 +521,11 @@ class AacAudioRtpPacket:
|
||||
)
|
||||
+ self.audio_mux_element.payload
|
||||
)
|
||||
|
||||
def __init__(self, audio_mux_element: AudioMuxElement) -> None:
|
||||
self.audio_mux_element = audio_mux_element
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
writer = BitWriter()
|
||||
self.audio_mux_element.to_bits(writer)
|
||||
return bytes(writer)
|
||||
|
||||
@@ -1571,14 +1571,22 @@ class Connection(CompositeEventEmitter):
|
||||
raise
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'Connection(handle=0x{self.handle:04X}, '
|
||||
f'role={self.role_name}, '
|
||||
f'self_address={self.self_address}, '
|
||||
f'self_resolvable_address={self.self_resolvable_address}, '
|
||||
f'peer_address={self.peer_address}, '
|
||||
f'peer_resolvable_address={self.peer_resolvable_address})'
|
||||
)
|
||||
if self.transport == BT_LE_TRANSPORT:
|
||||
return (
|
||||
f'Connection(transport=LE, handle=0x{self.handle:04X}, '
|
||||
f'role={self.role_name}, '
|
||||
f'self_address={self.self_address}, '
|
||||
f'self_resolvable_address={self.self_resolvable_address}, '
|
||||
f'peer_address={self.peer_address}, '
|
||||
f'peer_resolvable_address={self.peer_resolvable_address})'
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f'Connection(transport=BR/EDR, handle=0x{self.handle:04X}, '
|
||||
f'role={self.role_name}, '
|
||||
f'self_address={self.self_address}, '
|
||||
f'peer_address={self.peer_address})'
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1766,9 +1774,9 @@ device_host_event_handlers: List[str] = []
|
||||
# -----------------------------------------------------------------------------
|
||||
class Device(CompositeEventEmitter):
|
||||
# Incomplete list of fields.
|
||||
random_address: Address # Random address that may change with RPA
|
||||
public_address: Address # Public address (obtained from the controller)
|
||||
static_address: Address # Random address that can be set but does not change
|
||||
random_address: Address # Random private address that may change periodically
|
||||
public_address: Address # Public address that is globally unique (from controller)
|
||||
static_address: Address # Random static address that does not change once set
|
||||
classic_enabled: bool
|
||||
name: str
|
||||
class_of_device: int
|
||||
|
||||
@@ -898,6 +898,12 @@ class Client:
|
||||
) and subscriber in subscribers:
|
||||
subscribers.remove(subscriber)
|
||||
|
||||
# The characteristic itself is added as subscriber. If it is the
|
||||
# last remaining subscriber, we remove it, such that the clean up
|
||||
# works correctly. Otherwise the CCCD never is set back to 0.
|
||||
if len(subscribers) == 1 and characteristic in subscribers:
|
||||
subscribers.remove(characteristic)
|
||||
|
||||
# Cleanup if we removed the last one
|
||||
if not subscribers:
|
||||
del subscriber_set[characteristic.handle]
|
||||
|
||||
@@ -6065,6 +6065,32 @@ class HCI_Read_Remote_Version_Information_Complete_Event(HCI_Event):
|
||||
'''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event(
|
||||
[
|
||||
('status', STATUS_SPEC),
|
||||
('connection_handle', 2),
|
||||
('unused', 1),
|
||||
(
|
||||
'service_type',
|
||||
{
|
||||
'size': 1,
|
||||
'mapper': lambda x: HCI_QOS_Setup_Complete_Event.ServiceType(x).name,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
class HCI_QOS_Setup_Complete_Event(HCI_Event):
|
||||
'''
|
||||
See Bluetooth spec @ 7.7.13 QoS Setup Complete Event
|
||||
'''
|
||||
|
||||
class ServiceType(OpenIntEnum):
|
||||
NO_TRAFFIC_AVAILABLE = 0x00
|
||||
BEST_EFFORT_AVAILABLE = 0x01
|
||||
GUARANTEED_AVAILABLE = 0x02
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@HCI_Event.event(
|
||||
[
|
||||
|
||||
@@ -795,29 +795,32 @@ class HfProtocol(pyee.EventEmitter):
|
||||
# Append to the read buffer.
|
||||
self.read_buffer.extend(data)
|
||||
|
||||
# Locate header and trailer.
|
||||
header = self.read_buffer.find(b'\r\n')
|
||||
trailer = self.read_buffer.find(b'\r\n', header + 2)
|
||||
if header == -1 or trailer == -1:
|
||||
return
|
||||
while self.read_buffer:
|
||||
# Locate header and trailer.
|
||||
header = self.read_buffer.find(b'\r\n')
|
||||
trailer = self.read_buffer.find(b'\r\n', header + 2)
|
||||
if header == -1 or trailer == -1:
|
||||
return
|
||||
|
||||
# Isolate the AT response code and parameters.
|
||||
raw_response = self.read_buffer[header + 2 : trailer]
|
||||
response = AtResponse.parse_from(raw_response)
|
||||
logger.debug(f"<<< {raw_response.decode()}")
|
||||
# Isolate the AT response code and parameters.
|
||||
raw_response = self.read_buffer[header + 2 : trailer]
|
||||
response = AtResponse.parse_from(raw_response)
|
||||
logger.debug(f"<<< {raw_response.decode()}")
|
||||
|
||||
# Consume the response bytes.
|
||||
self.read_buffer = self.read_buffer[trailer + 2 :]
|
||||
# Consume the response bytes.
|
||||
self.read_buffer = self.read_buffer[trailer + 2 :]
|
||||
|
||||
# Forward the received code to the correct queue.
|
||||
if self.command_lock.locked() and (
|
||||
response.code in STATUS_CODES or response.code in RESPONSE_CODES
|
||||
):
|
||||
self.response_queue.put_nowait(response)
|
||||
elif response.code in UNSOLICITED_CODES:
|
||||
self.unsolicited_queue.put_nowait(response)
|
||||
else:
|
||||
logger.warning(f"dropping unexpected response with code '{response.code}'")
|
||||
# Forward the received code to the correct queue.
|
||||
if self.command_lock.locked() and (
|
||||
response.code in STATUS_CODES or response.code in RESPONSE_CODES
|
||||
):
|
||||
self.response_queue.put_nowait(response)
|
||||
elif response.code in UNSOLICITED_CODES:
|
||||
self.unsolicited_queue.put_nowait(response)
|
||||
else:
|
||||
logger.warning(
|
||||
f"dropping unexpected response with code '{response.code}'"
|
||||
)
|
||||
|
||||
async def execute_command(
|
||||
self,
|
||||
@@ -1244,31 +1247,32 @@ class AgProtocol(pyee.EventEmitter):
|
||||
# Append to the read buffer.
|
||||
self.read_buffer.extend(data)
|
||||
|
||||
# Locate the trailer.
|
||||
trailer = self.read_buffer.find(b'\r')
|
||||
if trailer == -1:
|
||||
return
|
||||
while self.read_buffer:
|
||||
# Locate the trailer.
|
||||
trailer = self.read_buffer.find(b'\r')
|
||||
if trailer == -1:
|
||||
return
|
||||
|
||||
# Isolate the AT response code and parameters.
|
||||
raw_command = self.read_buffer[:trailer]
|
||||
command = AtCommand.parse_from(raw_command)
|
||||
logger.debug(f"<<< {raw_command.decode()}")
|
||||
# Isolate the AT response code and parameters.
|
||||
raw_command = self.read_buffer[:trailer]
|
||||
command = AtCommand.parse_from(raw_command)
|
||||
logger.debug(f"<<< {raw_command.decode()}")
|
||||
|
||||
# Consume the response bytes.
|
||||
self.read_buffer = self.read_buffer[trailer + 1 :]
|
||||
# Consume the response bytes.
|
||||
self.read_buffer = self.read_buffer[trailer + 1 :]
|
||||
|
||||
if command.sub_code == AtCommand.SubCode.TEST:
|
||||
handler_name = f'_on_{command.code.lower()}_test'
|
||||
elif command.sub_code == AtCommand.SubCode.READ:
|
||||
handler_name = f'_on_{command.code.lower()}_read'
|
||||
else:
|
||||
handler_name = f'_on_{command.code.lower()}'
|
||||
if command.sub_code == AtCommand.SubCode.TEST:
|
||||
handler_name = f'_on_{command.code.lower()}_test'
|
||||
elif command.sub_code == AtCommand.SubCode.READ:
|
||||
handler_name = f'_on_{command.code.lower()}_read'
|
||||
else:
|
||||
handler_name = f'_on_{command.code.lower()}'
|
||||
|
||||
if handler := getattr(self, handler_name, None):
|
||||
handler(*command.parameters)
|
||||
else:
|
||||
logger.warning('Handler %s not found', handler_name)
|
||||
self.send_response('ERROR')
|
||||
if handler := getattr(self, handler_name, None):
|
||||
handler(*command.parameters)
|
||||
else:
|
||||
logger.warning('Handler %s not found', handler_name)
|
||||
self.send_response('ERROR')
|
||||
|
||||
def send_response(self, response: str) -> None:
|
||||
"""Sends an AT response."""
|
||||
|
||||
@@ -1106,6 +1106,18 @@ class Host(AbortableEventEmitter):
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_qos_setup_complete_event(self, event):
|
||||
if event.status == hci.HCI_SUCCESS:
|
||||
self.emit(
|
||||
'connection_qos_setup', event.connection_handle, event.service_type
|
||||
)
|
||||
else:
|
||||
self.emit(
|
||||
'connection_qos_setup_failure',
|
||||
event.connection_handle,
|
||||
event.status,
|
||||
)
|
||||
|
||||
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1911,6 +1911,7 @@ class ChannelManager:
|
||||
data = sum(1 << cid for cid in self.fixed_channels).to_bytes(8, 'little')
|
||||
else:
|
||||
result = L2CAP_Information_Response.NOT_SUPPORTED
|
||||
data = b''
|
||||
|
||||
self.send_control_frame(
|
||||
connection,
|
||||
|
||||
@@ -122,6 +122,8 @@ class LocalLink:
|
||||
elif transport == BT_BR_EDR_TRANSPORT:
|
||||
destination_controller = self.find_classic_controller(destination_address)
|
||||
source_address = sender_controller.public_address
|
||||
else:
|
||||
raise ValueError("unsupported transport type")
|
||||
|
||||
if destination_controller is not None:
|
||||
destination_controller.on_link_acl_data(source_address, transport, data)
|
||||
|
||||
@@ -102,6 +102,7 @@ class ContextType(enum.IntFlag):
|
||||
|
||||
# fmt: off
|
||||
PROHIBITED = 0x0000
|
||||
UNSPECIFIED = 0x0001
|
||||
CONVERSATIONAL = 0x0002
|
||||
MEDIA = 0x0004
|
||||
GAME = 0x0008
|
||||
@@ -350,6 +351,7 @@ class CodecSpecificCapabilities:
|
||||
supported_max_codec_frames_per_sdu = value
|
||||
|
||||
# It is expected here that if some fields are missing, an error should be raised.
|
||||
# pylint: disable=possibly-used-before-assignment,used-before-assignment
|
||||
return CodecSpecificCapabilities(
|
||||
supported_sampling_frequencies=supported_sampling_frequencies,
|
||||
supported_frame_durations=supported_frame_durations,
|
||||
@@ -426,6 +428,7 @@ class CodecSpecificConfiguration:
|
||||
codec_frames_per_sdu = value
|
||||
|
||||
# It is expected here that if some fields are missing, an error should be raised.
|
||||
# pylint: disable=possibly-used-before-assignment,used-before-assignment
|
||||
return CodecSpecificConfiguration(
|
||||
sampling_frequency=sampling_frequency,
|
||||
frame_duration=frame_duration,
|
||||
|
||||
@@ -25,7 +25,7 @@ from bumble.utils import AsyncRunner, OpenIntEnum
|
||||
from bumble.hci import Address
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Set, Union
|
||||
from typing import Any, Dict, List, Optional, Set, Union
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -271,24 +271,12 @@ class HearingAccessService(gatt.TemplateService):
|
||||
def on_disconnection(_reason) -> None:
|
||||
self.currently_connected_clients.remove(connection)
|
||||
|
||||
# TODO Should we filter on device bonded && device is HAP ?
|
||||
self.currently_connected_clients.add(connection)
|
||||
if (
|
||||
connection.peer_address
|
||||
not in self.preset_changed_operations_history_per_device
|
||||
):
|
||||
self.preset_changed_operations_history_per_device[
|
||||
connection.peer_address
|
||||
] = []
|
||||
return
|
||||
@connection.on('pairing') # type: ignore
|
||||
def on_pairing(*_: Any) -> None:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
|
||||
async def on_connection_async() -> None:
|
||||
# Send all the PresetChangedOperation that occur when not connected
|
||||
await self._preset_changed_operation(connection)
|
||||
# Update the active preset index if needed
|
||||
await self.notify_active_preset_for_connection(connection)
|
||||
|
||||
connection.abort_on('disconnection', on_connection_async())
|
||||
if connection.peer_resolvable_address:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
|
||||
self.hearing_aid_features_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC,
|
||||
@@ -325,6 +313,27 @@ class HearingAccessService(gatt.TemplateService):
|
||||
]
|
||||
)
|
||||
|
||||
def on_incoming_paired_connection(self, connection: Connection):
|
||||
'''Setup initial operations to handle a remote bonded HAP device'''
|
||||
# TODO Should we filter on HAP device only ?
|
||||
self.currently_connected_clients.add(connection)
|
||||
if (
|
||||
connection.peer_address
|
||||
not in self.preset_changed_operations_history_per_device
|
||||
):
|
||||
self.preset_changed_operations_history_per_device[
|
||||
connection.peer_address
|
||||
] = []
|
||||
return
|
||||
|
||||
async def on_connection_async() -> None:
|
||||
# Send all the PresetChangedOperation that occur when not connected
|
||||
await self._preset_changed_operation(connection)
|
||||
# Update the active preset index if needed
|
||||
await self.notify_active_preset_for_connection(connection)
|
||||
|
||||
connection.abort_on('disconnection', on_connection_async())
|
||||
|
||||
def _on_read_active_preset_index(
|
||||
self, __connection__: Optional[Connection]
|
||||
) -> bytes:
|
||||
|
||||
110
bumble/rtp.py
Normal file
110
bumble/rtp.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
import struct
|
||||
from typing import List
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class MediaPacket:
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes) -> MediaPacket:
|
||||
version = (data[0] >> 6) & 0x03
|
||||
padding = (data[0] >> 5) & 0x01
|
||||
extension = (data[0] >> 4) & 0x01
|
||||
csrc_count = data[0] & 0x0F
|
||||
marker = (data[1] >> 7) & 0x01
|
||||
payload_type = data[1] & 0x7F
|
||||
sequence_number = struct.unpack_from('>H', data, 2)[0]
|
||||
timestamp = struct.unpack_from('>I', data, 4)[0]
|
||||
ssrc = struct.unpack_from('>I', data, 8)[0]
|
||||
csrc_list = [
|
||||
struct.unpack_from('>I', data, 12 + i)[0] for i in range(csrc_count)
|
||||
]
|
||||
payload = data[12 + csrc_count * 4 :]
|
||||
|
||||
return MediaPacket(
|
||||
version,
|
||||
padding,
|
||||
extension,
|
||||
marker,
|
||||
sequence_number,
|
||||
timestamp,
|
||||
ssrc,
|
||||
csrc_list,
|
||||
payload_type,
|
||||
payload,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: int,
|
||||
padding: int,
|
||||
extension: int,
|
||||
marker: int,
|
||||
sequence_number: int,
|
||||
timestamp: int,
|
||||
ssrc: int,
|
||||
csrc_list: List[int],
|
||||
payload_type: int,
|
||||
payload: bytes,
|
||||
) -> None:
|
||||
self.version = version
|
||||
self.padding = padding
|
||||
self.extension = extension
|
||||
self.marker = marker
|
||||
self.sequence_number = sequence_number & 0xFFFF
|
||||
self.timestamp = timestamp & 0xFFFFFFFF
|
||||
self.timestamp_seconds = 0.0
|
||||
self.ssrc = ssrc
|
||||
self.csrc_list = csrc_list
|
||||
self.payload_type = payload_type
|
||||
self.payload = payload
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
header = bytes(
|
||||
[
|
||||
self.version << 6
|
||||
| self.padding << 5
|
||||
| self.extension << 4
|
||||
| len(self.csrc_list),
|
||||
self.marker << 7 | self.payload_type,
|
||||
]
|
||||
) + struct.pack(
|
||||
'>HII',
|
||||
self.sequence_number,
|
||||
self.timestamp,
|
||||
self.ssrc,
|
||||
)
|
||||
for csrc in self.csrc_list:
|
||||
header += struct.pack('>I', csrc)
|
||||
return header + self.payload
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'RTP(v={self.version},'
|
||||
f'p={self.padding},'
|
||||
f'x={self.extension},'
|
||||
f'm={self.marker},'
|
||||
f'pt={self.payload_type},'
|
||||
f'sn={self.sequence_number},'
|
||||
f'ts={self.timestamp},'
|
||||
f'ssrc={self.ssrc},'
|
||||
f'csrcs={self.csrc_list},'
|
||||
f'payload_size={len(self.payload)})'
|
||||
)
|
||||
@@ -434,6 +434,8 @@ class DataElement:
|
||||
if size != 1:
|
||||
raise InvalidArgumentError('boolean must be 1 byte')
|
||||
size_index = 0
|
||||
else:
|
||||
raise RuntimeError("internal error - self.type not supported")
|
||||
|
||||
self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data
|
||||
return self.bytes
|
||||
|
||||
@@ -1839,7 +1839,7 @@ class Session:
|
||||
if self.is_initiator:
|
||||
if self.pairing_method == PairingMethod.OOB:
|
||||
self.send_pairing_random_command()
|
||||
else:
|
||||
elif self.pairing_method == PairingMethod.PASSKEY:
|
||||
self.send_pairing_confirm_command()
|
||||
else:
|
||||
if self.pairing_method == PairingMethod.PASSKEY:
|
||||
|
||||
@@ -20,12 +20,14 @@ import atexit
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import sys
|
||||
from typing import Dict, Optional
|
||||
|
||||
import grpc.aio
|
||||
|
||||
from .common import (
|
||||
import bumble
|
||||
from bumble.transport.common import (
|
||||
ParserSource,
|
||||
PumpedTransport,
|
||||
PumpedPacketSource,
|
||||
@@ -36,15 +38,15 @@ from .common import (
|
||||
)
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from .grpc_protobuf.packet_streamer_pb2_grpc import (
|
||||
from .grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
|
||||
PacketStreamerStub,
|
||||
PacketStreamerServicer,
|
||||
add_PacketStreamerServicer_to_server,
|
||||
)
|
||||
from .grpc_protobuf.packet_streamer_pb2 import PacketRequest, PacketResponse
|
||||
from .grpc_protobuf.hci_packet_pb2 import HCIPacket
|
||||
from .grpc_protobuf.startup_pb2 import Chip, ChipInfo
|
||||
from .grpc_protobuf.common_pb2 import ChipKind
|
||||
from .grpc_protobuf.netsim.packet_streamer_pb2 import PacketRequest, PacketResponse
|
||||
from .grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
|
||||
from .grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
|
||||
from .grpc_protobuf.netsim.common_pb2 import ChipKind
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -58,6 +60,7 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
DEFAULT_NAME = 'bumble0'
|
||||
DEFAULT_MANUFACTURER = 'Bumble'
|
||||
DEFAULT_VARIANT = ''
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -199,7 +202,6 @@ async def open_android_netsim_controller_transport(
|
||||
data = (
|
||||
bytes([request.hci_packet.packet_type]) + request.hci_packet.packet
|
||||
)
|
||||
logger.debug(f'<<< PACKET: {data.hex()}')
|
||||
self.on_data_received(data)
|
||||
|
||||
async def send_packet(self, data):
|
||||
@@ -253,7 +255,7 @@ async def open_android_netsim_controller_transport(
|
||||
|
||||
# Check that we don't already have a device
|
||||
if self.device:
|
||||
logger.debug('busy, already serving a device')
|
||||
logger.debug('Busy, already serving a device')
|
||||
return PacketResponse(error='Busy')
|
||||
|
||||
# Instantiate a new device
|
||||
@@ -312,16 +314,24 @@ async def open_android_netsim_host_transport_with_channel(
|
||||
):
|
||||
# Wrapper for I/O operations
|
||||
class HciDevice:
|
||||
def __init__(self, name, manufacturer, hci_device):
|
||||
def __init__(self, name, variant, manufacturer, hci_device):
|
||||
self.name = name
|
||||
self.variant = variant
|
||||
self.manufacturer = manufacturer
|
||||
self.hci_device = hci_device
|
||||
|
||||
async def start(self): # Send the startup info
|
||||
chip_info = ChipInfo(
|
||||
device_info = DeviceInfo(
|
||||
name=self.name,
|
||||
chip=Chip(kind=ChipKind.BLUETOOTH, manufacturer=self.manufacturer),
|
||||
kind='BUMBLE',
|
||||
version=bumble.__version__,
|
||||
sdk_version=platform.python_version(),
|
||||
build_id=platform.platform(),
|
||||
arch=platform.machine(),
|
||||
variant=self.variant,
|
||||
)
|
||||
chip = Chip(kind=ChipKind.BLUETOOTH, manufacturer=self.manufacturer)
|
||||
chip_info = ChipInfo(name=self.name, chip=chip, device_info=device_info)
|
||||
logger.debug(f'Sending chip info to netsim: {chip_info}')
|
||||
await self.hci_device.write(PacketRequest(initial_info=chip_info))
|
||||
|
||||
@@ -349,12 +359,16 @@ async def open_android_netsim_host_transport_with_channel(
|
||||
)
|
||||
|
||||
name = DEFAULT_NAME if options is None else options.get('name', DEFAULT_NAME)
|
||||
variant = (
|
||||
DEFAULT_VARIANT if options is None else options.get('variant', DEFAULT_VARIANT)
|
||||
)
|
||||
manufacturer = DEFAULT_MANUFACTURER
|
||||
|
||||
# Connect as a host
|
||||
service = PacketStreamerStub(channel)
|
||||
hci_device = HciDevice(
|
||||
name=name,
|
||||
variant=variant,
|
||||
manufacturer=manufacturer,
|
||||
hci_device=service.StreamPackets(),
|
||||
)
|
||||
@@ -404,6 +418,9 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
|
||||
The "chip" name, used to identify the "chip" instance. This
|
||||
may be useful when several clients are connected, since each needs to use a
|
||||
different name.
|
||||
variant=<variant>
|
||||
The device info variant field, which may be used to convey a device or
|
||||
application type (ex: "virtual-speaker", or "keyboard")
|
||||
|
||||
In `controller` mode:
|
||||
The <host>:<port> part is required. <host> may be the address of a local network
|
||||
|
||||
@@ -370,11 +370,13 @@ class PumpedPacketSource(ParserSource):
|
||||
self.parser.feed_data(packet)
|
||||
except asyncio.CancelledError:
|
||||
logger.debug('source pump task done')
|
||||
self.terminated.set_result(None)
|
||||
if not self.terminated.done():
|
||||
self.terminated.set_result(None)
|
||||
break
|
||||
except Exception as error:
|
||||
logger.warning(f'exception while waiting for packet: {error}')
|
||||
self.terminated.set_exception(error)
|
||||
if not self.terminated.done():
|
||||
self.terminated.set_exception(error)
|
||||
break
|
||||
|
||||
self.pump_task = asyncio.create_task(pump_packets())
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: hci_packet.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10hci_packet.proto\x12\rnetsim.packet\"\xb2\x01\n\tHCIPacket\x12\x38\n\x0bpacket_type\x18\x01 \x01(\x0e\x32#.netsim.packet.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"[\n\nPacketType\x12\x1a\n\x16HCI_PACKET_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x43OMMAND\x10\x01\x12\x07\n\x03\x41\x43L\x10\x02\x12\x07\n\x03SCO\x10\x03\x12\t\n\x05\x45VENT\x10\x04\x12\x07\n\x03ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hci_packet_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
||||
_HCIPACKET._serialized_start=36
|
||||
_HCIPACKET._serialized_end=214
|
||||
_HCIPACKET_PACKETTYPE._serialized_start=123
|
||||
_HCIPACKET_PACKETTYPE._serialized_end=214
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
0
bumble/transport/grpc_protobuf/netsim/__init__.py
Normal file
0
bumble/transport/grpc_protobuf/netsim/__init__.py
Normal file
@@ -1,11 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: common.proto
|
||||
# source: netsim/common.proto
|
||||
# Protobuf Python Version: 4.25.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
@@ -13,13 +14,13 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63ommon.proto\x12\rnetsim.common*=\n\x08\x43hipKind\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\r\n\tBLUETOOTH\x10\x01\x12\x08\n\x04WIFI\x10\x02\x12\x07\n\x03UWB\x10\x03\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13netsim/common.proto\x12\rnetsim.common*S\n\x08\x43hipKind\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\r\n\tBLUETOOTH\x10\x01\x12\x08\n\x04WIFI\x10\x02\x12\x07\n\x03UWB\x10\x03\x12\x14\n\x10\x42LUETOOTH_BEACON\x10\x04\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'common_pb2', globals())
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'netsim.common_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_CHIPKIND._serialized_start=31
|
||||
_CHIPKIND._serialized_end=92
|
||||
_globals['_CHIPKIND']._serialized_start=38
|
||||
_globals['_CHIPKIND']._serialized_end=121
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -2,11 +2,17 @@ from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from typing import ClassVar as _ClassVar
|
||||
|
||||
BLUETOOTH: ChipKind
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
UNSPECIFIED: ChipKind
|
||||
UWB: ChipKind
|
||||
WIFI: ChipKind
|
||||
|
||||
class ChipKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
__slots__ = ()
|
||||
UNSPECIFIED: _ClassVar[ChipKind]
|
||||
BLUETOOTH: _ClassVar[ChipKind]
|
||||
WIFI: _ClassVar[ChipKind]
|
||||
UWB: _ClassVar[ChipKind]
|
||||
BLUETOOTH_BEACON: _ClassVar[ChipKind]
|
||||
UNSPECIFIED: ChipKind
|
||||
BLUETOOTH: ChipKind
|
||||
WIFI: ChipKind
|
||||
UWB: ChipKind
|
||||
BLUETOOTH_BEACON: ChipKind
|
||||
29
bumble/transport/grpc_protobuf/netsim/hci_packet_pb2.py
Normal file
29
bumble/transport/grpc_protobuf/netsim/hci_packet_pb2.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: netsim/hci_packet.proto
|
||||
# Protobuf Python Version: 4.25.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17netsim/hci_packet.proto\x12\rnetsim.packet\"\xb2\x01\n\tHCIPacket\x12\x38\n\x0bpacket_type\x18\x01 \x01(\x0e\x32#.netsim.packet.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"[\n\nPacketType\x12\x1a\n\x16HCI_PACKET_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x43OMMAND\x10\x01\x12\x07\n\x03\x41\x43L\x10\x02\x12\x07\n\x03SCO\x10\x03\x12\t\n\x05\x45VENT\x10\x04\x12\x07\n\x03ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'netsim.hci_packet_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
_globals['DESCRIPTOR']._options = None
|
||||
_globals['DESCRIPTOR']._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth'
|
||||
_globals['_HCIPACKET']._serialized_start=43
|
||||
_globals['_HCIPACKET']._serialized_end=221
|
||||
_globals['_HCIPACKET_PACKETTYPE']._serialized_start=130
|
||||
_globals['_HCIPACKET_PACKETTYPE']._serialized_end=221
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -6,17 +6,23 @@ from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class HCIPacket(_message.Message):
|
||||
__slots__ = ["packet", "packet_type"]
|
||||
__slots__ = ("packet_type", "packet")
|
||||
class PacketType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = []
|
||||
ACL: HCIPacket.PacketType
|
||||
COMMAND: HCIPacket.PacketType
|
||||
EVENT: HCIPacket.PacketType
|
||||
__slots__ = ()
|
||||
HCI_PACKET_UNSPECIFIED: _ClassVar[HCIPacket.PacketType]
|
||||
COMMAND: _ClassVar[HCIPacket.PacketType]
|
||||
ACL: _ClassVar[HCIPacket.PacketType]
|
||||
SCO: _ClassVar[HCIPacket.PacketType]
|
||||
EVENT: _ClassVar[HCIPacket.PacketType]
|
||||
ISO: _ClassVar[HCIPacket.PacketType]
|
||||
HCI_PACKET_UNSPECIFIED: HCIPacket.PacketType
|
||||
ISO: HCIPacket.PacketType
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKET_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
COMMAND: HCIPacket.PacketType
|
||||
ACL: HCIPacket.PacketType
|
||||
SCO: HCIPacket.PacketType
|
||||
packet: bytes
|
||||
EVENT: HCIPacket.PacketType
|
||||
ISO: HCIPacket.PacketType
|
||||
PACKET_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
packet_type: HCIPacket.PacketType
|
||||
packet: bytes
|
||||
def __init__(self, packet_type: _Optional[_Union[HCIPacket.PacketType, str]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||
63
bumble/transport/grpc_protobuf/netsim/model_pb2.py
Normal file
63
bumble/transport/grpc_protobuf/netsim/model_pb2.py
Normal file
File diff suppressed because one or more lines are too long
238
bumble/transport/grpc_protobuf/netsim/model_pb2.pyi
Normal file
238
bumble/transport/grpc_protobuf/netsim/model_pb2.pyi
Normal file
@@ -0,0 +1,238 @@
|
||||
from bumble.transport.grpc_protobuf.netsim import common_pb2 as _common_pb2
|
||||
from google.protobuf import timestamp_pb2 as _timestamp_pb2
|
||||
from bumble.transport.grpc_protobuf.rootcanal import configuration_pb2 as _configuration_pb2
|
||||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class PhyKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
NONE: _ClassVar[PhyKind]
|
||||
BLUETOOTH_CLASSIC: _ClassVar[PhyKind]
|
||||
BLUETOOTH_LOW_ENERGY: _ClassVar[PhyKind]
|
||||
WIFI: _ClassVar[PhyKind]
|
||||
UWB: _ClassVar[PhyKind]
|
||||
WIFI_RTT: _ClassVar[PhyKind]
|
||||
NONE: PhyKind
|
||||
BLUETOOTH_CLASSIC: PhyKind
|
||||
BLUETOOTH_LOW_ENERGY: PhyKind
|
||||
WIFI: PhyKind
|
||||
UWB: PhyKind
|
||||
WIFI_RTT: PhyKind
|
||||
|
||||
class Position(_message.Message):
|
||||
__slots__ = ("x", "y", "z")
|
||||
X_FIELD_NUMBER: _ClassVar[int]
|
||||
Y_FIELD_NUMBER: _ClassVar[int]
|
||||
Z_FIELD_NUMBER: _ClassVar[int]
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ..., z: _Optional[float] = ...) -> None: ...
|
||||
|
||||
class Orientation(_message.Message):
|
||||
__slots__ = ("yaw", "pitch", "roll")
|
||||
YAW_FIELD_NUMBER: _ClassVar[int]
|
||||
PITCH_FIELD_NUMBER: _ClassVar[int]
|
||||
ROLL_FIELD_NUMBER: _ClassVar[int]
|
||||
yaw: float
|
||||
pitch: float
|
||||
roll: float
|
||||
def __init__(self, yaw: _Optional[float] = ..., pitch: _Optional[float] = ..., roll: _Optional[float] = ...) -> None: ...
|
||||
|
||||
class Chip(_message.Message):
|
||||
__slots__ = ("kind", "id", "name", "manufacturer", "product_name", "bt", "ble_beacon", "uwb", "wifi", "offset")
|
||||
class Radio(_message.Message):
|
||||
__slots__ = ("state", "range", "tx_count", "rx_count")
|
||||
STATE_FIELD_NUMBER: _ClassVar[int]
|
||||
RANGE_FIELD_NUMBER: _ClassVar[int]
|
||||
TX_COUNT_FIELD_NUMBER: _ClassVar[int]
|
||||
RX_COUNT_FIELD_NUMBER: _ClassVar[int]
|
||||
state: bool
|
||||
range: float
|
||||
tx_count: int
|
||||
rx_count: int
|
||||
def __init__(self, state: bool = ..., range: _Optional[float] = ..., tx_count: _Optional[int] = ..., rx_count: _Optional[int] = ...) -> None: ...
|
||||
class Bluetooth(_message.Message):
|
||||
__slots__ = ("low_energy", "classic", "address", "bt_properties")
|
||||
LOW_ENERGY_FIELD_NUMBER: _ClassVar[int]
|
||||
CLASSIC_FIELD_NUMBER: _ClassVar[int]
|
||||
ADDRESS_FIELD_NUMBER: _ClassVar[int]
|
||||
BT_PROPERTIES_FIELD_NUMBER: _ClassVar[int]
|
||||
low_energy: Chip.Radio
|
||||
classic: Chip.Radio
|
||||
address: str
|
||||
bt_properties: _configuration_pb2.Controller
|
||||
def __init__(self, low_energy: _Optional[_Union[Chip.Radio, _Mapping]] = ..., classic: _Optional[_Union[Chip.Radio, _Mapping]] = ..., address: _Optional[str] = ..., bt_properties: _Optional[_Union[_configuration_pb2.Controller, _Mapping]] = ...) -> None: ...
|
||||
class BleBeacon(_message.Message):
|
||||
__slots__ = ("bt", "address", "settings", "adv_data", "scan_response")
|
||||
class AdvertiseSettings(_message.Message):
|
||||
__slots__ = ("advertise_mode", "milliseconds", "tx_power_level", "dbm", "scannable", "timeout")
|
||||
class AdvertiseMode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
LOW_POWER: _ClassVar[Chip.BleBeacon.AdvertiseSettings.AdvertiseMode]
|
||||
BALANCED: _ClassVar[Chip.BleBeacon.AdvertiseSettings.AdvertiseMode]
|
||||
LOW_LATENCY: _ClassVar[Chip.BleBeacon.AdvertiseSettings.AdvertiseMode]
|
||||
LOW_POWER: Chip.BleBeacon.AdvertiseSettings.AdvertiseMode
|
||||
BALANCED: Chip.BleBeacon.AdvertiseSettings.AdvertiseMode
|
||||
LOW_LATENCY: Chip.BleBeacon.AdvertiseSettings.AdvertiseMode
|
||||
class AdvertiseTxPower(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
ULTRA_LOW: _ClassVar[Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower]
|
||||
LOW: _ClassVar[Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower]
|
||||
MEDIUM: _ClassVar[Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower]
|
||||
HIGH: _ClassVar[Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower]
|
||||
ULTRA_LOW: Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower
|
||||
LOW: Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower
|
||||
MEDIUM: Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower
|
||||
HIGH: Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower
|
||||
ADVERTISE_MODE_FIELD_NUMBER: _ClassVar[int]
|
||||
MILLISECONDS_FIELD_NUMBER: _ClassVar[int]
|
||||
TX_POWER_LEVEL_FIELD_NUMBER: _ClassVar[int]
|
||||
DBM_FIELD_NUMBER: _ClassVar[int]
|
||||
SCANNABLE_FIELD_NUMBER: _ClassVar[int]
|
||||
TIMEOUT_FIELD_NUMBER: _ClassVar[int]
|
||||
advertise_mode: Chip.BleBeacon.AdvertiseSettings.AdvertiseMode
|
||||
milliseconds: int
|
||||
tx_power_level: Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower
|
||||
dbm: int
|
||||
scannable: bool
|
||||
timeout: int
|
||||
def __init__(self, advertise_mode: _Optional[_Union[Chip.BleBeacon.AdvertiseSettings.AdvertiseMode, str]] = ..., milliseconds: _Optional[int] = ..., tx_power_level: _Optional[_Union[Chip.BleBeacon.AdvertiseSettings.AdvertiseTxPower, str]] = ..., dbm: _Optional[int] = ..., scannable: bool = ..., timeout: _Optional[int] = ...) -> None: ...
|
||||
class AdvertiseData(_message.Message):
|
||||
__slots__ = ("include_device_name", "include_tx_power_level", "manufacturer_data", "services")
|
||||
class Service(_message.Message):
|
||||
__slots__ = ("uuid", "data")
|
||||
UUID_FIELD_NUMBER: _ClassVar[int]
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
uuid: str
|
||||
data: bytes
|
||||
def __init__(self, uuid: _Optional[str] = ..., data: _Optional[bytes] = ...) -> None: ...
|
||||
INCLUDE_DEVICE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
INCLUDE_TX_POWER_LEVEL_FIELD_NUMBER: _ClassVar[int]
|
||||
MANUFACTURER_DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
SERVICES_FIELD_NUMBER: _ClassVar[int]
|
||||
include_device_name: bool
|
||||
include_tx_power_level: bool
|
||||
manufacturer_data: bytes
|
||||
services: _containers.RepeatedCompositeFieldContainer[Chip.BleBeacon.AdvertiseData.Service]
|
||||
def __init__(self, include_device_name: bool = ..., include_tx_power_level: bool = ..., manufacturer_data: _Optional[bytes] = ..., services: _Optional[_Iterable[_Union[Chip.BleBeacon.AdvertiseData.Service, _Mapping]]] = ...) -> None: ...
|
||||
BT_FIELD_NUMBER: _ClassVar[int]
|
||||
ADDRESS_FIELD_NUMBER: _ClassVar[int]
|
||||
SETTINGS_FIELD_NUMBER: _ClassVar[int]
|
||||
ADV_DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
SCAN_RESPONSE_FIELD_NUMBER: _ClassVar[int]
|
||||
bt: Chip.Bluetooth
|
||||
address: str
|
||||
settings: Chip.BleBeacon.AdvertiseSettings
|
||||
adv_data: Chip.BleBeacon.AdvertiseData
|
||||
scan_response: Chip.BleBeacon.AdvertiseData
|
||||
def __init__(self, bt: _Optional[_Union[Chip.Bluetooth, _Mapping]] = ..., address: _Optional[str] = ..., settings: _Optional[_Union[Chip.BleBeacon.AdvertiseSettings, _Mapping]] = ..., adv_data: _Optional[_Union[Chip.BleBeacon.AdvertiseData, _Mapping]] = ..., scan_response: _Optional[_Union[Chip.BleBeacon.AdvertiseData, _Mapping]] = ...) -> None: ...
|
||||
KIND_FIELD_NUMBER: _ClassVar[int]
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
MANUFACTURER_FIELD_NUMBER: _ClassVar[int]
|
||||
PRODUCT_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
BT_FIELD_NUMBER: _ClassVar[int]
|
||||
BLE_BEACON_FIELD_NUMBER: _ClassVar[int]
|
||||
UWB_FIELD_NUMBER: _ClassVar[int]
|
||||
WIFI_FIELD_NUMBER: _ClassVar[int]
|
||||
OFFSET_FIELD_NUMBER: _ClassVar[int]
|
||||
kind: _common_pb2.ChipKind
|
||||
id: int
|
||||
name: str
|
||||
manufacturer: str
|
||||
product_name: str
|
||||
bt: Chip.Bluetooth
|
||||
ble_beacon: Chip.BleBeacon
|
||||
uwb: Chip.Radio
|
||||
wifi: Chip.Radio
|
||||
offset: Position
|
||||
def __init__(self, kind: _Optional[_Union[_common_pb2.ChipKind, str]] = ..., id: _Optional[int] = ..., name: _Optional[str] = ..., manufacturer: _Optional[str] = ..., product_name: _Optional[str] = ..., bt: _Optional[_Union[Chip.Bluetooth, _Mapping]] = ..., ble_beacon: _Optional[_Union[Chip.BleBeacon, _Mapping]] = ..., uwb: _Optional[_Union[Chip.Radio, _Mapping]] = ..., wifi: _Optional[_Union[Chip.Radio, _Mapping]] = ..., offset: _Optional[_Union[Position, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class ChipCreate(_message.Message):
|
||||
__slots__ = ("kind", "address", "name", "manufacturer", "product_name", "ble_beacon", "bt_properties")
|
||||
class BleBeaconCreate(_message.Message):
|
||||
__slots__ = ("address", "settings", "adv_data", "scan_response")
|
||||
ADDRESS_FIELD_NUMBER: _ClassVar[int]
|
||||
SETTINGS_FIELD_NUMBER: _ClassVar[int]
|
||||
ADV_DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
SCAN_RESPONSE_FIELD_NUMBER: _ClassVar[int]
|
||||
address: str
|
||||
settings: Chip.BleBeacon.AdvertiseSettings
|
||||
adv_data: Chip.BleBeacon.AdvertiseData
|
||||
scan_response: Chip.BleBeacon.AdvertiseData
|
||||
def __init__(self, address: _Optional[str] = ..., settings: _Optional[_Union[Chip.BleBeacon.AdvertiseSettings, _Mapping]] = ..., adv_data: _Optional[_Union[Chip.BleBeacon.AdvertiseData, _Mapping]] = ..., scan_response: _Optional[_Union[Chip.BleBeacon.AdvertiseData, _Mapping]] = ...) -> None: ...
|
||||
KIND_FIELD_NUMBER: _ClassVar[int]
|
||||
ADDRESS_FIELD_NUMBER: _ClassVar[int]
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
MANUFACTURER_FIELD_NUMBER: _ClassVar[int]
|
||||
PRODUCT_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
BLE_BEACON_FIELD_NUMBER: _ClassVar[int]
|
||||
BT_PROPERTIES_FIELD_NUMBER: _ClassVar[int]
|
||||
kind: _common_pb2.ChipKind
|
||||
address: str
|
||||
name: str
|
||||
manufacturer: str
|
||||
product_name: str
|
||||
ble_beacon: ChipCreate.BleBeaconCreate
|
||||
bt_properties: _configuration_pb2.Controller
|
||||
def __init__(self, kind: _Optional[_Union[_common_pb2.ChipKind, str]] = ..., address: _Optional[str] = ..., name: _Optional[str] = ..., manufacturer: _Optional[str] = ..., product_name: _Optional[str] = ..., ble_beacon: _Optional[_Union[ChipCreate.BleBeaconCreate, _Mapping]] = ..., bt_properties: _Optional[_Union[_configuration_pb2.Controller, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class Device(_message.Message):
|
||||
__slots__ = ("id", "name", "visible", "position", "orientation", "chips")
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
VISIBLE_FIELD_NUMBER: _ClassVar[int]
|
||||
POSITION_FIELD_NUMBER: _ClassVar[int]
|
||||
ORIENTATION_FIELD_NUMBER: _ClassVar[int]
|
||||
CHIPS_FIELD_NUMBER: _ClassVar[int]
|
||||
id: int
|
||||
name: str
|
||||
visible: bool
|
||||
position: Position
|
||||
orientation: Orientation
|
||||
chips: _containers.RepeatedCompositeFieldContainer[Chip]
|
||||
def __init__(self, id: _Optional[int] = ..., name: _Optional[str] = ..., visible: bool = ..., position: _Optional[_Union[Position, _Mapping]] = ..., orientation: _Optional[_Union[Orientation, _Mapping]] = ..., chips: _Optional[_Iterable[_Union[Chip, _Mapping]]] = ...) -> None: ...
|
||||
|
||||
class DeviceCreate(_message.Message):
|
||||
__slots__ = ("name", "position", "orientation", "chips")
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
POSITION_FIELD_NUMBER: _ClassVar[int]
|
||||
ORIENTATION_FIELD_NUMBER: _ClassVar[int]
|
||||
CHIPS_FIELD_NUMBER: _ClassVar[int]
|
||||
name: str
|
||||
position: Position
|
||||
orientation: Orientation
|
||||
chips: _containers.RepeatedCompositeFieldContainer[ChipCreate]
|
||||
def __init__(self, name: _Optional[str] = ..., position: _Optional[_Union[Position, _Mapping]] = ..., orientation: _Optional[_Union[Orientation, _Mapping]] = ..., chips: _Optional[_Iterable[_Union[ChipCreate, _Mapping]]] = ...) -> None: ...
|
||||
|
||||
class Scene(_message.Message):
|
||||
__slots__ = ("devices",)
|
||||
DEVICES_FIELD_NUMBER: _ClassVar[int]
|
||||
devices: _containers.RepeatedCompositeFieldContainer[Device]
|
||||
def __init__(self, devices: _Optional[_Iterable[_Union[Device, _Mapping]]] = ...) -> None: ...
|
||||
|
||||
class Capture(_message.Message):
|
||||
__slots__ = ("id", "chip_kind", "device_name", "state", "size", "records", "timestamp", "valid")
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
CHIP_KIND_FIELD_NUMBER: _ClassVar[int]
|
||||
DEVICE_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
STATE_FIELD_NUMBER: _ClassVar[int]
|
||||
SIZE_FIELD_NUMBER: _ClassVar[int]
|
||||
RECORDS_FIELD_NUMBER: _ClassVar[int]
|
||||
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
|
||||
VALID_FIELD_NUMBER: _ClassVar[int]
|
||||
id: int
|
||||
chip_kind: _common_pb2.ChipKind
|
||||
device_name: str
|
||||
state: bool
|
||||
size: int
|
||||
records: int
|
||||
timestamp: _timestamp_pb2.Timestamp
|
||||
valid: bool
|
||||
def __init__(self, id: _Optional[int] = ..., chip_kind: _Optional[_Union[_common_pb2.ChipKind, str]] = ..., device_name: _Optional[str] = ..., state: bool = ..., size: _Optional[int] = ..., records: _Optional[int] = ..., timestamp: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., valid: bool = ...) -> None: ...
|
||||
32
bumble/transport/grpc_protobuf/netsim/packet_streamer_pb2.py
Normal file
32
bumble/transport/grpc_protobuf/netsim/packet_streamer_pb2.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: netsim/packet_streamer.proto
|
||||
# Protobuf Python Version: 4.25.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from bumble.transport.grpc_protobuf.netsim import hci_packet_pb2 as netsim_dot_hci__packet__pb2
|
||||
from bumble.transport.grpc_protobuf.netsim import startup_pb2 as netsim_dot_startup__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cnetsim/packet_streamer.proto\x12\rnetsim.packet\x1a\x17netsim/hci_packet.proto\x1a\x14netsim/startup.proto\"\x93\x01\n\rPacketRequest\x12\x30\n\x0cinitial_info\x18\x01 \x01(\x0b\x32\x18.netsim.startup.ChipInfoH\x00\x12.\n\nhci_packet\x18\x02 \x01(\x0b\x32\x18.netsim.packet.HCIPacketH\x00\x12\x10\n\x06packet\x18\x03 \x01(\x0cH\x00\x42\x0e\n\x0crequest_type\"t\n\x0ePacketResponse\x12\x0f\n\x05\x65rror\x18\x01 \x01(\tH\x00\x12.\n\nhci_packet\x18\x02 \x01(\x0b\x32\x18.netsim.packet.HCIPacketH\x00\x12\x10\n\x06packet\x18\x03 \x01(\x0cH\x00\x42\x0f\n\rresponse_type2b\n\x0ePacketStreamer\x12P\n\rStreamPackets\x12\x1c.netsim.packet.PacketRequest\x1a\x1d.netsim.packet.PacketResponse(\x01\x30\x01\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'netsim.packet_streamer_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
_globals['_PACKETREQUEST']._serialized_start=95
|
||||
_globals['_PACKETREQUEST']._serialized_end=242
|
||||
_globals['_PACKETRESPONSE']._serialized_start=244
|
||||
_globals['_PACKETRESPONSE']._serialized_end=360
|
||||
_globals['_PACKETSTREAMER']._serialized_start=362
|
||||
_globals['_PACKETSTREAMER']._serialized_end=460
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -1,5 +1,5 @@
|
||||
from . import hci_packet_pb2 as _hci_packet_pb2
|
||||
from . import startup_pb2 as _startup_pb2
|
||||
from bumble.transport.grpc_protobuf.netsim import hci_packet_pb2 as _hci_packet_pb2
|
||||
from bumble.transport.grpc_protobuf.netsim import startup_pb2 as _startup_pb2
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
@@ -7,17 +7,17 @@ from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Opti
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class PacketRequest(_message.Message):
|
||||
__slots__ = ["hci_packet", "initial_info", "packet"]
|
||||
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
__slots__ = ("initial_info", "hci_packet", "packet")
|
||||
INITIAL_INFO_FIELD_NUMBER: _ClassVar[int]
|
||||
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
hci_packet: _hci_packet_pb2.HCIPacket
|
||||
initial_info: _startup_pb2.ChipInfo
|
||||
hci_packet: _hci_packet_pb2.HCIPacket
|
||||
packet: bytes
|
||||
def __init__(self, initial_info: _Optional[_Union[_startup_pb2.ChipInfo, _Mapping]] = ..., hci_packet: _Optional[_Union[_hci_packet_pb2.HCIPacket, _Mapping]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||
|
||||
class PacketResponse(_message.Message):
|
||||
__slots__ = ["error", "hci_packet", "packet"]
|
||||
__slots__ = ("error", "hci_packet", "packet")
|
||||
ERROR_FIELD_NUMBER: _ClassVar[int]
|
||||
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
from . import packet_streamer_pb2 as packet__streamer__pb2
|
||||
from bumble.transport.grpc_protobuf.netsim import packet_streamer_pb2 as netsim_dot_packet__streamer__pb2
|
||||
|
||||
|
||||
class PacketStreamerStub(object):
|
||||
@@ -30,8 +30,8 @@ class PacketStreamerStub(object):
|
||||
"""
|
||||
self.StreamPackets = channel.stream_stream(
|
||||
'/netsim.packet.PacketStreamer/StreamPackets',
|
||||
request_serializer=packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||
response_deserializer=packet__streamer__pb2.PacketResponse.FromString,
|
||||
request_serializer=netsim_dot_packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||
response_deserializer=netsim_dot_packet__streamer__pb2.PacketResponse.FromString,
|
||||
)
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ def add_PacketStreamerServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'StreamPackets': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.StreamPackets,
|
||||
request_deserializer=packet__streamer__pb2.PacketRequest.FromString,
|
||||
response_serializer=packet__streamer__pb2.PacketResponse.SerializeToString,
|
||||
request_deserializer=netsim_dot_packet__streamer__pb2.PacketRequest.FromString,
|
||||
response_serializer=netsim_dot_packet__streamer__pb2.PacketResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
@@ -103,7 +103,7 @@ class PacketStreamer(object):
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(request_iterator, target, '/netsim.packet.PacketStreamer/StreamPackets',
|
||||
packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||
packet__streamer__pb2.PacketResponse.FromString,
|
||||
netsim_dot_packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||
netsim_dot_packet__streamer__pb2.PacketResponse.FromString,
|
||||
options, channel_credentials,
|
||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
41
bumble/transport/grpc_protobuf/netsim/startup_pb2.py
Normal file
41
bumble/transport/grpc_protobuf/netsim/startup_pb2.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: netsim/startup.proto
|
||||
# Protobuf Python Version: 4.25.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from bumble.transport.grpc_protobuf.netsim import common_pb2 as netsim_dot_common__pb2
|
||||
from bumble.transport.grpc_protobuf.netsim import model_pb2 as netsim_dot_model__pb2
|
||||
from bumble.transport.grpc_protobuf.rootcanal import configuration_pb2 as rootcanal_dot_configuration__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14netsim/startup.proto\x12\x0enetsim.startup\x1a\x13netsim/common.proto\x1a\x12netsim/model.proto\x1a\x1drootcanal/configuration.proto\"\xb4\x01\n\x0bStartupInfo\x12\x33\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\".netsim.startup.StartupInfo.Device\x1ap\n\x06\x44\x65vice\x12\x10\n\x04name\x18\x01 \x01(\tB\x02\x18\x01\x12#\n\x05\x63hips\x18\x02 \x03(\x0b\x32\x14.netsim.startup.Chip\x12/\n\x0b\x64\x65vice_info\x18\x03 \x01(\x0b\x32\x1a.netsim.startup.DeviceInfo\"q\n\x08\x43hipInfo\x12\x10\n\x04name\x18\x01 \x01(\tB\x02\x18\x01\x12\"\n\x04\x63hip\x18\x02 \x01(\x0b\x32\x14.netsim.startup.Chip\x12/\n\x0b\x64\x65vice_info\x18\x03 \x01(\x0b\x32\x1a.netsim.startup.DeviceInfo\"\x7f\n\nDeviceInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x13\n\x0bsdk_version\x18\x04 \x01(\t\x12\x10\n\x08\x62uild_id\x18\x05 \x01(\t\x12\x0f\n\x07variant\x18\x06 \x01(\t\x12\x0c\n\x04\x61rch\x18\x07 \x01(\t\"\x9b\x02\n\x04\x43hip\x12%\n\x04kind\x18\x01 \x01(\x0e\x32\x17.netsim.common.ChipKind\x12\n\n\x02id\x18\x02 \x01(\t\x12\x14\n\x0cmanufacturer\x18\x03 \x01(\t\x12\x14\n\x0cproduct_name\x18\x04 \x01(\t\x12\r\n\x05\x66\x64_in\x18\x05 \x01(\x05\x12\x0e\n\x06\x66\x64_out\x18\x06 \x01(\x05\x12\x10\n\x08loopback\x18\x07 \x01(\x08\x12:\n\rbt_properties\x18\x08 \x01(\x0b\x32#.rootcanal.configuration.Controller\x12\x0f\n\x07\x61\x64\x64ress\x18\t \x01(\t\x12+\n\x06offset\x18\n \x01(\x0b\x32\x16.netsim.model.PositionH\x00\x88\x01\x01\x42\t\n\x07_offsetb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'netsim.startup_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
_globals['_STARTUPINFO_DEVICE'].fields_by_name['name']._options = None
|
||||
_globals['_STARTUPINFO_DEVICE'].fields_by_name['name']._serialized_options = b'\030\001'
|
||||
_globals['_CHIPINFO'].fields_by_name['name']._options = None
|
||||
_globals['_CHIPINFO'].fields_by_name['name']._serialized_options = b'\030\001'
|
||||
_globals['_STARTUPINFO']._serialized_start=113
|
||||
_globals['_STARTUPINFO']._serialized_end=293
|
||||
_globals['_STARTUPINFO_DEVICE']._serialized_start=181
|
||||
_globals['_STARTUPINFO_DEVICE']._serialized_end=293
|
||||
_globals['_CHIPINFO']._serialized_start=295
|
||||
_globals['_CHIPINFO']._serialized_end=408
|
||||
_globals['_DEVICEINFO']._serialized_start=410
|
||||
_globals['_DEVICEINFO']._serialized_end=537
|
||||
_globals['_CHIP']._serialized_start=540
|
||||
_globals['_CHIP']._serialized_end=823
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
76
bumble/transport/grpc_protobuf/netsim/startup_pb2.pyi
Normal file
76
bumble/transport/grpc_protobuf/netsim/startup_pb2.pyi
Normal file
@@ -0,0 +1,76 @@
|
||||
from bumble.transport.grpc_protobuf.netsim import common_pb2 as _common_pb2
|
||||
from bumble.transport.grpc_protobuf.netsim import model_pb2 as _model_pb2
|
||||
from bumble.transport.grpc_protobuf.rootcanal import configuration_pb2 as _configuration_pb2
|
||||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class StartupInfo(_message.Message):
|
||||
__slots__ = ("devices",)
|
||||
class Device(_message.Message):
|
||||
__slots__ = ("name", "chips", "device_info")
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
CHIPS_FIELD_NUMBER: _ClassVar[int]
|
||||
DEVICE_INFO_FIELD_NUMBER: _ClassVar[int]
|
||||
name: str
|
||||
chips: _containers.RepeatedCompositeFieldContainer[Chip]
|
||||
device_info: DeviceInfo
|
||||
def __init__(self, name: _Optional[str] = ..., chips: _Optional[_Iterable[_Union[Chip, _Mapping]]] = ..., device_info: _Optional[_Union[DeviceInfo, _Mapping]] = ...) -> None: ...
|
||||
DEVICES_FIELD_NUMBER: _ClassVar[int]
|
||||
devices: _containers.RepeatedCompositeFieldContainer[StartupInfo.Device]
|
||||
def __init__(self, devices: _Optional[_Iterable[_Union[StartupInfo.Device, _Mapping]]] = ...) -> None: ...
|
||||
|
||||
class ChipInfo(_message.Message):
|
||||
__slots__ = ("name", "chip", "device_info")
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
CHIP_FIELD_NUMBER: _ClassVar[int]
|
||||
DEVICE_INFO_FIELD_NUMBER: _ClassVar[int]
|
||||
name: str
|
||||
chip: Chip
|
||||
device_info: DeviceInfo
|
||||
def __init__(self, name: _Optional[str] = ..., chip: _Optional[_Union[Chip, _Mapping]] = ..., device_info: _Optional[_Union[DeviceInfo, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class DeviceInfo(_message.Message):
|
||||
__slots__ = ("name", "kind", "version", "sdk_version", "build_id", "variant", "arch")
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
KIND_FIELD_NUMBER: _ClassVar[int]
|
||||
VERSION_FIELD_NUMBER: _ClassVar[int]
|
||||
SDK_VERSION_FIELD_NUMBER: _ClassVar[int]
|
||||
BUILD_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
VARIANT_FIELD_NUMBER: _ClassVar[int]
|
||||
ARCH_FIELD_NUMBER: _ClassVar[int]
|
||||
name: str
|
||||
kind: str
|
||||
version: str
|
||||
sdk_version: str
|
||||
build_id: str
|
||||
variant: str
|
||||
arch: str
|
||||
def __init__(self, name: _Optional[str] = ..., kind: _Optional[str] = ..., version: _Optional[str] = ..., sdk_version: _Optional[str] = ..., build_id: _Optional[str] = ..., variant: _Optional[str] = ..., arch: _Optional[str] = ...) -> None: ...
|
||||
|
||||
class Chip(_message.Message):
|
||||
__slots__ = ("kind", "id", "manufacturer", "product_name", "fd_in", "fd_out", "loopback", "bt_properties", "address", "offset")
|
||||
KIND_FIELD_NUMBER: _ClassVar[int]
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
MANUFACTURER_FIELD_NUMBER: _ClassVar[int]
|
||||
PRODUCT_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
FD_IN_FIELD_NUMBER: _ClassVar[int]
|
||||
FD_OUT_FIELD_NUMBER: _ClassVar[int]
|
||||
LOOPBACK_FIELD_NUMBER: _ClassVar[int]
|
||||
BT_PROPERTIES_FIELD_NUMBER: _ClassVar[int]
|
||||
ADDRESS_FIELD_NUMBER: _ClassVar[int]
|
||||
OFFSET_FIELD_NUMBER: _ClassVar[int]
|
||||
kind: _common_pb2.ChipKind
|
||||
id: str
|
||||
manufacturer: str
|
||||
product_name: str
|
||||
fd_in: int
|
||||
fd_out: int
|
||||
loopback: bool
|
||||
bt_properties: _configuration_pb2.Controller
|
||||
address: str
|
||||
offset: _model_pb2.Position
|
||||
def __init__(self, kind: _Optional[_Union[_common_pb2.ChipKind, str]] = ..., id: _Optional[str] = ..., manufacturer: _Optional[str] = ..., product_name: _Optional[str] = ..., fd_in: _Optional[int] = ..., fd_out: _Optional[int] = ..., loopback: bool = ..., bt_properties: _Optional[_Union[_configuration_pb2.Controller, _Mapping]] = ..., address: _Optional[str] = ..., offset: _Optional[_Union[_model_pb2.Position, _Mapping]] = ...) -> None: ...
|
||||
@@ -0,0 +1,4 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: packet_streamer.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from . import hci_packet_pb2 as hci__packet__pb2
|
||||
from . import startup_pb2 as startup__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15packet_streamer.proto\x12\rnetsim.packet\x1a\x10hci_packet.proto\x1a\rstartup.proto\"\x93\x01\n\rPacketRequest\x12\x30\n\x0cinitial_info\x18\x01 \x01(\x0b\x32\x18.netsim.startup.ChipInfoH\x00\x12.\n\nhci_packet\x18\x02 \x01(\x0b\x32\x18.netsim.packet.HCIPacketH\x00\x12\x10\n\x06packet\x18\x03 \x01(\x0cH\x00\x42\x0e\n\x0crequest_type\"t\n\x0ePacketResponse\x12\x0f\n\x05\x65rror\x18\x01 \x01(\tH\x00\x12.\n\nhci_packet\x18\x02 \x01(\x0b\x32\x18.netsim.packet.HCIPacketH\x00\x12\x10\n\x06packet\x18\x03 \x01(\x0cH\x00\x42\x0f\n\rresponse_type2b\n\x0ePacketStreamer\x12P\n\rStreamPackets\x12\x1c.netsim.packet.PacketRequest\x1a\x1d.netsim.packet.PacketResponse(\x01\x30\x01\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'packet_streamer_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_PACKETREQUEST._serialized_start=74
|
||||
_PACKETREQUEST._serialized_end=221
|
||||
_PACKETRESPONSE._serialized_start=223
|
||||
_PACKETRESPONSE._serialized_end=339
|
||||
_PACKETSTREAMER._serialized_start=341
|
||||
_PACKETSTREAMER._serialized_end=439
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: rootcanal/configuration.proto
|
||||
# Protobuf Python Version: 4.25.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1drootcanal/configuration.proto\x12\x17rootcanal.configuration\"\xbc\x01\n\x12\x43ontrollerFeatures\x12\x1f\n\x17le_extended_advertising\x18\x01 \x01(\x08\x12\x1f\n\x17le_periodic_advertising\x18\x02 \x01(\x08\x12\x12\n\nll_privacy\x18\x03 \x01(\x08\x12\x11\n\tle_2m_phy\x18\x04 \x01(\x08\x12\x14\n\x0cle_coded_phy\x18\x05 \x01(\x08\x12\'\n\x1fle_connected_isochronous_stream\x18\x06 \x01(\x08\"\x8d\x01\n\x10\x43ontrollerQuirks\x12\x30\n(send_acl_data_before_connection_complete\x18\x01 \x01(\x08\x12\"\n\x1ahas_default_random_address\x18\x02 \x01(\x08\x12#\n\x1bhardware_error_before_reset\x18\x03 \x01(\x08\".\n\x0eVendorFeatures\x12\x0b\n\x03\x63sr\x18\x01 \x01(\x08\x12\x0f\n\x07\x61ndroid\x18\x02 \x01(\x08\"\x8a\x02\n\nController\x12\x39\n\x06preset\x18\x01 \x01(\x0e\x32).rootcanal.configuration.ControllerPreset\x12=\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32+.rootcanal.configuration.ControllerFeatures\x12\x39\n\x06quirks\x18\x03 \x01(\x0b\x32).rootcanal.configuration.ControllerQuirks\x12\x0e\n\x06strict\x18\x04 \x01(\x08\x12\x37\n\x06vendor\x18\x05 \x01(\x0b\x32\'.rootcanal.configuration.VendorFeatures\"Y\n\tTcpServer\x12\x10\n\x08tcp_port\x18\x01 \x02(\x05\x12:\n\rconfiguration\x18\x02 \x01(\x0b\x32#.rootcanal.configuration.Controller\"G\n\rConfiguration\x12\x36\n\ntcp_server\x18\x01 \x03(\x0b\x32\".rootcanal.configuration.TcpServer*H\n\x10\x43ontrollerPreset\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0f\n\x0bLAIRD_BL654\x10\x01\x12\x16\n\x12\x43SR_RCK_PTS_DONGLE\x10\x02\x42\x02H\x02')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'rootcanal.configuration_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
_globals['DESCRIPTOR']._options = None
|
||||
_globals['DESCRIPTOR']._serialized_options = b'H\002'
|
||||
_globals['_CONTROLLERPRESET']._serialized_start=874
|
||||
_globals['_CONTROLLERPRESET']._serialized_end=946
|
||||
_globals['_CONTROLLERFEATURES']._serialized_start=59
|
||||
_globals['_CONTROLLERFEATURES']._serialized_end=247
|
||||
_globals['_CONTROLLERQUIRKS']._serialized_start=250
|
||||
_globals['_CONTROLLERQUIRKS']._serialized_end=391
|
||||
_globals['_VENDORFEATURES']._serialized_start=393
|
||||
_globals['_VENDORFEATURES']._serialized_end=439
|
||||
_globals['_CONTROLLER']._serialized_start=442
|
||||
_globals['_CONTROLLER']._serialized_end=708
|
||||
_globals['_TCPSERVER']._serialized_start=710
|
||||
_globals['_TCPSERVER']._serialized_end=799
|
||||
_globals['_CONFIGURATION']._serialized_start=801
|
||||
_globals['_CONFIGURATION']._serialized_end=872
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -0,0 +1,78 @@
|
||||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class ControllerPreset(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
__slots__ = ()
|
||||
DEFAULT: _ClassVar[ControllerPreset]
|
||||
LAIRD_BL654: _ClassVar[ControllerPreset]
|
||||
CSR_RCK_PTS_DONGLE: _ClassVar[ControllerPreset]
|
||||
DEFAULT: ControllerPreset
|
||||
LAIRD_BL654: ControllerPreset
|
||||
CSR_RCK_PTS_DONGLE: ControllerPreset
|
||||
|
||||
class ControllerFeatures(_message.Message):
|
||||
__slots__ = ("le_extended_advertising", "le_periodic_advertising", "ll_privacy", "le_2m_phy", "le_coded_phy", "le_connected_isochronous_stream")
|
||||
LE_EXTENDED_ADVERTISING_FIELD_NUMBER: _ClassVar[int]
|
||||
LE_PERIODIC_ADVERTISING_FIELD_NUMBER: _ClassVar[int]
|
||||
LL_PRIVACY_FIELD_NUMBER: _ClassVar[int]
|
||||
LE_2M_PHY_FIELD_NUMBER: _ClassVar[int]
|
||||
LE_CODED_PHY_FIELD_NUMBER: _ClassVar[int]
|
||||
LE_CONNECTED_ISOCHRONOUS_STREAM_FIELD_NUMBER: _ClassVar[int]
|
||||
le_extended_advertising: bool
|
||||
le_periodic_advertising: bool
|
||||
ll_privacy: bool
|
||||
le_2m_phy: bool
|
||||
le_coded_phy: bool
|
||||
le_connected_isochronous_stream: bool
|
||||
def __init__(self, le_extended_advertising: bool = ..., le_periodic_advertising: bool = ..., ll_privacy: bool = ..., le_2m_phy: bool = ..., le_coded_phy: bool = ..., le_connected_isochronous_stream: bool = ...) -> None: ...
|
||||
|
||||
class ControllerQuirks(_message.Message):
|
||||
__slots__ = ("send_acl_data_before_connection_complete", "has_default_random_address", "hardware_error_before_reset")
|
||||
SEND_ACL_DATA_BEFORE_CONNECTION_COMPLETE_FIELD_NUMBER: _ClassVar[int]
|
||||
HAS_DEFAULT_RANDOM_ADDRESS_FIELD_NUMBER: _ClassVar[int]
|
||||
HARDWARE_ERROR_BEFORE_RESET_FIELD_NUMBER: _ClassVar[int]
|
||||
send_acl_data_before_connection_complete: bool
|
||||
has_default_random_address: bool
|
||||
hardware_error_before_reset: bool
|
||||
def __init__(self, send_acl_data_before_connection_complete: bool = ..., has_default_random_address: bool = ..., hardware_error_before_reset: bool = ...) -> None: ...
|
||||
|
||||
class VendorFeatures(_message.Message):
|
||||
__slots__ = ("csr", "android")
|
||||
CSR_FIELD_NUMBER: _ClassVar[int]
|
||||
ANDROID_FIELD_NUMBER: _ClassVar[int]
|
||||
csr: bool
|
||||
android: bool
|
||||
def __init__(self, csr: bool = ..., android: bool = ...) -> None: ...
|
||||
|
||||
class Controller(_message.Message):
|
||||
__slots__ = ("preset", "features", "quirks", "strict", "vendor")
|
||||
PRESET_FIELD_NUMBER: _ClassVar[int]
|
||||
FEATURES_FIELD_NUMBER: _ClassVar[int]
|
||||
QUIRKS_FIELD_NUMBER: _ClassVar[int]
|
||||
STRICT_FIELD_NUMBER: _ClassVar[int]
|
||||
VENDOR_FIELD_NUMBER: _ClassVar[int]
|
||||
preset: ControllerPreset
|
||||
features: ControllerFeatures
|
||||
quirks: ControllerQuirks
|
||||
strict: bool
|
||||
vendor: VendorFeatures
|
||||
def __init__(self, preset: _Optional[_Union[ControllerPreset, str]] = ..., features: _Optional[_Union[ControllerFeatures, _Mapping]] = ..., quirks: _Optional[_Union[ControllerQuirks, _Mapping]] = ..., strict: bool = ..., vendor: _Optional[_Union[VendorFeatures, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class TcpServer(_message.Message):
|
||||
__slots__ = ("tcp_port", "configuration")
|
||||
TCP_PORT_FIELD_NUMBER: _ClassVar[int]
|
||||
CONFIGURATION_FIELD_NUMBER: _ClassVar[int]
|
||||
tcp_port: int
|
||||
configuration: Controller
|
||||
def __init__(self, tcp_port: _Optional[int] = ..., configuration: _Optional[_Union[Controller, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class Configuration(_message.Message):
|
||||
__slots__ = ("tcp_server",)
|
||||
TCP_SERVER_FIELD_NUMBER: _ClassVar[int]
|
||||
tcp_server: _containers.RepeatedCompositeFieldContainer[TcpServer]
|
||||
def __init__(self, tcp_server: _Optional[_Iterable[_Union[TcpServer, _Mapping]]] = ...) -> None: ...
|
||||
@@ -0,0 +1,4 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: startup.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from . import common_pb2 as common__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rstartup.proto\x12\x0enetsim.startup\x1a\x0c\x63ommon.proto\"\x7f\n\x0bStartupInfo\x12\x33\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\".netsim.startup.StartupInfo.Device\x1a;\n\x06\x44\x65vice\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x05\x63hips\x18\x02 \x03(\x0b\x32\x14.netsim.startup.Chip\"<\n\x08\x43hipInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\"\n\x04\x63hip\x18\x02 \x01(\x0b\x32\x14.netsim.startup.Chip\"\x96\x01\n\x04\x43hip\x12%\n\x04kind\x18\x01 \x01(\x0e\x32\x17.netsim.common.ChipKind\x12\n\n\x02id\x18\x02 \x01(\t\x12\x14\n\x0cmanufacturer\x18\x03 \x01(\t\x12\x14\n\x0cproduct_name\x18\x04 \x01(\t\x12\r\n\x05\x66\x64_in\x18\x05 \x01(\x05\x12\x0e\n\x06\x66\x64_out\x18\x06 \x01(\x05\x12\x10\n\x08loopback\x18\x07 \x01(\x08\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'startup_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_STARTUPINFO._serialized_start=47
|
||||
_STARTUPINFO._serialized_end=174
|
||||
_STARTUPINFO_DEVICE._serialized_start=115
|
||||
_STARTUPINFO_DEVICE._serialized_end=174
|
||||
_CHIPINFO._serialized_start=176
|
||||
_CHIPINFO._serialized_end=236
|
||||
_CHIP._serialized_start=239
|
||||
_CHIP._serialized_end=389
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -1,46 +0,0 @@
|
||||
from . import common_pb2 as _common_pb2
|
||||
from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class Chip(_message.Message):
|
||||
__slots__ = ["fd_in", "fd_out", "id", "kind", "loopback", "manufacturer", "product_name"]
|
||||
FD_IN_FIELD_NUMBER: _ClassVar[int]
|
||||
FD_OUT_FIELD_NUMBER: _ClassVar[int]
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
KIND_FIELD_NUMBER: _ClassVar[int]
|
||||
LOOPBACK_FIELD_NUMBER: _ClassVar[int]
|
||||
MANUFACTURER_FIELD_NUMBER: _ClassVar[int]
|
||||
PRODUCT_NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
fd_in: int
|
||||
fd_out: int
|
||||
id: str
|
||||
kind: _common_pb2.ChipKind
|
||||
loopback: bool
|
||||
manufacturer: str
|
||||
product_name: str
|
||||
def __init__(self, kind: _Optional[_Union[_common_pb2.ChipKind, str]] = ..., id: _Optional[str] = ..., manufacturer: _Optional[str] = ..., product_name: _Optional[str] = ..., fd_in: _Optional[int] = ..., fd_out: _Optional[int] = ..., loopback: bool = ...) -> None: ...
|
||||
|
||||
class ChipInfo(_message.Message):
|
||||
__slots__ = ["chip", "name"]
|
||||
CHIP_FIELD_NUMBER: _ClassVar[int]
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
chip: Chip
|
||||
name: str
|
||||
def __init__(self, name: _Optional[str] = ..., chip: _Optional[_Union[Chip, _Mapping]] = ...) -> None: ...
|
||||
|
||||
class StartupInfo(_message.Message):
|
||||
__slots__ = ["devices"]
|
||||
class Device(_message.Message):
|
||||
__slots__ = ["chips", "name"]
|
||||
CHIPS_FIELD_NUMBER: _ClassVar[int]
|
||||
NAME_FIELD_NUMBER: _ClassVar[int]
|
||||
chips: _containers.RepeatedCompositeFieldContainer[Chip]
|
||||
name: str
|
||||
def __init__(self, name: _Optional[str] = ..., chips: _Optional[_Iterable[_Union[Chip, _Mapping]]] = ...) -> None: ...
|
||||
DEVICES_FIELD_NUMBER: _ClassVar[int]
|
||||
devices: _containers.RepeatedCompositeFieldContainer[StartupInfo.Device]
|
||||
def __init__(self, devices: _Optional[_Iterable[_Union[StartupInfo.Device, _Mapping]]] = ...) -> None: ...
|
||||
@@ -25,6 +25,9 @@ An app that implements a virtual Bluetooth speaker that can receive audio.
|
||||
## `run_advertiser.py`
|
||||
An app that runs a simple device that just advertises (BLE).
|
||||
|
||||
## `run_cig_setup.py`
|
||||
An app that creates a simple CIG containing two CISes. **Note**: If using the example config file (e.g. `device1.json`), the `address` needs to be removed, so that the devices are given different random addresses.
|
||||
|
||||
## `run_classic_connect.py`
|
||||
An app that connects to a Bluetooth Classic device and prints its services.
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ GETTING STARTED WITH BUMBLE
|
||||
|
||||
# Prerequisites
|
||||
|
||||
You need Python 3.8 or above. Python >= 3.9 is recommended, but 3.8 should be sufficient if
|
||||
necessary (there may be some optional functionality that will not work on some platforms with
|
||||
python 3.8).
|
||||
You need Python 3.9 or above.
|
||||
Visit the [Python site](https://www.python.org/) for instructions on how to install Python
|
||||
for your platform.
|
||||
Throughout the documentation, when shell commands are shown, it is assumed that you can
|
||||
|
||||
@@ -31,7 +31,7 @@ Some of the configurations that may be useful:
|
||||
|
||||
See the [use cases page](use_cases/index.md) for more use cases.
|
||||
|
||||
The project is implemented in Python (Python >= 3.8 is required). A number of APIs for functionality that is inherently I/O bound is implemented in terms of python coroutines with async IO. This means that all of the concurrent tasks run in the same thread, which makes everything much simpler and more predictable.
|
||||
The project is implemented in Python (Python >= 3.9 is required). A number of APIs for functionality that is inherently I/O bound is implemented in terms of python coroutines with async IO. This means that all of the concurrent tasks run in the same thread, which makes everything much simpler and more predictable.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PLATFORMS
|
||||
=========
|
||||
|
||||
Most of the code included in the project should run on any platform that supports Python >= 3.8. Not all features are supported on all platforms (for example, USB dongle support is only available on platforms where the python USB library is functional).
|
||||
Most of the code included in the project should run on any platform that supports Python >= 3.9. Not all features are supported on all platforms (for example, USB dongle support is only available on platforms where the python USB library is functional).
|
||||
|
||||
For platform-specific information, see the following pages:
|
||||
|
||||
|
||||
@@ -4,6 +4,6 @@ channels:
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- pip=23
|
||||
- python=3.8
|
||||
- python=3.9
|
||||
- pip:
|
||||
- --editable .[development,documentation,test]
|
||||
|
||||
@@ -61,20 +61,23 @@ def codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
||||
channel_modes=[
|
||||
SBC_MONO_CHANNEL_MODE,
|
||||
SBC_DUAL_CHANNEL_MODE,
|
||||
SBC_STEREO_CHANNEL_MODE,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
],
|
||||
block_lengths=[4, 8, 12, 16],
|
||||
subbands=[4, 8],
|
||||
allocation_methods=[
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
SBC_SNR_ALLOCATION_METHOD,
|
||||
],
|
||||
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,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
|
||||
@@ -33,8 +33,6 @@ from bumble.avdtp import (
|
||||
Listener,
|
||||
)
|
||||
from bumble.a2dp import (
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
make_audio_source_service_sdp_records,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
SbcMediaCodecInformation,
|
||||
@@ -59,12 +57,12 @@ def codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation.from_discrete_values(
|
||||
sampling_frequency=44100,
|
||||
channel_mode=SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
block_length=16,
|
||||
subbands=8,
|
||||
allocation_method=SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
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,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
@@ -73,11 +71,9 @@ def codec_capabilities():
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_avdtp_connection(read_function, protocol):
|
||||
packet_source = SbcPacketSource(
|
||||
read_function, protocol.l2cap_channel.peer_mtu, codec_capabilities()
|
||||
)
|
||||
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu)
|
||||
packet_pump = MediaPacketPump(packet_source.packets)
|
||||
protocol.add_source(packet_source.codec_capabilities, packet_pump)
|
||||
protocol.add_source(codec_capabilities(), packet_pump)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -97,11 +93,9 @@ async def stream_packets(read_function, protocol):
|
||||
print(f'### Selected sink: {sink.seid}')
|
||||
|
||||
# Stream the packets
|
||||
packet_source = SbcPacketSource(
|
||||
read_function, protocol.l2cap_channel.peer_mtu, codec_capabilities()
|
||||
)
|
||||
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu)
|
||||
packet_pump = MediaPacketPump(packet_source.packets)
|
||||
source = protocol.add_source(packet_source.codec_capabilities, packet_pump)
|
||||
source = protocol.add_source(codec_capabilities(), packet_pump)
|
||||
stream = await protocol.create_stream(source, sink)
|
||||
await stream.start()
|
||||
await asyncio.sleep(5)
|
||||
|
||||
@@ -60,20 +60,23 @@ def codec_capabilities():
|
||||
return avdtp.MediaCodecCapabilities(
|
||||
media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=a2dp.A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=a2dp.SbcMediaCodecInformation.from_lists(
|
||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
||||
channel_modes=[
|
||||
a2dp.SBC_MONO_CHANNEL_MODE,
|
||||
a2dp.SBC_DUAL_CHANNEL_MODE,
|
||||
a2dp.SBC_STEREO_CHANNEL_MODE,
|
||||
a2dp.SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
],
|
||||
block_lengths=[4, 8, 12, 16],
|
||||
subbands=[4, 8],
|
||||
allocation_methods=[
|
||||
a2dp.SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
a2dp.SBC_SNR_ALLOCATION_METHOD,
|
||||
],
|
||||
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,
|
||||
),
|
||||
|
||||
@@ -36,13 +36,10 @@ from bumble.transport import open_transport_or_link
|
||||
async def main() -> None:
|
||||
if len(sys.argv) < 3:
|
||||
print(
|
||||
'Usage: run_cig_setup.py <config-file>'
|
||||
'Usage: run_cig_setup.py <config-file> '
|
||||
'<transport-spec-for-device-1> <transport-spec-for-device-2>'
|
||||
)
|
||||
print(
|
||||
'example: run_cig_setup.py device1.json'
|
||||
'tcp-client:127.0.0.1:6402 tcp-client:127.0.0.1:6402'
|
||||
)
|
||||
print('example: run_cig_setup.py device1.json hci-socket:0 hci-socket:1')
|
||||
return
|
||||
|
||||
print('<<< connecting to HCI...')
|
||||
@@ -65,18 +62,18 @@ async def main() -> None:
|
||||
advertising_set = await devices[0].create_advertising_set()
|
||||
|
||||
connection = await devices[1].connect(
|
||||
devices[0].public_address, own_address_type=OwnAddressType.PUBLIC
|
||||
devices[0].random_address, own_address_type=OwnAddressType.RANDOM
|
||||
)
|
||||
|
||||
cid_ids = [2, 3]
|
||||
cis_handles = await devices[1].setup_cig(
|
||||
cig_id=1,
|
||||
cis_id=cid_ids,
|
||||
sdu_interval=(10000, 0),
|
||||
sdu_interval=(10000, 255),
|
||||
framing=0,
|
||||
max_sdu=(120, 0),
|
||||
retransmission_number=13,
|
||||
max_transport_latency=(100, 0),
|
||||
max_transport_latency=(100, 5),
|
||||
)
|
||||
|
||||
def on_cis_request(
|
||||
|
||||
@@ -64,6 +64,7 @@ async def main() -> None:
|
||||
[(AdvertisingData.COMPLETE_LOCAL_NAME, "Bumble 2".encode("utf-8"))]
|
||||
)
|
||||
|
||||
# pylint: disable=possibly-used-before-assignment
|
||||
if device.host.number_of_supported_advertising_sets >= 2:
|
||||
set2 = await device.create_advertising_set(
|
||||
random_address=Address("F0:F0:F0:F0:F0:F1"),
|
||||
|
||||
@@ -57,6 +57,9 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.HfConfiguration):
|
||||
esco_parameters = hfp.ESCO_PARAMETERS[
|
||||
hfp.DefaultCodecParameters.ESCO_CVSD_S4
|
||||
]
|
||||
else:
|
||||
raise RuntimeError("unknown active codec")
|
||||
|
||||
connection.abort_on(
|
||||
'disconnection',
|
||||
connection.device.send_command(
|
||||
|
||||
@@ -45,6 +45,7 @@ ignore="pandora" # FIXME: pylint does not support stubs yet:
|
||||
|
||||
[tool.pylint.typecheck]
|
||||
signature-mutators="AsyncRunner.run_in_task"
|
||||
disable="not-callable"
|
||||
|
||||
[tool.black]
|
||||
skip-string-normalization = true
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
# Invoke this script with an argument pointing to where the AOSP `tools/netsim/src/proto` is
|
||||
# Invoke this script with two arguments:
|
||||
# Arg 1: directory path where the AOSP `tools/netsim/proto` directory is located
|
||||
# Arg 2: directory path where the RootCanal `proto/rootcanal` directory is located
|
||||
PROTOC_OUT=bumble/transport/grpc_protobuf
|
||||
|
||||
proto_files=(common.proto packet_streamer.proto hci_packet.proto startup.proto)
|
||||
for proto_file in "${proto_files[@]}"
|
||||
netsim_proto_files=(netsim/common.proto netsim/packet_streamer.proto netsim/hci_packet.proto netsim/startup.proto netsim/model.proto)
|
||||
for proto_file in "${netsim_proto_files[@]}"
|
||||
do
|
||||
python -m grpc_tools.protoc -I$1 --proto_path=bumble/transport --python_out=$PROTOC_OUT --pyi_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $1/$proto_file
|
||||
python -m grpc_tools.protoc -I$1 -I$2 --proto_path=bumble/transport --python_out=$PROTOC_OUT --pyi_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $1/$proto_file
|
||||
done
|
||||
|
||||
python_files=(packet_streamer_pb2_grpc.py packet_streamer_pb2.py hci_packet_pb2.py startup_pb2.py)
|
||||
rootcanal_proto_files=(rootcanal/configuration.proto)
|
||||
for proto_file in "${rootcanal_proto_files[@]}"
|
||||
do
|
||||
python -m grpc_tools.protoc -I$1 -I$2 --proto_path=bumble/transport --python_out=$PROTOC_OUT --pyi_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $2/$proto_file
|
||||
done
|
||||
|
||||
python_files=(netsim/*.py netsim/*.pyi)
|
||||
for python_file in "${python_files[@]}"
|
||||
do
|
||||
sed -i 's/^import .*_pb2 as/from . \0/' $PROTOC_OUT/$python_file
|
||||
sed -i '' 's/^from netsim/from bumble.transport.grpc_protobuf.netsim/' $PROTOC_OUT/$python_file
|
||||
sed -i '' 's/^from rootcanal/from bumble.transport.grpc_protobuf.rootcanal/' $PROTOC_OUT/$python_file
|
||||
done
|
||||
|
||||
@@ -70,6 +70,7 @@ console_scripts =
|
||||
bumble-usb-probe = bumble.apps.usb_probe:main
|
||||
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
||||
bumble-bench = bumble.apps.bench:main
|
||||
bumble-player = bumble.apps.player.player:main
|
||||
bumble-speaker = bumble.apps.speaker.speaker:main
|
||||
bumble-pandora-server = bumble.apps.pandora_server:main
|
||||
bumble-rtk-util = bumble.tools.rtk_util:main
|
||||
@@ -91,9 +92,9 @@ development =
|
||||
grpcio-tools >= 1.62.1
|
||||
invoke >= 1.7.3
|
||||
mobly >= 1.12.2
|
||||
mypy == 1.10.0
|
||||
mypy == 1.12.0
|
||||
nox >= 2022
|
||||
pylint == 3.1.0
|
||||
pylint == 3.3.1
|
||||
pyyaml >= 6.0
|
||||
types-appdirs >= 1.4.3
|
||||
types-invoke >= 1.7.3
|
||||
|
||||
@@ -33,20 +33,16 @@ from bumble.avdtp import (
|
||||
Protocol,
|
||||
Listener,
|
||||
MediaCodecCapabilities,
|
||||
MediaPacket,
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
AVDTP_TSEP_SNK,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
)
|
||||
from bumble.a2dp import (
|
||||
AacMediaCodecInformation,
|
||||
OpusMediaCodecInformation,
|
||||
SbcMediaCodecInformation,
|
||||
SBC_MONO_CHANNEL_MODE,
|
||||
SBC_DUAL_CHANNEL_MODE,
|
||||
SBC_STEREO_CHANNEL_MODE,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
SBC_SNR_ALLOCATION_METHOD,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -125,12 +121,12 @@ def source_codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation.from_discrete_values(
|
||||
sampling_frequency=44100,
|
||||
channel_mode=SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
block_length=16,
|
||||
subbands=8,
|
||||
allocation_method=SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
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,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
@@ -142,20 +138,23 @@ def sink_codec_capabilities():
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
||||
channel_modes=[
|
||||
SBC_MONO_CHANNEL_MODE,
|
||||
SBC_DUAL_CHANNEL_MODE,
|
||||
SBC_STEREO_CHANNEL_MODE,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
],
|
||||
block_lengths=[4, 8, 12, 16],
|
||||
subbands=[4, 8],
|
||||
allocation_methods=[
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
SBC_SNR_ALLOCATION_METHOD,
|
||||
],
|
||||
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,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
@@ -273,7 +272,125 @@ async def test_source_sink_1():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run_test_self():
|
||||
def test_sbc_codec_specific_information():
|
||||
sbc_info = SbcMediaCodecInformation.from_bytes(bytes.fromhex("3fff0235"))
|
||||
assert (
|
||||
sbc_info.sampling_frequency
|
||||
== SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
assert (
|
||||
sbc_info.channel_mode
|
||||
== SbcMediaCodecInformation.ChannelMode.MONO
|
||||
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||
| 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
|
||||
)
|
||||
assert (
|
||||
sbc_info.subbands
|
||||
== SbcMediaCodecInformation.Subbands.S_4 | SbcMediaCodecInformation.Subbands.S_8
|
||||
)
|
||||
assert (
|
||||
sbc_info.allocation_method
|
||||
== SbcMediaCodecInformation.AllocationMethod.SNR
|
||||
| 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,
|
||||
2,
|
||||
53,
|
||||
)
|
||||
assert sbc_info == sbc_info2
|
||||
assert bytes(sbc_info2) == bytes.fromhex("3fff0235")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_aac_codec_specific_information():
|
||||
aac_info = 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
|
||||
)
|
||||
assert (
|
||||
aac_info.sampling_frequency
|
||||
== AacMediaCodecInformation.SamplingFrequency.SF_44100
|
||||
| AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
assert (
|
||||
aac_info.channels
|
||||
== AacMediaCodecInformation.Channels.MONO
|
||||
| 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,
|
||||
1,
|
||||
256000,
|
||||
)
|
||||
assert aac_info == aac_info2
|
||||
assert bytes(aac_info2) == bytes.fromhex("f0018c83e800")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
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
|
||||
assert (
|
||||
opus_info.sampling_frequency
|
||||
== OpusMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
)
|
||||
|
||||
opus_info2 = OpusMediaCodecInformation(
|
||||
OpusMediaCodecInformation.ChannelMode.STEREO,
|
||||
OpusMediaCodecInformation.FrameSize.FS_20MS,
|
||||
OpusMediaCodecInformation.SamplingFrequency.SF_48000,
|
||||
)
|
||||
assert opus_info2 == opus_info
|
||||
assert opus_info2.value == bytes([0x92])
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main():
|
||||
test_sbc_codec_specific_information()
|
||||
test_aac_codec_specific_information()
|
||||
test_opus_codec_specific_information()
|
||||
await test_self_connection()
|
||||
await test_source_sink_1()
|
||||
|
||||
@@ -281,4 +398,4 @@ async def run_test_self():
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(run_test_self())
|
||||
asyncio.run(async_main())
|
||||
|
||||
@@ -23,13 +23,12 @@ from bumble.avdtp import (
|
||||
AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY,
|
||||
AVDTP_SET_CONFIGURATION,
|
||||
Message,
|
||||
MediaPacket,
|
||||
Get_Capabilities_Response,
|
||||
Set_Configuration_Command,
|
||||
Set_Configuration_Response,
|
||||
ServiceCapabilities,
|
||||
MediaCodecCapabilities,
|
||||
)
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import random
|
||||
import pytest
|
||||
from bumble.codecs import AacAudioRtpPacket, BitReader
|
||||
from bumble.codecs import AacAudioRtpPacket, BitReader, BitWriter
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -49,19 +50,58 @@ def test_reader():
|
||||
assert value == int.from_bytes(data, byteorder='big')
|
||||
|
||||
|
||||
def test_writer():
|
||||
writer = BitWriter()
|
||||
assert bytes(writer) == b''
|
||||
|
||||
for i in range(100):
|
||||
for j in range(1, 10):
|
||||
writer = BitWriter()
|
||||
chunks = []
|
||||
for k in range(j):
|
||||
n_bits = random.randint(1, 32)
|
||||
random_bits = random.getrandbits(n_bits)
|
||||
chunks.append((n_bits, random_bits))
|
||||
writer.write(random_bits, n_bits)
|
||||
|
||||
written_data = bytes(writer)
|
||||
reader = BitReader(written_data)
|
||||
for n_bits, written_bits in chunks:
|
||||
read_bits = reader.read(n_bits)
|
||||
assert read_bits == written_bits
|
||||
|
||||
|
||||
def test_aac_rtp():
|
||||
# pylint: disable=line-too-long
|
||||
packet_data = bytes.fromhex(
|
||||
'47fc0000b090800300202066000198000de120000000000000000000000000000000000000000000001c'
|
||||
)
|
||||
packet = AacAudioRtpPacket(packet_data)
|
||||
packet = AacAudioRtpPacket.from_bytes(packet_data)
|
||||
adts = packet.to_adts()
|
||||
assert adts == bytes.fromhex(
|
||||
'fff1508004fffc2066000198000de120000000000000000000000000000000000000000000001c'
|
||||
)
|
||||
|
||||
payload = bytes(list(range(1, 200)))
|
||||
rtp = AacAudioRtpPacket.for_simple_aac(44100, 2, payload)
|
||||
assert rtp.audio_mux_element.payload == payload
|
||||
assert (
|
||||
rtp.audio_mux_element.stream_mux_config.audio_specific_config.sampling_frequency
|
||||
== 44100
|
||||
)
|
||||
assert (
|
||||
rtp.audio_mux_element.stream_mux_config.audio_specific_config.channel_configuration
|
||||
== 2
|
||||
)
|
||||
rtp2 = AacAudioRtpPacket.from_bytes(bytes(rtp))
|
||||
assert str(rtp2.audio_mux_element.stream_mux_config) == str(
|
||||
rtp.audio_mux_element.stream_mux_config
|
||||
)
|
||||
assert rtp2.audio_mux_element.payload == rtp.audio_mux_element.payload
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
test_reader()
|
||||
test_writer()
|
||||
test_aac_rtp()
|
||||
|
||||
@@ -851,7 +851,12 @@ async def test_unsubscribe():
|
||||
await async_barrier()
|
||||
mock1.assert_called_once_with(ANY, True, False)
|
||||
|
||||
await c2.subscribe()
|
||||
assert len(server.gatt_server.subscribers) == 1
|
||||
|
||||
def callback(_):
|
||||
pass
|
||||
|
||||
await c2.subscribe(callback)
|
||||
await async_barrier()
|
||||
mock2.assert_called_once_with(ANY, True, False)
|
||||
|
||||
@@ -861,10 +866,16 @@ async def test_unsubscribe():
|
||||
mock1.assert_called_once_with(ANY, False, False)
|
||||
|
||||
mock2.reset_mock()
|
||||
await c2.unsubscribe()
|
||||
await c2.unsubscribe(callback)
|
||||
await async_barrier()
|
||||
mock2.assert_called_once_with(ANY, False, False)
|
||||
|
||||
# All CCCDs should be zeros now
|
||||
assert list(server.gatt_server.subscribers.values())[0] == {
|
||||
c1.handle: bytes([0, 0]),
|
||||
c2.handle: bytes([0, 0]),
|
||||
}
|
||||
|
||||
mock1.reset_mock()
|
||||
await c1.unsubscribe()
|
||||
await async_barrier()
|
||||
|
||||
@@ -25,6 +25,7 @@ import sys
|
||||
from bumble import att, device
|
||||
from bumble.profiles import hap
|
||||
from .test_utils import TwoDevices
|
||||
from bumble.keys import PairingKeys
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -86,6 +87,10 @@ async def hap_client():
|
||||
devices.connections[0].encryption = 1 # type: ignore
|
||||
devices.connections[1].encryption = 1 # type: ignore
|
||||
|
||||
devices[0].on_pairing(
|
||||
devices.connections[0], devices.connections[0].peer_address, PairingKeys(), True
|
||||
)
|
||||
|
||||
peer = device.Peer(devices.connections[1]) # type: ignore
|
||||
hap_client = await peer.discover_service_and_create_proxy(
|
||||
hap.HearingAccessServiceProxy
|
||||
|
||||
@@ -569,6 +569,37 @@ async def test_sco_setup():
|
||||
await asyncio.gather(*sco_disconnection_futures)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_hf_batched_response(
|
||||
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
|
||||
):
|
||||
hf, ag = hfp_connections
|
||||
|
||||
ag.dlc.write(b'\r\n+BIND: (1,2)\r\n\r\nOK\r\n')
|
||||
|
||||
await hf.execute_command("AT+BIND=?", response_type=hfp.AtResponseType.SINGLE)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.asyncio
|
||||
async def test_ag_batched_commands(
|
||||
hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
|
||||
):
|
||||
hf, ag = hfp_connections
|
||||
|
||||
answer_future = asyncio.get_running_loop().create_future()
|
||||
ag.on('answer', lambda: answer_future.set_result(None))
|
||||
|
||||
hang_up_future = asyncio.get_running_loop().create_future()
|
||||
ag.on('hang_up', lambda: hang_up_future.set_result(None))
|
||||
|
||||
hf.dlc.write(b'ATA\rAT+CHUP\r')
|
||||
|
||||
await answer_future
|
||||
await hang_up_future
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run():
|
||||
await test_slc()
|
||||
|
||||
@@ -28,26 +28,18 @@ from bumble.avdtp import (
|
||||
AVDTP_AUDIO_MEDIA_TYPE,
|
||||
Listener,
|
||||
MediaCodecCapabilities,
|
||||
MediaPacket,
|
||||
Protocol,
|
||||
)
|
||||
from bumble.a2dp import (
|
||||
make_audio_sink_service_sdp_records,
|
||||
MPEG_2_AAC_LC_OBJECT_TYPE,
|
||||
A2DP_SBC_CODEC_TYPE,
|
||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
SBC_MONO_CHANNEL_MODE,
|
||||
SBC_DUAL_CHANNEL_MODE,
|
||||
SBC_SNR_ALLOCATION_METHOD,
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
SBC_STEREO_CHANNEL_MODE,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
SbcMediaCodecInformation,
|
||||
AacMediaCodecInformation,
|
||||
)
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.codecs import AacAudioRtpPacket
|
||||
from bumble.hci import HCI_Reset_Command
|
||||
from bumble.rtp import MediaPacket
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -72,7 +64,7 @@ class AudioExtractor:
|
||||
# -----------------------------------------------------------------------------
|
||||
class AacAudioExtractor:
|
||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
||||
return AacAudioRtpPacket(packet.payload).to_adts()
|
||||
return AacAudioRtpPacket.from_bytes(packet.payload).to_adts()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -130,10 +122,12 @@ class Speaker:
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||
media_codec_information=AacMediaCodecInformation.from_lists(
|
||||
object_types=[MPEG_2_AAC_LC_OBJECT_TYPE],
|
||||
sampling_frequencies=[48000, 44100],
|
||||
channels=[1, 2],
|
||||
media_codec_information=AacMediaCodecInformation(
|
||||
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||
channels=AacMediaCodecInformation.Channels.MONO
|
||||
| AacMediaCodecInformation.Channels.STEREO,
|
||||
vbr=1,
|
||||
bitrate=256000,
|
||||
),
|
||||
@@ -143,20 +137,23 @@ class Speaker:
|
||||
return MediaCodecCapabilities(
|
||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
||||
channel_modes=[
|
||||
SBC_MONO_CHANNEL_MODE,
|
||||
SBC_DUAL_CHANNEL_MODE,
|
||||
SBC_STEREO_CHANNEL_MODE,
|
||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
||||
],
|
||||
block_lengths=[4, 8, 12, 16],
|
||||
subbands=[4, 8],
|
||||
allocation_methods=[
|
||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
||||
SBC_SNR_ALLOCATION_METHOD,
|
||||
],
|
||||
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,
|
||||
minimum_bitpool_value=2,
|
||||
maximum_bitpool_value=53,
|
||||
),
|
||||
@@ -282,9 +279,6 @@ class Speaker:
|
||||
mitm=False
|
||||
)
|
||||
|
||||
# Start the controller
|
||||
await self.device.power_on()
|
||||
|
||||
# Listen for Bluetooth connections
|
||||
self.device.on('connection', self.on_bluetooth_connection)
|
||||
|
||||
@@ -295,6 +289,9 @@ class Speaker:
|
||||
self.avdtp_listener = Listener.for_device(self.device)
|
||||
self.avdtp_listener.on('connection', self.on_avdtp_connection)
|
||||
|
||||
# Start the controller
|
||||
await self.device.power_on()
|
||||
|
||||
print(f'Speaker ready to play, codec={self.codec}')
|
||||
|
||||
if connect_address:
|
||||
|
||||
Reference in New Issue
Block a user