Skip to content

Commit 6aa7972

Browse files
committed
L2CAP cases
1 parent 083da7d commit 6aa7972

6 files changed

Lines changed: 307 additions & 8 deletions

File tree

avatar/cases/l2cap_test.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import asyncio
16+
import avatar
17+
import logging
18+
19+
from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices
20+
from avatar.common import make_bredr_connection, make_le_connection
21+
from mobly import base_test, test_runner
22+
from mobly.asserts import assert_equal # type: ignore
23+
from mobly.asserts import assert_is_not_none # type: ignore
24+
from pandora import host_pb2
25+
from pandora import l2cap_pb2
26+
from typing import Any, Dict, Literal, Optional, Union, Callable, Tuple, Awaitable
27+
28+
CONNECTORS: Dict[
29+
str,
30+
Callable[[avatar.PandoraDevice, avatar.PandoraDevice], Awaitable[Tuple[host_pb2.Connection, host_pb2.Connection]]],
31+
] = {
32+
'Classic': make_bredr_connection,
33+
'LE': make_le_connection,
34+
}
35+
36+
FIXED_CHANNEL_CID = 0x3E
37+
CLASSIC_PSM = 0xFEFF
38+
LE_SPSM = 0xF0
39+
40+
41+
class L2capTest(base_test.BaseTestClass): # type: ignore[misc]
42+
devices: Optional[PandoraDevices] = None
43+
44+
# pandora devices.
45+
dut: PandoraDevice
46+
ref: PandoraDevice
47+
48+
def setup_class(self) -> None:
49+
self.devices = PandoraDevices(self)
50+
self.dut, self.ref, *_ = self.devices
51+
52+
# Enable BR/EDR mode for Bumble devices.
53+
for device in self.devices:
54+
if isinstance(device, BumblePandoraDevice):
55+
device.config.setdefault("classic_enabled", True)
56+
57+
def teardown_class(self) -> None:
58+
if self.devices:
59+
self.devices.stop_all()
60+
61+
@avatar.asynchronous
62+
async def setup_test(self) -> None: # pytype: disable=wrong-arg-types
63+
await asyncio.gather(self.dut.reset(), self.ref.reset())
64+
65+
@avatar.parameterized(
66+
('Classic', dict(fixed=l2cap_pb2.FixedChannelRequest(cid=FIXED_CHANNEL_CID))),
67+
('LE', dict(fixed=l2cap_pb2.FixedChannelRequest(cid=FIXED_CHANNEL_CID))),
68+
('Classic', dict(basic=l2cap_pb2.ConnectionOrientedChannelRequest(psm=CLASSIC_PSM))),
69+
(
70+
'LE',
71+
dict(
72+
le_credit_based=l2cap_pb2.CreditBasedChannelRequest(
73+
spsm=LE_SPSM, mtu=2046, mps=2048, initial_credit=256
74+
)
75+
),
76+
),
77+
) # type: ignore[misc]
78+
@avatar.asynchronous
79+
async def test_connect(
80+
self,
81+
transport: Union[Literal['Classic'], Literal['LE']],
82+
request: Dict[str, Any],
83+
) -> None:
84+
dut_ref_acl, ref_dut_acl = await CONNECTORS[transport](self.dut, self.ref)
85+
server = self.ref.aio.l2cap.OnConnection(connection=ref_dut_acl, **request)
86+
ref_dut_res, dut_ref_res = await asyncio.gather(
87+
anext(aiter(server)),
88+
self.dut.aio.l2cap.Connect(connection=dut_ref_acl, **request),
89+
)
90+
assert_is_not_none(ref_dut_res.channel)
91+
assert_is_not_none(dut_ref_res.channel)
92+
93+
@avatar.parameterized(
94+
('Classic', dict(fixed=l2cap_pb2.FixedChannelRequest(cid=FIXED_CHANNEL_CID))),
95+
('LE', dict(fixed=l2cap_pb2.FixedChannelRequest(cid=FIXED_CHANNEL_CID))),
96+
('Classic', dict(basic=l2cap_pb2.ConnectionOrientedChannelRequest(psm=CLASSIC_PSM))),
97+
(
98+
'LE',
99+
dict(
100+
le_credit_based=l2cap_pb2.CreditBasedChannelRequest(
101+
spsm=LE_SPSM, mtu=2046, mps=2048, initial_credit=256
102+
)
103+
),
104+
),
105+
) # type: ignore[misc]
106+
@avatar.asynchronous
107+
async def test_on_connection(
108+
self,
109+
transport: Union[Literal['Classic'], Literal['LE']],
110+
request: Dict[str, Any],
111+
) -> None:
112+
dut_ref_acl, ref_dut_acl = await CONNECTORS[transport](self.dut, self.ref)
113+
server = self.dut.aio.l2cap.OnConnection(connection=dut_ref_acl, **request)
114+
ref_dut_res, dut_ref_res = await asyncio.gather(
115+
self.ref.aio.l2cap.Connect(connection=ref_dut_acl, **request),
116+
anext(aiter(server)),
117+
)
118+
assert_is_not_none(ref_dut_res.channel)
119+
assert_is_not_none(dut_ref_res.channel)
120+
121+
@avatar.parameterized(
122+
('Classic', dict(basic=l2cap_pb2.ConnectionOrientedChannelRequest(psm=CLASSIC_PSM))),
123+
(
124+
'LE',
125+
dict(
126+
le_credit_based=l2cap_pb2.CreditBasedChannelRequest(
127+
spsm=LE_SPSM, mtu=2046, mps=2048, initial_credit=256
128+
)
129+
),
130+
),
131+
) # type: ignore[misc]
132+
@avatar.asynchronous
133+
async def test_disconnect(
134+
self,
135+
transport: Union[Literal['Classic'], Literal['LE']],
136+
request: Dict[str, Any],
137+
) -> None:
138+
dut_ref_acl, ref_dut_acl = await CONNECTORS[transport](self.dut, self.ref)
139+
server = self.ref.aio.l2cap.OnConnection(connection=ref_dut_acl, **request)
140+
ref_dut_res, dut_ref_res = await asyncio.gather(
141+
anext(aiter(server)),
142+
self.dut.aio.l2cap.Connect(connection=dut_ref_acl, **request),
143+
)
144+
assert dut_ref_res.channel and ref_dut_res.channel
145+
146+
await asyncio.gather(
147+
self.dut.aio.l2cap.Disconnect(channel=dut_ref_res.channel),
148+
self.ref.aio.l2cap.WaitDisconnection(channel=ref_dut_res.channel),
149+
)
150+
151+
@avatar.parameterized(
152+
('Classic', dict(basic=l2cap_pb2.ConnectionOrientedChannelRequest(psm=CLASSIC_PSM))),
153+
(
154+
'LE',
155+
dict(
156+
le_credit_based=l2cap_pb2.CreditBasedChannelRequest(
157+
spsm=LE_SPSM, mtu=2046, mps=2048, initial_credit=256
158+
)
159+
),
160+
),
161+
) # type: ignore[misc]
162+
@avatar.asynchronous
163+
async def test_wait_disconnection(
164+
self,
165+
transport: Union[Literal['Classic'], Literal['LE']],
166+
request: Dict[str, Any],
167+
) -> None:
168+
dut_ref_acl, ref_dut_acl = await CONNECTORS[transport](self.dut, self.ref)
169+
server = self.ref.aio.l2cap.OnConnection(connection=ref_dut_acl, **request)
170+
ref_dut_res, dut_ref_res = await asyncio.gather(
171+
anext(aiter(server)),
172+
self.dut.aio.l2cap.Connect(connection=dut_ref_acl, **request),
173+
)
174+
assert dut_ref_res.channel and ref_dut_res.channel
175+
176+
await asyncio.gather(
177+
self.ref.aio.l2cap.Disconnect(channel=ref_dut_res.channel),
178+
self.dut.aio.l2cap.WaitDisconnection(channel=dut_ref_res.channel),
179+
)
180+
181+
@avatar.parameterized(
182+
('Classic', dict(fixed=l2cap_pb2.FixedChannelRequest(cid=FIXED_CHANNEL_CID))),
183+
('LE', dict(fixed=l2cap_pb2.FixedChannelRequest(cid=FIXED_CHANNEL_CID))),
184+
('Classic', dict(basic=l2cap_pb2.ConnectionOrientedChannelRequest(psm=CLASSIC_PSM))),
185+
(
186+
'LE',
187+
dict(
188+
le_credit_based=l2cap_pb2.CreditBasedChannelRequest(
189+
spsm=LE_SPSM, mtu=2046, mps=2048, initial_credit=256
190+
)
191+
),
192+
),
193+
) # type: ignore[misc]
194+
@avatar.asynchronous
195+
async def test_send(
196+
self,
197+
transport: Union[Literal['Classic'], Literal['LE']],
198+
request: Dict[str, Any],
199+
) -> None:
200+
dut_ref_acl, ref_dut_acl = await CONNECTORS[transport](self.dut, self.ref)
201+
server = self.dut.aio.l2cap.OnConnection(connection=dut_ref_acl, **request)
202+
ref_dut_res, dut_ref_res = await asyncio.gather(
203+
self.ref.aio.l2cap.Connect(connection=ref_dut_acl, **request),
204+
anext(aiter(server)),
205+
)
206+
ref_dut_channel = ref_dut_res.channel
207+
dut_ref_channel = dut_ref_res.channel
208+
assert_is_not_none(ref_dut_res.channel)
209+
assert_is_not_none(dut_ref_res.channel)
210+
assert ref_dut_channel and dut_ref_channel
211+
212+
dut_ref_stream = self.ref.aio.l2cap.Receive(channel=dut_ref_channel)
213+
_send_res, recv_res = await asyncio.gather(
214+
self.dut.aio.l2cap.Send(channel=ref_dut_channel, data=b"The quick brown fox jumps over the lazy dog"),
215+
anext(aiter(dut_ref_stream)),
216+
)
217+
assert recv_res.data
218+
assert_equal(recv_res.data, b"The quick brown fox jumps over the lazy dog")
219+
220+
221+
if __name__ == "__main__":
222+
logging.basicConfig(level=logging.DEBUG)
223+
test_runner.main() # type: ignore

avatar/cases/le_security_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from avatar import BumblePandoraDevice
2121
from avatar import PandoraDevice
2222
from avatar import PandoraDevices
23-
from avatar import pandora
23+
from avatar import pandora_snippet
2424
from bumble.pairing import PairingConfig
2525
from bumble.pairing import PairingDelegate
2626
from mobly import base_test
@@ -226,7 +226,7 @@ async def connect_le(
226226
scan.cancel()
227227

228228
# Initiator - LE connect
229-
return await pandora.connect_le(initiator, advertisement, acceptor_scan, initiator_addr_type)
229+
return await pandora_snippet.connect_le(initiator, advertisement, acceptor_scan, initiator_addr_type)
230230

231231
# Make LE connection.
232232
if connect == 'incoming_connection':
@@ -376,7 +376,7 @@ def on_done(_: Any) -> None:
376376
assert ref_dut_classic_res.connection
377377
ref_dut_classic = ref_dut_classic_res.connection
378378
else:
379-
ref_dut_classic, _ = await pandora.connect(self.ref, self.dut)
379+
ref_dut_classic, _ = await pandora_snippet.connect(self.ref, self.dut)
380380
# Try to encrypt Classic connection
381381
ref_dut_secure = await self.ref.aio.security.Secure(ref_dut_classic, classic=LEVEL2)
382382
assert_equal(ref_dut_secure.result_variant(), 'success')

avatar/cases/security_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from avatar import BumblePandoraDevice
2121
from avatar import PandoraDevice
2222
from avatar import PandoraDevices
23-
from avatar import pandora
23+
from avatar import pandora_snippet
2424
from bumble.hci import HCI_CENTRAL_ROLE
2525
from bumble.hci import HCI_PERIPHERAL_ROLE
2626
from bumble.hci import HCI_Write_Default_Link_Policy_Settings_Command
@@ -249,16 +249,16 @@ async def connect_and_pair() -> Tuple[SecureResponse, WaitSecurityResponse]:
249249

250250
# Make classic connection.
251251
if connect == 'incoming_connection':
252-
ref_dut, dut_ref = await pandora.connect(initiator=self.ref, acceptor=self.dut)
252+
ref_dut, dut_ref = await pandora_snippet.connect(initiator=self.ref, acceptor=self.dut)
253253
else:
254-
dut_ref, ref_dut = await pandora.connect(initiator=self.dut, acceptor=self.ref)
254+
dut_ref, ref_dut = await pandora_snippet.connect(initiator=self.dut, acceptor=self.ref)
255255

256256
# Retrieve Bumble connection
257257
if isinstance(self.dut, BumblePandoraDevice):
258-
dut_ref_bumble = pandora.get_raw_connection(self.dut, dut_ref)
258+
dut_ref_bumble = pandora_snippet.get_raw_connection(self.dut, dut_ref)
259259
# Role switch.
260260
if isinstance(self.ref, BumblePandoraDevice):
261-
ref_dut_bumble = pandora.get_raw_connection(self.ref, ref_dut)
261+
ref_dut_bumble = pandora_snippet.get_raw_connection(self.ref, ref_dut)
262262
if ref_dut_bumble is not None:
263263
role = {
264264
'against_central': HCI_CENTRAL_ROLE,

avatar/common.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import asyncio
16+
17+
from avatar import PandoraDevice
18+
from mobly.asserts import assert_equal # type: ignore
19+
from mobly.asserts import assert_is_not_none # type: ignore
20+
from pandora.host_pb2 import RANDOM, Connection, DataTypes, OwnAddressType
21+
from typing import Tuple
22+
23+
24+
# Make classic connection task.
25+
async def make_bredr_connection(initiator: PandoraDevice, acceptor: PandoraDevice) -> Tuple[Connection, Connection]:
26+
init_res, wait_res = await asyncio.gather(
27+
initiator.aio.host.Connect(address=acceptor.address),
28+
acceptor.aio.host.WaitConnection(address=initiator.address),
29+
)
30+
assert_equal(init_res.result_variant(), 'connection')
31+
assert_equal(wait_res.result_variant(), 'connection')
32+
assert init_res.connection is not None and wait_res.connection is not None
33+
return init_res.connection, wait_res.connection
34+
35+
36+
# Make LE connection task.
37+
async def make_le_connection(
38+
central: PandoraDevice,
39+
peripheral: PandoraDevice,
40+
central_address_type: OwnAddressType = RANDOM,
41+
peripheral_address_type: OwnAddressType = RANDOM,
42+
) -> Tuple[Connection, Connection]:
43+
advertise = peripheral.aio.host.Advertise(
44+
legacy=True,
45+
connectable=True,
46+
own_address_type=peripheral_address_type,
47+
data=DataTypes(manufacturer_specific_data=b'pause cafe'),
48+
)
49+
50+
scan = central.aio.host.Scan(own_address_type=central_address_type)
51+
ref = await anext((x async for x in scan if x.data.manufacturer_specific_data == b'pause cafe'))
52+
scan.cancel()
53+
54+
adv_res, conn_res = await asyncio.gather(
55+
anext(aiter(advertise)),
56+
central.aio.host.ConnectLE(**ref.address_asdict(), own_address_type=central_address_type),
57+
)
58+
assert_equal(conn_res.result_variant(), 'connection')
59+
cen_per, per_cen = conn_res.connection, adv_res.connection
60+
assert_is_not_none(cen_per)
61+
assert_is_not_none(per_cen)
62+
assert cen_per, per_cen
63+
advertise.cancel()
64+
return cen_per, per_cen

avatar/pandora_client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
from pandora import host_grpc_aio
3434
from pandora import security_grpc
3535
from pandora import security_grpc_aio
36+
from pandora import l2cap_grpc
37+
from pandora import l2cap_grpc_aio
3638
from typing import Any, Dict, MutableMapping, Optional, Tuple, Union
3739

3840

@@ -152,6 +154,11 @@ def security_storage(self) -> security_grpc.SecurityStorage:
152154
"""Returns the Pandora SecurityStorage gRPC interface."""
153155
return security_grpc.SecurityStorage(self.channel)
154156

157+
@property
158+
def l2cap(self) -> l2cap_grpc.L2CAP:
159+
"""Returns the Pandora SecurityStorage gRPC interface."""
160+
return l2cap_grpc.L2CAP(self.channel)
161+
155162
@dataclass
156163
class Aio:
157164
channel: grpc.aio.Channel
@@ -171,6 +178,11 @@ def security_storage(self) -> security_grpc_aio.SecurityStorage:
171178
"""Returns the Pandora SecurityStorage gRPC interface."""
172179
return security_grpc_aio.SecurityStorage(self.channel)
173180

181+
@property
182+
def l2cap(self) -> l2cap_grpc_aio.L2CAP:
183+
"""Returns the Pandora SecurityStorage gRPC interface."""
184+
return l2cap_grpc_aio.L2CAP(self.channel)
185+
174186
@property
175187
def aio(self) -> 'PandoraClient.Aio':
176188
if not self._aio:

0 commit comments

Comments
 (0)