forked from auracaster/bumble_mirror
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fa517a4f6 | |||
| a11962a487 | |||
| 32d448edf3 | |||
| 3d615b13ce | |||
| 1ad92dc759 | |||
| aacfd4328c | |||
| 6aa1f5211c | |||
| df8e454ee5 | |||
| aec50ac616 | |||
| 6a3eaa457f | |||
| 6e6b4cd4b2 | |||
| aa1d7933da | |||
| 34e0f293c2 | |||
| 85215df2c3 | |||
| 8a5f6a61d5 |
@@ -50,7 +50,7 @@ Bumble is easiest to use with a dedicated USB dongle.
|
||||
This is because internal Bluetooth interfaces tend to be locked down by the operating system.
|
||||
You can use the [usb_probe](/docs/mkdocs/src/apps_and_tools/usb_probe.md) tool (all platforms) or `lsusb` (Linux or macOS) to list the available USB devices on your system.
|
||||
|
||||
See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices. Also, if your are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md).
|
||||
See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices. Also, if you are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+7
-12
@@ -2171,7 +2171,7 @@ def with_connection_from_address(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(device: Device, address: hci.Address, *args, **kwargs):
|
||||
if connection := device.pending_connections.get(address):
|
||||
return function(device, connection, address, *args, **kwargs)
|
||||
return function(device, connection, *args, **kwargs)
|
||||
for connection in device.connections.values():
|
||||
if connection.peer_address == address:
|
||||
return function(device, connection, *args, **kwargs)
|
||||
@@ -2263,8 +2263,6 @@ class Device(utils.CompositeEventEmitter):
|
||||
EVENT_CONNECTION_FAILURE = "connection_failure"
|
||||
EVENT_SCO_REQUEST = "sco_request"
|
||||
EVENT_INQUIRY_COMPLETE = "inquiry_complete"
|
||||
EVENT_REMOTE_NAME = "remote_name"
|
||||
EVENT_REMOTE_NAME_FAILURE = "remote_name_failure"
|
||||
EVENT_SCO_CONNECTION = "sco_connection"
|
||||
EVENT_SCO_CONNECTION_FAILURE = "sco_connection_failure"
|
||||
EVENT_CIS_REQUEST = "cis_request"
|
||||
@@ -4727,7 +4725,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
self, cis_acl_pairs: Sequence[tuple[int, Connection]]
|
||||
) -> list[CisLink]:
|
||||
for cis_handle, acl_connection in cis_acl_pairs:
|
||||
cis_id, cig_id = self._pending_cis.pop(cis_handle)
|
||||
cis_id, cig_id = self._pending_cis[cis_handle]
|
||||
self.cis_links[cis_handle] = CisLink(
|
||||
device=self,
|
||||
acl_connection=acl_connection,
|
||||
@@ -4743,6 +4741,7 @@ class Device(utils.CompositeEventEmitter):
|
||||
}
|
||||
|
||||
def on_cis_establishment(cis_link: CisLink) -> None:
|
||||
self._pending_cis.pop(cis_link.handle)
|
||||
if pending_future := pending_cis_establishments.get(cis_link.handle):
|
||||
pending_future.set_result(cis_link)
|
||||
|
||||
@@ -6443,18 +6442,14 @@ class Device(utils.CompositeEventEmitter):
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
@try_with_connection_from_address
|
||||
@with_connection_from_address
|
||||
def on_role_change(
|
||||
self,
|
||||
connection: Optional[Connection],
|
||||
peer_address: hci.Address,
|
||||
connection: Connection,
|
||||
new_role: hci.Role,
|
||||
):
|
||||
if connection:
|
||||
connection.role = new_role
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
|
||||
else:
|
||||
logger.warning("Role change to unknown connection %s", peer_address)
|
||||
connection.role = new_role
|
||||
connection.emit(connection.EVENT_ROLE_CHANGE, new_role)
|
||||
|
||||
# [Classic only]
|
||||
@host_event_handler
|
||||
|
||||
+1
-1
@@ -550,7 +550,7 @@ class Host(utils.EventEmitter):
|
||||
logger.debug(
|
||||
'HCI LE flow control: '
|
||||
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
||||
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets},'
|
||||
f'iso_data_packet_length={iso_data_packet_length},'
|
||||
f'total_num_iso_data_packets={total_num_iso_data_packets}'
|
||||
)
|
||||
|
||||
+33
-4
@@ -273,12 +273,19 @@ class HearingAccessService(gatt.TemplateService):
|
||||
def on_disconnection(_reason) -> None:
|
||||
self.currently_connected_clients.discard(connection)
|
||||
|
||||
@connection.on(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
|
||||
def on_mtu_update(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
@connection.on(connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
|
||||
def on_encryption_change(*_: Any) -> None:
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
@connection.on(connection.EVENT_PAIRING)
|
||||
def on_pairing(*_: Any) -> None:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
if connection.peer_resolvable_address:
|
||||
self.on_incoming_paired_connection(connection)
|
||||
self.on_incoming_connection(connection)
|
||||
|
||||
self.hearing_aid_features_characteristic = gatt.Characteristic(
|
||||
uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC,
|
||||
@@ -315,9 +322,30 @@ class HearingAccessService(gatt.TemplateService):
|
||||
]
|
||||
)
|
||||
|
||||
def on_incoming_paired_connection(self, connection: Connection):
|
||||
def on_incoming_connection(self, connection: Connection):
|
||||
'''Setup initial operations to handle a remote bonded HAP device'''
|
||||
# TODO Should we filter on HAP device only ?
|
||||
|
||||
if not connection.is_encrypted:
|
||||
logging.debug(f'HAS: {connection.peer_address} is not encrypted')
|
||||
return
|
||||
|
||||
if not connection.peer_resolvable_address:
|
||||
logging.debug(f'HAS: {connection.peer_address} is not paired')
|
||||
return
|
||||
|
||||
if connection.att_mtu < 49:
|
||||
logging.debug(
|
||||
f'HAS: {connection.peer_address} invalid MTU={connection.att_mtu}'
|
||||
)
|
||||
return
|
||||
|
||||
if connection.peer_address in self.currently_connected_clients:
|
||||
logging.debug(
|
||||
f'HAS: Already connected to {connection.peer_address} nothing to do'
|
||||
)
|
||||
return
|
||||
|
||||
self.currently_connected_clients.add(connection)
|
||||
if (
|
||||
connection.peer_address
|
||||
@@ -457,6 +485,7 @@ class HearingAccessService(gatt.TemplateService):
|
||||
connection,
|
||||
self.hearing_aid_preset_control_point,
|
||||
value=op_list[0].to_bytes(len(op_list) == 1),
|
||||
force=True, # TODO GATT notification subscription should be persistent
|
||||
)
|
||||
# Remove item once sent, and keep the non sent item in the list
|
||||
op_list.pop(0)
|
||||
|
||||
@@ -131,7 +131,11 @@ def publish_grpc_port(grpc_port: int, instance_number: int) -> bool:
|
||||
|
||||
def cleanup():
|
||||
logger.debug("removing .ini file")
|
||||
ini_file.unlink()
|
||||
try:
|
||||
ini_file.unlink()
|
||||
except OSError as error:
|
||||
# Don't log at exception level, since this may happen normally.
|
||||
logger.debug(f'failed to remove .ini file ({error})')
|
||||
|
||||
atexit.register(cleanup)
|
||||
return True
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import serial_asyncio
|
||||
|
||||
@@ -28,25 +29,56 @@ from bumble.transport.common import StreamPacketSink, StreamPacketSource, Transp
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants
|
||||
# -----------------------------------------------------------------------------
|
||||
DEFAULT_POST_OPEN_DELAY = 0.5 # in seconds
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Classes and Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
class SerialPacketSource(StreamPacketSource):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._ready = asyncio.Event()
|
||||
|
||||
async def wait_until_ready(self) -> None:
|
||||
await self._ready.wait()
|
||||
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
logger.debug('connection made')
|
||||
self._ready.set()
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
logger.debug('connection lost')
|
||||
self.on_transport_lost()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def open_serial_transport(spec: str) -> Transport:
|
||||
'''
|
||||
Open a serial port transport.
|
||||
The parameter string has this syntax:
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr]
|
||||
<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]
|
||||
When <speed> is omitted, the default value of 1000000 is used
|
||||
When "rtscts" is specified, RTS/CTS hardware flow control is enabled
|
||||
When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled
|
||||
When "delay" is specified, a short delay is added after opening the port
|
||||
|
||||
Examples:
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
/dev/tty.usbmodem0006839912172,rtscts,delay
|
||||
'''
|
||||
|
||||
speed = 1000000
|
||||
rtscts = False
|
||||
dsrdtr = False
|
||||
delay = 0.0
|
||||
if ',' in spec:
|
||||
parts = spec.split(',')
|
||||
device = parts[0]
|
||||
@@ -55,13 +87,16 @@ async def open_serial_transport(spec: str) -> Transport:
|
||||
rtscts = True
|
||||
elif part == 'dsrdtr':
|
||||
dsrdtr = True
|
||||
elif part == 'delay':
|
||||
delay = DEFAULT_POST_OPEN_DELAY
|
||||
elif part.isnumeric():
|
||||
speed = int(part)
|
||||
else:
|
||||
device = spec
|
||||
|
||||
serial_transport, packet_source = await serial_asyncio.create_serial_connection(
|
||||
asyncio.get_running_loop(),
|
||||
StreamPacketSource,
|
||||
SerialPacketSource,
|
||||
device,
|
||||
baudrate=speed,
|
||||
rtscts=rtscts,
|
||||
@@ -69,4 +104,23 @@ async def open_serial_transport(spec: str) -> Transport:
|
||||
)
|
||||
packet_sink = StreamPacketSink(serial_transport)
|
||||
|
||||
logger.debug('waiting for the port to be ready')
|
||||
await packet_source.wait_until_ready()
|
||||
logger.debug('port is ready')
|
||||
|
||||
# Try to assert DTR
|
||||
assert serial_transport.serial is not None
|
||||
try:
|
||||
serial_transport.serial.dtr = True
|
||||
logger.debug(
|
||||
f"DSR={serial_transport.serial.dsr}, DTR={serial_transport.serial.dtr}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'could not assert DTR: {e}')
|
||||
|
||||
# Wait a bit after opening the port, if requested
|
||||
if delay > 0.0:
|
||||
logger.debug(f'waiting {delay} seconds after opening the port')
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
return Transport(packet_source, packet_sink)
|
||||
|
||||
@@ -4,9 +4,18 @@ SERIAL TRANSPORT
|
||||
The serial transport implements sending/receiving HCI packets over a UART (a.k.a serial port).
|
||||
|
||||
## Moniker
|
||||
The moniker syntax for a serial transport is: `serial:<device-path>[,<speed>]`
|
||||
When `<speed>` is omitted, the default value of 1000000 is used
|
||||
The moniker syntax for a serial transport is:
|
||||
`<device-path>[,<speed>][,rtscts][,dsrdtr][,delay]`
|
||||
|
||||
When `<speed>` is omitted, the default value of 1000000 is used.
|
||||
When `rtscts` is specified, RTS/CTS hardware flow control is enabled.
|
||||
When `dsrdtr` is specified, DSR/DTR hardware flow control is enabled.
|
||||
When `delay` is specified, a short delay is added after opening the port.
|
||||
|
||||
!!! example
|
||||
`serial:/dev/tty.usbmodem0006839912172,1000000`
|
||||
Opens the serial port `/dev/tty.usbmodem0006839912172` at `1000000`bps
|
||||
```
|
||||
/dev/tty.usbmodem0006839912172
|
||||
/dev/tty.usbmodem0006839912172,1000000
|
||||
/dev/tty.usbmodem0006839912172,rtscts
|
||||
/dev/tty.usbmodem0006839912172,rtscts,delay
|
||||
```
|
||||
@@ -761,6 +761,34 @@ async def test_inquiry_result_with_rssi():
|
||||
m.assert_called_with(hci.Address("00:11:22:33:44:55/P"), 3, mock.ANY, 5)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@pytest.mark.parametrize(
|
||||
"roles",
|
||||
(
|
||||
(hci.Role.PERIPHERAL, hci.Role.CENTRAL),
|
||||
(hci.Role.CENTRAL, hci.Role.PERIPHERAL),
|
||||
),
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_accept_classic_connection(roles: tuple[hci.Role, hci.Role]):
|
||||
devices = TwoDevices()
|
||||
devices[0].classic_enabled = True
|
||||
devices[1].classic_enabled = True
|
||||
await devices[0].power_on()
|
||||
await devices[1].power_on()
|
||||
|
||||
accept_task = asyncio.create_task(devices[1].accept(role=roles[1]))
|
||||
await devices[0].connect(
|
||||
devices[1].public_address, transport=PhysicalTransport.BR_EDR
|
||||
)
|
||||
await accept_task
|
||||
|
||||
assert devices.connections[0]
|
||||
assert devices.connections[0].role == roles[0]
|
||||
assert devices.connections[1]
|
||||
assert devices.connections[1].role == roles[1]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def run_test_device():
|
||||
await test_device_connect_parallel()
|
||||
|
||||
+3
-1
@@ -82,7 +82,6 @@ async def hap_client():
|
||||
)
|
||||
|
||||
await devices.setup_connection()
|
||||
# TODO negotiate MTU > 49 to not truncate preset names
|
||||
|
||||
# Mock encryption.
|
||||
devices.connections[0].encryption = 1 # type: ignore
|
||||
@@ -93,6 +92,9 @@ async def hap_client():
|
||||
)
|
||||
|
||||
peer = device.Peer(devices.connections[1]) # type: ignore
|
||||
await peer.request_mtu(49)
|
||||
peer2 = device.Peer(devices.connections[0]) # type: ignore
|
||||
await peer2.request_mtu(49)
|
||||
hap_client = await peer.discover_service_and_create_proxy(
|
||||
hap.HearingAccessServiceProxy
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user