forked from auracaster/bumble_mirror
Compare commits
31 Commits
gbg/driver
...
v0.0.190
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2698d4534e | ||
|
|
bbcd64286a | ||
|
|
9140afbf8c | ||
|
|
90a682c71b | ||
|
|
e8737a8243 | ||
|
|
72fceca72e | ||
|
|
732294abbc | ||
|
|
dc1204531e | ||
|
|
962114379c | ||
|
|
e6913a3055 | ||
|
|
e21d122aef | ||
|
|
58d4ab913a | ||
|
|
76bca03fe3 | ||
|
|
f1e5c9e59e | ||
|
|
ec82242462 | ||
|
|
a4efdd3f3e | ||
|
|
69c6643bb8 | ||
|
|
b8214bf948 | ||
|
|
a9c62c44b3 | ||
|
|
7d0b4ef4e0 | ||
|
|
313340f1c6 | ||
|
|
e8ed69fb09 | ||
|
|
16d5cf6770 | ||
|
|
a2caf1deb2 | ||
|
|
01bfdd2c98 | ||
|
|
4a60df108a | ||
|
|
ad48109748 | ||
|
|
1ceeccbbc0 | ||
|
|
44c51c13ac | ||
|
|
35db4a4c93 | ||
|
|
6205199d7f |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,6 +6,8 @@ dist/
|
||||
docs/mkdocs/site
|
||||
test-results.xml
|
||||
__pycache__
|
||||
# Vim
|
||||
.*.sw*
|
||||
# generated by setuptools_scm
|
||||
bumble/_version.py
|
||||
.vscode/launch.json
|
||||
|
||||
@@ -509,9 +509,11 @@ class Ping:
|
||||
packet = struct.pack(
|
||||
'>bbI',
|
||||
PacketType.SEQUENCE,
|
||||
PACKET_FLAG_LAST
|
||||
if self.current_packet_index == self.tx_packet_count - 1
|
||||
else 0,
|
||||
(
|
||||
PACKET_FLAG_LAST
|
||||
if self.current_packet_index == self.tx_packet_count - 1
|
||||
else 0
|
||||
),
|
||||
self.current_packet_index,
|
||||
) + bytes(self.tx_packet_size - 6)
|
||||
logging.info(color(f'Sending packet {self.current_packet_index}', 'yellow'))
|
||||
@@ -1062,9 +1064,9 @@ class Central(Connection.Listener):
|
||||
|
||||
if self.phy not in (None, HCI_LE_1M_PHY):
|
||||
# Add an connections parameters entry for this PHY.
|
||||
self.connection_parameter_preferences[
|
||||
self.phy
|
||||
] = connection_parameter_preferences
|
||||
self.connection_parameter_preferences[self.phy] = (
|
||||
connection_parameter_preferences
|
||||
)
|
||||
else:
|
||||
self.connection_parameter_preferences = None
|
||||
|
||||
@@ -1232,6 +1234,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
||||
'cyan',
|
||||
)
|
||||
)
|
||||
|
||||
await self.connected.wait()
|
||||
logging.info(color('### Connected', 'cyan'))
|
||||
|
||||
@@ -1591,8 +1594,8 @@ def central(
|
||||
mode_factory = create_mode_factory(ctx, 'gatt-client')
|
||||
classic = ctx.obj['classic']
|
||||
|
||||
asyncio.run(
|
||||
Central(
|
||||
async def run_central():
|
||||
await Central(
|
||||
transport,
|
||||
peripheral_address,
|
||||
classic,
|
||||
@@ -1604,7 +1607,8 @@ def central(
|
||||
encrypt or authenticate,
|
||||
ctx.obj['extended_data_length'],
|
||||
).run()
|
||||
)
|
||||
|
||||
asyncio.run(run_central())
|
||||
|
||||
|
||||
@bench.command()
|
||||
@@ -1615,15 +1619,16 @@ def peripheral(ctx, transport):
|
||||
role_factory = create_role_factory(ctx, 'receiver')
|
||||
mode_factory = create_mode_factory(ctx, 'gatt-server')
|
||||
|
||||
asyncio.run(
|
||||
Peripheral(
|
||||
async def run_peripheral():
|
||||
await Peripheral(
|
||||
transport,
|
||||
ctx.obj['classic'],
|
||||
ctx.obj['extended_data_length'],
|
||||
role_factory,
|
||||
mode_factory,
|
||||
).run()
|
||||
)
|
||||
|
||||
asyncio.run(run_peripheral())
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -76,6 +76,7 @@ logger = logging.getLogger(__name__)
|
||||
# -----------------------------------------------------------------------------
|
||||
DEFAULT_UI_PORT = 7654
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class AudioExtractor:
|
||||
@staticmethod
|
||||
|
||||
@@ -24,6 +24,7 @@ from bumble.device import Device
|
||||
from bumble.keys import JsonKeyStore
|
||||
from bumble.transport import open_transport
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def unbond_with_keystore(keystore, address):
|
||||
if address is None:
|
||||
|
||||
@@ -652,7 +652,9 @@ class SbcPacketSource:
|
||||
|
||||
# Prepare for next packets
|
||||
sequence_number += 1
|
||||
sequence_number &= 0xFFFF
|
||||
timestamp += sum((frame.sample_count for frame in frames))
|
||||
timestamp &= 0xFFFFFFFF
|
||||
frames = [frame]
|
||||
frames_size = len(frame.payload)
|
||||
else:
|
||||
|
||||
@@ -655,7 +655,7 @@ class ATT_Write_Command(ATT_PDU):
|
||||
@ATT_PDU.subclass(
|
||||
[
|
||||
('attribute_handle', HANDLE_FIELD_SPEC),
|
||||
('attribute_value', '*')
|
||||
('attribute_value', '*'),
|
||||
# ('authentication_signature', 'TODO')
|
||||
]
|
||||
)
|
||||
|
||||
@@ -325,8 +325,8 @@ class MediaPacket:
|
||||
self.padding = padding
|
||||
self.extension = extension
|
||||
self.marker = marker
|
||||
self.sequence_number = sequence_number
|
||||
self.timestamp = timestamp
|
||||
self.sequence_number = sequence_number & 0xFFFF
|
||||
self.timestamp = timestamp & 0xFFFFFFFF
|
||||
self.ssrc = ssrc
|
||||
self.csrc_list = csrc_list
|
||||
self.payload_type = payload_type
|
||||
@@ -341,7 +341,12 @@ class MediaPacket:
|
||||
| len(self.csrc_list),
|
||||
self.marker << 7 | self.payload_type,
|
||||
]
|
||||
) + struct.pack('>HII', self.sequence_number, self.timestamp, self.ssrc)
|
||||
) + 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
|
||||
@@ -1545,9 +1550,10 @@ class Protocol(EventEmitter):
|
||||
|
||||
assert False # Should never reach this
|
||||
|
||||
async def get_capabilities(
|
||||
self, seid: int
|
||||
) -> Union[Get_Capabilities_Response, Get_All_Capabilities_Response,]:
|
||||
async def get_capabilities(self, seid: int) -> Union[
|
||||
Get_Capabilities_Response,
|
||||
Get_All_Capabilities_Response,
|
||||
]:
|
||||
if self.version > (1, 2):
|
||||
return await self.send_command(Get_All_Capabilities_Command(seid))
|
||||
|
||||
|
||||
@@ -1745,9 +1745,11 @@ class Protocol(pyee.EventEmitter):
|
||||
avc.CommandFrame.CommandType.CONTROL,
|
||||
avc.Frame.SubunitType.PANEL,
|
||||
0,
|
||||
avc.PassThroughFrame.StateFlag.PRESSED
|
||||
if pressed
|
||||
else avc.PassThroughFrame.StateFlag.RELEASED,
|
||||
(
|
||||
avc.PassThroughFrame.StateFlag.PRESSED
|
||||
if pressed
|
||||
else avc.PassThroughFrame.StateFlag.RELEASED
|
||||
),
|
||||
key,
|
||||
b'',
|
||||
)
|
||||
|
||||
@@ -134,15 +134,15 @@ class Controller:
|
||||
self.hci_sink = None
|
||||
self.link = link
|
||||
|
||||
self.central_connections: Dict[
|
||||
Address, Connection
|
||||
] = {} # Connections where this controller is the central
|
||||
self.peripheral_connections: Dict[
|
||||
Address, Connection
|
||||
] = {} # Connections where this controller is the peripheral
|
||||
self.classic_connections: Dict[
|
||||
Address, Connection
|
||||
] = {} # Connections in BR/EDR
|
||||
self.central_connections: Dict[Address, Connection] = (
|
||||
{}
|
||||
) # Connections where this controller is the central
|
||||
self.peripheral_connections: Dict[Address, Connection] = (
|
||||
{}
|
||||
) # Connections where this controller is the peripheral
|
||||
self.classic_connections: Dict[Address, Connection] = (
|
||||
{}
|
||||
) # Connections in BR/EDR
|
||||
self.central_cis_links: Dict[int, CisLink] = {} # CIS links by handle
|
||||
self.peripheral_cis_links: Dict[int, CisLink] = {} # CIS links by handle
|
||||
|
||||
|
||||
@@ -276,12 +276,12 @@ class Advertisement:
|
||||
data_bytes: bytes = b''
|
||||
|
||||
# Constants
|
||||
TX_POWER_NOT_AVAILABLE: ClassVar[
|
||||
int
|
||||
] = HCI_LE_Extended_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
|
||||
RSSI_NOT_AVAILABLE: ClassVar[
|
||||
int
|
||||
] = HCI_LE_Extended_Advertising_Report_Event.RSSI_NOT_AVAILABLE
|
||||
TX_POWER_NOT_AVAILABLE: ClassVar[int] = (
|
||||
HCI_LE_Extended_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
|
||||
)
|
||||
RSSI_NOT_AVAILABLE: ClassVar[int] = (
|
||||
HCI_LE_Extended_Advertising_Report_Event.RSSI_NOT_AVAILABLE
|
||||
)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.data = AdvertisingData.from_bytes(self.data_bytes)
|
||||
@@ -558,7 +558,9 @@ class AdvertisingParameters:
|
||||
)
|
||||
primary_advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
||||
primary_advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
||||
primary_advertising_channel_map: HCI_LE_Set_Extended_Advertising_Parameters_Command.ChannelMap = (
|
||||
primary_advertising_channel_map: (
|
||||
HCI_LE_Set_Extended_Advertising_Parameters_Command.ChannelMap
|
||||
) = (
|
||||
AdvertisingChannelMap.CHANNEL_37
|
||||
| AdvertisingChannelMap.CHANNEL_38
|
||||
| AdvertisingChannelMap.CHANNEL_39
|
||||
@@ -1138,14 +1140,12 @@ class Connection(CompositeEventEmitter):
|
||||
@overload
|
||||
async def create_l2cap_channel(
|
||||
self, spec: l2cap.ClassicChannelSpec
|
||||
) -> l2cap.ClassicChannel:
|
||||
...
|
||||
) -> l2cap.ClassicChannel: ...
|
||||
|
||||
@overload
|
||||
async def create_l2cap_channel(
|
||||
self, spec: l2cap.LeCreditBasedChannelSpec
|
||||
) -> l2cap.LeCreditBasedChannel:
|
||||
...
|
||||
) -> l2cap.LeCreditBasedChannel: ...
|
||||
|
||||
async def create_l2cap_channel(
|
||||
self, spec: Union[l2cap.ClassicChannelSpec, l2cap.LeCreditBasedChannelSpec]
|
||||
@@ -1723,16 +1723,14 @@ class Device(CompositeEventEmitter):
|
||||
self,
|
||||
connection: Connection,
|
||||
spec: l2cap.ClassicChannelSpec,
|
||||
) -> l2cap.ClassicChannel:
|
||||
...
|
||||
) -> l2cap.ClassicChannel: ...
|
||||
|
||||
@overload
|
||||
async def create_l2cap_channel(
|
||||
self,
|
||||
connection: Connection,
|
||||
spec: l2cap.LeCreditBasedChannelSpec,
|
||||
) -> l2cap.LeCreditBasedChannel:
|
||||
...
|
||||
) -> l2cap.LeCreditBasedChannel: ...
|
||||
|
||||
async def create_l2cap_channel(
|
||||
self,
|
||||
@@ -1753,16 +1751,14 @@ class Device(CompositeEventEmitter):
|
||||
self,
|
||||
spec: l2cap.ClassicChannelSpec,
|
||||
handler: Optional[Callable[[l2cap.ClassicChannel], Any]] = None,
|
||||
) -> l2cap.ClassicChannelServer:
|
||||
...
|
||||
) -> l2cap.ClassicChannelServer: ...
|
||||
|
||||
@overload
|
||||
def create_l2cap_server(
|
||||
self,
|
||||
spec: l2cap.LeCreditBasedChannelSpec,
|
||||
handler: Optional[Callable[[l2cap.LeCreditBasedChannel], Any]] = None,
|
||||
) -> l2cap.LeCreditBasedChannelServer:
|
||||
...
|
||||
) -> l2cap.LeCreditBasedChannelServer: ...
|
||||
|
||||
def create_l2cap_server(
|
||||
self,
|
||||
@@ -3289,17 +3285,19 @@ class Device(CompositeEventEmitter):
|
||||
|
||||
handler = self.on(
|
||||
'remote_name',
|
||||
lambda address, remote_name: pending_name.set_result(remote_name)
|
||||
if address == peer_address
|
||||
else None,
|
||||
lambda address, remote_name: (
|
||||
pending_name.set_result(remote_name)
|
||||
if address == peer_address
|
||||
else None
|
||||
),
|
||||
)
|
||||
failure_handler = self.on(
|
||||
'remote_name_failure',
|
||||
lambda address, error_code: pending_name.set_exception(
|
||||
HCI_Error(error_code)
|
||||
)
|
||||
if address == peer_address
|
||||
else None,
|
||||
lambda address, error_code: (
|
||||
pending_name.set_exception(HCI_Error(error_code))
|
||||
if address == peer_address
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -3475,9 +3473,9 @@ class Device(CompositeEventEmitter):
|
||||
LE features supported by the remote device.
|
||||
"""
|
||||
with closing(EventWatcher()) as watcher:
|
||||
read_feature_future: asyncio.Future[
|
||||
LeFeatureMask
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
read_feature_future: asyncio.Future[LeFeatureMask] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
|
||||
def on_le_remote_features(handle: int, features: int):
|
||||
if handle == connection.handle:
|
||||
|
||||
@@ -29,6 +29,17 @@ from bumble.hci import (
|
||||
# -----------------------------------------------------------------------------
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constant
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
INTEL_USB_PRODUCTS = {
|
||||
# Intel AX210
|
||||
(0x8087, 0x0032),
|
||||
# Intel BE200
|
||||
(0x8087, 0x0036),
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# HCI Commands
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -52,13 +63,34 @@ class Driver(common.Driver):
|
||||
def __init__(self, host):
|
||||
self.host = host
|
||||
|
||||
@classmethod
|
||||
async def for_host(cls, host): # type: ignore
|
||||
# Only instantiate this driver if explicitly selected
|
||||
if host.hci_metadata.get("driver") == "intel":
|
||||
return cls(host)
|
||||
@staticmethod
|
||||
def check(host):
|
||||
driver = host.hci_metadata.get("driver")
|
||||
if driver == "intel":
|
||||
return True
|
||||
|
||||
return None
|
||||
vendor_id = host.hci_metadata.get("vendor_id")
|
||||
product_id = host.hci_metadata.get("product_id")
|
||||
|
||||
if vendor_id is None or product_id is None:
|
||||
logger.debug("USB metadata not sufficient")
|
||||
return False
|
||||
|
||||
if (vendor_id, product_id) not in INTEL_USB_PRODUCTS:
|
||||
logger.debug(
|
||||
f"USB device ({vendor_id:04X}, {product_id:04X}) " "not in known list"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def for_host(cls, host, force=False): # type: ignore
|
||||
# Only instantiate this driver if explicitly selected
|
||||
if not force and not cls.check(host):
|
||||
return None
|
||||
|
||||
return cls(host)
|
||||
|
||||
async def init_controller(self):
|
||||
self.host.ready = True
|
||||
|
||||
@@ -36,6 +36,7 @@ logger = logging.getLogger(__name__)
|
||||
# Classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class GenericAccessService(Service):
|
||||
def __init__(self, device_name, appearance=(0, 0)):
|
||||
|
||||
@@ -342,9 +342,11 @@ class Service(Attribute):
|
||||
uuid = UUID(uuid)
|
||||
|
||||
super().__init__(
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
|
||||
if primary
|
||||
else GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
||||
(
|
||||
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
|
||||
if primary
|
||||
else GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE
|
||||
),
|
||||
Attribute.READABLE,
|
||||
uuid.to_pdu_bytes(),
|
||||
)
|
||||
@@ -560,9 +562,9 @@ class CharacteristicAdapter:
|
||||
|
||||
def __init__(self, characteristic: Union[Characteristic, AttributeProxy]):
|
||||
self.wrapped_characteristic = characteristic
|
||||
self.subscribers: Dict[
|
||||
Callable, Callable
|
||||
] = {} # Map from subscriber to proxy subscriber
|
||||
self.subscribers: Dict[Callable, Callable] = (
|
||||
{}
|
||||
) # Map from subscriber to proxy subscriber
|
||||
|
||||
if isinstance(characteristic, Characteristic):
|
||||
self.read_value = self.read_encoded_value
|
||||
|
||||
@@ -352,9 +352,7 @@ class Client:
|
||||
if c.uuid == uuid
|
||||
]
|
||||
|
||||
def get_attribute_grouping(
|
||||
self, attribute_handle: int
|
||||
) -> Optional[
|
||||
def get_attribute_grouping(self, attribute_handle: int) -> Optional[
|
||||
Union[
|
||||
ServiceProxy,
|
||||
Tuple[ServiceProxy, CharacteristicProxy],
|
||||
|
||||
@@ -445,9 +445,9 @@ class Server(EventEmitter):
|
||||
assert self.pending_confirmations[connection.handle] is None
|
||||
|
||||
# Create a future value to hold the eventual response
|
||||
pending_confirmation = self.pending_confirmations[
|
||||
connection.handle
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
pending_confirmation = self.pending_confirmations[connection.handle] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
|
||||
try:
|
||||
self.send_gatt_pdu(connection.handle, indication.to_bytes())
|
||||
|
||||
@@ -4249,9 +4249,11 @@ class HCI_LE_Set_Extended_Scan_Parameters_Command(HCI_Command):
|
||||
fields.append(
|
||||
(
|
||||
f'{scanning_phy_str}.scan_type: ',
|
||||
'PASSIVE'
|
||||
if self.scan_types[i] == self.PASSIVE_SCANNING
|
||||
else 'ACTIVE',
|
||||
(
|
||||
'PASSIVE'
|
||||
if self.scan_types[i] == self.PASSIVE_SCANNING
|
||||
else 'ACTIVE'
|
||||
),
|
||||
)
|
||||
)
|
||||
fields.append(
|
||||
@@ -5010,9 +5012,9 @@ class HCI_LE_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
return f'{color(self.subevent_name(self.subevent_code), "magenta")}:\n{reports}'
|
||||
|
||||
|
||||
HCI_LE_Meta_Event.subevent_classes[
|
||||
HCI_LE_ADVERTISING_REPORT_EVENT
|
||||
] = HCI_LE_Advertising_Report_Event
|
||||
HCI_LE_Meta_Event.subevent_classes[HCI_LE_ADVERTISING_REPORT_EVENT] = (
|
||||
HCI_LE_Advertising_Report_Event
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -5264,9 +5266,9 @@ class HCI_LE_Extended_Advertising_Report_Event(HCI_LE_Meta_Event):
|
||||
return f'{color(self.subevent_name(self.subevent_code), "magenta")}:\n{reports}'
|
||||
|
||||
|
||||
HCI_LE_Meta_Event.subevent_classes[
|
||||
HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT
|
||||
] = HCI_LE_Extended_Advertising_Report_Event
|
||||
HCI_LE_Meta_Event.subevent_classes[HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT] = (
|
||||
HCI_LE_Extended_Advertising_Report_Event
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -22,7 +22,8 @@ import dataclasses
|
||||
import enum
|
||||
import traceback
|
||||
import pyee
|
||||
from typing import Dict, List, Union, Set, Any, Optional, TYPE_CHECKING
|
||||
from typing import Dict, List, Union, Set, Any, Optional, Type, TYPE_CHECKING
|
||||
from typing_extensions import Self
|
||||
|
||||
from bumble import at
|
||||
from bumble import rfcomm
|
||||
@@ -417,17 +418,21 @@ class AtResponseType(enum.Enum):
|
||||
MULTIPLE = 2
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AtResponse:
|
||||
code: str
|
||||
parameters: list
|
||||
|
||||
def __init__(self, response: bytearray):
|
||||
code_and_parameters = response.split(b':')
|
||||
@classmethod
|
||||
def parse_from(cls: Type[Self], buffer: bytearray) -> Self:
|
||||
code_and_parameters = buffer.split(b':')
|
||||
parameters = (
|
||||
code_and_parameters[1] if len(code_and_parameters) > 1 else bytearray()
|
||||
)
|
||||
self.code = code_and_parameters[0].decode()
|
||||
self.parameters = at.parse_parameters(parameters)
|
||||
return cls(
|
||||
code=code_and_parameters[0].decode(),
|
||||
parameters=at.parse_parameters(parameters),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -530,7 +535,7 @@ class HfProtocol(pyee.EventEmitter):
|
||||
|
||||
# Isolate the AT response code and parameters.
|
||||
raw_response = self.read_buffer[header + 2 : trailer]
|
||||
response = AtResponse(raw_response)
|
||||
response = AtResponse.parse_from(raw_response)
|
||||
logger.debug(f"<<< {raw_response.decode()}")
|
||||
|
||||
# Consume the response bytes.
|
||||
@@ -815,11 +820,11 @@ class HfProtocol(pyee.EventEmitter):
|
||||
return calls
|
||||
|
||||
async def update_ag_indicator(self, index: int, value: int):
|
||||
self.ag_indicators[index].current_status = value
|
||||
self.emit('ag_indicator', self.ag_indicators[index])
|
||||
logger.info(
|
||||
f"AG indicator updated: {self.ag_indicators[index].description}, {value}"
|
||||
)
|
||||
# CIEV is in 1-index, while ag_indicators is in 0-index.
|
||||
ag_indicator = self.ag_indicators[index - 1]
|
||||
ag_indicator.current_status = value
|
||||
self.emit('ag_indicator', ag_indicator)
|
||||
logger.info(f"AG indicator updated: {ag_indicator.description}, {value}")
|
||||
|
||||
async def handle_unsolicited(self):
|
||||
"""Handle unsolicited result codes sent by the audio gateway."""
|
||||
@@ -1006,7 +1011,9 @@ class EscoParameters:
|
||||
transmit_coding_format: CodingFormat
|
||||
receive_coding_format: CodingFormat
|
||||
packet_type: HCI_Enhanced_Setup_Synchronous_Connection_Command.PacketType
|
||||
retransmission_effort: HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort
|
||||
retransmission_effort: (
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command.RetransmissionEffort
|
||||
)
|
||||
max_latency: int
|
||||
|
||||
# Common
|
||||
@@ -1014,12 +1021,12 @@ class EscoParameters:
|
||||
output_coding_format: CodingFormat = CodingFormat(CodecID.LINEAR_PCM)
|
||||
input_coded_data_size: int = 16
|
||||
output_coded_data_size: int = 16
|
||||
input_pcm_data_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat = (
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
|
||||
)
|
||||
output_pcm_data_format: HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat = (
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
|
||||
)
|
||||
input_pcm_data_format: (
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat
|
||||
) = HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
|
||||
output_pcm_data_format: (
|
||||
HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat
|
||||
) = HCI_Enhanced_Setup_Synchronous_Connection_Command.PcmDataFormat.TWOS_COMPLEMENT
|
||||
input_pcm_sample_payload_msb_position: int = 0
|
||||
output_pcm_sample_payload_msb_position: int = 0
|
||||
input_data_path: HCI_Enhanced_Setup_Synchronous_Connection_Command.DataPath = (
|
||||
|
||||
@@ -48,6 +48,7 @@ HID_INTERRUPT_PSM = 0x0013
|
||||
|
||||
class Message:
|
||||
message_type: MessageType
|
||||
|
||||
# Report types
|
||||
class ReportType(enum.IntEnum):
|
||||
OTHER_REPORT = 0x00
|
||||
|
||||
@@ -128,10 +128,10 @@ class PairingKeys:
|
||||
|
||||
def print(self, prefix=''):
|
||||
keys_dict = self.to_dict()
|
||||
for (container_property, value) in keys_dict.items():
|
||||
for container_property, value in keys_dict.items():
|
||||
if isinstance(value, dict):
|
||||
print(f'{prefix}{color(container_property, "cyan")}:')
|
||||
for (key_property, key_value) in value.items():
|
||||
for key_property, key_value in value.items():
|
||||
print(f'{prefix} {color(key_property, "green")}: {key_value}')
|
||||
else:
|
||||
print(f'{prefix}{color(container_property, "cyan")}: {value}')
|
||||
@@ -158,7 +158,7 @@ class KeyStore:
|
||||
async def get_resolving_keys(self):
|
||||
all_keys = await self.get_all()
|
||||
resolving_keys = []
|
||||
for (name, keys) in all_keys:
|
||||
for name, keys in all_keys:
|
||||
if keys.irk is not None:
|
||||
if keys.address_type is None:
|
||||
address_type = Address.RANDOM_DEVICE_ADDRESS
|
||||
@@ -171,7 +171,7 @@ class KeyStore:
|
||||
async def print(self, prefix=''):
|
||||
entries = await self.get_all()
|
||||
separator = ''
|
||||
for (name, keys) in entries:
|
||||
for name, keys in entries:
|
||||
print(separator + prefix + color(name, 'yellow'))
|
||||
keys.print(prefix=prefix + ' ')
|
||||
separator = '\n'
|
||||
|
||||
@@ -54,6 +54,8 @@ from pandora import host_pb2
|
||||
from pandora.host_pb2 import (
|
||||
NOT_CONNECTABLE,
|
||||
NOT_DISCOVERABLE,
|
||||
DISCOVERABLE_LIMITED,
|
||||
DISCOVERABLE_GENERAL,
|
||||
PRIMARY_1M,
|
||||
PRIMARY_CODED,
|
||||
SECONDARY_1M,
|
||||
@@ -69,6 +71,7 @@ from pandora.host_pb2 import (
|
||||
ConnectResponse,
|
||||
DataTypes,
|
||||
DisconnectRequest,
|
||||
DiscoverabilityMode,
|
||||
InquiryResponse,
|
||||
PrimaryPhy,
|
||||
ReadLocalAddressResponse,
|
||||
@@ -284,9 +287,9 @@ class HostService(HostServicer):
|
||||
self.log.debug(f"WaitDisconnection: {connection_handle}")
|
||||
|
||||
if connection := self.device.lookup_connection(connection_handle):
|
||||
disconnection_future: asyncio.Future[
|
||||
None
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
disconnection_future: asyncio.Future[None] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
|
||||
def on_disconnection(_: None) -> None:
|
||||
disconnection_future.set_result(None)
|
||||
@@ -367,9 +370,9 @@ class HostService(HostServicer):
|
||||
scan_response_data=scan_response_data,
|
||||
)
|
||||
|
||||
pending_connection: asyncio.Future[
|
||||
bumble.device.Connection
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
pending_connection: asyncio.Future[bumble.device.Connection] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
|
||||
if request.connectable:
|
||||
|
||||
@@ -483,14 +486,10 @@ class HostService(HostServicer):
|
||||
target_bytes = bytes(reversed(request.target))
|
||||
if request.target_variant() == "public":
|
||||
target = Address(target_bytes, Address.PUBLIC_DEVICE_ADDRESS)
|
||||
advertising_type = (
|
||||
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY
|
||||
) # FIXME: HIGH_DUTY ?
|
||||
advertising_type = AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY
|
||||
else:
|
||||
target = Address(target_bytes, Address.RANDOM_DEVICE_ADDRESS)
|
||||
advertising_type = (
|
||||
AdvertisingType.DIRECTED_CONNECTABLE_HIGH_DUTY
|
||||
) # FIXME: HIGH_DUTY ?
|
||||
advertising_type = AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY
|
||||
|
||||
if request.connectable:
|
||||
|
||||
@@ -517,9 +516,9 @@ class HostService(HostServicer):
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
pending_connection: asyncio.Future[
|
||||
bumble.device.Connection
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
pending_connection: asyncio.Future[bumble.device.Connection] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
|
||||
self.log.debug('Wait for LE connection...')
|
||||
connection = await pending_connection
|
||||
@@ -564,12 +563,14 @@ class HostService(HostServicer):
|
||||
legacy=request.legacy,
|
||||
active=not request.passive,
|
||||
own_address_type=request.own_address_type,
|
||||
scan_interval=int(request.interval)
|
||||
if request.interval
|
||||
else DEVICE_DEFAULT_SCAN_INTERVAL,
|
||||
scan_window=int(request.window)
|
||||
if request.window
|
||||
else DEVICE_DEFAULT_SCAN_WINDOW,
|
||||
scan_interval=(
|
||||
int(request.interval)
|
||||
if request.interval
|
||||
else DEVICE_DEFAULT_SCAN_INTERVAL
|
||||
),
|
||||
scan_window=(
|
||||
int(request.window) if request.window else DEVICE_DEFAULT_SCAN_WINDOW
|
||||
),
|
||||
scanning_phys=scanning_phys,
|
||||
)
|
||||
|
||||
@@ -783,9 +784,11 @@ class HostService(HostServicer):
|
||||
*struct.pack('<H', dt.peripheral_connection_interval_min),
|
||||
*struct.pack(
|
||||
'<H',
|
||||
dt.peripheral_connection_interval_max
|
||||
if dt.peripheral_connection_interval_max
|
||||
else dt.peripheral_connection_interval_min,
|
||||
(
|
||||
dt.peripheral_connection_interval_max
|
||||
if dt.peripheral_connection_interval_max
|
||||
else dt.peripheral_connection_interval_min
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
@@ -867,6 +870,16 @@ class HostService(HostServicer):
|
||||
)
|
||||
)
|
||||
|
||||
flag_map = {
|
||||
NOT_DISCOVERABLE: 0x00,
|
||||
DISCOVERABLE_LIMITED: AdvertisingData.LE_LIMITED_DISCOVERABLE_MODE_FLAG,
|
||||
DISCOVERABLE_GENERAL: AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG,
|
||||
}
|
||||
|
||||
if dt.le_discoverability_mode:
|
||||
flags = flag_map[dt.le_discoverability_mode]
|
||||
ad_structures.append((AdvertisingData.FLAGS, flags.to_bytes(1, 'big')))
|
||||
|
||||
return AdvertisingData(ad_structures)
|
||||
|
||||
def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
|
||||
|
||||
@@ -383,9 +383,9 @@ class SecurityService(SecurityServicer):
|
||||
connection.transport
|
||||
] == request.level_variant()
|
||||
|
||||
wait_for_security: asyncio.Future[
|
||||
str
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
wait_for_security: asyncio.Future[str] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
authenticate_task: Optional[asyncio.Future[None]] = None
|
||||
pair_task: Optional[asyncio.Future[None]] = None
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ import enum
|
||||
import struct
|
||||
import functools
|
||||
import logging
|
||||
from typing import Optional, List, Union, Type, Dict, Any, Tuple, cast
|
||||
from typing import Optional, List, Union, Type, Dict, Any, Tuple
|
||||
|
||||
from bumble import core
|
||||
from bumble import colors
|
||||
from bumble import device
|
||||
from bumble import hci
|
||||
@@ -228,6 +229,14 @@ class SupportedFrameDuration(enum.IntFlag):
|
||||
DURATION_10000_US_PREFERRED = 0b0010
|
||||
|
||||
|
||||
class AnnouncementType(enum.IntEnum):
|
||||
'''Basic Audio Profile, 3.5.3. Additional Audio Stream Control Service requirements'''
|
||||
|
||||
# fmt: off
|
||||
GENERAL = 0x00
|
||||
TARGETED = 0x01
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ASE Operations
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -453,6 +462,34 @@ class AudioRole(enum.IntEnum):
|
||||
SOURCE = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.HOST_TO_CONTROLLER
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class UnicastServerAdvertisingData:
|
||||
"""Advertising Data for ASCS."""
|
||||
|
||||
announcement_type: AnnouncementType = AnnouncementType.TARGETED
|
||||
available_audio_contexts: ContextType = ContextType.MEDIA
|
||||
metadata: bytes = b''
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(
|
||||
core.AdvertisingData(
|
||||
[
|
||||
(
|
||||
core.AdvertisingData.SERVICE_DATA_16_BIT_UUID,
|
||||
struct.pack(
|
||||
'<2sBIB',
|
||||
gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE.to_bytes(),
|
||||
self.announcement_type,
|
||||
self.available_audio_contexts,
|
||||
len(self.metadata),
|
||||
)
|
||||
+ self.metadata,
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utils
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -59,7 +59,7 @@ class DeviceInformationService(TemplateService):
|
||||
firmware_revision: Optional[str] = None,
|
||||
software_revision: Optional[str] = None,
|
||||
system_id: Optional[Tuple[int, int]] = None, # (OUI, Manufacturer ID)
|
||||
ieee_regulatory_certification_data_list: Optional[bytes] = None
|
||||
ieee_regulatory_certification_data_list: Optional[bytes] = None,
|
||||
# TODO: pnp_id
|
||||
):
|
||||
characteristics = [
|
||||
@@ -107,7 +107,7 @@ class DeviceInformationServiceProxy(ProfileServiceProxy):
|
||||
def __init__(self, service_proxy):
|
||||
self.service_proxy = service_proxy
|
||||
|
||||
for (field, uuid) in (
|
||||
for field, uuid in (
|
||||
('manufacturer_name', GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
|
||||
('model_number', GATT_MODEL_NUMBER_STRING_CHARACTERISTIC),
|
||||
('serial_number', GATT_SERIAL_NUMBER_STRING_CHARACTERISTIC),
|
||||
|
||||
@@ -825,11 +825,13 @@ class Client:
|
||||
)
|
||||
attribute_id_list = DataElement.sequence(
|
||||
[
|
||||
DataElement.unsigned_integer(
|
||||
attribute_id[0], value_size=attribute_id[1]
|
||||
(
|
||||
DataElement.unsigned_integer(
|
||||
attribute_id[0], value_size=attribute_id[1]
|
||||
)
|
||||
if isinstance(attribute_id, tuple)
|
||||
else DataElement.unsigned_integer_16(attribute_id)
|
||||
)
|
||||
if isinstance(attribute_id, tuple)
|
||||
else DataElement.unsigned_integer_16(attribute_id)
|
||||
for attribute_id in attribute_ids
|
||||
]
|
||||
)
|
||||
@@ -881,11 +883,13 @@ class Client:
|
||||
|
||||
attribute_id_list = DataElement.sequence(
|
||||
[
|
||||
DataElement.unsigned_integer(
|
||||
attribute_id[0], value_size=attribute_id[1]
|
||||
(
|
||||
DataElement.unsigned_integer(
|
||||
attribute_id[0], value_size=attribute_id[1]
|
||||
)
|
||||
if isinstance(attribute_id, tuple)
|
||||
else DataElement.unsigned_integer_16(attribute_id)
|
||||
)
|
||||
if isinstance(attribute_id, tuple)
|
||||
else DataElement.unsigned_integer_16(attribute_id)
|
||||
for attribute_id in attribute_ids
|
||||
]
|
||||
)
|
||||
|
||||
@@ -737,9 +737,9 @@ class Session:
|
||||
|
||||
# Create a future that can be used to wait for the session to complete
|
||||
if self.is_initiator:
|
||||
self.pairing_result: Optional[
|
||||
asyncio.Future[None]
|
||||
] = asyncio.get_running_loop().create_future()
|
||||
self.pairing_result: Optional[asyncio.Future[None]] = (
|
||||
asyncio.get_running_loop().create_future()
|
||||
)
|
||||
else:
|
||||
self.pairing_result = None
|
||||
|
||||
|
||||
@@ -59,15 +59,13 @@ class TransportLostError(Exception):
|
||||
# Typing Protocols
|
||||
# -----------------------------------------------------------------------------
|
||||
class TransportSink(Protocol):
|
||||
def on_packet(self, packet: bytes) -> None:
|
||||
...
|
||||
def on_packet(self, packet: bytes) -> None: ...
|
||||
|
||||
|
||||
class TransportSource(Protocol):
|
||||
terminated: asyncio.Future[None]
|
||||
|
||||
def set_packet_sink(self, sink: TransportSink) -> None:
|
||||
...
|
||||
def set_packet_sink(self, sink: TransportSink) -> None: ...
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -113,9 +113,10 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
||||
self.loop.call_soon_threadsafe(self.stop_event.set)
|
||||
|
||||
class UsbPacketSource(asyncio.Protocol, ParserSource):
|
||||
def __init__(self, device, sco_enabled):
|
||||
def __init__(self, device, metadata, sco_enabled):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.metadata = metadata
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.queue = asyncio.Queue()
|
||||
self.dequeue_task = None
|
||||
@@ -216,6 +217,15 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
||||
if ':' in spec:
|
||||
vendor_id, product_id = spec.split(':')
|
||||
device = usb_find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16))
|
||||
elif '-' in spec:
|
||||
|
||||
def device_path(device):
|
||||
if device.port_numbers:
|
||||
return f'{device.bus}-{".".join(map(str, device.port_numbers))}'
|
||||
else:
|
||||
return str(device.bus)
|
||||
|
||||
device = usb_find(custom_match=lambda device: device_path(device) == spec)
|
||||
else:
|
||||
device_index = int(spec)
|
||||
devices = list(
|
||||
@@ -235,6 +245,9 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
||||
raise ValueError('device not found')
|
||||
logger.debug(f'USB Device: {device}')
|
||||
|
||||
# Collect the metadata
|
||||
device_metadata = {'vendor_id': device.idVendor, 'product_id': device.idProduct}
|
||||
|
||||
# Detach the kernel driver if needed
|
||||
if device.is_kernel_driver_active(0):
|
||||
logger.debug("detaching kernel driver")
|
||||
@@ -289,7 +302,7 @@ async def open_pyusb_transport(spec: str) -> Transport:
|
||||
# except usb.USBError:
|
||||
# logger.warning('failed to set alternate setting')
|
||||
|
||||
packet_source = UsbPacketSource(device, sco_enabled)
|
||||
packet_source = UsbPacketSource(device, device_metadata, sco_enabled)
|
||||
packet_sink = UsbPacketSink(device)
|
||||
packet_source.start()
|
||||
packet_sink.start()
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from .common import Transport, StreamPacketSource
|
||||
|
||||
@@ -28,6 +29,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# A pass-through function to ease mock testing.
|
||||
async def _create_server(*args, **kw_args):
|
||||
await asyncio.get_running_loop().create_server(*args, **kw_args)
|
||||
|
||||
|
||||
async def open_tcp_server_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a TCP server transport.
|
||||
@@ -38,7 +45,22 @@ async def open_tcp_server_transport(spec: str) -> Transport:
|
||||
|
||||
Example: _:9001
|
||||
'''
|
||||
local_host, local_port = spec.split(':')
|
||||
return await _open_tcp_server_transport_impl(
|
||||
host=local_host if local_host != '_' else None, port=int(local_port)
|
||||
)
|
||||
|
||||
|
||||
async def open_tcp_server_transport_with_socket(sock: socket.socket) -> Transport:
|
||||
'''
|
||||
Open a TCP server transport with an existing socket.
|
||||
|
||||
One reason to use this variant is to let python pick an unused port.
|
||||
'''
|
||||
return await _open_tcp_server_transport_impl(sock=sock)
|
||||
|
||||
|
||||
async def _open_tcp_server_transport_impl(**kwargs) -> Transport:
|
||||
class TcpServerTransport(Transport):
|
||||
async def close(self):
|
||||
await super().close()
|
||||
@@ -77,13 +99,10 @@ async def open_tcp_server_transport(spec: str) -> Transport:
|
||||
else:
|
||||
logger.debug('no client, dropping packet')
|
||||
|
||||
local_host, local_port = spec.split(':')
|
||||
packet_source = StreamPacketSource()
|
||||
packet_sink = TcpServerPacketSink()
|
||||
await asyncio.get_running_loop().create_server(
|
||||
lambda: TcpServerProtocol(packet_source, packet_sink),
|
||||
host=local_host if local_host != '_' else None,
|
||||
port=int(local_port),
|
||||
await _create_server(
|
||||
lambda: TcpServerProtocol(packet_source, packet_sink), **kwargs
|
||||
)
|
||||
|
||||
return TcpServerTransport(packet_source, packet_sink)
|
||||
|
||||
@@ -396,6 +396,16 @@ async def open_usb_transport(spec: str) -> Transport:
|
||||
break
|
||||
device_index -= 1
|
||||
device.close()
|
||||
elif '-' in spec:
|
||||
|
||||
def device_path(device):
|
||||
return f'{device.getBusNumber()}-{".".join(map(str, device.getPortNumberList()))}'
|
||||
|
||||
for device in context.getDeviceIterator(skip_on_error=True):
|
||||
if device_path(device) == spec:
|
||||
found = device
|
||||
break
|
||||
device.close()
|
||||
else:
|
||||
# Look for a compatible device by index
|
||||
def device_is_bluetooth_hci(device):
|
||||
@@ -439,7 +449,7 @@ async def open_usb_transport(spec: str) -> Transport:
|
||||
# Look for the first interface with the right class and endpoints
|
||||
def find_endpoints(device):
|
||||
# pylint: disable-next=too-many-nested-blocks
|
||||
for (configuration_index, configuration) in enumerate(device):
|
||||
for configuration_index, configuration in enumerate(device):
|
||||
interface = None
|
||||
for interface in configuration:
|
||||
setting = None
|
||||
|
||||
@@ -117,12 +117,12 @@ class EventWatcher:
|
||||
self.handlers = []
|
||||
|
||||
@overload
|
||||
def on(self, emitter: EventEmitter, event: str) -> Callable[[_Handler], _Handler]:
|
||||
...
|
||||
def on(
|
||||
self, emitter: EventEmitter, event: str
|
||||
) -> Callable[[_Handler], _Handler]: ...
|
||||
|
||||
@overload
|
||||
def on(self, emitter: EventEmitter, event: str, handler: _Handler) -> _Handler:
|
||||
...
|
||||
def on(self, emitter: EventEmitter, event: str, handler: _Handler) -> _Handler: ...
|
||||
|
||||
def on(
|
||||
self, emitter: EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||
@@ -144,12 +144,14 @@ class EventWatcher:
|
||||
return wrapper if handler is None else wrapper(handler)
|
||||
|
||||
@overload
|
||||
def once(self, emitter: EventEmitter, event: str) -> Callable[[_Handler], _Handler]:
|
||||
...
|
||||
def once(
|
||||
self, emitter: EventEmitter, event: str
|
||||
) -> Callable[[_Handler], _Handler]: ...
|
||||
|
||||
@overload
|
||||
def once(self, emitter: EventEmitter, event: str, handler: _Handler) -> _Handler:
|
||||
...
|
||||
def once(
|
||||
self, emitter: EventEmitter, event: str, handler: _Handler
|
||||
) -> _Handler: ...
|
||||
|
||||
def once(
|
||||
self, emitter: EventEmitter, event: str, handler: Optional[_Handler] = None
|
||||
|
||||
@@ -10,6 +10,7 @@ The moniker for a USB transport is either:
|
||||
* `usb:<vendor>:<product>`
|
||||
* `usb:<vendor>:<product>/<serial-number>`
|
||||
* `usb:<vendor>:<product>#<index>`
|
||||
* `usb:<bus>-<port_numbers>`
|
||||
|
||||
with `<index>` as a 0-based index (0 being the first one) to select amongst all the matching devices when there are more than one.
|
||||
In the `usb:<index>` form, matching devices are the ones supporting Bluetooth HCI, as declared by their Class, Subclass and Protocol.
|
||||
@@ -17,6 +18,8 @@ In the `usb:<vendor>:<product>#<index>` form, matching devices are the ones with
|
||||
|
||||
`<vendor>` and `<product>` are a vendor ID and product ID in hexadecimal.
|
||||
|
||||
with `<port_numbers>` as a list of all port numbers from root separated with dots `.`
|
||||
|
||||
In addition, if the moniker ends with the symbol "!", the device will be used in "forced" mode:
|
||||
the first USB interface of the device will be used, regardless of the interface class/subclass.
|
||||
This may be useful for some devices that use a custom class/subclass but may nonetheless work as-is.
|
||||
@@ -37,6 +40,9 @@ This may be useful for some devices that use a custom class/subclass but may non
|
||||
`usb:0B05:17CB!`
|
||||
The BT USB dongle vendor=0B05 and product=17CB, in "forced" mode.
|
||||
|
||||
`usb:3-3.4.1`
|
||||
The BT USB dongle on bus 3 on port path 3, 4, 1.
|
||||
|
||||
|
||||
## Alternative
|
||||
The library includes two different implementations of the USB transport, implemented using different python bindings for `libusb`.
|
||||
|
||||
@@ -25,6 +25,7 @@ from bumble.utils import AsyncRunner
|
||||
my_work_queue1 = AsyncRunner.WorkQueue()
|
||||
my_work_queue2 = AsyncRunner.WorkQueue(create_task=False)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@AsyncRunner.run_in_task()
|
||||
async def func1(x, y):
|
||||
|
||||
@@ -1,79 +1,132 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
<html data-bs-theme="dark">
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<span class="navbar-brand mb-0 h1">Bumble Handsfree</span>
|
||||
</div>
|
||||
</nav>
|
||||
<br>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<label class="form-label">Server Port</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" aria-label="Port Number" value="8989" id="port">
|
||||
<button class="btn btn-primary" type="button" onclick="connect()">Connect</button>
|
||||
</div>
|
||||
|
||||
<label class="form-label">Dial Phone Number</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" placeholder="Phone Number" aria-label="Phone Number"
|
||||
id="dial_number">
|
||||
<button class="btn btn-primary" type="button"
|
||||
onclick="send_at_command(`ATD${dialNumberInput.value}`)">Dial</button>
|
||||
</div>
|
||||
|
||||
<label class="form-label">Send AT Command</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" placeholder="AT Command" aria-label="AT command" id="at_command">
|
||||
<button class="btn btn-primary" type="button"
|
||||
onclick="send_at_command(document.getElementById('at_command').value)">Send</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Battery Level</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" placeholder="0 - 100" aria-label="Battery Level"
|
||||
id="battery_level">
|
||||
<button class="btn btn-primary" type="button"
|
||||
onclick="send_at_command(`AT+BIEV=2,${document.getElementById('battery_level').value}`)">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Speaker Volume</label>
|
||||
<div class="input-group mb-3 col-auto">
|
||||
<input type="text" class="form-control" placeholder="0 - 15" aria-label="Speaker Volume"
|
||||
id="speaker_volume">
|
||||
<button class="btn btn-primary" type="button"
|
||||
onclick="send_at_command(`AT+VGS=${document.getElementById('speaker_volume').value}`)">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label">Mic Volume</label>
|
||||
<div class="input-group mb-3 col-auto">
|
||||
<input type="text" class="form-control" placeholder="0 - 15" aria-label="Mic Volume"
|
||||
id="mic_volume">
|
||||
<button class="btn btn-primary" type="button"
|
||||
onclick="send_at_command(`AT+VGM=${document.getElementById('mic_volume').value}`)">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="send_at_command('ATA')">Answer</button>
|
||||
<button class="btn btn-primary" onclick="send_at_command('AT+CHUP')">Hang Up</button>
|
||||
<button class="btn btn-primary" onclick="send_at_command('AT+BLDN')">Redial</button>
|
||||
<button class="btn btn-primary" onclick="send({ type: 'query_call'})">Get Call Status</button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button class="btn btn-primary" onclick="send_at_command('AT+BVRA=1')">Start Voice Assistant</button>
|
||||
<button class="btn btn-primary" onclick="send_at_command('AT+BVRA=0')">Stop Voice Assistant</button>
|
||||
|
||||
input, label {
|
||||
margin: .4rem 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
Server Port <input id="port" type="text" value="8989"></input> <button onclick="connect()">Connect</button><br>
|
||||
AT Command <input type="text" id="at_command" required size="10"> <button onclick="send_at_command()">Send</button><br>
|
||||
Dial Phone Number <input type="text" id="dial_number" required size="10"> <button onclick="dial()">Dial</button><br>
|
||||
<button onclick="answer()">Answer</button>
|
||||
<button onclick="hangup()">Hang Up</button>
|
||||
<button onclick="start_voice_assistant()">Start Voice Assistant</button>
|
||||
<button onclick="stop_voice_assistant()">Stop Voice Assistant</button>
|
||||
<hr>
|
||||
<div id="socketState"></div>
|
||||
<script>
|
||||
|
||||
<div id="socketStateContainer" class="bg-body-tertiary p-3 rounded-2">
|
||||
<h3>Log</h3>
|
||||
<code id="log" style="white-space: pre-line;"></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
let portInput = document.getElementById("port")
|
||||
let atCommandInput = document.getElementById("at_command")
|
||||
let dialNumberInput = document.getElementById("dial_number")
|
||||
let socketState = document.getElementById("socketState")
|
||||
let log = document.getElementById("log")
|
||||
let socket
|
||||
|
||||
function connect() {
|
||||
socket = new WebSocket(`ws://localhost:${portInput.value}`);
|
||||
socket.onopen = _ => {
|
||||
socketState.innerText = 'OPEN'
|
||||
log.textContent += 'OPEN\n'
|
||||
}
|
||||
socket.onclose = _ => {
|
||||
socketState.innerText = 'CLOSED'
|
||||
log.textContent += 'CLOSED\n'
|
||||
}
|
||||
socket.onerror = (error) => {
|
||||
socketState.innerText = 'ERROR'
|
||||
log.textContent += 'ERROR\n'
|
||||
console.log(`ERROR: ${error}`)
|
||||
}
|
||||
socket.onmessage = (event) => {
|
||||
log.textContent += `<-- ${event.data}\n`
|
||||
let volume_state = JSON.parse(event.data)
|
||||
volumeSetting.value = volume_state.volume_setting
|
||||
changeCounter.value = volume_state.change_counter
|
||||
muted.checked = volume_state.muted ? true : false
|
||||
}
|
||||
}
|
||||
|
||||
function send(message) {
|
||||
if (socket && socket.readyState == WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(message))
|
||||
let jsonMessage = JSON.stringify(message)
|
||||
log.textContent += `--> ${jsonMessage}\n`
|
||||
socket.send(jsonMessage)
|
||||
} else {
|
||||
log.textContent += 'NOT CONNECTED\n'
|
||||
}
|
||||
}
|
||||
|
||||
function send_at_command() {
|
||||
send({ type:'at_command', command: atCommandInput.value })
|
||||
function send_at_command(command) {
|
||||
send({ type: 'at_command', 'command': command })
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
function answer() {
|
||||
send({ type:'at_command', command: 'ATA' })
|
||||
}
|
||||
|
||||
function hangup() {
|
||||
send({ type:'at_command', command: 'AT+CHUP' })
|
||||
}
|
||||
|
||||
function dial() {
|
||||
send({ type:'at_command', command: `ATD${dialNumberInput.value}` })
|
||||
}
|
||||
|
||||
function start_voice_assistant() {
|
||||
send(({ type:'at_command', command: 'AT+BVRA=1' }))
|
||||
}
|
||||
|
||||
function stop_voice_assistant() {
|
||||
send(({ type:'at_command', command: 'AT+BVRA=0' }))
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -25,6 +25,7 @@ from bumble.device import Device
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.snoop import BtSnooper
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main():
|
||||
if len(sys.argv) != 3:
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import contextlib
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
@@ -31,39 +32,16 @@ from bumble.transport import open_transport_or_link
|
||||
from bumble import hfp
|
||||
from bumble.hfp import HfProtocol
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class UiServer:
|
||||
protocol: Optional[HfProtocol] = None
|
||||
|
||||
async def start(self):
|
||||
"""Start a Websocket server to receive events from a web page."""
|
||||
|
||||
async def serve(websocket, _path):
|
||||
while True:
|
||||
try:
|
||||
message = await websocket.recv()
|
||||
print('Received: ', str(message))
|
||||
|
||||
parsed = json.loads(message)
|
||||
message_type = parsed['type']
|
||||
if message_type == 'at_command':
|
||||
if self.protocol is not None:
|
||||
await self.protocol.execute_command(parsed['command'])
|
||||
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
|
||||
# pylint: disable=no-member
|
||||
await websockets.serve(serve, 'localhost', 8989)
|
||||
ws: Optional[websockets.WebSocketServerProtocol] = None
|
||||
hf_protocol: Optional[HfProtocol] = None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def on_dlc(dlc: rfcomm.DLC, configuration: hfp.Configuration):
|
||||
print('*** DLC connected', dlc)
|
||||
protocol = HfProtocol(dlc, configuration)
|
||||
UiServer.protocol = protocol
|
||||
asyncio.create_task(protocol.run())
|
||||
global hf_protocol
|
||||
hf_protocol = HfProtocol(dlc, configuration)
|
||||
asyncio.create_task(hf_protocol.run())
|
||||
|
||||
def on_sco_request(connection: Connection, link_type: int, protocol: HfProtocol):
|
||||
if connection == protocol.dlc.multiplexer.l2cap_channel.connection:
|
||||
@@ -88,7 +66,7 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.Configuration):
|
||||
),
|
||||
)
|
||||
|
||||
handler = functools.partial(on_sco_request, protocol=protocol)
|
||||
handler = functools.partial(on_sco_request, protocol=hf_protocol)
|
||||
dlc.multiplexer.l2cap_channel.connection.device.on('sco_request', handler)
|
||||
dlc.multiplexer.l2cap_channel.once(
|
||||
'close',
|
||||
@@ -97,6 +75,13 @@ def on_dlc(dlc: rfcomm.DLC, configuration: hfp.Configuration):
|
||||
),
|
||||
)
|
||||
|
||||
def on_ag_indicator(indicator):
|
||||
global ws
|
||||
if ws:
|
||||
asyncio.create_task(ws.send(str(indicator)))
|
||||
|
||||
hf_protocol.on('ag_indicator', on_ag_indicator)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def main():
|
||||
@@ -154,8 +139,30 @@ async def main():
|
||||
await device.set_connectable(True)
|
||||
|
||||
# Start the UI websocket server to offer a few buttons and input boxes
|
||||
ui_server = UiServer()
|
||||
await ui_server.start()
|
||||
async def serve(websocket: websockets.WebSocketServerProtocol, _path):
|
||||
global ws
|
||||
ws = websocket
|
||||
async for message in websocket:
|
||||
with contextlib.suppress(websockets.exceptions.ConnectionClosedOK):
|
||||
print('Received: ', str(message))
|
||||
|
||||
parsed = json.loads(message)
|
||||
message_type = parsed['type']
|
||||
if message_type == 'at_command':
|
||||
if hf_protocol is not None:
|
||||
response = str(
|
||||
await hf_protocol.execute_command(
|
||||
parsed['command'],
|
||||
response_type=hfp.AtResponseType.MULTIPLE,
|
||||
)
|
||||
)
|
||||
await websocket.send(response)
|
||||
elif message_type == 'query_call':
|
||||
if hf_protocol:
|
||||
response = str(await hf_protocol.query_current_calls())
|
||||
await websocket.send(response)
|
||||
|
||||
await websockets.serve(serve, 'localhost', 8989)
|
||||
|
||||
await hci_source.wait_for_termination()
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ HID_REPORT_MAP = bytes( # Text String, 50 Octet Report Descriptor
|
||||
# Default protocol mode set to report protocol
|
||||
protocol_mode = Message.ProtocolMode.REPORT_PROTOCOL
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def sdp_records():
|
||||
service_record_handle = 0x00010002
|
||||
@@ -427,6 +428,7 @@ class DeviceData:
|
||||
# Device's live data - Mouse and Keyboard will be stored in this
|
||||
deviceData = DeviceData()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def keyboard_device(hid_device):
|
||||
|
||||
|
||||
@@ -22,14 +22,14 @@ import os
|
||||
import struct
|
||||
import secrets
|
||||
from bumble.core import AdvertisingData
|
||||
from bumble.device import Device, CisLink, AdvertisingParameters
|
||||
from bumble.device import Device, CisLink
|
||||
from bumble.hci import (
|
||||
CodecID,
|
||||
CodingFormat,
|
||||
OwnAddressType,
|
||||
HCI_IsoDataPacket,
|
||||
)
|
||||
from bumble.profiles.bap import (
|
||||
UnicastServerAdvertisingData,
|
||||
CodecSpecificCapabilities,
|
||||
ContextType,
|
||||
AudioLocation,
|
||||
@@ -141,6 +141,7 @@ async def main() -> None:
|
||||
)
|
||||
)
|
||||
+ csis.get_advertising_data()
|
||||
+ bytes(UnicastServerAdvertisingData())
|
||||
)
|
||||
subprocess = await asyncio.create_subprocess_shell(
|
||||
f'dlc3 | ffplay pipe:0',
|
||||
@@ -178,7 +179,7 @@ async def main() -> None:
|
||||
|
||||
device.once('cis_establishment', on_cis)
|
||||
|
||||
advertising_set = await device.create_advertising_set(
|
||||
await device.create_advertising_set(
|
||||
advertising_data=advertising_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from bumble.hci import (
|
||||
OwnAddressType,
|
||||
)
|
||||
from bumble.profiles.bap import (
|
||||
UnicastServerAdvertisingData,
|
||||
CodecSpecificCapabilities,
|
||||
ContextType,
|
||||
AudioLocation,
|
||||
@@ -151,6 +152,7 @@ async def main() -> None:
|
||||
)
|
||||
)
|
||||
+ csis.get_advertising_data()
|
||||
+ bytes(UnicastServerAdvertisingData())
|
||||
)
|
||||
|
||||
await device.create_advertising_set(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Next
|
||||
# 0.2.0
|
||||
|
||||
- Code-gen company ID table
|
||||
- Unstable support for extended advertisements
|
||||
- CLI tools for downloading Realtek firmware
|
||||
- PDL-generated types for HCI commands
|
||||
|
||||
# 0.1.0
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
|
||||
2
rust/Cargo.lock
generated
2
rust/Cargo.lock
generated
@@ -182,7 +182,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumble"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bumble"
|
||||
description = "Rust API for the Bumble Bluetooth stack"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://google.github.io/bumble/index.html"
|
||||
|
||||
@@ -37,6 +37,11 @@ PYTHONPATH=..:[virtualenv site-packages] \
|
||||
cargo run --features bumble-tools --bin bumble -- --help
|
||||
```
|
||||
|
||||
Notable subcommands:
|
||||
|
||||
- `firmware realtek download`: download Realtek firmware for various chipsets so that it can be automatically loaded when needed
|
||||
- `usb probe`: show USB devices, highlighting the ones usable for Bluetooth
|
||||
|
||||
# Development
|
||||
|
||||
Run the tests:
|
||||
@@ -63,4 +68,4 @@ To regenerate the assigned number tables based on the Python codebase:
|
||||
|
||||
```
|
||||
PYTHONPATH=.. cargo run --bin gen-assigned-numbers --features dev-tools
|
||||
```
|
||||
```
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Controller {
|
||||
/// module specifies the defaults. Must be called from a thread with a Python event loop, which
|
||||
/// should be true on `tokio::main` and `async_std::main`.
|
||||
///
|
||||
/// For more info, see https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars.
|
||||
/// For more info, see <https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars>.
|
||||
pub async fn new(
|
||||
name: &str,
|
||||
host_source: Option<TransportSource>,
|
||||
|
||||
@@ -149,7 +149,7 @@ impl ToPyObject for Address {
|
||||
|
||||
/// An error meaning that the u64 value did not represent a valid BT address.
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidAddress(u64);
|
||||
pub struct InvalidAddress(#[allow(unused)] u64);
|
||||
|
||||
impl TryInto<packets::Address> for Address {
|
||||
type Error = ConversionError<InvalidAddress>;
|
||||
|
||||
@@ -71,7 +71,7 @@ impl LeConnectionOrientedChannel {
|
||||
/// Must be called from a thread with a Python event loop, which should be true on
|
||||
/// `tokio::main` and `async_std::main`.
|
||||
///
|
||||
/// For more info, see https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars.
|
||||
/// For more info, see <https://awestlake87.github.io/pyo3-asyncio/master/doc/pyo3_asyncio/#event-loop-references-and-contextvars>.
|
||||
pub async fn disconnect(&mut self) -> PyResult<()> {
|
||||
Python::with_gil(|py| {
|
||||
self.0
|
||||
|
||||
18
setup.cfg
18
setup.cfg
@@ -33,18 +33,18 @@ include_package_data = True
|
||||
install_requires =
|
||||
aiohttp ~= 3.8; platform_system!='Emscripten'
|
||||
appdirs >= 1.4; platform_system!='Emscripten'
|
||||
bt-test-interfaces >= 0.0.2; platform_system!='Emscripten'
|
||||
click == 8.1.3; platform_system!='Emscripten'
|
||||
bt-test-interfaces >= 0.0.6; platform_system!='Emscripten'
|
||||
click >= 8.1.3; platform_system!='Emscripten'
|
||||
cryptography == 39; platform_system!='Emscripten'
|
||||
# Pyodide bundles a version of cryptography that is built for wasm, which may not match the
|
||||
# versions available on PyPI. Relax the version requirement since it's better than being
|
||||
# completely unable to import the package in case of version mismatch.
|
||||
cryptography >= 39.0; platform_system=='Emscripten'
|
||||
grpcio == 1.57.0; platform_system!='Emscripten'
|
||||
grpcio >= 1.62.1; platform_system!='Emscripten'
|
||||
humanize >= 4.6.0; platform_system!='Emscripten'
|
||||
libusb1 >= 2.0.1; platform_system!='Emscripten'
|
||||
libusb-package == 1.0.26.1; platform_system!='Emscripten'
|
||||
platformdirs == 3.10.0; platform_system!='Emscripten'
|
||||
platformdirs >= 3.10.0; platform_system!='Emscripten'
|
||||
prompt_toolkit >= 3.0.16; platform_system!='Emscripten'
|
||||
prettytable >= 3.6.0; platform_system!='Emscripten'
|
||||
protobuf >= 3.12.4; platform_system!='Emscripten'
|
||||
@@ -83,12 +83,12 @@ build =
|
||||
build >= 0.7
|
||||
test =
|
||||
pytest >= 8.0
|
||||
pytest-asyncio == 0.21.1
|
||||
pytest-asyncio >= 0.21.1
|
||||
pytest-html >= 3.2.0
|
||||
coverage >= 6.4
|
||||
development =
|
||||
black == 22.10
|
||||
grpcio-tools >= 1.57.0
|
||||
black == 24.3
|
||||
grpcio-tools >= 1.62.1
|
||||
invoke >= 1.7.3
|
||||
mypy == 1.8.0
|
||||
nox >= 2022
|
||||
@@ -98,8 +98,8 @@ development =
|
||||
types-invoke >= 1.7.3
|
||||
types-protobuf >= 4.21.0
|
||||
avatar =
|
||||
pandora-avatar == 0.0.5
|
||||
rootcanal == 1.7.0 ; python_version>='3.10'
|
||||
pandora-avatar == 0.0.9
|
||||
rootcanal == 1.10.0 ; python_version>='3.10'
|
||||
documentation =
|
||||
mkdocs >= 1.4.0
|
||||
mkdocs-material >= 8.5.6
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
from bumble.core import AdvertisingData, UUID, get_dict_key_by_value
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def test_ad_data():
|
||||
data = bytes([2, AdvertisingData.TX_POWER_LEVEL, 123])
|
||||
|
||||
@@ -316,13 +316,13 @@ async def _test_self_smp_with_configs(pairing_config1, pairing_config2):
|
||||
|
||||
# Set up the pairing configs
|
||||
if pairing_config1:
|
||||
two_devices.devices[
|
||||
0
|
||||
].pairing_config_factory = lambda connection: pairing_config1
|
||||
two_devices.devices[0].pairing_config_factory = (
|
||||
lambda connection: pairing_config1
|
||||
)
|
||||
if pairing_config2:
|
||||
two_devices.devices[
|
||||
1
|
||||
].pairing_config_factory = lambda connection: pairing_config2
|
||||
two_devices.devices[1].pairing_config_factory = (
|
||||
lambda connection: pairing_config2
|
||||
)
|
||||
|
||||
# Pair
|
||||
await two_devices.devices[0].pair(connection)
|
||||
|
||||
64
tests/transport_tcp_server_test.py
Normal file
64
tests/transport_tcp_server_test.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import pytest
|
||||
import socket
|
||||
import unittest
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
from bumble.transport.tcp_server import (
|
||||
open_tcp_server_transport,
|
||||
open_tcp_server_transport_with_socket,
|
||||
)
|
||||
|
||||
|
||||
class OpenTcpServerTransportTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.patcher = patch('bumble.transport.tcp_server._create_server')
|
||||
self.mock_create_server = self.patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.patcher.stop()
|
||||
|
||||
def test_open_with_spec(self):
|
||||
asyncio.run(open_tcp_server_transport('localhost:32100'))
|
||||
self.mock_create_server.assert_awaited_once_with(
|
||||
ANY, host='localhost', port=32100
|
||||
)
|
||||
|
||||
def test_open_with_port_only_spec(self):
|
||||
asyncio.run(open_tcp_server_transport('_:32100'))
|
||||
self.mock_create_server.assert_awaited_once_with(ANY, host=None, port=32100)
|
||||
|
||||
def test_open_with_socket(self):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
asyncio.run(open_tcp_server_transport_with_socket(sock=sock))
|
||||
self.mock_create_server.assert_awaited_once_with(ANY, sock=sock)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.environ.get('PYTEST_NOSKIP', 0),
|
||||
reason='''\
|
||||
Not hermetic. Should only run manually with
|
||||
$ PYTEST_NOSKIP=1 pytest tests
|
||||
''',
|
||||
)
|
||||
def test_open_with_real_socket():
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(('localhost', 0))
|
||||
port = sock.getsockname()[1]
|
||||
assert port != 0
|
||||
asyncio.run(open_tcp_server_transport_with_socket(sock=sock))
|
||||
@@ -49,6 +49,7 @@ LINUX_FROM_SCRATCH_SOURCE = (
|
||||
False,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -111,7 +112,7 @@ def main(output_dir, source, single, force, parse):
|
||||
for driver_info in rtk.Driver.DRIVER_INFOS
|
||||
]
|
||||
|
||||
for (fw_name, config_name, config_needed) in images:
|
||||
for fw_name, config_name, config_needed in images:
|
||||
print(color("---", "yellow"))
|
||||
fw_image_out = output_dir / fw_name
|
||||
if not force and fw_image_out.exists():
|
||||
|
||||
Reference in New Issue
Block a user