Skip to content

Commit e64ebd5

Browse files
zxzxwuuael
authored andcommitted
L2CAP cases
1 parent 8e5c0a8 commit e64ebd5

File tree

3 files changed

+306
-0
lines changed

3 files changed

+306
-0
lines changed

avatar/cases/l2cap_test.py

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

avatar/common.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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
21+
from pandora.host_pb2 import Connection
22+
from pandora.host_pb2 import DataTypes
23+
from pandora.host_pb2 import OwnAddressType
24+
from typing import Tuple
25+
26+
27+
# Make classic connection task.
28+
async def make_bredr_connection(initiator: PandoraDevice, acceptor: PandoraDevice) -> Tuple[Connection, Connection]:
29+
init_res, wait_res = await asyncio.gather(
30+
initiator.aio.host.Connect(address=acceptor.address),
31+
acceptor.aio.host.WaitConnection(address=initiator.address),
32+
)
33+
assert_equal(init_res.result_variant(), 'connection')
34+
assert_equal(wait_res.result_variant(), 'connection')
35+
assert init_res.connection is not None and wait_res.connection is not None
36+
return init_res.connection, wait_res.connection
37+
38+
39+
# Make LE connection task.
40+
async def make_le_connection(
41+
central: PandoraDevice,
42+
peripheral: PandoraDevice,
43+
central_address_type: OwnAddressType = RANDOM,
44+
peripheral_address_type: OwnAddressType = RANDOM,
45+
) -> Tuple[Connection, Connection]:
46+
advertise = peripheral.aio.host.Advertise(
47+
legacy=True,
48+
connectable=True,
49+
own_address_type=peripheral_address_type,
50+
data=DataTypes(manufacturer_specific_data=b'pause cafe'),
51+
)
52+
53+
scan = central.aio.host.Scan(own_address_type=central_address_type)
54+
ref = await anext((x async for x in scan if x.data.manufacturer_specific_data == b'pause cafe'))
55+
scan.cancel()
56+
57+
adv_res, conn_res = await asyncio.gather(
58+
anext(aiter(advertise)),
59+
central.aio.host.ConnectLE(**ref.address_asdict(), own_address_type=central_address_type),
60+
)
61+
assert_equal(conn_res.result_variant(), 'connection')
62+
cen_per, per_cen = conn_res.connection, adv_res.connection
63+
assert_is_not_none(cen_per)
64+
assert_is_not_none(per_cen)
65+
assert cen_per, per_cen
66+
advertise.cancel()
67+
return cen_per, per_cen

avatar/pandora_client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
from dataclasses import dataclass
3232
from pandora import host_grpc
3333
from pandora import host_grpc_aio
34+
from pandora import l2cap_grpc
35+
from pandora import l2cap_grpc_aio
3436
from pandora import security_grpc
3537
from pandora import security_grpc_aio
3638
from typing import Any, Dict, MutableMapping, Optional, Tuple, Union
@@ -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)