forked from auracaster/bumble_mirror
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
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
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
|
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
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
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" ]
|
rust-version: [ "1.76.0", "stable" ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -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
|
||||||
+25
-27
@@ -44,25 +44,18 @@ from bumble.avdtp import (
|
|||||||
AVDTP_AUDIO_MEDIA_TYPE,
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
Listener,
|
Listener,
|
||||||
MediaCodecCapabilities,
|
MediaCodecCapabilities,
|
||||||
MediaPacket,
|
|
||||||
Protocol,
|
Protocol,
|
||||||
)
|
)
|
||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
MPEG_2_AAC_LC_OBJECT_TYPE,
|
|
||||||
make_audio_sink_service_sdp_records,
|
make_audio_sink_service_sdp_records,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
A2DP_MPEG_2_4_AAC_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,
|
SbcMediaCodecInformation,
|
||||||
AacMediaCodecInformation,
|
AacMediaCodecInformation,
|
||||||
)
|
)
|
||||||
from bumble.utils import AsyncRunner
|
from bumble.utils import AsyncRunner
|
||||||
from bumble.codecs import AacAudioRtpPacket
|
from bumble.codecs import AacAudioRtpPacket
|
||||||
|
from bumble.rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -93,7 +86,7 @@ class AudioExtractor:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AacAudioExtractor:
|
class AacAudioExtractor:
|
||||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
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(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
media_codec_information=AacMediaCodecInformation.from_lists(
|
media_codec_information=AacMediaCodecInformation(
|
||||||
object_types=[MPEG_2_AAC_LC_OBJECT_TYPE],
|
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||||
sampling_frequencies=[48000, 44100],
|
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channels=[1, 2],
|
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||||
|
channels=AacMediaCodecInformation.Channels.MONO
|
||||||
|
| AacMediaCodecInformation.Channels.STEREO,
|
||||||
vbr=1,
|
vbr=1,
|
||||||
bitrate=256000,
|
bitrate=256000,
|
||||||
),
|
),
|
||||||
@@ -464,20 +459,23 @@ class Speaker:
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
SBC_MONO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
| 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,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
|
|||||||
+504
-204
@@ -17,12 +17,16 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
|
||||||
import struct
|
|
||||||
import logging
|
|
||||||
from collections.abc import AsyncGenerator
|
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 .company_ids import COMPANY_IDENTIFIERS
|
||||||
from .sdp import (
|
from .sdp import (
|
||||||
DataElement,
|
DataElement,
|
||||||
@@ -42,6 +46,7 @@ from .core import (
|
|||||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||||
name_or_number,
|
name_or_number,
|
||||||
)
|
)
|
||||||
|
from .rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -103,6 +108,8 @@ SBC_ALLOCATION_METHOD_NAMES = {
|
|||||||
SBC_LOUDNESS_ALLOCATION_METHOD: 'SBC_LOUDNESS_ALLOCATION_METHOD'
|
SBC_LOUDNESS_ALLOCATION_METHOD: 'SBC_LOUDNESS_ALLOCATION_METHOD'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SBC_MAX_FRAMES_IN_RTP_PAYLOAD = 15
|
||||||
|
|
||||||
MPEG_2_4_AAC_SAMPLING_FREQUENCIES = [
|
MPEG_2_4_AAC_SAMPLING_FREQUENCIES = [
|
||||||
8000,
|
8000,
|
||||||
11025,
|
11025,
|
||||||
@@ -130,6 +137,9 @@ MPEG_2_4_OBJECT_TYPE_NAMES = {
|
|||||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE'
|
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OPUS_MAX_FRAMES_IN_RTP_PAYLOAD = 15
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
@@ -257,38 +267,61 @@ class SbcMediaCodecInformation:
|
|||||||
A2DP spec - 4.3.2 Codec Specific Information Elements
|
A2DP spec - 4.3.2 Codec Specific Information Elements
|
||||||
'''
|
'''
|
||||||
|
|
||||||
sampling_frequency: int
|
sampling_frequency: SamplingFrequency
|
||||||
channel_mode: int
|
channel_mode: ChannelMode
|
||||||
block_length: int
|
block_length: BlockLength
|
||||||
subbands: int
|
subbands: Subbands
|
||||||
allocation_method: int
|
allocation_method: AllocationMethod
|
||||||
minimum_bitpool_value: int
|
minimum_bitpool_value: int
|
||||||
maximum_bitpool_value: int
|
maximum_bitpool_value: int
|
||||||
|
|
||||||
SAMPLING_FREQUENCY_BITS = {16000: 1 << 3, 32000: 1 << 2, 44100: 1 << 1, 48000: 1}
|
class SamplingFrequency(enum.IntFlag):
|
||||||
CHANNEL_MODE_BITS = {
|
SF_16000 = 1 << 3
|
||||||
SBC_MONO_CHANNEL_MODE: 1 << 3,
|
SF_32000 = 1 << 2
|
||||||
SBC_DUAL_CHANNEL_MODE: 1 << 2,
|
SF_44100 = 1 << 1
|
||||||
SBC_STEREO_CHANNEL_MODE: 1 << 1,
|
SF_48000 = 1 << 0
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_bytes(data: bytes) -> SbcMediaCodecInformation:
|
def from_int(cls, sampling_frequency: int) -> Self:
|
||||||
sampling_frequency = (data[0] >> 4) & 0x0F
|
sampling_frequencies = [
|
||||||
channel_mode = (data[0] >> 0) & 0x0F
|
16000,
|
||||||
block_length = (data[1] >> 4) & 0x0F
|
32000,
|
||||||
subbands = (data[1] >> 2) & 0x03
|
44100,
|
||||||
allocation_method = (data[1] >> 0) & 0x03
|
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
|
minimum_bitpool_value = (data[2] >> 0) & 0xFF
|
||||||
maximum_bitpool_value = (data[3] >> 0) & 0xFF
|
maximum_bitpool_value = (data[3] >> 0) & 0xFF
|
||||||
return SbcMediaCodecInformation(
|
return cls(
|
||||||
sampling_frequency,
|
sampling_frequency,
|
||||||
channel_mode,
|
channel_mode,
|
||||||
block_length,
|
block_length,
|
||||||
@@ -298,52 +331,6 @@ class SbcMediaCodecInformation:
|
|||||||
maximum_bitpool_value,
|
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:
|
def __bytes__(self) -> bytes:
|
||||||
return 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
|
@dataclasses.dataclass
|
||||||
@@ -381,83 +351,66 @@ class AacMediaCodecInformation:
|
|||||||
A2DP spec - 4.5.2 Codec Specific Information Elements
|
A2DP spec - 4.5.2 Codec Specific Information Elements
|
||||||
'''
|
'''
|
||||||
|
|
||||||
object_type: int
|
object_type: ObjectType
|
||||||
sampling_frequency: int
|
sampling_frequency: SamplingFrequency
|
||||||
channels: int
|
channels: Channels
|
||||||
rfa: int
|
|
||||||
vbr: int
|
vbr: int
|
||||||
bitrate: int
|
bitrate: int
|
||||||
|
|
||||||
OBJECT_TYPE_BITS = {
|
class ObjectType(enum.IntFlag):
|
||||||
MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
|
MPEG_2_AAC_LC = 1 << 7
|
||||||
MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
|
MPEG_4_AAC_LC = 1 << 6
|
||||||
MPEG_4_AAC_LTP_OBJECT_TYPE: 1 << 5,
|
MPEG_4_AAC_LTP = 1 << 5
|
||||||
MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 1 << 4,
|
MPEG_4_AAC_SCALABLE = 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}
|
|
||||||
|
|
||||||
@staticmethod
|
class SamplingFrequency(enum.IntFlag):
|
||||||
def from_bytes(data: bytes) -> AacMediaCodecInformation:
|
SF_8000 = 1 << 11
|
||||||
object_type = data[0]
|
SF_11025 = 1 << 10
|
||||||
sampling_frequency = (data[1] << 4) | ((data[2] >> 4) & 0x0F)
|
SF_12000 = 1 << 9
|
||||||
channels = (data[2] >> 2) & 0x03
|
SF_16000 = 1 << 8
|
||||||
rfa = 0
|
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
|
vbr = (data[3] >> 7) & 0x01
|
||||||
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
bitrate = ((data[3] & 0x7F) << 16) | (data[4] << 8) | data[5]
|
||||||
return AacMediaCodecInformation(
|
return AacMediaCodecInformation(
|
||||||
object_type, sampling_frequency, channels, rfa, vbr, bitrate
|
object_type, sampling_frequency, channels, 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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __bytes__(self) -> bytes:
|
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
|
@dataclasses.dataclass
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -514,7 +443,7 @@ class VendorSpecificMediaCodecInformation:
|
|||||||
return VendorSpecificMediaCodecInformation(vendor_id, codec_id, data[6:])
|
return VendorSpecificMediaCodecInformation(vendor_id, codec_id, data[6:])
|
||||||
|
|
||||||
def __bytes__(self) -> bytes:
|
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:
|
def __str__(self) -> str:
|
||||||
# pylint: disable=line-too-long
|
# 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
|
@dataclasses.dataclass
|
||||||
class SbcFrame:
|
class SbcFrame:
|
||||||
sampling_frequency: int
|
sampling_frequency: int
|
||||||
block_count: int
|
block_count: int
|
||||||
channel_mode: int
|
channel_mode: int
|
||||||
|
allocation_method: int
|
||||||
subband_count: int
|
subband_count: int
|
||||||
|
bitpool: int
|
||||||
payload: bytes
|
payload: bytes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -553,8 +538,10 @@ class SbcFrame:
|
|||||||
return (
|
return (
|
||||||
f'SBC(sf={self.sampling_frequency},'
|
f'SBC(sf={self.sampling_frequency},'
|
||||||
f'cm={self.channel_mode},'
|
f'cm={self.channel_mode},'
|
||||||
|
f'am={self.allocation_method},'
|
||||||
f'br={self.bitrate},'
|
f'br={self.bitrate},'
|
||||||
f'sc={self.sample_count},'
|
f'sc={self.sample_count},'
|
||||||
|
f'bp={self.bitpool},'
|
||||||
f'size={len(self.payload)})'
|
f'size={len(self.payload)})'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -583,6 +570,7 @@ class SbcParser:
|
|||||||
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
blocks = 4 * (1 + ((header[1] >> 4) & 3))
|
||||||
channel_mode = (header[1] >> 2) & 3
|
channel_mode = (header[1] >> 2) & 3
|
||||||
channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2
|
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
|
subbands = 8 if ((header[1]) & 1) else 4
|
||||||
bitpool = header[2]
|
bitpool = header[2]
|
||||||
|
|
||||||
@@ -602,7 +590,13 @@ class SbcParser:
|
|||||||
|
|
||||||
# Emit the next frame
|
# Emit the next frame
|
||||||
yield SbcFrame(
|
yield SbcFrame(
|
||||||
sampling_frequency, blocks, channel_mode, subbands, payload
|
sampling_frequency,
|
||||||
|
blocks,
|
||||||
|
channel_mode,
|
||||||
|
allocation_method,
|
||||||
|
subbands,
|
||||||
|
bitpool,
|
||||||
|
payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
return generate_frames()
|
return generate_frames()
|
||||||
@@ -610,21 +604,15 @@ class SbcParser:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class SbcPacketSource:
|
class SbcPacketSource:
|
||||||
def __init__(
|
def __init__(self, read: Callable[[int], Awaitable[bytes]], mtu: int) -> None:
|
||||||
self, read: Callable[[int], Awaitable[bytes]], mtu: int, codec_capabilities
|
|
||||||
) -> None:
|
|
||||||
self.read = read
|
self.read = read
|
||||||
self.mtu = mtu
|
self.mtu = mtu
|
||||||
self.codec_capabilities = codec_capabilities
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def packets(self):
|
def packets(self):
|
||||||
async def generate_packets():
|
async def generate_packets():
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from .avdtp import MediaPacket # Import here to avoid a circular reference
|
|
||||||
|
|
||||||
sequence_number = 0
|
sequence_number = 0
|
||||||
timestamp = 0
|
sample_count = 0
|
||||||
frames = []
|
frames = []
|
||||||
frames_size = 0
|
frames_size = 0
|
||||||
max_rtp_payload = self.mtu - 12 - 1
|
max_rtp_payload = self.mtu - 12 - 1
|
||||||
@@ -632,29 +620,29 @@ class SbcPacketSource:
|
|||||||
# NOTE: this doesn't support frame fragments
|
# NOTE: this doesn't support frame fragments
|
||||||
sbc_parser = SbcParser(self.read)
|
sbc_parser = SbcParser(self.read)
|
||||||
async for frame in sbc_parser.frames:
|
async for frame in sbc_parser.frames:
|
||||||
print(frame)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
frames_size + len(frame.payload) > max_rtp_payload
|
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
|
# Need to flush what has been accumulated so far
|
||||||
|
logger.debug(f"yielding {len(frames)} frames")
|
||||||
|
|
||||||
# Emit a packet
|
# Emit a packet
|
||||||
sbc_payload = bytes([len(frames)]) + b''.join(
|
sbc_payload = bytes([len(frames) & 0x0F]) + b''.join(
|
||||||
[frame.payload for frame in frames]
|
[frame.payload for frame in frames]
|
||||||
)
|
)
|
||||||
|
timestamp_seconds = sample_count / frame.sampling_frequency
|
||||||
|
timestamp = int(1000 * timestamp_seconds)
|
||||||
packet = MediaPacket(
|
packet = MediaPacket(
|
||||||
2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload
|
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
|
yield packet
|
||||||
|
|
||||||
# Prepare for next packets
|
# Prepare for next packets
|
||||||
sequence_number += 1
|
sequence_number += 1
|
||||||
sequence_number &= 0xFFFF
|
sequence_number &= 0xFFFF
|
||||||
timestamp += sum((frame.sample_count for frame in frames))
|
sample_count += sum((frame.sample_count for frame in frames))
|
||||||
timestamp &= 0xFFFFFFFF
|
|
||||||
frames = [frame]
|
frames = [frame]
|
||||||
frames_size = len(frame.payload)
|
frames_size = len(frame.payload)
|
||||||
else:
|
else:
|
||||||
@@ -663,3 +651,315 @@ class SbcPacketSource:
|
|||||||
frames_size += len(frame.payload)
|
frames_size += len(frame.payload)
|
||||||
|
|
||||||
return generate_packets()
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -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):
|
class ATT_Execute_Write_Request(ATT_PDU):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ Vol 3, Part F - 3.4.6.3 Execute Write Request
|
See Bluetooth spec @ Vol 3, Part F - 3.4.6.3 Execute Write Request
|
||||||
|
|||||||
+3
-2
@@ -119,7 +119,7 @@ class Frame:
|
|||||||
# Not supported
|
# Not supported
|
||||||
raise NotImplementedError("extended subunit types not supported")
|
raise NotImplementedError("extended subunit types not supported")
|
||||||
|
|
||||||
if subunit_id < 5:
|
if subunit_id < 5 or subunit_id == 7:
|
||||||
opcode_offset = 2
|
opcode_offset = 2
|
||||||
elif subunit_id == 5:
|
elif subunit_id == 5:
|
||||||
# Extended to the next byte
|
# Extended to the next byte
|
||||||
@@ -132,9 +132,10 @@ class Frame:
|
|||||||
else:
|
else:
|
||||||
subunit_id = 5 + extension
|
subunit_id = 5 + extension
|
||||||
opcode_offset = 3
|
opcode_offset = 3
|
||||||
|
|
||||||
elif subunit_id == 6:
|
elif subunit_id == 6:
|
||||||
raise core.InvalidPacketError("reserved subunit ID")
|
raise core.InvalidPacketError("reserved subunit ID")
|
||||||
|
else:
|
||||||
|
raise core.InvalidPacketError("invalid subunit ID")
|
||||||
|
|
||||||
opcode = Frame.OperationCode(data[opcode_offset])
|
opcode = Frame.OperationCode(data[opcode_offset])
|
||||||
operands = data[opcode_offset + 1 :]
|
operands = data[opcode_offset + 1 :]
|
||||||
|
|||||||
+54
-97
@@ -17,12 +17,10 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import struct
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import enum
|
import enum
|
||||||
import warnings
|
import warnings
|
||||||
from pyee import EventEmitter
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
@@ -39,6 +37,8 @@ from typing import (
|
|||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from .core import (
|
from .core import (
|
||||||
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE,
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
@@ -51,13 +51,16 @@ from .a2dp import (
|
|||||||
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
A2DP_NON_A2DP_CODEC_TYPE,
|
A2DP_NON_A2DP_CODEC_TYPE,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
|
A2DP_VENDOR_MEDIA_CODEC_INFORMATION_CLASSES,
|
||||||
AacMediaCodecInformation,
|
AacMediaCodecInformation,
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
VendorSpecificMediaCodecInformation,
|
VendorSpecificMediaCodecInformation,
|
||||||
)
|
)
|
||||||
|
from .rtp import MediaPacket
|
||||||
from . import sdp, device, l2cap
|
from . import sdp, device, l2cap
|
||||||
from .colors import color
|
from .colors import color
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -278,95 +281,6 @@ class RealtimeClock:
|
|||||||
await asyncio.sleep(duration)
|
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:
|
class MediaPacketPump:
|
||||||
pump_task: Optional[asyncio.Task]
|
pump_task: Optional[asyncio.Task]
|
||||||
@@ -377,6 +291,7 @@ class MediaPacketPump:
|
|||||||
self.packets = packets
|
self.packets = packets
|
||||||
self.clock = clock
|
self.clock = clock
|
||||||
self.pump_task = None
|
self.pump_task = None
|
||||||
|
self.completed = asyncio.Event()
|
||||||
|
|
||||||
async def start(self, rtp_channel: l2cap.ClassicChannel) -> None:
|
async def start(self, rtp_channel: l2cap.ClassicChannel) -> None:
|
||||||
async def pump_packets():
|
async def pump_packets():
|
||||||
@@ -406,6 +321,8 @@ class MediaPacketPump:
|
|||||||
)
|
)
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
logger.debug('pump canceled')
|
logger.debug('pump canceled')
|
||||||
|
finally:
|
||||||
|
self.completed.set()
|
||||||
|
|
||||||
# Pump packets
|
# Pump packets
|
||||||
self.pump_task = asyncio.create_task(pump_packets())
|
self.pump_task = asyncio.create_task(pump_packets())
|
||||||
@@ -417,6 +334,9 @@ class MediaPacketPump:
|
|||||||
await self.pump_task
|
await self.pump_task
|
||||||
self.pump_task = None
|
self.pump_task = None
|
||||||
|
|
||||||
|
async def wait_for_completion(self) -> None:
|
||||||
|
await self.completed.wait()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class MessageAssembler:
|
class MessageAssembler:
|
||||||
@@ -615,11 +535,25 @@ class MediaCodecCapabilities(ServiceCapabilities):
|
|||||||
self.media_codec_information
|
self.media_codec_information
|
||||||
)
|
)
|
||||||
elif self.media_codec_type == A2DP_NON_A2DP_CODEC_TYPE:
|
elif self.media_codec_type == A2DP_NON_A2DP_CODEC_TYPE:
|
||||||
self.media_codec_information = (
|
vendor_media_codec_information = (
|
||||||
VendorSpecificMediaCodecInformation.from_bytes(
|
VendorSpecificMediaCodecInformation.from_bytes(
|
||||||
self.media_codec_information
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -1316,10 +1250,20 @@ class Protocol(EventEmitter):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def add_source(
|
def add_source(
|
||||||
self, codec_capabilities: MediaCodecCapabilities, packet_pump: MediaPacketPump
|
self,
|
||||||
|
codec_capabilities: MediaCodecCapabilities,
|
||||||
|
packet_pump: MediaPacketPump,
|
||||||
|
delay_reporting: bool = False,
|
||||||
) -> LocalSource:
|
) -> LocalSource:
|
||||||
seid = len(self.local_endpoints) + 1
|
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)
|
self.local_endpoints.append(source)
|
||||||
|
|
||||||
return source
|
return source
|
||||||
@@ -1372,7 +1316,7 @@ class Protocol(EventEmitter):
|
|||||||
return self.remote_endpoints.values()
|
return self.remote_endpoints.values()
|
||||||
|
|
||||||
def find_remote_sink_by_codec(
|
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]:
|
) -> Optional[DiscoveredStreamEndPoint]:
|
||||||
for endpoint in self.remote_endpoints.values():
|
for endpoint in self.remote_endpoints.values():
|
||||||
if (
|
if (
|
||||||
@@ -1397,7 +1341,19 @@ class Protocol(EventEmitter):
|
|||||||
codec_capabilities.media_type == AVDTP_AUDIO_MEDIA_TYPE
|
codec_capabilities.media_type == AVDTP_AUDIO_MEDIA_TYPE
|
||||||
and codec_capabilities.media_codec_type == codec_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:
|
if has_media_transport and has_codec:
|
||||||
return endpoint
|
return endpoint
|
||||||
|
|
||||||
@@ -2180,12 +2136,13 @@ class LocalSource(LocalStreamEndPoint):
|
|||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
seid: int,
|
seid: int,
|
||||||
codec_capabilities: MediaCodecCapabilities,
|
codec_capabilities: MediaCodecCapabilities,
|
||||||
|
other_capabilitiles: Iterable[ServiceCapabilities],
|
||||||
packet_pump: MediaPacketPump,
|
packet_pump: MediaPacketPump,
|
||||||
) -> None:
|
) -> None:
|
||||||
capabilities = [
|
capabilities = [
|
||||||
ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY),
|
ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY),
|
||||||
codec_capabilities,
|
codec_capabilities,
|
||||||
]
|
] + list(other_capabilitiles)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
protocol,
|
protocol,
|
||||||
seid,
|
seid,
|
||||||
|
|||||||
+48
-29
@@ -1491,10 +1491,14 @@ class Protocol(pyee.EventEmitter):
|
|||||||
f"<<< AVCTP Command, transaction_label={transaction_label}: " f"{command}"
|
f"<<< AVCTP Command, transaction_label={transaction_label}: " f"{command}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only the PANEL subunit type with subunit ID 0 is supported in this profile.
|
# Only addressing the unit, or the PANEL subunit with subunit ID 0 is supported
|
||||||
if (
|
# in this profile.
|
||||||
command.subunit_type != avc.Frame.SubunitType.PANEL
|
if not (
|
||||||
or command.subunit_id != 0
|
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")
|
logger.debug("subunit not supported")
|
||||||
self.send_not_implemented_response(transaction_label, command)
|
self.send_not_implemented_response(transaction_label, command)
|
||||||
@@ -1528,8 +1532,8 @@ class Protocol(pyee.EventEmitter):
|
|||||||
# TODO: delegate
|
# TODO: delegate
|
||||||
response = avc.PassThroughResponseFrame(
|
response = avc.PassThroughResponseFrame(
|
||||||
avc.ResponseFrame.ResponseCode.ACCEPTED,
|
avc.ResponseFrame.ResponseCode.ACCEPTED,
|
||||||
avc.Frame.SubunitType.PANEL,
|
command.subunit_type,
|
||||||
0,
|
command.subunit_id,
|
||||||
command.state_flag,
|
command.state_flag,
|
||||||
command.operation_id,
|
command.operation_id,
|
||||||
command.operation_data,
|
command.operation_data,
|
||||||
@@ -1846,6 +1850,15 @@ class Protocol(pyee.EventEmitter):
|
|||||||
RejectedResponse(pdu_id, status_code),
|
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(
|
def _on_get_capabilities_command(
|
||||||
self, transaction_label: int, command: GetCapabilitiesCommand
|
self, transaction_label: int, command: GetCapabilitiesCommand
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1891,29 +1904,35 @@ class Protocol(pyee.EventEmitter):
|
|||||||
async def register_notification():
|
async def register_notification():
|
||||||
# Check if the event is supported.
|
# Check if the event is supported.
|
||||||
supported_events = await self.delegate.get_supported_events()
|
supported_events = await self.delegate.get_supported_events()
|
||||||
if command.event_id in supported_events:
|
if command.event_id not in supported_events:
|
||||||
if command.event_id == EventId.VOLUME_CHANGED:
|
logger.debug("event not supported")
|
||||||
volume = await self.delegate.get_absolute_volume()
|
self.send_not_implemented_avrcp_response(
|
||||||
response = RegisterNotificationResponse(VolumeChangedEvent(volume))
|
transaction_label, self.PduId.REGISTER_NOTIFICATION
|
||||||
self.send_avrcp_response(
|
)
|
||||||
transaction_label,
|
return
|
||||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
|
||||||
response,
|
|
||||||
)
|
|
||||||
self._register_notification_listener(transaction_label, command)
|
|
||||||
return
|
|
||||||
|
|
||||||
if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
|
if command.event_id == EventId.VOLUME_CHANGED:
|
||||||
# TODO: testing only, use delegate
|
volume = await self.delegate.get_absolute_volume()
|
||||||
response = RegisterNotificationResponse(
|
response = RegisterNotificationResponse(VolumeChangedEvent(volume))
|
||||||
PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
|
self.send_avrcp_response(
|
||||||
)
|
transaction_label,
|
||||||
self.send_avrcp_response(
|
avc.ResponseFrame.ResponseCode.INTERIM,
|
||||||
transaction_label,
|
response,
|
||||||
avc.ResponseFrame.ResponseCode.INTERIM,
|
)
|
||||||
response,
|
self._register_notification_listener(transaction_label, command)
|
||||||
)
|
return
|
||||||
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())
|
self._delegate_command(transaction_label, command, register_notification())
|
||||||
|
|||||||
+218
-72
@@ -17,6 +17,7 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
from bumble import core
|
from bumble import core
|
||||||
|
|
||||||
@@ -101,12 +102,40 @@ class BitReader:
|
|||||||
break
|
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:
|
class AacAudioRtpPacket:
|
||||||
"""AAC payload encapsulated in an RTP packet payload"""
|
"""AAC payload encapsulated in an RTP packet payload"""
|
||||||
|
|
||||||
|
audio_mux_element: AudioMuxElement
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def latm_value(reader: BitReader) -> int:
|
def read_latm_value(reader: BitReader) -> int:
|
||||||
bytes_for_value = reader.read(2)
|
bytes_for_value = reader.read(2)
|
||||||
value = 0
|
value = 0
|
||||||
for _ in range(bytes_for_value + 1):
|
for _ in range(bytes_for_value + 1):
|
||||||
@@ -114,24 +143,33 @@ class AacAudioRtpPacket:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def program_config_element(reader: BitReader):
|
def read_audio_object_type(reader: BitReader):
|
||||||
raise core.InvalidPacketError('program_config_element not supported')
|
# 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
|
@dataclass
|
||||||
class GASpecificConfig:
|
class GASpecificConfig:
|
||||||
def __init__(
|
audio_object_type: int
|
||||||
self, reader: BitReader, channel_configuration: int, audio_object_type: int
|
# NOTE: other fields not supported
|
||||||
) -> None:
|
|
||||||
|
@classmethod
|
||||||
|
def from_bits(
|
||||||
|
cls, reader: BitReader, channel_configuration: int, audio_object_type: int
|
||||||
|
) -> Self:
|
||||||
# GASpecificConfig - ISO/EIC 14496-3 Table 4.1
|
# GASpecificConfig - ISO/EIC 14496-3 Table 4.1
|
||||||
frame_length_flag = reader.read(1)
|
frame_length_flag = reader.read(1)
|
||||||
depends_on_core_coder = reader.read(1)
|
depends_on_core_coder = reader.read(1)
|
||||||
if depends_on_core_coder:
|
if depends_on_core_coder:
|
||||||
self.core_coder_delay = reader.read(14)
|
core_coder_delay = reader.read(14)
|
||||||
extension_flag = reader.read(1)
|
extension_flag = reader.read(1)
|
||||||
if not channel_configuration:
|
if not channel_configuration:
|
||||||
AacAudioRtpPacket.program_config_element(reader)
|
raise core.InvalidPacketError('program_config_element not supported')
|
||||||
if audio_object_type in (6, 20):
|
if audio_object_type in (6, 20):
|
||||||
self.layer_nr = reader.read(3)
|
layer_nr = reader.read(3)
|
||||||
if extension_flag:
|
if extension_flag:
|
||||||
if audio_object_type == 22:
|
if audio_object_type == 22:
|
||||||
num_of_sub_frame = reader.read(5)
|
num_of_sub_frame = reader.read(5)
|
||||||
@@ -144,14 +182,13 @@ class AacAudioRtpPacket:
|
|||||||
if extension_flag_3 == 1:
|
if extension_flag_3 == 1:
|
||||||
raise core.InvalidPacketError('extensionFlag3 == 1 not supported')
|
raise core.InvalidPacketError('extensionFlag3 == 1 not supported')
|
||||||
|
|
||||||
@staticmethod
|
return cls(audio_object_type)
|
||||||
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 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
|
@dataclass
|
||||||
class AudioSpecificConfig:
|
class AudioSpecificConfig:
|
||||||
@@ -159,6 +196,7 @@ class AacAudioRtpPacket:
|
|||||||
sampling_frequency_index: int
|
sampling_frequency_index: int
|
||||||
sampling_frequency: int
|
sampling_frequency: int
|
||||||
channel_configuration: int
|
channel_configuration: int
|
||||||
|
ga_specific_config: AacAudioRtpPacket.GASpecificConfig
|
||||||
sbr_present_flag: int
|
sbr_present_flag: int
|
||||||
ps_present_flag: int
|
ps_present_flag: int
|
||||||
extension_audio_object_type: int
|
extension_audio_object_type: int
|
||||||
@@ -182,44 +220,73 @@ class AacAudioRtpPacket:
|
|||||||
7350,
|
7350,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, reader: BitReader) -> None:
|
@classmethod
|
||||||
# AudioSpecificConfig - ISO/EIC 14496-3 Table 1.15
|
def for_simple_aac(
|
||||||
self.audio_object_type = AacAudioRtpPacket.audio_object_type(reader)
|
cls,
|
||||||
self.sampling_frequency_index = reader.read(4)
|
audio_object_type: int,
|
||||||
if self.sampling_frequency_index == 0xF:
|
sampling_frequency: int,
|
||||||
self.sampling_frequency = reader.read(24)
|
channel_configuration: int,
|
||||||
else:
|
) -> Self:
|
||||||
self.sampling_frequency = self.SAMPLING_FREQUENCIES[
|
if sampling_frequency not in cls.SAMPLING_FREQUENCIES:
|
||||||
self.sampling_frequency_index
|
raise ValueError(f'invalid sampling frequency {sampling_frequency}')
|
||||||
]
|
|
||||||
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
|
|
||||||
|
|
||||||
if self.audio_object_type in (1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23):
|
ga_specific_config = AacAudioRtpPacket.GASpecificConfig(audio_object_type)
|
||||||
ga_specific_config = AacAudioRtpPacket.GASpecificConfig(
|
|
||||||
reader, self.channel_configuration, self.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:
|
else:
|
||||||
raise core.InvalidPacketError(
|
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:
|
# 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_sampling_frequency = self.SAMPLING_FREQUENCIES[self.extension_sampling_frequency_index]
|
||||||
# self.extension_channel_configuration = reader.read(4)
|
# 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
|
@dataclass
|
||||||
class StreamMuxConfig:
|
class StreamMuxConfig:
|
||||||
other_data_present: int
|
other_data_present: int
|
||||||
other_data_len_bits: int
|
other_data_len_bits: int
|
||||||
audio_specific_config: AacAudioRtpPacket.AudioSpecificConfig
|
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
|
# StreamMuxConfig - ISO/EIC 14496-3 Table 1.42
|
||||||
audio_mux_version = reader.read(1)
|
audio_mux_version = reader.read(1)
|
||||||
if audio_mux_version == 1:
|
if audio_mux_version == 1:
|
||||||
@@ -264,7 +362,7 @@ class AacAudioRtpPacket:
|
|||||||
if audio_mux_version_a != 0:
|
if audio_mux_version_a != 0:
|
||||||
raise core.InvalidPacketError('audioMuxVersionA != 0 not supported')
|
raise core.InvalidPacketError('audioMuxVersionA != 0 not supported')
|
||||||
if audio_mux_version == 1:
|
if audio_mux_version == 1:
|
||||||
tara_buffer_fullness = AacAudioRtpPacket.latm_value(reader)
|
tara_buffer_fullness = AacAudioRtpPacket.read_latm_value(reader)
|
||||||
stream_cnt = 0
|
stream_cnt = 0
|
||||||
all_streams_same_time_framing = reader.read(1)
|
all_streams_same_time_framing = reader.read(1)
|
||||||
num_sub_frames = reader.read(6)
|
num_sub_frames = reader.read(6)
|
||||||
@@ -275,13 +373,13 @@ class AacAudioRtpPacket:
|
|||||||
if num_layer != 0:
|
if num_layer != 0:
|
||||||
raise core.InvalidPacketError('num_layer != 0 not supported')
|
raise core.InvalidPacketError('num_layer != 0 not supported')
|
||||||
if audio_mux_version == 0:
|
if audio_mux_version == 0:
|
||||||
self.audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig(
|
audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig.from_bits(
|
||||||
reader
|
reader
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
asc_len = AacAudioRtpPacket.latm_value(reader)
|
asc_len = AacAudioRtpPacket.read_latm_value(reader)
|
||||||
marker = reader.bit_position
|
marker = reader.bit_position
|
||||||
self.audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig(
|
audio_specific_config = AacAudioRtpPacket.AudioSpecificConfig.from_bits(
|
||||||
reader
|
reader
|
||||||
)
|
)
|
||||||
audio_specific_config_len = reader.bit_position - marker
|
audio_specific_config_len = reader.bit_position - marker
|
||||||
@@ -299,36 +397,49 @@ class AacAudioRtpPacket:
|
|||||||
f'frame_length_type {frame_length_type} not supported'
|
f'frame_length_type {frame_length_type} not supported'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.other_data_present = reader.read(1)
|
other_data_present = reader.read(1)
|
||||||
if self.other_data_present:
|
other_data_len_bits = 0
|
||||||
|
if other_data_present:
|
||||||
if audio_mux_version == 1:
|
if audio_mux_version == 1:
|
||||||
self.other_data_len_bits = AacAudioRtpPacket.latm_value(reader)
|
other_data_len_bits = AacAudioRtpPacket.read_latm_value(reader)
|
||||||
else:
|
else:
|
||||||
self.other_data_len_bits = 0
|
|
||||||
while True:
|
while True:
|
||||||
self.other_data_len_bits *= 256
|
other_data_len_bits *= 256
|
||||||
other_data_len_esc = reader.read(1)
|
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:
|
if other_data_len_esc == 0:
|
||||||
break
|
break
|
||||||
crc_check_present = reader.read(1)
|
crc_check_present = reader.read(1)
|
||||||
if crc_check_present:
|
if crc_check_present:
|
||||||
crc_checksum = reader.read(8)
|
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
|
@dataclass
|
||||||
class AudioMuxElement:
|
class AudioMuxElement:
|
||||||
payload: bytes
|
|
||||||
stream_mux_config: AacAudioRtpPacket.StreamMuxConfig
|
stream_mux_config: AacAudioRtpPacket.StreamMuxConfig
|
||||||
|
payload: bytes
|
||||||
|
|
||||||
def __init__(self, reader: BitReader, mux_config_present: int):
|
@classmethod
|
||||||
if mux_config_present == 0:
|
def from_bits(cls, reader: BitReader) -> Self:
|
||||||
raise core.InvalidPacketError('muxConfigPresent == 0 not supported')
|
|
||||||
|
|
||||||
# AudioMuxElement - ISO/EIC 14496-3 Table 1.41
|
# AudioMuxElement - ISO/EIC 14496-3 Table 1.41
|
||||||
|
# (only supports mux_config_present=1)
|
||||||
use_same_stream_mux = reader.read(1)
|
use_same_stream_mux = reader.read(1)
|
||||||
if use_same_stream_mux:
|
if use_same_stream_mux:
|
||||||
raise core.InvalidPacketError('useSameStreamMux == 1 not supported')
|
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:
|
# We only support:
|
||||||
# allStreamsSameTimeFraming == 1
|
# allStreamsSameTimeFraming == 1
|
||||||
@@ -344,19 +455,46 @@ class AacAudioRtpPacket:
|
|||||||
if tmp != 255:
|
if tmp != 255:
|
||||||
break
|
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:
|
if stream_mux_config.other_data_present:
|
||||||
reader.skip(self.stream_mux_config.other_data_len_bits)
|
reader.skip(stream_mux_config.other_data_len_bits)
|
||||||
|
|
||||||
# ByteAlign
|
# ByteAlign
|
||||||
while reader.bit_position % 8:
|
while reader.bit_position % 8:
|
||||||
reader.read(1)
|
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
|
# Parse the bit stream
|
||||||
reader = BitReader(data)
|
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):
|
def to_adts(self):
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
@@ -383,3 +521,11 @@ class AacAudioRtpPacket:
|
|||||||
)
|
)
|
||||||
+ self.audio_mux_element.payload
|
+ 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)
|
||||||
|
|||||||
+19
-11
@@ -1571,14 +1571,22 @@ class Connection(CompositeEventEmitter):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
if self.transport == BT_LE_TRANSPORT:
|
||||||
f'Connection(handle=0x{self.handle:04X}, '
|
return (
|
||||||
f'role={self.role_name}, '
|
f'Connection(transport=LE, handle=0x{self.handle:04X}, '
|
||||||
f'self_address={self.self_address}, '
|
f'role={self.role_name}, '
|
||||||
f'self_resolvable_address={self.self_resolvable_address}, '
|
f'self_address={self.self_address}, '
|
||||||
f'peer_address={self.peer_address}, '
|
f'self_resolvable_address={self.self_resolvable_address}, '
|
||||||
f'peer_resolvable_address={self.peer_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):
|
class Device(CompositeEventEmitter):
|
||||||
# Incomplete list of fields.
|
# Incomplete list of fields.
|
||||||
random_address: Address # Random address that may change with RPA
|
random_address: Address # Random private address that may change periodically
|
||||||
public_address: Address # Public address (obtained from the controller)
|
public_address: Address # Public address that is globally unique (from controller)
|
||||||
static_address: Address # Random address that can be set but does not change
|
static_address: Address # Random static address that does not change once set
|
||||||
classic_enabled: bool
|
classic_enabled: bool
|
||||||
name: str
|
name: str
|
||||||
class_of_device: int
|
class_of_device: int
|
||||||
|
|||||||
@@ -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(
|
@HCI_Event.event(
|
||||||
[
|
[
|
||||||
|
|||||||
+45
-41
@@ -795,29 +795,32 @@ class HfProtocol(pyee.EventEmitter):
|
|||||||
# Append to the read buffer.
|
# Append to the read buffer.
|
||||||
self.read_buffer.extend(data)
|
self.read_buffer.extend(data)
|
||||||
|
|
||||||
# Locate header and trailer.
|
while self.read_buffer:
|
||||||
header = self.read_buffer.find(b'\r\n')
|
# Locate header and trailer.
|
||||||
trailer = self.read_buffer.find(b'\r\n', header + 2)
|
header = self.read_buffer.find(b'\r\n')
|
||||||
if header == -1 or trailer == -1:
|
trailer = self.read_buffer.find(b'\r\n', header + 2)
|
||||||
return
|
if header == -1 or trailer == -1:
|
||||||
|
return
|
||||||
|
|
||||||
# Isolate the AT response code and parameters.
|
# Isolate the AT response code and parameters.
|
||||||
raw_response = self.read_buffer[header + 2 : trailer]
|
raw_response = self.read_buffer[header + 2 : trailer]
|
||||||
response = AtResponse.parse_from(raw_response)
|
response = AtResponse.parse_from(raw_response)
|
||||||
logger.debug(f"<<< {raw_response.decode()}")
|
logger.debug(f"<<< {raw_response.decode()}")
|
||||||
|
|
||||||
# Consume the response bytes.
|
# Consume the response bytes.
|
||||||
self.read_buffer = self.read_buffer[trailer + 2 :]
|
self.read_buffer = self.read_buffer[trailer + 2 :]
|
||||||
|
|
||||||
# Forward the received code to the correct queue.
|
# Forward the received code to the correct queue.
|
||||||
if self.command_lock.locked() and (
|
if self.command_lock.locked() and (
|
||||||
response.code in STATUS_CODES or response.code in RESPONSE_CODES
|
response.code in STATUS_CODES or response.code in RESPONSE_CODES
|
||||||
):
|
):
|
||||||
self.response_queue.put_nowait(response)
|
self.response_queue.put_nowait(response)
|
||||||
elif response.code in UNSOLICITED_CODES:
|
elif response.code in UNSOLICITED_CODES:
|
||||||
self.unsolicited_queue.put_nowait(response)
|
self.unsolicited_queue.put_nowait(response)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"dropping unexpected response with code '{response.code}'")
|
logger.warning(
|
||||||
|
f"dropping unexpected response with code '{response.code}'"
|
||||||
|
)
|
||||||
|
|
||||||
async def execute_command(
|
async def execute_command(
|
||||||
self,
|
self,
|
||||||
@@ -1244,31 +1247,32 @@ class AgProtocol(pyee.EventEmitter):
|
|||||||
# Append to the read buffer.
|
# Append to the read buffer.
|
||||||
self.read_buffer.extend(data)
|
self.read_buffer.extend(data)
|
||||||
|
|
||||||
# Locate the trailer.
|
while self.read_buffer:
|
||||||
trailer = self.read_buffer.find(b'\r')
|
# Locate the trailer.
|
||||||
if trailer == -1:
|
trailer = self.read_buffer.find(b'\r')
|
||||||
return
|
if trailer == -1:
|
||||||
|
return
|
||||||
|
|
||||||
# Isolate the AT response code and parameters.
|
# Isolate the AT response code and parameters.
|
||||||
raw_command = self.read_buffer[:trailer]
|
raw_command = self.read_buffer[:trailer]
|
||||||
command = AtCommand.parse_from(raw_command)
|
command = AtCommand.parse_from(raw_command)
|
||||||
logger.debug(f"<<< {raw_command.decode()}")
|
logger.debug(f"<<< {raw_command.decode()}")
|
||||||
|
|
||||||
# Consume the response bytes.
|
# Consume the response bytes.
|
||||||
self.read_buffer = self.read_buffer[trailer + 1 :]
|
self.read_buffer = self.read_buffer[trailer + 1 :]
|
||||||
|
|
||||||
if command.sub_code == AtCommand.SubCode.TEST:
|
if command.sub_code == AtCommand.SubCode.TEST:
|
||||||
handler_name = f'_on_{command.code.lower()}_test'
|
handler_name = f'_on_{command.code.lower()}_test'
|
||||||
elif command.sub_code == AtCommand.SubCode.READ:
|
elif command.sub_code == AtCommand.SubCode.READ:
|
||||||
handler_name = f'_on_{command.code.lower()}_read'
|
handler_name = f'_on_{command.code.lower()}_read'
|
||||||
else:
|
else:
|
||||||
handler_name = f'_on_{command.code.lower()}'
|
handler_name = f'_on_{command.code.lower()}'
|
||||||
|
|
||||||
if handler := getattr(self, handler_name, None):
|
if handler := getattr(self, handler_name, None):
|
||||||
handler(*command.parameters)
|
handler(*command.parameters)
|
||||||
else:
|
else:
|
||||||
logger.warning('Handler %s not found', handler_name)
|
logger.warning('Handler %s not found', handler_name)
|
||||||
self.send_response('ERROR')
|
self.send_response('ERROR')
|
||||||
|
|
||||||
def send_response(self, response: str) -> None:
|
def send_response(self, response: str) -> None:
|
||||||
"""Sends an AT response."""
|
"""Sends an AT response."""
|
||||||
|
|||||||
@@ -1106,6 +1106,18 @@ class Host(AbortableEventEmitter):
|
|||||||
event.status,
|
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):
|
def on_hci_link_supervision_timeout_changed_event(self, event):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1911,6 +1911,7 @@ class ChannelManager:
|
|||||||
data = sum(1 << cid for cid in self.fixed_channels).to_bytes(8, 'little')
|
data = sum(1 << cid for cid in self.fixed_channels).to_bytes(8, 'little')
|
||||||
else:
|
else:
|
||||||
result = L2CAP_Information_Response.NOT_SUPPORTED
|
result = L2CAP_Information_Response.NOT_SUPPORTED
|
||||||
|
data = b''
|
||||||
|
|
||||||
self.send_control_frame(
|
self.send_control_frame(
|
||||||
connection,
|
connection,
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ class LocalLink:
|
|||||||
elif transport == BT_BR_EDR_TRANSPORT:
|
elif transport == BT_BR_EDR_TRANSPORT:
|
||||||
destination_controller = self.find_classic_controller(destination_address)
|
destination_controller = self.find_classic_controller(destination_address)
|
||||||
source_address = sender_controller.public_address
|
source_address = sender_controller.public_address
|
||||||
|
else:
|
||||||
|
raise ValueError("unsupported transport type")
|
||||||
|
|
||||||
if destination_controller is not None:
|
if destination_controller is not None:
|
||||||
destination_controller.on_link_acl_data(source_address, transport, data)
|
destination_controller.on_link_acl_data(source_address, transport, data)
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ class CodecSpecificCapabilities:
|
|||||||
supported_max_codec_frames_per_sdu = value
|
supported_max_codec_frames_per_sdu = value
|
||||||
|
|
||||||
# It is expected here that if some fields are missing, an error should be raised.
|
# 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(
|
return CodecSpecificCapabilities(
|
||||||
supported_sampling_frequencies=supported_sampling_frequencies,
|
supported_sampling_frequencies=supported_sampling_frequencies,
|
||||||
supported_frame_durations=supported_frame_durations,
|
supported_frame_durations=supported_frame_durations,
|
||||||
@@ -426,6 +427,7 @@ class CodecSpecificConfiguration:
|
|||||||
codec_frames_per_sdu = value
|
codec_frames_per_sdu = value
|
||||||
|
|
||||||
# It is expected here that if some fields are missing, an error should be raised.
|
# 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(
|
return CodecSpecificConfiguration(
|
||||||
sampling_frequency=sampling_frequency,
|
sampling_frequency=sampling_frequency,
|
||||||
frame_duration=frame_duration,
|
frame_duration=frame_duration,
|
||||||
|
|||||||
+27
-18
@@ -25,7 +25,7 @@ from bumble.utils import AsyncRunner, OpenIntEnum
|
|||||||
from bumble.hci import Address
|
from bumble.hci import Address
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import logging
|
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:
|
def on_disconnection(_reason) -> None:
|
||||||
self.currently_connected_clients.remove(connection)
|
self.currently_connected_clients.remove(connection)
|
||||||
|
|
||||||
# TODO Should we filter on device bonded && device is HAP ?
|
@connection.on('pairing') # type: ignore
|
||||||
self.currently_connected_clients.add(connection)
|
def on_pairing(*_: Any) -> None:
|
||||||
if (
|
self.on_incoming_paired_connection(connection)
|
||||||
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:
|
if connection.peer_resolvable_address:
|
||||||
# Send all the PresetChangedOperation that occur when not connected
|
self.on_incoming_paired_connection(connection)
|
||||||
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())
|
|
||||||
|
|
||||||
self.hearing_aid_features_characteristic = gatt.Characteristic(
|
self.hearing_aid_features_characteristic = gatt.Characteristic(
|
||||||
uuid=gatt.GATT_HEARING_AID_FEATURES_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(
|
def _on_read_active_preset_index(
|
||||||
self, __connection__: Optional[Connection]
|
self, __connection__: Optional[Connection]
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
|
|||||||
+110
@@ -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:
|
if size != 1:
|
||||||
raise InvalidArgumentError('boolean must be 1 byte')
|
raise InvalidArgumentError('boolean must be 1 byte')
|
||||||
size_index = 0
|
size_index = 0
|
||||||
|
else:
|
||||||
|
raise RuntimeError("internal error - self.type not supported")
|
||||||
|
|
||||||
self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data
|
self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data
|
||||||
return self.bytes
|
return self.bytes
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ import atexit
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import platform
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import grpc.aio
|
import grpc.aio
|
||||||
|
|
||||||
from .common import (
|
import bumble
|
||||||
|
from bumble.transport.common import (
|
||||||
ParserSource,
|
ParserSource,
|
||||||
PumpedTransport,
|
PumpedTransport,
|
||||||
PumpedPacketSource,
|
PumpedPacketSource,
|
||||||
@@ -36,15 +38,15 @@ from .common import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=no-name-in-module
|
# pylint: disable=no-name-in-module
|
||||||
from .grpc_protobuf.packet_streamer_pb2_grpc import (
|
from .grpc_protobuf.netsim.packet_streamer_pb2_grpc import (
|
||||||
PacketStreamerStub,
|
PacketStreamerStub,
|
||||||
PacketStreamerServicer,
|
PacketStreamerServicer,
|
||||||
add_PacketStreamerServicer_to_server,
|
add_PacketStreamerServicer_to_server,
|
||||||
)
|
)
|
||||||
from .grpc_protobuf.packet_streamer_pb2 import PacketRequest, PacketResponse
|
from .grpc_protobuf.netsim.packet_streamer_pb2 import PacketRequest, PacketResponse
|
||||||
from .grpc_protobuf.hci_packet_pb2 import HCIPacket
|
from .grpc_protobuf.netsim.hci_packet_pb2 import HCIPacket
|
||||||
from .grpc_protobuf.startup_pb2 import Chip, ChipInfo
|
from .grpc_protobuf.netsim.startup_pb2 import Chip, ChipInfo, DeviceInfo
|
||||||
from .grpc_protobuf.common_pb2 import ChipKind
|
from .grpc_protobuf.netsim.common_pb2 import ChipKind
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -58,6 +60,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
DEFAULT_NAME = 'bumble0'
|
DEFAULT_NAME = 'bumble0'
|
||||||
DEFAULT_MANUFACTURER = 'Bumble'
|
DEFAULT_MANUFACTURER = 'Bumble'
|
||||||
|
DEFAULT_VARIANT = ''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -199,7 +202,6 @@ async def open_android_netsim_controller_transport(
|
|||||||
data = (
|
data = (
|
||||||
bytes([request.hci_packet.packet_type]) + request.hci_packet.packet
|
bytes([request.hci_packet.packet_type]) + request.hci_packet.packet
|
||||||
)
|
)
|
||||||
logger.debug(f'<<< PACKET: {data.hex()}')
|
|
||||||
self.on_data_received(data)
|
self.on_data_received(data)
|
||||||
|
|
||||||
async def send_packet(self, 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
|
# Check that we don't already have a device
|
||||||
if self.device:
|
if self.device:
|
||||||
logger.debug('busy, already serving a device')
|
logger.debug('Busy, already serving a device')
|
||||||
return PacketResponse(error='Busy')
|
return PacketResponse(error='Busy')
|
||||||
|
|
||||||
# Instantiate a new device
|
# Instantiate a new device
|
||||||
@@ -312,16 +314,24 @@ async def open_android_netsim_host_transport_with_channel(
|
|||||||
):
|
):
|
||||||
# Wrapper for I/O operations
|
# Wrapper for I/O operations
|
||||||
class HciDevice:
|
class HciDevice:
|
||||||
def __init__(self, name, manufacturer, hci_device):
|
def __init__(self, name, variant, manufacturer, hci_device):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.variant = variant
|
||||||
self.manufacturer = manufacturer
|
self.manufacturer = manufacturer
|
||||||
self.hci_device = hci_device
|
self.hci_device = hci_device
|
||||||
|
|
||||||
async def start(self): # Send the startup info
|
async def start(self): # Send the startup info
|
||||||
chip_info = ChipInfo(
|
device_info = DeviceInfo(
|
||||||
name=self.name,
|
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}')
|
logger.debug(f'Sending chip info to netsim: {chip_info}')
|
||||||
await self.hci_device.write(PacketRequest(initial_info=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)
|
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
|
manufacturer = DEFAULT_MANUFACTURER
|
||||||
|
|
||||||
# Connect as a host
|
# Connect as a host
|
||||||
service = PacketStreamerStub(channel)
|
service = PacketStreamerStub(channel)
|
||||||
hci_device = HciDevice(
|
hci_device = HciDevice(
|
||||||
name=name,
|
name=name,
|
||||||
|
variant=variant,
|
||||||
manufacturer=manufacturer,
|
manufacturer=manufacturer,
|
||||||
hci_device=service.StreamPackets(),
|
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
|
The "chip" name, used to identify the "chip" instance. This
|
||||||
may be useful when several clients are connected, since each needs to use a
|
may be useful when several clients are connected, since each needs to use a
|
||||||
different name.
|
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:
|
In `controller` mode:
|
||||||
The <host>:<port> part is required. <host> may be the address of a local network
|
The <host>:<port> part is required. <host> may be the address of a local network
|
||||||
|
|||||||
@@ -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)
|
|
||||||
+9
-8
@@ -1,11 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
# 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."""
|
"""Generated protocol buffer code."""
|
||||||
from google.protobuf.internal import builder as _builder
|
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||||
from google.protobuf import symbol_database as _symbol_database
|
from google.protobuf import symbol_database as _symbol_database
|
||||||
|
from google.protobuf.internal import builder as _builder
|
||||||
# @@protoc_insertion_point(imports)
|
# @@protoc_insertion_point(imports)
|
||||||
|
|
||||||
_sym_db = _symbol_database.Default()
|
_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())
|
_globals = globals()
|
||||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'common_pb2', globals())
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||||
|
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'netsim.common_pb2', _globals)
|
||||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||||
|
|
||||||
DESCRIPTOR._options = None
|
DESCRIPTOR._options = None
|
||||||
_CHIPKIND._serialized_start=31
|
_globals['_CHIPKIND']._serialized_start=38
|
||||||
_CHIPKIND._serialized_end=92
|
_globals['_CHIPKIND']._serialized_end=121
|
||||||
# @@protoc_insertion_point(module_scope)
|
# @@protoc_insertion_point(module_scope)
|
||||||
+11
-5
@@ -2,11 +2,17 @@ from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
|||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from typing import ClassVar as _ClassVar
|
from typing import ClassVar as _ClassVar
|
||||||
|
|
||||||
BLUETOOTH: ChipKind
|
|
||||||
DESCRIPTOR: _descriptor.FileDescriptor
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
UNSPECIFIED: ChipKind
|
|
||||||
UWB: ChipKind
|
|
||||||
WIFI: ChipKind
|
|
||||||
|
|
||||||
class ChipKind(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
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
|
||||||
@@ -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)
|
||||||
+15
-9
@@ -6,17 +6,23 @@ from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
|
|||||||
DESCRIPTOR: _descriptor.FileDescriptor
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
class HCIPacket(_message.Message):
|
class HCIPacket(_message.Message):
|
||||||
__slots__ = ["packet", "packet_type"]
|
__slots__ = ("packet_type", "packet")
|
||||||
class PacketType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
class PacketType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||||
__slots__ = []
|
__slots__ = ()
|
||||||
ACL: HCIPacket.PacketType
|
HCI_PACKET_UNSPECIFIED: _ClassVar[HCIPacket.PacketType]
|
||||||
COMMAND: HCIPacket.PacketType
|
COMMAND: _ClassVar[HCIPacket.PacketType]
|
||||||
EVENT: HCIPacket.PacketType
|
ACL: _ClassVar[HCIPacket.PacketType]
|
||||||
|
SCO: _ClassVar[HCIPacket.PacketType]
|
||||||
|
EVENT: _ClassVar[HCIPacket.PacketType]
|
||||||
|
ISO: _ClassVar[HCIPacket.PacketType]
|
||||||
HCI_PACKET_UNSPECIFIED: HCIPacket.PacketType
|
HCI_PACKET_UNSPECIFIED: HCIPacket.PacketType
|
||||||
ISO: HCIPacket.PacketType
|
COMMAND: HCIPacket.PacketType
|
||||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
ACL: HCIPacket.PacketType
|
||||||
PACKET_TYPE_FIELD_NUMBER: _ClassVar[int]
|
|
||||||
SCO: 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_type: HCIPacket.PacketType
|
||||||
|
packet: bytes
|
||||||
def __init__(self, packet_type: _Optional[_Union[HCIPacket.PacketType, str]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
def __init__(self, packet_type: _Optional[_Union[HCIPacket.PacketType, str]] = ..., packet: _Optional[bytes] = ...) -> None: ...
|
||||||
File diff suppressed because one or more lines are too long
@@ -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: ...
|
||||||
@@ -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)
|
||||||
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
from . import hci_packet_pb2 as _hci_packet_pb2
|
from bumble.transport.grpc_protobuf.netsim import hci_packet_pb2 as _hci_packet_pb2
|
||||||
from . import startup_pb2 as _startup_pb2
|
from bumble.transport.grpc_protobuf.netsim import startup_pb2 as _startup_pb2
|
||||||
from google.protobuf import descriptor as _descriptor
|
from google.protobuf import descriptor as _descriptor
|
||||||
from google.protobuf import message as _message
|
from google.protobuf import message as _message
|
||||||
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
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
|
DESCRIPTOR: _descriptor.FileDescriptor
|
||||||
|
|
||||||
class PacketRequest(_message.Message):
|
class PacketRequest(_message.Message):
|
||||||
__slots__ = ["hci_packet", "initial_info", "packet"]
|
__slots__ = ("initial_info", "hci_packet", "packet")
|
||||||
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
|
||||||
INITIAL_INFO_FIELD_NUMBER: _ClassVar[int]
|
INITIAL_INFO_FIELD_NUMBER: _ClassVar[int]
|
||||||
|
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
hci_packet: _hci_packet_pb2.HCIPacket
|
|
||||||
initial_info: _startup_pb2.ChipInfo
|
initial_info: _startup_pb2.ChipInfo
|
||||||
|
hci_packet: _hci_packet_pb2.HCIPacket
|
||||||
packet: bytes
|
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: ...
|
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):
|
class PacketResponse(_message.Message):
|
||||||
__slots__ = ["error", "hci_packet", "packet"]
|
__slots__ = ("error", "hci_packet", "packet")
|
||||||
ERROR_FIELD_NUMBER: _ClassVar[int]
|
ERROR_FIELD_NUMBER: _ClassVar[int]
|
||||||
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
HCI_PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
PACKET_FIELD_NUMBER: _ClassVar[int]
|
PACKET_FIELD_NUMBER: _ClassVar[int]
|
||||||
+7
-7
@@ -2,7 +2,7 @@
|
|||||||
"""Client and server classes corresponding to protobuf-defined services."""
|
"""Client and server classes corresponding to protobuf-defined services."""
|
||||||
import grpc
|
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):
|
class PacketStreamerStub(object):
|
||||||
@@ -30,8 +30,8 @@ class PacketStreamerStub(object):
|
|||||||
"""
|
"""
|
||||||
self.StreamPackets = channel.stream_stream(
|
self.StreamPackets = channel.stream_stream(
|
||||||
'/netsim.packet.PacketStreamer/StreamPackets',
|
'/netsim.packet.PacketStreamer/StreamPackets',
|
||||||
request_serializer=packet__streamer__pb2.PacketRequest.SerializeToString,
|
request_serializer=netsim_dot_packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||||
response_deserializer=packet__streamer__pb2.PacketResponse.FromString,
|
response_deserializer=netsim_dot_packet__streamer__pb2.PacketResponse.FromString,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@ def add_PacketStreamerServicer_to_server(servicer, server):
|
|||||||
rpc_method_handlers = {
|
rpc_method_handlers = {
|
||||||
'StreamPackets': grpc.stream_stream_rpc_method_handler(
|
'StreamPackets': grpc.stream_stream_rpc_method_handler(
|
||||||
servicer.StreamPackets,
|
servicer.StreamPackets,
|
||||||
request_deserializer=packet__streamer__pb2.PacketRequest.FromString,
|
request_deserializer=netsim_dot_packet__streamer__pb2.PacketRequest.FromString,
|
||||||
response_serializer=packet__streamer__pb2.PacketResponse.SerializeToString,
|
response_serializer=netsim_dot_packet__streamer__pb2.PacketResponse.SerializeToString,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
generic_handler = grpc.method_handlers_generic_handler(
|
generic_handler = grpc.method_handlers_generic_handler(
|
||||||
@@ -103,7 +103,7 @@ class PacketStreamer(object):
|
|||||||
timeout=None,
|
timeout=None,
|
||||||
metadata=None):
|
metadata=None):
|
||||||
return grpc.experimental.stream_stream(request_iterator, target, '/netsim.packet.PacketStreamer/StreamPackets',
|
return grpc.experimental.stream_stream(request_iterator, target, '/netsim.packet.PacketStreamer/StreamPackets',
|
||||||
packet__streamer__pb2.PacketRequest.SerializeToString,
|
netsim_dot_packet__streamer__pb2.PacketRequest.SerializeToString,
|
||||||
packet__streamer__pb2.PacketResponse.FromString,
|
netsim_dot_packet__streamer__pb2.PacketResponse.FromString,
|
||||||
options, channel_credentials,
|
options, channel_credentials,
|
||||||
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||||
@@ -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)
|
||||||
@@ -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`
|
## `run_advertiser.py`
|
||||||
An app that runs a simple device that just advertises (BLE).
|
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`
|
## `run_classic_connect.py`
|
||||||
An app that connects to a Bluetooth Classic device and prints its services.
|
An app that connects to a Bluetooth Classic device and prints its services.
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ GETTING STARTED WITH BUMBLE
|
|||||||
|
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
|
|
||||||
You need Python 3.8 or above. Python >= 3.9 is recommended, but 3.8 should be sufficient if
|
You need Python 3.9 or above.
|
||||||
necessary (there may be some optional functionality that will not work on some platforms with
|
|
||||||
python 3.8).
|
|
||||||
Visit the [Python site](https://www.python.org/) for instructions on how to install Python
|
Visit the [Python site](https://www.python.org/) for instructions on how to install Python
|
||||||
for your platform.
|
for your platform.
|
||||||
Throughout the documentation, when shell commands are shown, it is assumed that you can
|
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.
|
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
|
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:
|
For platform-specific information, see the following pages:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,6 +4,6 @@ channels:
|
|||||||
- conda-forge
|
- conda-forge
|
||||||
dependencies:
|
dependencies:
|
||||||
- pip=23
|
- pip=23
|
||||||
- python=3.8
|
- python=3.9
|
||||||
- pip:
|
- pip:
|
||||||
- --editable .[development,documentation,test]
|
- --editable .[development,documentation,test]
|
||||||
|
|||||||
+17
-14
@@ -61,20 +61,23 @@ def codec_capabilities():
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
SBC_MONO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
| 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,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
|
|||||||
+10
-16
@@ -33,8 +33,6 @@ from bumble.avdtp import (
|
|||||||
Listener,
|
Listener,
|
||||||
)
|
)
|
||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
|
||||||
make_audio_source_service_sdp_records,
|
make_audio_source_service_sdp_records,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
SbcMediaCodecInformation,
|
SbcMediaCodecInformation,
|
||||||
@@ -59,12 +57,12 @@ def codec_capabilities():
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_discrete_values(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequency=44100,
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||||
channel_mode=SBC_JOINT_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
block_length=16,
|
block_length=SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
subbands=8,
|
subbands=SbcMediaCodecInformation.Subbands.S_8,
|
||||||
allocation_method=SBC_LOUDNESS_ALLOCATION_METHOD,
|
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
@@ -73,11 +71,9 @@ def codec_capabilities():
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def on_avdtp_connection(read_function, protocol):
|
def on_avdtp_connection(read_function, protocol):
|
||||||
packet_source = SbcPacketSource(
|
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu)
|
||||||
read_function, protocol.l2cap_channel.peer_mtu, codec_capabilities()
|
|
||||||
)
|
|
||||||
packet_pump = MediaPacketPump(packet_source.packets)
|
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}')
|
print(f'### Selected sink: {sink.seid}')
|
||||||
|
|
||||||
# Stream the packets
|
# Stream the packets
|
||||||
packet_source = SbcPacketSource(
|
packet_source = SbcPacketSource(read_function, protocol.l2cap_channel.peer_mtu)
|
||||||
read_function, protocol.l2cap_channel.peer_mtu, codec_capabilities()
|
|
||||||
)
|
|
||||||
packet_pump = MediaPacketPump(packet_source.packets)
|
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)
|
stream = await protocol.create_stream(source, sink)
|
||||||
await stream.start()
|
await stream.start()
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|||||||
+17
-14
@@ -60,20 +60,23 @@ def codec_capabilities():
|
|||||||
return avdtp.MediaCodecCapabilities(
|
return avdtp.MediaCodecCapabilities(
|
||||||
media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=a2dp.A2DP_SBC_CODEC_TYPE,
|
media_codec_type=a2dp.A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=a2dp.SbcMediaCodecInformation.from_lists(
|
media_codec_information=a2dp.SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
a2dp.SBC_MONO_CHANNEL_MODE,
|
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
a2dp.SBC_DUAL_CHANNEL_MODE,
|
| a2dp.SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
a2dp.SBC_STEREO_CHANNEL_MODE,
|
channel_mode=a2dp.SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
a2dp.SBC_JOINT_STEREO_CHANNEL_MODE,
|
| a2dp.SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| a2dp.SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| a2dp.SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=a2dp.SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| a2dp.SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
a2dp.SBC_LOUDNESS_ALLOCATION_METHOD,
|
| a2dp.SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
a2dp.SBC_SNR_ALLOCATION_METHOD,
|
| 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,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -36,13 +36,10 @@ from bumble.transport import open_transport_or_link
|
|||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
print(
|
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>'
|
'<transport-spec-for-device-1> <transport-spec-for-device-2>'
|
||||||
)
|
)
|
||||||
print(
|
print('example: run_cig_setup.py device1.json hci-socket:0 hci-socket:1')
|
||||||
'example: run_cig_setup.py device1.json'
|
|
||||||
'tcp-client:127.0.0.1:6402 tcp-client:127.0.0.1:6402'
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print('<<< connecting to HCI...')
|
print('<<< connecting to HCI...')
|
||||||
@@ -65,18 +62,18 @@ async def main() -> None:
|
|||||||
advertising_set = await devices[0].create_advertising_set()
|
advertising_set = await devices[0].create_advertising_set()
|
||||||
|
|
||||||
connection = await devices[1].connect(
|
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]
|
cid_ids = [2, 3]
|
||||||
cis_handles = await devices[1].setup_cig(
|
cis_handles = await devices[1].setup_cig(
|
||||||
cig_id=1,
|
cig_id=1,
|
||||||
cis_id=cid_ids,
|
cis_id=cid_ids,
|
||||||
sdu_interval=(10000, 0),
|
sdu_interval=(10000, 255),
|
||||||
framing=0,
|
framing=0,
|
||||||
max_sdu=(120, 0),
|
max_sdu=(120, 0),
|
||||||
retransmission_number=13,
|
retransmission_number=13,
|
||||||
max_transport_latency=(100, 0),
|
max_transport_latency=(100, 5),
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_cis_request(
|
def on_cis_request(
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ async def main() -> None:
|
|||||||
[(AdvertisingData.COMPLETE_LOCAL_NAME, "Bumble 2".encode("utf-8"))]
|
[(AdvertisingData.COMPLETE_LOCAL_NAME, "Bumble 2".encode("utf-8"))]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=possibly-used-before-assignment
|
||||||
if device.host.number_of_supported_advertising_sets >= 2:
|
if device.host.number_of_supported_advertising_sets >= 2:
|
||||||
set2 = await device.create_advertising_set(
|
set2 = await device.create_advertising_set(
|
||||||
random_address=Address("F0:F0:F0:F0:F0:F1"),
|
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[
|
esco_parameters = hfp.ESCO_PARAMETERS[
|
||||||
hfp.DefaultCodecParameters.ESCO_CVSD_S4
|
hfp.DefaultCodecParameters.ESCO_CVSD_S4
|
||||||
]
|
]
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unknown active codec")
|
||||||
|
|
||||||
connection.abort_on(
|
connection.abort_on(
|
||||||
'disconnection',
|
'disconnection',
|
||||||
connection.device.send_command(
|
connection.device.send_command(
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ ignore="pandora" # FIXME: pylint does not support stubs yet:
|
|||||||
|
|
||||||
[tool.pylint.typecheck]
|
[tool.pylint.typecheck]
|
||||||
signature-mutators="AsyncRunner.run_in_task"
|
signature-mutators="AsyncRunner.run_in_task"
|
||||||
|
disable="not-callable"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
skip-string-normalization = true
|
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
|
PROTOC_OUT=bumble/transport/grpc_protobuf
|
||||||
|
|
||||||
proto_files=(common.proto packet_streamer.proto hci_packet.proto startup.proto)
|
netsim_proto_files=(netsim/common.proto netsim/packet_streamer.proto netsim/hci_packet.proto netsim/startup.proto netsim/model.proto)
|
||||||
for proto_file in "${proto_files[@]}"
|
for proto_file in "${netsim_proto_files[@]}"
|
||||||
do
|
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
|
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[@]}"
|
for python_file in "${python_files[@]}"
|
||||||
do
|
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
|
done
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ console_scripts =
|
|||||||
bumble-usb-probe = bumble.apps.usb_probe:main
|
bumble-usb-probe = bumble.apps.usb_probe:main
|
||||||
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
||||||
bumble-bench = bumble.apps.bench:main
|
bumble-bench = bumble.apps.bench:main
|
||||||
|
bumble-player = bumble.apps.player.player:main
|
||||||
bumble-speaker = bumble.apps.speaker.speaker:main
|
bumble-speaker = bumble.apps.speaker.speaker:main
|
||||||
bumble-pandora-server = bumble.apps.pandora_server:main
|
bumble-pandora-server = bumble.apps.pandora_server:main
|
||||||
bumble-rtk-util = bumble.tools.rtk_util:main
|
bumble-rtk-util = bumble.tools.rtk_util:main
|
||||||
@@ -91,9 +92,9 @@ development =
|
|||||||
grpcio-tools >= 1.62.1
|
grpcio-tools >= 1.62.1
|
||||||
invoke >= 1.7.3
|
invoke >= 1.7.3
|
||||||
mobly >= 1.12.2
|
mobly >= 1.12.2
|
||||||
mypy == 1.10.0
|
mypy == 1.12.0
|
||||||
nox >= 2022
|
nox >= 2022
|
||||||
pylint == 3.1.0
|
pylint == 3.3.1
|
||||||
pyyaml >= 6.0
|
pyyaml >= 6.0
|
||||||
types-appdirs >= 1.4.3
|
types-appdirs >= 1.4.3
|
||||||
types-invoke >= 1.7.3
|
types-invoke >= 1.7.3
|
||||||
|
|||||||
+146
-29
@@ -33,20 +33,16 @@ from bumble.avdtp import (
|
|||||||
Protocol,
|
Protocol,
|
||||||
Listener,
|
Listener,
|
||||||
MediaCodecCapabilities,
|
MediaCodecCapabilities,
|
||||||
MediaPacket,
|
|
||||||
AVDTP_AUDIO_MEDIA_TYPE,
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
AVDTP_TSEP_SNK,
|
AVDTP_TSEP_SNK,
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
)
|
)
|
||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
|
AacMediaCodecInformation,
|
||||||
|
OpusMediaCodecInformation,
|
||||||
SbcMediaCodecInformation,
|
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
|
# Logging
|
||||||
@@ -125,12 +121,12 @@ def source_codec_capabilities():
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_discrete_values(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequency=44100,
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||||
channel_mode=SBC_JOINT_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
block_length=16,
|
block_length=SbcMediaCodecInformation.BlockLength.BL_16,
|
||||||
subbands=8,
|
subbands=SbcMediaCodecInformation.Subbands.S_8,
|
||||||
allocation_method=SBC_LOUDNESS_ALLOCATION_METHOD,
|
allocation_method=SbcMediaCodecInformation.AllocationMethod.LOUDNESS,
|
||||||
minimum_bitpool_value=2,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
@@ -142,20 +138,23 @@ def sink_codec_capabilities():
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
SBC_MONO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
| 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,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
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_self_connection()
|
||||||
await test_source_sink_1()
|
await test_source_sink_1()
|
||||||
|
|
||||||
@@ -281,4 +398,4 @@ async def run_test_self():
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||||
asyncio.run(run_test_self())
|
asyncio.run(async_main())
|
||||||
|
|||||||
+1
-2
@@ -23,13 +23,12 @@ from bumble.avdtp import (
|
|||||||
AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY,
|
AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY,
|
||||||
AVDTP_SET_CONFIGURATION,
|
AVDTP_SET_CONFIGURATION,
|
||||||
Message,
|
Message,
|
||||||
MediaPacket,
|
|
||||||
Get_Capabilities_Response,
|
Get_Capabilities_Response,
|
||||||
Set_Configuration_Command,
|
Set_Configuration_Command,
|
||||||
Set_Configuration_Response,
|
|
||||||
ServiceCapabilities,
|
ServiceCapabilities,
|
||||||
MediaCodecCapabilities,
|
MediaCodecCapabilities,
|
||||||
)
|
)
|
||||||
|
from bumble.rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
+42
-2
@@ -15,8 +15,9 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
import random
|
||||||
import pytest
|
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')
|
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():
|
def test_aac_rtp():
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
packet_data = bytes.fromhex(
|
packet_data = bytes.fromhex(
|
||||||
'47fc0000b090800300202066000198000de120000000000000000000000000000000000000000000001c'
|
'47fc0000b090800300202066000198000de120000000000000000000000000000000000000000000001c'
|
||||||
)
|
)
|
||||||
packet = AacAudioRtpPacket(packet_data)
|
packet = AacAudioRtpPacket.from_bytes(packet_data)
|
||||||
adts = packet.to_adts()
|
adts = packet.to_adts()
|
||||||
assert adts == bytes.fromhex(
|
assert adts == bytes.fromhex(
|
||||||
'fff1508004fffc2066000198000de120000000000000000000000000000000000000000000001c'
|
'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__':
|
if __name__ == '__main__':
|
||||||
test_reader()
|
test_reader()
|
||||||
|
test_writer()
|
||||||
test_aac_rtp()
|
test_aac_rtp()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import sys
|
|||||||
from bumble import att, device
|
from bumble import att, device
|
||||||
from bumble.profiles import hap
|
from bumble.profiles import hap
|
||||||
from .test_utils import TwoDevices
|
from .test_utils import TwoDevices
|
||||||
|
from bumble.keys import PairingKeys
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -86,6 +87,10 @@ async def hap_client():
|
|||||||
devices.connections[0].encryption = 1 # type: ignore
|
devices.connections[0].encryption = 1 # type: ignore
|
||||||
devices.connections[1].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
|
peer = device.Peer(devices.connections[1]) # type: ignore
|
||||||
hap_client = await peer.discover_service_and_create_proxy(
|
hap_client = await peer.discover_service_and_create_proxy(
|
||||||
hap.HearingAccessServiceProxy
|
hap.HearingAccessServiceProxy
|
||||||
|
|||||||
@@ -569,6 +569,37 @@ async def test_sco_setup():
|
|||||||
await asyncio.gather(*sco_disconnection_futures)
|
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():
|
async def run():
|
||||||
await test_slc()
|
await test_slc()
|
||||||
|
|||||||
+28
-31
@@ -28,26 +28,18 @@ from bumble.avdtp import (
|
|||||||
AVDTP_AUDIO_MEDIA_TYPE,
|
AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
Listener,
|
Listener,
|
||||||
MediaCodecCapabilities,
|
MediaCodecCapabilities,
|
||||||
MediaPacket,
|
|
||||||
Protocol,
|
Protocol,
|
||||||
)
|
)
|
||||||
from bumble.a2dp import (
|
from bumble.a2dp import (
|
||||||
make_audio_sink_service_sdp_records,
|
make_audio_sink_service_sdp_records,
|
||||||
MPEG_2_AAC_LC_OBJECT_TYPE,
|
|
||||||
A2DP_SBC_CODEC_TYPE,
|
A2DP_SBC_CODEC_TYPE,
|
||||||
A2DP_MPEG_2_4_AAC_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,
|
SbcMediaCodecInformation,
|
||||||
AacMediaCodecInformation,
|
AacMediaCodecInformation,
|
||||||
)
|
)
|
||||||
from bumble.utils import AsyncRunner
|
|
||||||
from bumble.codecs import AacAudioRtpPacket
|
from bumble.codecs import AacAudioRtpPacket
|
||||||
from bumble.hci import HCI_Reset_Command
|
from bumble.hci import HCI_Reset_Command
|
||||||
|
from bumble.rtp import MediaPacket
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -72,7 +64,7 @@ class AudioExtractor:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AacAudioExtractor:
|
class AacAudioExtractor:
|
||||||
def extract_audio(self, packet: MediaPacket) -> bytes:
|
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(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
|
||||||
media_codec_information=AacMediaCodecInformation.from_lists(
|
media_codec_information=AacMediaCodecInformation(
|
||||||
object_types=[MPEG_2_AAC_LC_OBJECT_TYPE],
|
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
|
||||||
sampling_frequencies=[48000, 44100],
|
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channels=[1, 2],
|
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
|
||||||
|
channels=AacMediaCodecInformation.Channels.MONO
|
||||||
|
| AacMediaCodecInformation.Channels.STEREO,
|
||||||
vbr=1,
|
vbr=1,
|
||||||
bitrate=256000,
|
bitrate=256000,
|
||||||
),
|
),
|
||||||
@@ -143,20 +137,23 @@ class Speaker:
|
|||||||
return MediaCodecCapabilities(
|
return MediaCodecCapabilities(
|
||||||
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
media_type=AVDTP_AUDIO_MEDIA_TYPE,
|
||||||
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
media_codec_type=A2DP_SBC_CODEC_TYPE,
|
||||||
media_codec_information=SbcMediaCodecInformation.from_lists(
|
media_codec_information=SbcMediaCodecInformation(
|
||||||
sampling_frequencies=[48000, 44100, 32000, 16000],
|
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000
|
||||||
channel_modes=[
|
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
|
||||||
SBC_MONO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
|
||||||
SBC_DUAL_CHANNEL_MODE,
|
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
|
||||||
SBC_STEREO_CHANNEL_MODE,
|
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
|
||||||
SBC_JOINT_STEREO_CHANNEL_MODE,
|
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
|
||||||
],
|
| SbcMediaCodecInformation.ChannelMode.STEREO
|
||||||
block_lengths=[4, 8, 12, 16],
|
| SbcMediaCodecInformation.ChannelMode.JOINT_STEREO,
|
||||||
subbands=[4, 8],
|
block_length=SbcMediaCodecInformation.BlockLength.BL_4
|
||||||
allocation_methods=[
|
| SbcMediaCodecInformation.BlockLength.BL_8
|
||||||
SBC_LOUDNESS_ALLOCATION_METHOD,
|
| SbcMediaCodecInformation.BlockLength.BL_12
|
||||||
SBC_SNR_ALLOCATION_METHOD,
|
| 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,
|
minimum_bitpool_value=2,
|
||||||
maximum_bitpool_value=53,
|
maximum_bitpool_value=53,
|
||||||
),
|
),
|
||||||
@@ -282,9 +279,6 @@ class Speaker:
|
|||||||
mitm=False
|
mitm=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start the controller
|
|
||||||
await self.device.power_on()
|
|
||||||
|
|
||||||
# Listen for Bluetooth connections
|
# Listen for Bluetooth connections
|
||||||
self.device.on('connection', self.on_bluetooth_connection)
|
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 = Listener.for_device(self.device)
|
||||||
self.avdtp_listener.on('connection', self.on_avdtp_connection)
|
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}')
|
print(f'Speaker ready to play, codec={self.codec}')
|
||||||
|
|
||||||
if connect_address:
|
if connect_address:
|
||||||
|
|||||||
Reference in New Issue
Block a user