forked from auracaster/bumble_mirror
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b173cb879 | |||
| 8b04161da3 | |||
| 333940919b | |||
| b9476be9ad | |||
| 704c60491c | |||
| 4a8e612c6e | |||
| 4e71ec5738 | |||
| 7255a09705 | |||
| c2bf6b5f13 | |||
| d8e699b588 | |||
| 3e4d4705f5 | |||
| c8b2804446 | |||
| e732f2589f | |||
| aec5543081 | |||
| e03d90ca57 | |||
| 495ce62d9c | |||
| fbc3959a5a |
@@ -0,0 +1,43 @@
|
|||||||
|
name: Python Avatar
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Avatar [${{ matrix.shard }}]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
shard: [
|
||||||
|
1/24, 2/24, 3/24, 4/24,
|
||||||
|
5/24, 6/24, 7/24, 8/24,
|
||||||
|
9/24, 10/24, 11/24, 12/24,
|
||||||
|
13/24, 14/24, 15/24, 16/24,
|
||||||
|
17/24, 18/24, 19/24, 20/24,
|
||||||
|
21/24, 22/24, 23/24, 24/24,
|
||||||
|
]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set Up Python 3.11
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.11
|
||||||
|
- name: Install
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install .[avatar]
|
||||||
|
- name: Rootcanal
|
||||||
|
run: nohup python -m rootcanal > rootcanal.log &
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
avatar --list | grep -Ev '^=' > test-names.txt
|
||||||
|
timeout 5m avatar --test-beds bumble.bumbles --tests $(split test-names.txt -n l/${{ matrix.shard }})
|
||||||
|
- name: Rootcanal Logs
|
||||||
|
run: cat rootcanal.log
|
||||||
+1
-1
@@ -306,6 +306,7 @@ async def pair(
|
|||||||
# Expose a GATT characteristic that can be used to trigger pairing by
|
# Expose a GATT characteristic that can be used to trigger pairing by
|
||||||
# responding with an authentication error when read
|
# responding with an authentication error when read
|
||||||
if mode == 'le':
|
if mode == 'le':
|
||||||
|
device.le_enabled = True
|
||||||
device.add_service(
|
device.add_service(
|
||||||
Service(
|
Service(
|
||||||
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
'50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
|
||||||
@@ -326,7 +327,6 @@ async def pair(
|
|||||||
# Select LE or Classic
|
# Select LE or Classic
|
||||||
if mode == 'classic':
|
if mode == 'classic':
|
||||||
device.classic_enabled = True
|
device.classic_enabled = True
|
||||||
device.le_enabled = False
|
|
||||||
device.classic_smp_enabled = ctkd
|
device.classic_smp_enabled = ctkd
|
||||||
|
|
||||||
# Get things going
|
# Get things going
|
||||||
|
|||||||
+303
-177
File diff suppressed because it is too large
Load Diff
+813
-173
File diff suppressed because it is too large
Load Diff
+92
-9
@@ -33,6 +33,8 @@ from typing import (
|
|||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
|
cast,
|
||||||
|
overload,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,6 +153,7 @@ from .utils import (
|
|||||||
CompositeEventEmitter,
|
CompositeEventEmitter,
|
||||||
setup_event_forwarding,
|
setup_event_forwarding,
|
||||||
composite_listener,
|
composite_listener,
|
||||||
|
deprecated,
|
||||||
)
|
)
|
||||||
from .keys import (
|
from .keys import (
|
||||||
KeyStore,
|
KeyStore,
|
||||||
@@ -670,9 +673,7 @@ class Connection(CompositeEventEmitter):
|
|||||||
def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None:
|
def send_l2cap_pdu(self, cid: int, pdu: bytes) -> None:
|
||||||
self.device.send_l2cap_pdu(self.handle, cid, pdu)
|
self.device.send_l2cap_pdu(self.handle, cid, pdu)
|
||||||
|
|
||||||
def create_l2cap_connector(self, psm):
|
@deprecated("Please use create_l2cap_channel()")
|
||||||
return self.device.create_l2cap_connector(self, psm)
|
|
||||||
|
|
||||||
async def open_l2cap_channel(
|
async def open_l2cap_channel(
|
||||||
self,
|
self,
|
||||||
psm,
|
psm,
|
||||||
@@ -682,6 +683,23 @@ class Connection(CompositeEventEmitter):
|
|||||||
):
|
):
|
||||||
return await self.device.open_l2cap_channel(self, psm, max_credits, mtu, mps)
|
return await self.device.open_l2cap_channel(self, psm, max_credits, mtu, mps)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def create_l2cap_channel(
|
||||||
|
self, spec: l2cap.ClassicChannelSpec
|
||||||
|
) -> l2cap.ClassicChannel:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def create_l2cap_channel(
|
||||||
|
self, spec: l2cap.LeCreditBasedChannelSpec
|
||||||
|
) -> l2cap.LeCreditBasedChannel:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def create_l2cap_channel(
|
||||||
|
self, spec: Union[l2cap.ClassicChannelSpec, l2cap.LeCreditBasedChannelSpec]
|
||||||
|
) -> Union[l2cap.ClassicChannel, l2cap.LeCreditBasedChannel]:
|
||||||
|
return await self.device.create_l2cap_channel(connection=self, spec=spec)
|
||||||
|
|
||||||
async def disconnect(
|
async def disconnect(
|
||||||
self, reason: int = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR
|
self, reason: int = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1180,15 +1198,11 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def create_l2cap_connector(self, connection, psm):
|
@deprecated("Please use create_l2cap_server()")
|
||||||
return lambda: self.l2cap_channel_manager.connect(connection, psm)
|
|
||||||
|
|
||||||
def create_l2cap_registrar(self, psm):
|
|
||||||
return lambda handler: self.register_l2cap_server(psm, handler)
|
|
||||||
|
|
||||||
def register_l2cap_server(self, psm, server) -> int:
|
def register_l2cap_server(self, psm, server) -> int:
|
||||||
return self.l2cap_channel_manager.register_server(psm, server)
|
return self.l2cap_channel_manager.register_server(psm, server)
|
||||||
|
|
||||||
|
@deprecated("Please use create_l2cap_server()")
|
||||||
def register_l2cap_channel_server(
|
def register_l2cap_channel_server(
|
||||||
self,
|
self,
|
||||||
psm,
|
psm,
|
||||||
@@ -1201,6 +1215,7 @@ class Device(CompositeEventEmitter):
|
|||||||
psm, server, max_credits, mtu, mps
|
psm, server, max_credits, mtu, mps
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@deprecated("Please use create_l2cap_channel()")
|
||||||
async def open_l2cap_channel(
|
async def open_l2cap_channel(
|
||||||
self,
|
self,
|
||||||
connection,
|
connection,
|
||||||
@@ -1213,6 +1228,74 @@ class Device(CompositeEventEmitter):
|
|||||||
connection, psm, max_credits, mtu, mps
|
connection, psm, max_credits, mtu, mps
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def create_l2cap_channel(
|
||||||
|
self,
|
||||||
|
connection: Connection,
|
||||||
|
spec: l2cap.ClassicChannelSpec,
|
||||||
|
) -> l2cap.ClassicChannel:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def create_l2cap_channel(
|
||||||
|
self,
|
||||||
|
connection: Connection,
|
||||||
|
spec: l2cap.LeCreditBasedChannelSpec,
|
||||||
|
) -> l2cap.LeCreditBasedChannel:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def create_l2cap_channel(
|
||||||
|
self,
|
||||||
|
connection: Connection,
|
||||||
|
spec: Union[l2cap.ClassicChannelSpec, l2cap.LeCreditBasedChannelSpec],
|
||||||
|
) -> Union[l2cap.ClassicChannel, l2cap.LeCreditBasedChannel]:
|
||||||
|
if isinstance(spec, l2cap.ClassicChannelSpec):
|
||||||
|
return await self.l2cap_channel_manager.create_classic_channel(
|
||||||
|
connection=connection, spec=spec
|
||||||
|
)
|
||||||
|
if isinstance(spec, l2cap.LeCreditBasedChannelSpec):
|
||||||
|
return await self.l2cap_channel_manager.create_le_credit_based_channel(
|
||||||
|
connection=connection, spec=spec
|
||||||
|
)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def create_l2cap_server(
|
||||||
|
self,
|
||||||
|
spec: l2cap.ClassicChannelSpec,
|
||||||
|
handler: Optional[Callable[[l2cap.ClassicChannel], Any]] = None,
|
||||||
|
) -> l2cap.ClassicChannelServer:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def create_l2cap_server(
|
||||||
|
self,
|
||||||
|
spec: l2cap.LeCreditBasedChannelSpec,
|
||||||
|
handler: Optional[Callable[[l2cap.LeCreditBasedChannel], Any]] = None,
|
||||||
|
) -> l2cap.LeCreditBasedChannelServer:
|
||||||
|
...
|
||||||
|
|
||||||
|
def create_l2cap_server(
|
||||||
|
self,
|
||||||
|
spec: Union[l2cap.ClassicChannelSpec, l2cap.LeCreditBasedChannelSpec],
|
||||||
|
handler: Union[
|
||||||
|
Callable[[l2cap.ClassicChannel], Any],
|
||||||
|
Callable[[l2cap.LeCreditBasedChannel], Any],
|
||||||
|
None,
|
||||||
|
] = None,
|
||||||
|
) -> Union[l2cap.ClassicChannelServer, l2cap.LeCreditBasedChannelServer]:
|
||||||
|
if isinstance(spec, l2cap.ClassicChannelSpec):
|
||||||
|
return self.l2cap_channel_manager.create_classic_server(
|
||||||
|
spec=spec,
|
||||||
|
handler=cast(Callable[[l2cap.ClassicChannel], Any], handler),
|
||||||
|
)
|
||||||
|
elif isinstance(spec, l2cap.LeCreditBasedChannelSpec):
|
||||||
|
return self.l2cap_channel_manager.create_le_credit_based_server(
|
||||||
|
handler=cast(Callable[[l2cap.LeCreditBasedChannel], Any], handler),
|
||||||
|
spec=spec,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unexpected mode {spec}')
|
||||||
|
|
||||||
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
||||||
self.host.send_l2cap_pdu(connection_handle, cid, pdu)
|
self.host.send_l2cap_pdu(connection_handle, cid, pdu)
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -121,6 +121,7 @@ HCI_VERSION_BLUETOOTH_CORE_5_0 = 9
|
|||||||
HCI_VERSION_BLUETOOTH_CORE_5_1 = 10
|
HCI_VERSION_BLUETOOTH_CORE_5_1 = 10
|
||||||
HCI_VERSION_BLUETOOTH_CORE_5_2 = 11
|
HCI_VERSION_BLUETOOTH_CORE_5_2 = 11
|
||||||
HCI_VERSION_BLUETOOTH_CORE_5_3 = 12
|
HCI_VERSION_BLUETOOTH_CORE_5_3 = 12
|
||||||
|
HCI_VERSION_BLUETOOTH_CORE_5_4 = 13
|
||||||
|
|
||||||
HCI_VERSION_NAMES = {
|
HCI_VERSION_NAMES = {
|
||||||
HCI_VERSION_BLUETOOTH_CORE_1_0B: 'HCI_VERSION_BLUETOOTH_CORE_1_0B',
|
HCI_VERSION_BLUETOOTH_CORE_1_0B: 'HCI_VERSION_BLUETOOTH_CORE_1_0B',
|
||||||
@@ -135,7 +136,8 @@ HCI_VERSION_NAMES = {
|
|||||||
HCI_VERSION_BLUETOOTH_CORE_5_0: 'HCI_VERSION_BLUETOOTH_CORE_5_0',
|
HCI_VERSION_BLUETOOTH_CORE_5_0: 'HCI_VERSION_BLUETOOTH_CORE_5_0',
|
||||||
HCI_VERSION_BLUETOOTH_CORE_5_1: 'HCI_VERSION_BLUETOOTH_CORE_5_1',
|
HCI_VERSION_BLUETOOTH_CORE_5_1: 'HCI_VERSION_BLUETOOTH_CORE_5_1',
|
||||||
HCI_VERSION_BLUETOOTH_CORE_5_2: 'HCI_VERSION_BLUETOOTH_CORE_5_2',
|
HCI_VERSION_BLUETOOTH_CORE_5_2: 'HCI_VERSION_BLUETOOTH_CORE_5_2',
|
||||||
HCI_VERSION_BLUETOOTH_CORE_5_3: 'HCI_VERSION_BLUETOOTH_CORE_5_3'
|
HCI_VERSION_BLUETOOTH_CORE_5_3: 'HCI_VERSION_BLUETOOTH_CORE_5_3',
|
||||||
|
HCI_VERSION_BLUETOOTH_CORE_5_4: 'HCI_VERSION_BLUETOOTH_CORE_5_4',
|
||||||
}
|
}
|
||||||
|
|
||||||
# LMP Version
|
# LMP Version
|
||||||
|
|||||||
+219
-76
@@ -17,6 +17,7 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
@@ -38,6 +39,7 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .utils import deprecated
|
||||||
from .colors import color
|
from .colors import color
|
||||||
from .core import BT_CENTRAL_ROLE, InvalidStateError, ProtocolError
|
from .core import BT_CENTRAL_ROLE, InvalidStateError, ProtocolError
|
||||||
from .hci import (
|
from .hci import (
|
||||||
@@ -167,6 +169,34 @@ L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01
|
|||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class ClassicChannelSpec:
|
||||||
|
psm: Optional[int] = None
|
||||||
|
mtu: int = L2CAP_MIN_BR_EDR_MTU
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class LeCreditBasedChannelSpec:
|
||||||
|
psm: Optional[int] = None
|
||||||
|
mtu: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU
|
||||||
|
mps: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS
|
||||||
|
max_credits: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if (
|
||||||
|
self.max_credits < 1
|
||||||
|
or self.max_credits > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_CREDITS
|
||||||
|
):
|
||||||
|
raise ValueError('max credits out of range')
|
||||||
|
if self.mtu < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU:
|
||||||
|
raise ValueError('MTU too small')
|
||||||
|
if (
|
||||||
|
self.mps < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS
|
||||||
|
or self.mps > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS
|
||||||
|
):
|
||||||
|
raise ValueError('MPS out of range')
|
||||||
|
|
||||||
|
|
||||||
class L2CAP_PDU:
|
class L2CAP_PDU:
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
|
See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
|
||||||
@@ -676,7 +706,7 @@ class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Channel(EventEmitter):
|
class ClassicChannel(EventEmitter):
|
||||||
class State(enum.IntEnum):
|
class State(enum.IntEnum):
|
||||||
# States
|
# States
|
||||||
CLOSED = 0x00
|
CLOSED = 0x00
|
||||||
@@ -990,7 +1020,7 @@ class Channel(EventEmitter):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class LeConnectionOrientedChannel(EventEmitter):
|
class LeCreditBasedChannel(EventEmitter):
|
||||||
"""
|
"""
|
||||||
LE Credit-based Connection Oriented Channel
|
LE Credit-based Connection Oriented Channel
|
||||||
"""
|
"""
|
||||||
@@ -1004,7 +1034,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
CONNECTION_ERROR = 5
|
CONNECTION_ERROR = 5
|
||||||
|
|
||||||
out_queue: Deque[bytes]
|
out_queue: Deque[bytes]
|
||||||
connection_result: Optional[asyncio.Future[LeConnectionOrientedChannel]]
|
connection_result: Optional[asyncio.Future[LeCreditBasedChannel]]
|
||||||
disconnection_result: Optional[asyncio.Future[None]]
|
disconnection_result: Optional[asyncio.Future[None]]
|
||||||
out_sdu: Optional[bytes]
|
out_sdu: Optional[bytes]
|
||||||
state: State
|
state: State
|
||||||
@@ -1071,7 +1101,7 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
|
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
|
||||||
self.manager.send_control_frame(self.connection, L2CAP_LE_SIGNALING_CID, frame)
|
self.manager.send_control_frame(self.connection, L2CAP_LE_SIGNALING_CID, frame)
|
||||||
|
|
||||||
async def connect(self) -> LeConnectionOrientedChannel:
|
async def connect(self) -> LeCreditBasedChannel:
|
||||||
# Check that we're in the right state
|
# Check that we're in the right state
|
||||||
if self.state != self.State.INIT:
|
if self.state != self.State.INIT:
|
||||||
raise InvalidStateError('not in a connectable state')
|
raise InvalidStateError('not in a connectable state')
|
||||||
@@ -1342,15 +1372,67 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class ClassicChannelServer(EventEmitter):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
manager: ChannelManager,
|
||||||
|
psm: int,
|
||||||
|
handler: Optional[Callable[[ClassicChannel], Any]],
|
||||||
|
mtu: int,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.manager = manager
|
||||||
|
self.handler = handler
|
||||||
|
self.psm = psm
|
||||||
|
self.mtu = mtu
|
||||||
|
|
||||||
|
def on_connection(self, channel: ClassicChannel) -> None:
|
||||||
|
self.emit('connection', channel)
|
||||||
|
if self.handler:
|
||||||
|
self.handler(channel)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self.psm in self.manager.servers:
|
||||||
|
del self.manager.servers[self.psm]
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class LeCreditBasedChannelServer(EventEmitter):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
manager: ChannelManager,
|
||||||
|
psm: int,
|
||||||
|
handler: Optional[Callable[[LeCreditBasedChannel], Any]],
|
||||||
|
max_credits: int,
|
||||||
|
mtu: int,
|
||||||
|
mps: int,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.manager = manager
|
||||||
|
self.handler = handler
|
||||||
|
self.psm = psm
|
||||||
|
self.max_credits = max_credits
|
||||||
|
self.mtu = mtu
|
||||||
|
self.mps = mps
|
||||||
|
|
||||||
|
def on_connection(self, channel: LeCreditBasedChannel) -> None:
|
||||||
|
self.emit('connection', channel)
|
||||||
|
if self.handler:
|
||||||
|
self.handler(channel)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self.psm in self.manager.le_coc_servers:
|
||||||
|
del self.manager.le_coc_servers[self.psm]
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ChannelManager:
|
class ChannelManager:
|
||||||
identifiers: Dict[int, int]
|
identifiers: Dict[int, int]
|
||||||
channels: Dict[int, Dict[int, Union[Channel, LeConnectionOrientedChannel]]]
|
channels: Dict[int, Dict[int, Union[ClassicChannel, LeCreditBasedChannel]]]
|
||||||
servers: Dict[int, Callable[[Channel], Any]]
|
servers: Dict[int, ClassicChannelServer]
|
||||||
le_coc_channels: Dict[int, Dict[int, LeConnectionOrientedChannel]]
|
le_coc_channels: Dict[int, Dict[int, LeCreditBasedChannel]]
|
||||||
le_coc_servers: Dict[
|
le_coc_servers: Dict[int, LeCreditBasedChannelServer]
|
||||||
int, Tuple[Callable[[LeConnectionOrientedChannel], Any], int, int, int]
|
|
||||||
]
|
|
||||||
le_coc_requests: Dict[int, L2CAP_LE_Credit_Based_Connection_Request]
|
le_coc_requests: Dict[int, L2CAP_LE_Credit_Based_Connection_Request]
|
||||||
fixed_channels: Dict[int, Optional[Callable[[int, bytes], Any]]]
|
fixed_channels: Dict[int, Optional[Callable[[int, bytes], Any]]]
|
||||||
_host: Optional[Host]
|
_host: Optional[Host]
|
||||||
@@ -1429,21 +1511,6 @@ class ChannelManager:
|
|||||||
|
|
||||||
raise RuntimeError('no free CID')
|
raise RuntimeError('no free CID')
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_le_coc_parameters(max_credits: int, mtu: int, mps: int) -> None:
|
|
||||||
if (
|
|
||||||
max_credits < 1
|
|
||||||
or max_credits > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_CREDITS
|
|
||||||
):
|
|
||||||
raise ValueError('max credits out of range')
|
|
||||||
if mtu < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU:
|
|
||||||
raise ValueError('MTU too small')
|
|
||||||
if (
|
|
||||||
mps < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS
|
|
||||||
or mps > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS
|
|
||||||
):
|
|
||||||
raise ValueError('MPS out of range')
|
|
||||||
|
|
||||||
def next_identifier(self, connection: Connection) -> int:
|
def next_identifier(self, connection: Connection) -> int:
|
||||||
identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
|
identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
|
||||||
self.identifiers[connection.handle] = identifier
|
self.identifiers[connection.handle] = identifier
|
||||||
@@ -1458,8 +1525,22 @@ class ChannelManager:
|
|||||||
if cid in self.fixed_channels:
|
if cid in self.fixed_channels:
|
||||||
del self.fixed_channels[cid]
|
del self.fixed_channels[cid]
|
||||||
|
|
||||||
def register_server(self, psm: int, server: Callable[[Channel], Any]) -> int:
|
@deprecated("Please use create_classic_channel_server")
|
||||||
if psm == 0:
|
def register_server(
|
||||||
|
self,
|
||||||
|
psm: int,
|
||||||
|
server: Callable[[ClassicChannel], Any],
|
||||||
|
) -> int:
|
||||||
|
return self.create_classic_server(
|
||||||
|
handler=server, spec=ClassicChannelSpec(psm=psm)
|
||||||
|
).psm
|
||||||
|
|
||||||
|
def create_classic_server(
|
||||||
|
self,
|
||||||
|
spec: ClassicChannelSpec,
|
||||||
|
handler: Optional[Callable[[ClassicChannel], Any]] = None,
|
||||||
|
) -> ClassicChannelServer:
|
||||||
|
if spec.psm is None:
|
||||||
# Find a free PSM
|
# Find a free PSM
|
||||||
for candidate in range(
|
for candidate in range(
|
||||||
L2CAP_PSM_DYNAMIC_RANGE_START, L2CAP_PSM_DYNAMIC_RANGE_END + 1, 2
|
L2CAP_PSM_DYNAMIC_RANGE_START, L2CAP_PSM_DYNAMIC_RANGE_END + 1, 2
|
||||||
@@ -1468,62 +1549,75 @@ class ChannelManager:
|
|||||||
continue
|
continue
|
||||||
if candidate in self.servers:
|
if candidate in self.servers:
|
||||||
continue
|
continue
|
||||||
psm = candidate
|
spec.psm = candidate
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise InvalidStateError('no free PSM')
|
raise InvalidStateError('no free PSM')
|
||||||
else:
|
else:
|
||||||
# Check that the PSM isn't already in use
|
# Check that the PSM isn't already in use
|
||||||
if psm in self.servers:
|
if spec.psm in self.servers:
|
||||||
raise ValueError('PSM already in use')
|
raise ValueError('PSM already in use')
|
||||||
|
|
||||||
# Check that the PSM is valid
|
# Check that the PSM is valid
|
||||||
if psm % 2 == 0:
|
if spec.psm % 2 == 0:
|
||||||
raise ValueError('invalid PSM (not odd)')
|
raise ValueError('invalid PSM (not odd)')
|
||||||
check = psm >> 8
|
check = spec.psm >> 8
|
||||||
while check:
|
while check:
|
||||||
if check % 2 != 0:
|
if check % 2 != 0:
|
||||||
raise ValueError('invalid PSM')
|
raise ValueError('invalid PSM')
|
||||||
check >>= 8
|
check >>= 8
|
||||||
|
|
||||||
self.servers[psm] = server
|
self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec.mtu)
|
||||||
|
|
||||||
return psm
|
return self.servers[spec.psm]
|
||||||
|
|
||||||
|
@deprecated("Please use create_le_credit_based_server()")
|
||||||
def register_le_coc_server(
|
def register_le_coc_server(
|
||||||
self,
|
self,
|
||||||
psm: int,
|
psm: int,
|
||||||
server: Callable[[LeConnectionOrientedChannel], Any],
|
server: Callable[[LeCreditBasedChannel], Any],
|
||||||
max_credits: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS,
|
max_credits: int,
|
||||||
mtu: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
|
mtu: int,
|
||||||
mps: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
|
mps: int,
|
||||||
) -> int:
|
) -> int:
|
||||||
self.check_le_coc_parameters(max_credits, mtu, mps)
|
return self.create_le_credit_based_server(
|
||||||
|
spec=LeCreditBasedChannelSpec(
|
||||||
|
psm=None if psm == 0 else psm, mtu=mtu, mps=mps, max_credits=max_credits
|
||||||
|
),
|
||||||
|
handler=server,
|
||||||
|
).psm
|
||||||
|
|
||||||
if psm == 0:
|
def create_le_credit_based_server(
|
||||||
|
self,
|
||||||
|
spec: LeCreditBasedChannelSpec,
|
||||||
|
handler: Optional[Callable[[LeCreditBasedChannel], Any]] = None,
|
||||||
|
) -> LeCreditBasedChannelServer:
|
||||||
|
if spec.psm is None:
|
||||||
# Find a free PSM
|
# Find a free PSM
|
||||||
for candidate in range(
|
for candidate in range(
|
||||||
L2CAP_LE_PSM_DYNAMIC_RANGE_START, L2CAP_LE_PSM_DYNAMIC_RANGE_END + 1
|
L2CAP_LE_PSM_DYNAMIC_RANGE_START, L2CAP_LE_PSM_DYNAMIC_RANGE_END + 1
|
||||||
):
|
):
|
||||||
if candidate in self.le_coc_servers:
|
if candidate in self.le_coc_servers:
|
||||||
continue
|
continue
|
||||||
psm = candidate
|
spec.psm = candidate
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise InvalidStateError('no free PSM')
|
raise InvalidStateError('no free PSM')
|
||||||
else:
|
else:
|
||||||
# Check that the PSM isn't already in use
|
# Check that the PSM isn't already in use
|
||||||
if psm in self.le_coc_servers:
|
if spec.psm in self.le_coc_servers:
|
||||||
raise ValueError('PSM already in use')
|
raise ValueError('PSM already in use')
|
||||||
|
|
||||||
self.le_coc_servers[psm] = (
|
self.le_coc_servers[spec.psm] = LeCreditBasedChannelServer(
|
||||||
server,
|
self,
|
||||||
max_credits or L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS,
|
spec.psm,
|
||||||
mtu or L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
|
handler,
|
||||||
mps or L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
|
max_credits=spec.max_credits,
|
||||||
|
mtu=spec.mtu,
|
||||||
|
mps=spec.mps,
|
||||||
)
|
)
|
||||||
|
|
||||||
return psm
|
return self.le_coc_servers[spec.psm]
|
||||||
|
|
||||||
def on_disconnection(self, connection_handle: int, _reason: int) -> None:
|
def on_disconnection(self, connection_handle: int, _reason: int) -> None:
|
||||||
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
||||||
@@ -1650,13 +1744,13 @@ class ChannelManager:
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
f'creating server channel with cid={source_cid} for psm {request.psm}'
|
f'creating server channel with cid={source_cid} for psm {request.psm}'
|
||||||
)
|
)
|
||||||
channel = Channel(
|
channel = ClassicChannel(
|
||||||
self, connection, cid, request.psm, source_cid, L2CAP_MIN_BR_EDR_MTU
|
self, connection, cid, request.psm, source_cid, server.mtu
|
||||||
)
|
)
|
||||||
connection_channels[source_cid] = channel
|
connection_channels[source_cid] = channel
|
||||||
|
|
||||||
# Notify
|
# Notify
|
||||||
server(channel)
|
server.on_connection(channel)
|
||||||
channel.on_connection_request(request)
|
channel.on_connection_request(request)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -1878,7 +1972,7 @@ class ChannelManager:
|
|||||||
self, connection: Connection, cid: int, request
|
self, connection: Connection, cid: int, request
|
||||||
) -> None:
|
) -> None:
|
||||||
if request.le_psm in self.le_coc_servers:
|
if request.le_psm in self.le_coc_servers:
|
||||||
(server, max_credits, mtu, mps) = self.le_coc_servers[request.le_psm]
|
server = self.le_coc_servers[request.le_psm]
|
||||||
|
|
||||||
# Check that the CID isn't already used
|
# Check that the CID isn't already used
|
||||||
le_connection_channels = self.le_coc_channels.setdefault(
|
le_connection_channels = self.le_coc_channels.setdefault(
|
||||||
@@ -1892,8 +1986,8 @@ class ChannelManager:
|
|||||||
L2CAP_LE_Credit_Based_Connection_Response(
|
L2CAP_LE_Credit_Based_Connection_Response(
|
||||||
identifier=request.identifier,
|
identifier=request.identifier,
|
||||||
destination_cid=0,
|
destination_cid=0,
|
||||||
mtu=mtu,
|
mtu=server.mtu,
|
||||||
mps=mps,
|
mps=server.mps,
|
||||||
initial_credits=0,
|
initial_credits=0,
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
|
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
|
||||||
@@ -1911,8 +2005,8 @@ class ChannelManager:
|
|||||||
L2CAP_LE_Credit_Based_Connection_Response(
|
L2CAP_LE_Credit_Based_Connection_Response(
|
||||||
identifier=request.identifier,
|
identifier=request.identifier,
|
||||||
destination_cid=0,
|
destination_cid=0,
|
||||||
mtu=mtu,
|
mtu=server.mtu,
|
||||||
mps=mps,
|
mps=server.mps,
|
||||||
initial_credits=0,
|
initial_credits=0,
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
|
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
|
||||||
@@ -1925,18 +2019,18 @@ class ChannelManager:
|
|||||||
f'creating LE CoC server channel with cid={source_cid} for psm '
|
f'creating LE CoC server channel with cid={source_cid} for psm '
|
||||||
f'{request.le_psm}'
|
f'{request.le_psm}'
|
||||||
)
|
)
|
||||||
channel = LeConnectionOrientedChannel(
|
channel = LeCreditBasedChannel(
|
||||||
self,
|
self,
|
||||||
connection,
|
connection,
|
||||||
request.le_psm,
|
request.le_psm,
|
||||||
source_cid,
|
source_cid,
|
||||||
request.source_cid,
|
request.source_cid,
|
||||||
mtu,
|
server.mtu,
|
||||||
mps,
|
server.mps,
|
||||||
request.initial_credits,
|
request.initial_credits,
|
||||||
request.mtu,
|
request.mtu,
|
||||||
request.mps,
|
request.mps,
|
||||||
max_credits,
|
server.max_credits,
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
connection_channels[source_cid] = channel
|
connection_channels[source_cid] = channel
|
||||||
@@ -1949,16 +2043,16 @@ class ChannelManager:
|
|||||||
L2CAP_LE_Credit_Based_Connection_Response(
|
L2CAP_LE_Credit_Based_Connection_Response(
|
||||||
identifier=request.identifier,
|
identifier=request.identifier,
|
||||||
destination_cid=source_cid,
|
destination_cid=source_cid,
|
||||||
mtu=mtu,
|
mtu=server.mtu,
|
||||||
mps=mps,
|
mps=server.mps,
|
||||||
initial_credits=max_credits,
|
initial_credits=server.max_credits,
|
||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_SUCCESSFUL,
|
result=L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_SUCCESSFUL,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Notify
|
# Notify
|
||||||
server(channel)
|
server.on_connection(channel)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
f'No LE server for connection 0x{connection.handle:04X} '
|
f'No LE server for connection 0x{connection.handle:04X} '
|
||||||
@@ -2013,37 +2107,51 @@ class ChannelManager:
|
|||||||
|
|
||||||
channel.on_credits(credit.credits)
|
channel.on_credits(credit.credits)
|
||||||
|
|
||||||
def on_channel_closed(self, channel: Channel) -> None:
|
def on_channel_closed(self, channel: ClassicChannel) -> None:
|
||||||
connection_channels = self.channels.get(channel.connection.handle)
|
connection_channels = self.channels.get(channel.connection.handle)
|
||||||
if connection_channels:
|
if connection_channels:
|
||||||
if channel.source_cid in connection_channels:
|
if channel.source_cid in connection_channels:
|
||||||
del connection_channels[channel.source_cid]
|
del connection_channels[channel.source_cid]
|
||||||
|
|
||||||
|
@deprecated("Please use create_le_credit_based_channel()")
|
||||||
async def open_le_coc(
|
async def open_le_coc(
|
||||||
self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
|
self, connection: Connection, psm: int, max_credits: int, mtu: int, mps: int
|
||||||
) -> LeConnectionOrientedChannel:
|
) -> LeCreditBasedChannel:
|
||||||
self.check_le_coc_parameters(max_credits, mtu, mps)
|
return await self.create_le_credit_based_channel(
|
||||||
|
connection=connection,
|
||||||
|
spec=LeCreditBasedChannelSpec(
|
||||||
|
psm=psm, max_credits=max_credits, mtu=mtu, mps=mps
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def create_le_credit_based_channel(
|
||||||
|
self,
|
||||||
|
connection: Connection,
|
||||||
|
spec: LeCreditBasedChannelSpec,
|
||||||
|
) -> LeCreditBasedChannel:
|
||||||
# Find a free CID for the new channel
|
# Find a free CID for the new channel
|
||||||
connection_channels = self.channels.setdefault(connection.handle, {})
|
connection_channels = self.channels.setdefault(connection.handle, {})
|
||||||
source_cid = self.find_free_le_cid(connection_channels)
|
source_cid = self.find_free_le_cid(connection_channels)
|
||||||
if source_cid is None: # Should never happen!
|
if source_cid is None: # Should never happen!
|
||||||
raise RuntimeError('all CIDs already in use')
|
raise RuntimeError('all CIDs already in use')
|
||||||
|
|
||||||
|
if spec.psm is None:
|
||||||
|
raise ValueError('PSM cannot be None')
|
||||||
|
|
||||||
# Create the channel
|
# Create the channel
|
||||||
logger.debug(f'creating coc channel with cid={source_cid} for psm {psm}')
|
logger.debug(f'creating coc channel with cid={source_cid} for psm {spec.psm}')
|
||||||
channel = LeConnectionOrientedChannel(
|
channel = LeCreditBasedChannel(
|
||||||
manager=self,
|
manager=self,
|
||||||
connection=connection,
|
connection=connection,
|
||||||
le_psm=psm,
|
le_psm=spec.psm,
|
||||||
source_cid=source_cid,
|
source_cid=source_cid,
|
||||||
destination_cid=0,
|
destination_cid=0,
|
||||||
mtu=mtu,
|
mtu=spec.mtu,
|
||||||
mps=mps,
|
mps=spec.mps,
|
||||||
credits=0,
|
credits=0,
|
||||||
peer_mtu=0,
|
peer_mtu=0,
|
||||||
peer_mps=0,
|
peer_mps=0,
|
||||||
peer_credits=max_credits,
|
peer_credits=spec.max_credits,
|
||||||
connected=False,
|
connected=False,
|
||||||
)
|
)
|
||||||
connection_channels[source_cid] = channel
|
connection_channels[source_cid] = channel
|
||||||
@@ -2062,7 +2170,15 @@ class ChannelManager:
|
|||||||
|
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
async def connect(self, connection: Connection, psm: int) -> Channel:
|
@deprecated("Please use create_classic_channel()")
|
||||||
|
async def connect(self, connection: Connection, psm: int) -> ClassicChannel:
|
||||||
|
return await self.create_classic_channel(
|
||||||
|
connection=connection, spec=ClassicChannelSpec(psm=psm)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def create_classic_channel(
|
||||||
|
self, connection: Connection, spec: ClassicChannelSpec
|
||||||
|
) -> ClassicChannel:
|
||||||
# NOTE: this implementation hard-codes BR/EDR
|
# NOTE: this implementation hard-codes BR/EDR
|
||||||
|
|
||||||
# Find a free CID for a new channel
|
# Find a free CID for a new channel
|
||||||
@@ -2071,10 +2187,20 @@ class ChannelManager:
|
|||||||
if source_cid is None: # Should never happen!
|
if source_cid is None: # Should never happen!
|
||||||
raise RuntimeError('all CIDs already in use')
|
raise RuntimeError('all CIDs already in use')
|
||||||
|
|
||||||
|
if spec.psm is None:
|
||||||
|
raise ValueError('PSM cannot be None')
|
||||||
|
|
||||||
# Create the channel
|
# Create the channel
|
||||||
logger.debug(f'creating client channel with cid={source_cid} for psm {psm}')
|
logger.debug(
|
||||||
channel = Channel(
|
f'creating client channel with cid={source_cid} for psm {spec.psm}'
|
||||||
self, connection, L2CAP_SIGNALING_CID, psm, source_cid, L2CAP_MIN_BR_EDR_MTU
|
)
|
||||||
|
channel = ClassicChannel(
|
||||||
|
self,
|
||||||
|
connection,
|
||||||
|
L2CAP_SIGNALING_CID,
|
||||||
|
spec.psm,
|
||||||
|
source_cid,
|
||||||
|
spec.mtu,
|
||||||
)
|
)
|
||||||
connection_channels[source_cid] = channel
|
connection_channels[source_cid] = channel
|
||||||
|
|
||||||
@@ -2086,3 +2212,20 @@ class ChannelManager:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Deprecated Classes
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class Channel(ClassicChannel):
|
||||||
|
@deprecated("Please use ClassicChannel")
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LeConnectionOrientedChannel(LeCreditBasedChannel):
|
||||||
|
@deprecated("Please use LeCreditBasedChannel")
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
+10
-13
@@ -450,21 +450,18 @@ class SecurityService(SecurityServicer):
|
|||||||
'security_request': pair,
|
'security_request': pair,
|
||||||
}
|
}
|
||||||
|
|
||||||
# register event handlers
|
with contextlib.closing(EventWatcher()) as watcher:
|
||||||
for event, listener in listeners.items():
|
# register event handlers
|
||||||
connection.on(event, listener)
|
for event, listener in listeners.items():
|
||||||
|
watcher.on(connection, event, listener)
|
||||||
|
|
||||||
# security level already reached
|
# security level already reached
|
||||||
if self.reached_security_level(connection, level):
|
if self.reached_security_level(connection, level):
|
||||||
return WaitSecurityResponse(success=empty_pb2.Empty())
|
return WaitSecurityResponse(success=empty_pb2.Empty())
|
||||||
|
|
||||||
self.log.debug('Wait for security...')
|
self.log.debug('Wait for security...')
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
kwargs[await wait_for_security] = empty_pb2.Empty()
|
kwargs[await wait_for_security] = empty_pb2.Empty()
|
||||||
|
|
||||||
# remove event handlers
|
|
||||||
for event, listener in listeners.items():
|
|
||||||
connection.remove_listener(event, listener) # type: ignore
|
|
||||||
|
|
||||||
# wait for `authenticate` to finish if any
|
# wait for `authenticate` to finish if any
|
||||||
if authenticate_task is not None:
|
if authenticate_task is not None:
|
||||||
|
|||||||
+4
-4
@@ -674,7 +674,7 @@ class Multiplexer(EventEmitter):
|
|||||||
acceptor: Optional[Callable[[int], bool]]
|
acceptor: Optional[Callable[[int], bool]]
|
||||||
dlcs: Dict[int, DLC]
|
dlcs: Dict[int, DLC]
|
||||||
|
|
||||||
def __init__(self, l2cap_channel: l2cap.Channel, role: Role) -> None:
|
def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.role = role
|
self.role = role
|
||||||
self.l2cap_channel = l2cap_channel
|
self.l2cap_channel = l2cap_channel
|
||||||
@@ -887,7 +887,7 @@ class Multiplexer(EventEmitter):
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Client:
|
class Client:
|
||||||
multiplexer: Optional[Multiplexer]
|
multiplexer: Optional[Multiplexer]
|
||||||
l2cap_channel: Optional[l2cap.Channel]
|
l2cap_channel: Optional[l2cap.ClassicChannel]
|
||||||
|
|
||||||
def __init__(self, device: Device, connection: Connection) -> None:
|
def __init__(self, device: Device, connection: Connection) -> None:
|
||||||
self.device = device
|
self.device = device
|
||||||
@@ -960,11 +960,11 @@ class Server(EventEmitter):
|
|||||||
self.acceptors[channel] = acceptor
|
self.acceptors[channel] = acceptor
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
def on_connection(self, l2cap_channel: l2cap.Channel) -> None:
|
def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||||
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
||||||
l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
|
l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
|
||||||
|
|
||||||
def on_l2cap_channel_open(self, l2cap_channel: l2cap.Channel) -> None:
|
def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
|
||||||
logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
|
logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
|
||||||
|
|
||||||
# Create a new multiplexer for the channel
|
# Create a new multiplexer for the channel
|
||||||
|
|||||||
+2
-2
@@ -758,7 +758,7 @@ class SDP_ServiceSearchAttributeResponse(SDP_PDU):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Client:
|
class Client:
|
||||||
channel: Optional[l2cap.Channel]
|
channel: Optional[l2cap.ClassicChannel]
|
||||||
|
|
||||||
def __init__(self, device: Device) -> None:
|
def __init__(self, device: Device) -> None:
|
||||||
self.device = device
|
self.device = device
|
||||||
@@ -921,7 +921,7 @@ class Client:
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Server:
|
class Server:
|
||||||
CONTINUATION_STATE = bytes([0x01, 0x43])
|
CONTINUATION_STATE = bytes([0x01, 0x43])
|
||||||
channel: Optional[l2cap.Channel]
|
channel: Optional[l2cap.ClassicChannel]
|
||||||
Service = NewType('Service', List[ServiceAttribute])
|
Service = NewType('Service', List[ServiceAttribute])
|
||||||
service_records: Dict[int, Service]
|
service_records: Dict[int, Service]
|
||||||
current_response: Union[None, bytes, Tuple[int, List[int]]]
|
current_response: Union[None, bytes, Tuple[int, List[int]]]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import logging
|
|||||||
import traceback
|
import traceback
|
||||||
import collections
|
import collections
|
||||||
import sys
|
import sys
|
||||||
|
import warnings
|
||||||
from typing import (
|
from typing import (
|
||||||
Awaitable,
|
Awaitable,
|
||||||
Set,
|
Set,
|
||||||
@@ -427,3 +428,19 @@ def wrap_async(function):
|
|||||||
Wraps the provided function in an async function.
|
Wraps the provided function in an async function.
|
||||||
"""
|
"""
|
||||||
return partial(async_call, function)
|
return partial(async_call, function)
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated(msg: str):
|
||||||
|
"""
|
||||||
|
Throw deprecation warning before execution
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(function):
|
||||||
|
@wraps(function)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -91,9 +91,13 @@ development =
|
|||||||
mypy == 1.5.0
|
mypy == 1.5.0
|
||||||
nox >= 2022
|
nox >= 2022
|
||||||
pylint == 2.15.8
|
pylint == 2.15.8
|
||||||
|
pyyaml >= 6.0
|
||||||
types-appdirs >= 1.4.3
|
types-appdirs >= 1.4.3
|
||||||
types-invoke >= 1.7.3
|
types-invoke >= 1.7.3
|
||||||
types-protobuf >= 4.21.0
|
types-protobuf >= 4.21.0
|
||||||
|
avatar =
|
||||||
|
pandora-avatar == 0.0.5
|
||||||
|
rootcanal == 1.3.0 ; python_version>='3.10'
|
||||||
documentation =
|
documentation =
|
||||||
mkdocs >= 1.4.0
|
mkdocs >= 1.4.0
|
||||||
mkdocs-material >= 8.5.6
|
mkdocs-material >= 8.5.6
|
||||||
|
|||||||
+4
-2
@@ -45,12 +45,14 @@ def test_messages():
|
|||||||
]
|
]
|
||||||
message = Get_Capabilities_Response(capabilities)
|
message = Get_Capabilities_Response(capabilities)
|
||||||
parsed = Message.create(
|
parsed = Message.create(
|
||||||
AVDTP_GET_CAPABILITIES, Message.RESPONSE_ACCEPT, message.payload
|
AVDTP_GET_CAPABILITIES, Message.MessageType.RESPONSE_ACCEPT, message.payload
|
||||||
)
|
)
|
||||||
assert message.payload == parsed.payload
|
assert message.payload == parsed.payload
|
||||||
|
|
||||||
message = Set_Configuration_Command(3, 4, capabilities)
|
message = Set_Configuration_Command(3, 4, capabilities)
|
||||||
parsed = Message.create(AVDTP_SET_CONFIGURATION, Message.COMMAND, message.payload)
|
parsed = Message.create(
|
||||||
|
AVDTP_SET_CONFIGURATION, Message.MessageType.COMMAND, message.payload
|
||||||
|
)
|
||||||
assert message.payload == parsed.payload
|
assert message.payload == parsed.payload
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ def temporary_file():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_basic(temporary_file):
|
async def test_basic(temporary_file):
|
||||||
with open(temporary_file, mode='w', encoding='utf-8') as file:
|
with open(temporary_file, mode='w', encoding='utf-8') as file:
|
||||||
file.write("{}")
|
file.write("{}")
|
||||||
@@ -125,6 +126,7 @@ async def test_basic(temporary_file):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_parsing(temporary_file):
|
async def test_parsing(temporary_file):
|
||||||
with open(temporary_file, mode='w', encoding='utf-8') as file:
|
with open(temporary_file, mode='w', encoding='utf-8') as file:
|
||||||
file.write(JSON1)
|
file.write(JSON1)
|
||||||
@@ -137,6 +139,7 @@ async def test_parsing(temporary_file):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_default_namespace(temporary_file):
|
async def test_default_namespace(temporary_file):
|
||||||
with open(temporary_file, mode='w', encoding='utf-8') as file:
|
with open(temporary_file, mode='w', encoding='utf-8') as file:
|
||||||
file.write(JSON1)
|
file.write(JSON1)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pytest
|
||||||
|
|
||||||
from bumble.core import UUID, BT_L2CAP_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID
|
from bumble.core import UUID, BT_L2CAP_PROTOCOL_ID, BT_RFCOMM_PROTOCOL_ID
|
||||||
from bumble.sdp import (
|
from bumble.sdp import (
|
||||||
@@ -202,6 +203,7 @@ def sdp_records():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_service_search():
|
async def test_service_search():
|
||||||
# Setup connections
|
# Setup connections
|
||||||
devices = TwoDevices()
|
devices = TwoDevices()
|
||||||
@@ -224,6 +226,7 @@ async def test_service_search():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_service_attribute():
|
async def test_service_attribute():
|
||||||
# Setup connections
|
# Setup connections
|
||||||
devices = TwoDevices()
|
devices = TwoDevices()
|
||||||
@@ -244,6 +247,7 @@ async def test_service_attribute():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_service_search_attribute():
|
async def test_service_search_attribute():
|
||||||
# Setup connections
|
# Setup connections
|
||||||
devices = TwoDevices()
|
devices = TwoDevices()
|
||||||
|
|||||||
@@ -14,25 +14,25 @@
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# This script generates a python-syntax list of dictionary entries for the
|
# This script generates a python-syntax list of dictionary entries for the
|
||||||
# company IDs listed at: https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/
|
# company IDs listed at:
|
||||||
# The input to this script is the CSV file that can be obtained at that URL
|
# https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
|
||||||
|
# The input to this script is the YAML file that can be obtained at that URL
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import sys
|
import sys
|
||||||
import csv
|
import yaml
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
with open(sys.argv[1], newline='') as csvfile:
|
with open(sys.argv[1], "r") as yaml_file:
|
||||||
reader = csv.reader(csvfile, delimiter=',', quotechar='"')
|
root = yaml.safe_load(yaml_file)
|
||||||
lines = []
|
companies = {}
|
||||||
for row in reader:
|
for company in root["company_identifiers"]:
|
||||||
if len(row) == 3 and row[1].startswith('0x'):
|
companies[company["value"]] = company["name"]
|
||||||
company_id = row[1]
|
|
||||||
company_name = row[2]
|
|
||||||
escaped_company_name = company_name.replace('"', '\\"')
|
|
||||||
lines.append(f' {company_id}: "{escaped_company_name}"')
|
|
||||||
|
|
||||||
print(',\n'.join(reversed(lines)))
|
for company_id in sorted(companies.keys()):
|
||||||
|
company_name = companies[company_id]
|
||||||
|
escaped_company_name = company_name.replace('"', '\\"')
|
||||||
|
print(f' 0x{company_id:04X}: "{escaped_company_name}",')
|
||||||
|
|||||||
Reference in New Issue
Block a user