From 274cd35efdf48fc371875c9271a343b8e8643e3a Mon Sep 17 00:00:00 2001 From: Jiawei Yang Date: Mon, 21 Apr 2025 22:38:57 +0800 Subject: [PATCH] tests: Bluetooth: BR: Add test suite gap_client IUT works as a Bluetooth GAP Client. The peer device (tester) is configured to respond to inquiries and process connection requests. Add tests to verify GAP client functionality for inquiry, connection, and disconnection scenarios. In the test suite, there are six test cases: Case 1: General Inquiry followed by Connection and Active Disconnection. Verifies that DUT can perform general inquiry, establish connection, and actively disconnect from tester. Case 2: General Inquiry followed by Connection and Passive Disconnection. Verifies that DUT can perform general inquiry, establish connection, and handle disconnection initiated by tester. Case 3: General Inquiry followed by Rejected Connection Request. Verifies that DUT can perform general inquiry and handle connection rejection from tester. Case 4: Limited Inquiry followed by Connection and Active Disconnection. Verifies that DUT can perform limited inquiry, establish connection, and actively disconnect from tester. Case 5: Limited Inquiry followed by Connection and Passive Disconnection. Verifies that DUT can perform limited inquiry, establish connection, and handle disconnection initiated by tester. Case 6: Limited Inquiry followed by Rejected Connection Request. Verifies that DUT can perform limited inquiry and handle connection rejection from tester. Signed-off-by: Jiawei Yang --- tests/bluetooth/classic/gap_c/CMakeLists.txt | 10 + tests/bluetooth/classic/gap_c/README.rst | 74 +++ .../mimxrt1170_evk_mimxrt1176_cm7_B.conf | 10 + .../mimxrt1170_evk_mimxrt1176_cm7_B.overlay | 11 + tests/bluetooth/classic/gap_c/prj.conf | 12 + .../classic/gap_c/pytest/conftest.py | 55 ++ .../classic/gap_c/pytest/test_gap_c.py | 488 ++++++++++++++++++ tests/bluetooth/classic/gap_c/src/gap_c.c | 7 + tests/bluetooth/classic/gap_c/testcase.yaml | 24 + 9 files changed, 691 insertions(+) create mode 100644 tests/bluetooth/classic/gap_c/CMakeLists.txt create mode 100644 tests/bluetooth/classic/gap_c/README.rst create mode 100644 tests/bluetooth/classic/gap_c/boards/mimxrt1170_evk_mimxrt1176_cm7_B.conf create mode 100644 tests/bluetooth/classic/gap_c/boards/mimxrt1170_evk_mimxrt1176_cm7_B.overlay create mode 100644 tests/bluetooth/classic/gap_c/prj.conf create mode 100644 tests/bluetooth/classic/gap_c/pytest/conftest.py create mode 100644 tests/bluetooth/classic/gap_c/pytest/test_gap_c.py create mode 100644 tests/bluetooth/classic/gap_c/src/gap_c.c create mode 100644 tests/bluetooth/classic/gap_c/testcase.yaml diff --git a/tests/bluetooth/classic/gap_c/CMakeLists.txt b/tests/bluetooth/classic/gap_c/CMakeLists.txt new file mode 100644 index 0000000000000..89de0b86386a9 --- /dev/null +++ b/tests/bluetooth/classic/gap_c/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +set(NO_QEMU_SERIAL_BT_SERVER 1) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(bluetooth) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/bluetooth/classic/gap_c/README.rst b/tests/bluetooth/classic/gap_c/README.rst new file mode 100644 index 0000000000000..d10f722a2c716 --- /dev/null +++ b/tests/bluetooth/classic/gap_c/README.rst @@ -0,0 +1,74 @@ +.. _bluetooth_classic_gap_server_tests: + +Bluetooth Classic gap client Tests +################################## + +Overview +******** + +This test suite uses ``bumble`` for testing Bluetooth Classic communication between a host +PC (running :ref:`Twister `) and a device under test (DUT) running Zephyr. + +Prerequisites +************* + +The test suite has the following prerequisites: + +* The ``bumble`` library installed on the host PC. +The Bluetooth Classic controller on PC side is required. Refer to getting started of `bumble`_ +for details. + +The HCI transport for ``bumble`` can be configured as follows: + +* A specific configuration context can be provided along with the ``usb_hci`` fixture separated by + a ``:`` (i.e. specify fixture ``usb_hci:usb:0`` to use the ``usb:0`` as hci transport for + ``bumble``). +* The configuration context can be overridden using the `hci transport`_ can be provided using the + ``--hci-transport`` test suite argument (i.e. run ``twister`` with the + ``--pytest-args=--hci-transport=usb:0`` argument to use the ``usb:0`` as hci transport for + ``bumble``). + +Building and Running +******************** + +Running on mimxrt1170_evk@B/mimxrt1176/cm7 +========================================== + +Running the test suite on :ref:`mimxrt1170_evk` relies on configuration of ``bumble``. + +On the host PC, a HCI transport needs to be required. Refer to `bumble platforms`_ page of +``bumble`` for details. + +For example, on windows, a PTS dongle is used. After `WinUSB driver`_ has been installed, +the HCI transport would be USB transport interface ``usb:``. + +If the HCI transport is ``usb:0`` and debug console port is ``COM4``, the test suite can be +launched using Twister: + +.. code-block:: shell + + west twister -v -p mimxrt1170_evk@B/mimxrt1176/cm7 --device-testing --device-serial COM4 -T tests/bluetooth/classic/gap_c -O gap_c --force-platform --west-flash --west-runner=jlink -X usb_hci:usb:0 + +Running on Hardware +=================== + +Running the test suite on hardware requires a HCI transport connected to the host PC. + +The test suite can be launched using Twister. Below is an example for running on the +:zephyr:board:`mimxrt1170_evk@B/mimxrt1176/cm7`: + +.. code-block:: shell + + west twister -v -p mimxrt1170_evk@B/mimxrt1176/cm7 --device-testing --device-serial COM4 -T tests/bluetooth/classic/gap_c -O gap_c --force-platform --west-flash --west-runner=jlink -X usb_hci:usb:0 + +.. _bumble: + https://google.github.io/bumble/getting_started.html + +.. _hci transport: + https://google.github.io/bumble/transports/index.html + +.. _bumble platforms: + https://google.github.io/bumble/platforms/index.html + +.. _WinUSB driver: + https://google.github.io/bumble/platforms/windows.html diff --git a/tests/bluetooth/classic/gap_c/boards/mimxrt1170_evk_mimxrt1176_cm7_B.conf b/tests/bluetooth/classic/gap_c/boards/mimxrt1170_evk_mimxrt1176_cm7_B.conf new file mode 100644 index 0000000000000..289b5a597386c --- /dev/null +++ b/tests/bluetooth/classic/gap_c/boards/mimxrt1170_evk_mimxrt1176_cm7_B.conf @@ -0,0 +1,10 @@ +#select NXP NW612 Chipset +CONFIG_BT_NXP_NW612=y + +CONFIG_BT_SETTINGS=n +CONFIG_FLASH=n +CONFIG_FLASH_MAP=n +CONFIG_NVS=n +CONFIG_SETTINGS=n + +CONFIG_ENTROPY_GENERATOR=y diff --git a/tests/bluetooth/classic/gap_c/boards/mimxrt1170_evk_mimxrt1176_cm7_B.overlay b/tests/bluetooth/classic/gap_c/boards/mimxrt1170_evk_mimxrt1176_cm7_B.overlay new file mode 100644 index 0000000000000..96ef63ad60b2e --- /dev/null +++ b/tests/bluetooth/classic/gap_c/boards/mimxrt1170_evk_mimxrt1176_cm7_B.overlay @@ -0,0 +1,11 @@ +/* + * Copyright 2024 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + chosen { + zephyr,sram = &dtcm; + }; +}; diff --git a/tests/bluetooth/classic/gap_c/prj.conf b/tests/bluetooth/classic/gap_c/prj.conf new file mode 100644 index 0000000000000..aa716123dbbb5 --- /dev/null +++ b/tests/bluetooth/classic/gap_c/prj.conf @@ -0,0 +1,12 @@ +CONFIG_BT=y +CONFIG_BT_CLASSIC=y +CONFIG_BT_SHELL=y +CONFIG_LOG=y +CONFIG_ZTEST=y + +CONFIG_BT_RFCOMM=y + +CONFIG_BT_DEVICE_NAME="gap_c" + +CONFIG_BT_CREATE_CONN_TIMEOUT=30 +CONFIG_BT_PAGE_TIMEOUT=0xFFFF diff --git a/tests/bluetooth/classic/gap_c/pytest/conftest.py b/tests/bluetooth/classic/gap_c/pytest/conftest.py new file mode 100644 index 0000000000000..512036b948544 --- /dev/null +++ b/tests/bluetooth/classic/gap_c/pytest/conftest.py @@ -0,0 +1,55 @@ +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 + +import logging +import re + +import pytest +from twister_harness import DeviceAdapter, Shell + +logger = logging.getLogger(__name__) + + +def pytest_addoption(parser) -> None: + """Add local parser options to pytest.""" + parser.addoption('--hci-transport', default=None, help='Configuration HCI transport for bumble') + + +@pytest.fixture(name='initialize', scope='session') +def fixture_initialize(request, shell: Shell, dut: DeviceAdapter): + """Session initializtion""" + # Get HCI transport for bumble + hci = request.config.getoption('--hci-transport') + + if hci is None: + for fixture in dut.device_config.fixtures: + if fixture.startswith('usb_hci:'): + hci = fixture.split(sep=':', maxsplit=1)[1] + break + + assert hci is not None + + lines = shell.exec_command("bt init") + lines = dut.readlines_until("Bluetooth initialized") + regex = r'Identity: (?P(.*?):(.*?):(.*?):(.*?):(.*?):(.*?) *\((.*?)\))' + bd_addr = None + for line in lines: + logger.info(f"Shell log {line}") + m = re.search(regex, line) + if m: + bd_addr = m.group('bd_addr') + + if bd_addr is None: + logger.error('Fail to get IUT BD address') + raise AssertionError + + logger.info('initialized') + return hci, bd_addr + + +@pytest.fixture +def device_under_test(initialize): + logger.info('Start running testcase') + yield initialize + logger.info('Done') diff --git a/tests/bluetooth/classic/gap_c/pytest/test_gap_c.py b/tests/bluetooth/classic/gap_c/pytest/test_gap_c.py new file mode 100644 index 0000000000000..b94223e8ebf8b --- /dev/null +++ b/tests/bluetooth/classic/gap_c/pytest/test_gap_c.py @@ -0,0 +1,488 @@ +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 +import asyncio +import logging +import sys + +from bumble import hci +from bumble.core import DeviceClass +from bumble.device import Device +from bumble.hci import Address, HCI_Write_Page_Timeout_Command +from bumble.snoop import BtSnooper +from bumble.transport import open_transport_or_link +from twister_harness import DeviceAdapter, Shell + +logger = logging.getLogger(__name__) + + +async def device_power_on(device) -> None: + while True: + try: + await device.power_on() + break + except Exception: + continue + + +# wait for shell response +async def _wait_for_shell_response(dut, response, max_wait_sec=20): + """ + _wait_for_shell_response() is used to wait for shell response. + It will return after finding a specific 'response' or waiting long enough. + :param dut: + :param response: shell response that you want to monitor. + :param max_wait_sec: maximum waiting time + :return: found: whether the 'response' is found; lines: DUT shell response + """ + found = False + lines = [] + try: + for _ in range(0, max_wait_sec): + read_lines = dut.readlines() + for line in read_lines: + if response in line: + found = True + break + lines = lines + read_lines + await asyncio.sleep(1) + logger.info(f'{str(lines)}') + except Exception as e: + logger.error(f'{e}!', exc_info=True) + raise e + return found, lines + + +# interact between script and DUT +async def send_cmd_to_iut( + shell, dut, cmd, response=None, expect_to_find_resp=True, max_wait_sec=20 +): + """ + send_cmd_to_iut() is used to send shell cmd to DUT and monitor the response. + It can choose whether to monitor the shell response of DUT. + Use 'expect_to_find_resp' to set whether to expect the response to contain certain 'response'. + 'max_wait_sec' indicates the maximum waiting time. + For 'expect_to_find_resp=False', this is useful + because we need to wait long enough to get enough response + to more accurately judge that the response does not contain specific characters. + + :param shell: + :param dut: + :param cmd: shell cmd sent to DUT + :param response: shell response that you want to monitor. + 'None' means not to monitor any response. + :param expect_to_find_resp: set whether to expect the response to contain certain 'response' + :param max_wait_sec: maximum monitoring time + :return: DUT shell response + """ + shell.exec_command(cmd) + if response is not None: + found, lines = await _wait_for_shell_response(dut, response, max_wait_sec) + else: + found = True + lines = '' + assert found is expect_to_find_resp + return lines + + +# set limited discoverab mode of dongle +async def set_limited_discoverable(device, discoverable=True): + # Read current class of device + response = await device.send_command( + hci.HCI_Command( + op_code=0x0C23, # Read Class of Device + parameters=b'', + ) + ) + current_cod = response.return_parameters.class_of_device + + if discoverable: + # set Limited Discoverable Mode (bit 13) + new_cod = (current_cod | 0x2000).to_bytes(3, byteorder='little') + # Limited Inquiry Access Code(LIAC) = 0x9E8B00 + iac = hci.HCI_LIMITED_DEDICATED_INQUIRY_LAP.to_bytes(3, byteorder='little') + else: + mask = ~0x2000 + new_cod = (current_cod & mask).to_bytes(3, byteorder='little') + # General Inquiry Access Code(GIAC) = 0x9E8B33 + iac = hci.HCI_GENERAL_INQUIRY_LAP.to_bytes(3, byteorder='little') + + await device.send_command( + hci.HCI_Command( + op_code=0x0C24, # Write Class of Device + parameters=new_cod, + ) + ) + + await device.send_command( + hci.HCI_Command( + op_code=0x0C3A, # Write Current IAC LAP + parameters=bytes([0x01]) + iac, # num_current_iac=1, iac_lap + ) + ) + + device.discoverable = discoverable + + +# dongle listener for receiving scan results +class DiscoveryListener(Device.Listener): + def __init__(self): + self.discovered_addresses = set() + + def on_inquiry_result(self, address, class_of_device, data, rssi): + DeviceClass.split_class_of_device(class_of_device) + found_address = str(address).replace(r'/P', '') + logger.info(f'Found addr: {found_address}') + self.discovered_addresses.add(found_address) + + def has_found_target_addr(self, target_addr): + return str(target_addr).upper() in self.discovered_addresses + + +async def tc_gap_c_1(hci_port, shell, dut, address) -> None: + case_name = 'GAP-C-1: General Inquiry followed by Connection and Active Disconnection' + logger.info(f'<<< Start {case_name} ...') + + async with await open_transport_or_link(hci_port) as hci_transport: + # init Dongle bluetooth + device = Device.with_hci( + 'Bumble', + Address('F0:F1:F2:F3:F4:F5'), + hci_transport.source, + hci_transport.sink, + ) + device.classic_enabled = True + device.le_enabled = False + device.listener = DiscoveryListener() + + with open(f"bumble_hci_{sys._getframe().f_code.co_name}.log", "wb") as snoop_file: + device.host.snooper = BtSnooper(snoop_file) + await device_power_on(device) + dongle_address = str(device.public_address).replace(r'/P', '') + # Start of Initial Condition + await device.set_discoverable(True) # Set peripheral as discoverable + await device.set_connectable(True) # Set peripheral as connectable + await device.send_command(HCI_Write_Page_Timeout_Command(page_timeout=0xFFFF)) + shell.exec_command("bt disconnect") + # End of Initial Condition + + # Test Start + logger.info("Step 1: DUT initiates general inquiry") + # Use limited inquiry as the control group + await send_cmd_to_iut(shell, dut, "br discovery on 8 limited", dongle_address, False) + await send_cmd_to_iut(shell, dut, "br discovery on", dongle_address) + + logger.info("Step 2: Tester responds to the inquiry") + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 3: DUT sends connect request to tester") + await send_cmd_to_iut(shell, dut, f"br connect {dongle_address}", "Connected") + + logger.info( + "Step 5: Tester accepts the connection request and connected event is received" + ) + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 6: DUT initiates disconnection") + await send_cmd_to_iut(shell, dut, "bt disconnect", "Disconnected") + + logger.info("Step 7: Connection is terminated") + logger.info("This is a passive step and it is verified in previous step.") + + +async def tc_gap_c_2(hci_port, shell, dut, address) -> None: + case_name = 'GAP-C-2: General Inquiry followed by Connection and Passive Disconnection' + logger.info(f'<<< Start {case_name} ...') + dut_address = address.split(" ")[0] + + async with await open_transport_or_link(hci_port) as hci_transport: + # init Dongle bluetooth + device = Device.with_hci( + 'Bumble', + Address('F0:F1:F2:F3:F4:F5'), + hci_transport.source, + hci_transport.sink, + ) + device.classic_enabled = True + device.le_enabled = False + device.listener = DiscoveryListener() + + with open(f"bumble_hci_{sys._getframe().f_code.co_name}.log", "wb") as snoop_file: + device.host.snooper = BtSnooper(snoop_file) + await device_power_on(device) + dongle_address = str(device.public_address).replace(r'/P', '') + # Start of Initial Condition + await device.set_discoverable(True) # Set peripheral as discoverable + await device.set_connectable(True) # Set peripheral as connectable + await device.send_command(HCI_Write_Page_Timeout_Command(page_timeout=0xFFFF)) + shell.exec_command("bt disconnect") + # End of Initial Condition + + # Test Start + logger.info("Step 1: DUT initiates general inquiry") + # Use limited inquiry as the control group + await send_cmd_to_iut(shell, dut, "br discovery on", dongle_address) + + logger.info("Step 2: Tester responds to the inquiry") + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 3: DUT sends connect request to tester") + await send_cmd_to_iut(shell, dut, f"br connect {dongle_address}", "Connected") + + logger.info( + "Step 4: Tester accepts the connection request and connected event is received" + ) + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 5: Tester initiates disconnection") + connection = device.find_connection_by_bd_addr(Address(dut_address)) + assert connection is not None, "No connection found with the DUT" + await connection.disconnect() + + logger.info("Step 6: Connection is terminated") + found, _ = await _wait_for_shell_response(dut, "Disconnected") + assert found, "Disconnection event not received" + + +async def tc_gap_c_3(hci_port, shell, dut, address) -> None: + case_name = 'GAP-C-3: General Inquiry followed by Rejected Connection Request' + logger.info(f'<<< Start {case_name} ...') + + async with await open_transport_or_link(hci_port) as hci_transport: + # init Dongle bluetooth + device = Device.with_hci( + 'Bumble', + Address('F0:F1:F2:F3:F4:F5'), + hci_transport.source, + hci_transport.sink, + ) + device.classic_enabled = True + device.le_enabled = False + device.listener = DiscoveryListener() + device.classic_accept_any = False + + with open(f"bumble_hci_{sys._getframe().f_code.co_name}.log", "wb") as snoop_file: + device.host.snooper = BtSnooper(snoop_file) + await device_power_on(device) + dongle_address = str(device.public_address).replace(r'/P', '') + # Start of Initial Condition + await device.set_discoverable(True) # Set peripheral as discoverable + await device.set_connectable(True) # Set peripheral to reject connections + await device.send_command(HCI_Write_Page_Timeout_Command(page_timeout=0xFFFF)) + shell.exec_command("bt disconnect") + # End of Initial Condition + + # Test Start + logger.info("Step 1: DUT initiates general inquiry") + await send_cmd_to_iut(shell, dut, "br discovery on", dongle_address) + + logger.info("Step 2: Tester responds to the inquiry") + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 3: DUT sends connect request to tester") + shell.exec_command(f"br connect {dongle_address}") + + logger.info("Step 4: Tester rejects the connection request") + logger.info("This is a passive step since tester is set to reject connections.") + + logger.info("Step 5: Wait some time for the connection attempt to fail") + # Wait some time for the connection attempt to fail + await asyncio.sleep(5) + # Verify connection failure - Connected message should not appear + found, _ = await _wait_for_shell_response(dut, "Failed to connect", 5) + assert found, "Connected event was received when it should have failed" + + +async def tc_gap_c_4(hci_port, shell, dut, address) -> None: + case_name = 'GAP-C-4: Limited Inquiry followed by Connection and Active Disconnection' + logger.info(f'<<< Start {case_name} ...') + + async with await open_transport_or_link(hci_port) as hci_transport: + # init Dongle bluetooth + device = Device.with_hci( + 'Bumble', + Address('F0:F1:F2:F3:F4:F5'), + hci_transport.source, + hci_transport.sink, + ) + device.classic_enabled = True + device.le_enabled = False + device.listener = DiscoveryListener() + + with open(f"bumble_hci_{sys._getframe().f_code.co_name}.log", "wb") as snoop_file: + device.host.snooper = BtSnooper(snoop_file) + await device_power_on(device) + dongle_address = str(device.public_address).replace(r'/P', '') + # Start of Initial Condition + await set_limited_discoverable(device, True) # Set peripheral as limited discoverable + await device.set_connectable(True) # Set peripheral as connectable + await device.send_command(HCI_Write_Page_Timeout_Command(page_timeout=0xFFFF)) + shell.exec_command("bt disconnect") + # End of Initial Condition + + # Test Start + logger.info("Step 1: DUT initiates limited inquiry") + await send_cmd_to_iut(shell, dut, "br discovery off") # Reset discovery first + # Use general inquiry as the control group + await send_cmd_to_iut( + shell, dut, "br discovery on", dongle_address, False, max_wait_sec=30 + ) + await send_cmd_to_iut(shell, dut, "br discovery on 8 limited", dongle_address) + + logger.info("Step 2: Tester responds to the inquiry") + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 3: DUT sends connect request to tester") + await send_cmd_to_iut(shell, dut, f"br connect {dongle_address}", "Connected") + + logger.info( + "Step 4: Tester accepts the connection request and connected event is received" + ) + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 5: DUT initiates disconnection") + await send_cmd_to_iut(shell, dut, "bt disconnect", "Disconnected") + + logger.info("Step 6: Connection is terminated") + logger.info("This is a passive step and it is verified in previous step.") + + +async def tc_gap_c_5(hci_port, shell, dut, address) -> None: + case_name = 'GAP-C-5: Limited Inquiry followed by Connection and Passive Disconnection' + logger.info(f'<<< Start {case_name} ...') + dut_address = address.split(" ")[0] + + async with await open_transport_or_link(hci_port) as hci_transport: + # init Dongle bluetooth + device = Device.with_hci( + 'Bumble', + Address('F0:F1:F2:F3:F4:F5'), + hci_transport.source, + hci_transport.sink, + ) + device.classic_enabled = True + device.le_enabled = False + device.listener = DiscoveryListener() + + with open(f"bumble_hci_{sys._getframe().f_code.co_name}.log", "wb") as snoop_file: + device.host.snooper = BtSnooper(snoop_file) + await device_power_on(device) + dongle_address = str(device.public_address).replace(r'/P', '') + # Start of Initial Condition + await set_limited_discoverable(device, True) # Set peripheral as limited discoverable + await device.set_connectable(True) # Set peripheral as connectable + await device.send_command(HCI_Write_Page_Timeout_Command(page_timeout=0xFFFF)) + shell.exec_command("bt disconnect") + # End of Initial Condition + + # Test Start + logger.info("Step 1: DUT initiates limited inquiry") + await send_cmd_to_iut(shell, dut, "br discovery off") # Reset discovery first + await send_cmd_to_iut(shell, dut, "br discovery on 8 limited", dongle_address) + + logger.info("Step 2: Tester responds to the inquiry") + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 3: DUT sends connect request to tester") + await send_cmd_to_iut(shell, dut, f"br connect {dongle_address}", "Connected") + + logger.info( + "Step 4: Tester accepts the connection request and connected event is received" + ) + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 5: Tester initiates disconnection") + connection = device.find_connection_by_bd_addr(Address(dut_address)) + assert connection is not None, "No connection found with the DUT" + await connection.disconnect() + + logger.info("Step 6: Connection is terminated") + found, _ = await _wait_for_shell_response(dut, "Disconnected") + assert found, "Disconnection event not received" + + +async def tc_gap_c_6(hci_port, shell, dut, address) -> None: + case_name = 'GAP-C-6: Limited Inquiry followed by Rejected Connection Request' + logger.info(f'<<< Start {case_name} ...') + + async with await open_transport_or_link(hci_port) as hci_transport: + # init Dongle bluetooth + device = Device.with_hci( + 'Bumble', + Address('F0:F1:F2:F3:F4:F5'), + hci_transport.source, + hci_transport.sink, + ) + device.classic_enabled = True + device.le_enabled = False + device.listener = DiscoveryListener() + device.classic_accept_any = False + + with open(f"bumble_hci_{sys._getframe().f_code.co_name}.log", "wb") as snoop_file: + device.host.snooper = BtSnooper(snoop_file) + await device_power_on(device) + dongle_address = str(device.public_address).replace(r'/P', '') + # Start of Initial Condition + await set_limited_discoverable(device, True) # Set peripheral as limited discoverable + await device.set_connectable(True) # Set peripheral to reject connections + await device.send_command(HCI_Write_Page_Timeout_Command(page_timeout=0xFFFF)) + shell.exec_command("bt disconnect") + # End of Initial Condition + + # Test Start + logger.info("Step 1: DUT initiates limited inquiry") + await send_cmd_to_iut(shell, dut, "br discovery off") # Reset discovery first + await send_cmd_to_iut(shell, dut, "br discovery on 8 limited", dongle_address) + + logger.info("Step 2: Tester responds to the inquiry") + logger.info("This is a passive step and it always succeed.") + + logger.info("Step 3: DUT sends connect request to tester") + shell.exec_command(f"br connect {dongle_address}") + + logger.info("Step 4: Tester rejects the connection request") + logger.info("This is a passive step since tester is set to reject connections.") + + logger.info("Step 5: Wait some time for the connection attempt to fail") + await asyncio.sleep(5) + # Verify connection failure - Connected message should not appear + found, _ = await _wait_for_shell_response(dut, "Failed to connect", 5) + assert found, "Connected event was received when it should have failed" + + +class TestGAPCentral: + def test_gap_c_1(self, shell: Shell, dut: DeviceAdapter, device_under_test): + """Test GAP-C-1: General Inquiry followed by Connection and Active Disconnection.""" + logger.info(f'Running test_gap_c_1 {device_under_test}') + hci, iut_address = device_under_test + asyncio.run(tc_gap_c_1(hci, shell, dut, iut_address)) + + def test_gap_c_2(self, shell: Shell, dut: DeviceAdapter, device_under_test): + """Test GAP-C-2: General Inquiry with Connection and Passive Disconnection.""" + logger.info(f'Running test_gap_c_2 {device_under_test}') + hci, iut_address = device_under_test + asyncio.run(tc_gap_c_2(hci, shell, dut, iut_address)) + + def test_gap_c_3(self, shell: Shell, dut: DeviceAdapter, device_under_test): + """Test GAP-C-3: General Inquiry with Connection Rejection.""" + logger.info(f'Running test_gap_c_3 {device_under_test}') + hci, iut_address = device_under_test + asyncio.run(tc_gap_c_3(hci, shell, dut, iut_address)) + + def test_gap_c_4(self, shell: Shell, dut: DeviceAdapter, device_under_test): + """Test GAP-C-4: Limited Inquiry with Successful Connection and Active Disconnection.""" + logger.info(f'Running test_gap_c_4 {device_under_test}') + hci, iut_address = device_under_test + asyncio.run(tc_gap_c_4(hci, shell, dut, iut_address)) + + def test_gap_c_5(self, shell: Shell, dut: DeviceAdapter, device_under_test): + """Test GAP-C-5: Limited Inquiry with Connection and Passive Disconnection.""" + logger.info(f'Running test_gap_c_5 {device_under_test}') + hci, iut_address = device_under_test + asyncio.run(tc_gap_c_5(hci, shell, dut, iut_address)) + + def test_gap_c_6(self, shell: Shell, dut: DeviceAdapter, device_under_test): + """Test GAP-C-6: Limited Inquiry with Connection Rejection.""" + logger.info(f'Running test_gap_c_6 {device_under_test}') + hci, iut_address = device_under_test + asyncio.run(tc_gap_c_6(hci, shell, dut, iut_address)) diff --git a/tests/bluetooth/classic/gap_c/src/gap_c.c b/tests/bluetooth/classic/gap_c/src/gap_c.c new file mode 100644 index 0000000000000..b0e51753bdd3e --- /dev/null +++ b/tests/bluetooth/classic/gap_c/src/gap_c.c @@ -0,0 +1,7 @@ +/* gap_c.c - Bluetooth classic gap client smoke test */ + +/* + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/tests/bluetooth/classic/gap_c/testcase.yaml b/tests/bluetooth/classic/gap_c/testcase.yaml new file mode 100644 index 0000000000000..bd0c34558aada --- /dev/null +++ b/tests/bluetooth/classic/gap_c/testcase.yaml @@ -0,0 +1,24 @@ +tests: + bluetooth.classic.gap.client: + platform_allow: + - native_sim + integration_platforms: + - native_sim + tags: + - bluetooth + - gap + harness: pytest + harness_config: + pytest_dut_scope: session + fixture: usb_hci + timeout: 600 + bluetooth.classic.gap.client.no_blobs: + platform_allow: + - mimxrt1170_evk@B/mimxrt1176/cm7 + tags: + - bluetooth + - gap + extra_args: + - CONFIG_BUILD_ONLY_NO_BLOBS=y + timeout: 600 + build_only: true