diff --git a/.github/workflows/bt_classic_sim_tests.yml b/.github/workflows/bt_classic_sim_tests.yml new file mode 100644 index 0000000000000..1f50e176d24f7 --- /dev/null +++ b/.github/workflows/bt_classic_sim_tests.yml @@ -0,0 +1,151 @@ +name: Run Bluetooth Classic Simulation tests + +on: + pull_request: + paths: + - '.github/workflows/bluetooth_classic_sim_tests.yml' + - "west.yml" + - 'include/zephyr/bluetooth/**' + - "subsys/bluetooth/**" + - 'tests/bluetooth/classic/**' + - "samples/bluetooth/classic/**" + - "boards/native/**" + - "soc/native/**" + - "arch/posix/**" + - "include/zephyr/arch/posix/**" + - "scripts/native_simulator/**" + - "modules/mbedtls/**" + - '!**.rst' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + bluetooth_classic_sim_tests: + if: github.repository_owner == 'zephyrproject-rtos' + runs-on: + group: zephyr-runner-v2-linux-x64-4xlarge + container: + image: ghcr.io/zephyrproject-rtos/ci-repo-cache:v0.28.7.20251127 + options: '--entrypoint /bin/bash' + env: + ZEPHYR_TOOLCHAIN_VARIANT: zephyr + permissions: + checks: write # to create the check run entry with test results + + steps: + - name: Apply container owner mismatch workaround + run: | + # FIXME: The owner UID of the GITHUB_WORKSPACE directory may not + # match the container user UID because of the way GitHub + # Actions runner is implemented. Remove this workaround when + # GitHub comes up with a fundamental fix for this problem. + git config --global --add safe.directory ${GITHUB_WORKSPACE} + + - name: Print cloud service information + run: | + echo "ZEPHYR_RUNNER_CLOUD_PROVIDER = ${ZEPHYR_RUNNER_CLOUD_PROVIDER}" + echo "ZEPHYR_RUNNER_CLOUD_NODE = ${ZEPHYR_RUNNER_CLOUD_NODE}" + echo "ZEPHYR_RUNNER_CLOUD_POD = ${ZEPHYR_RUNNER_CLOUD_POD}" + + - name: Clone cached Zephyr repository + continue-on-error: true + run: | + git clone --shared /repo-cache/zephyrproject/zephyr . + git remote set-url origin ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} + + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + + - name: Environment Setup + env: + BASE_REF: ${{ github.base_ref }} + run: | + git config --global user.email "bot@zephyrproject.org" + git config --global user.name "Zephyr Bot" + rm -fr ".git/rebase-apply" + rm -fr ".git/rebase-merge" + git rebase origin/${BASE_REF} + git clean -f -d + git log --pretty=oneline | head -n 10 + west init -l . || true + west config manifest.group-filter -- +ci + west config --global update.narrow true + west update --path-cache /repo-cache/zephyrproject 2>&1 1> west.update.log || west update --path-cache /repo-cache/zephyrproject 2>&1 1> west.update.log || ( rm -rf ../modules ../bootloader ../tools && west update --path-cache /repo-cache/zephyrproject) + west forall -c 'git reset --hard HEAD' + + echo "ZEPHYR_SDK_INSTALL_DIR=/opt/toolchains/zephyr-sdk-$( cat SDK_VERSION )" >> $GITHUB_ENV + + - name: Install Python packages + run: | + pip install -r scripts/requirements-actions.txt --require-hashes + + - name: Install Bluetooth Classic Simulation dependencies + run: | + pip install bumble==0.0.220 + + - name: Make test scripts executable + run: | + find tests/bluetooth/classic/sim -name "*.sh" -type f \ + -exec chmod +x {} \; + + - name: Run Bluetooth Classic Sim tests + id: run_tests + run: | + export ZEPHYR_BASE=${PWD} + export ZEPHYR_TOOLCHAIN_VARIANT=zephyr + tests/bluetooth/classic/sim/ci.bt.classic.sh + + - name: Create Test Results + run: | + junit2html tests/bluetooth/classic/sim/test_logs/junit.xml tests/bluetooth/classic/sim/test_logs/junit.html + + - name: Upload Unit Test Results in HTML + if: always() + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: HTML Unit Test Results + if-no-files-found: ignore + path: | + tests/bluetooth/classic/sim/test_logs/junit.html + + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@34d7c956a59aed1bfebf31df77b8de55db9bbaaf # v2.21.0 + with: + check_name: Bluetooth Classic Simulation Test Results + files: "tests/bluetooth/classic/sim/test_logs/junit.xml" + comment_mode: off + + - name: Upload test logs + if: always() + uses: EnricoMi/publish-unit-test-result-action@34d7c956a59aed1bfebf31df77b8de55db9bbaaf # v2.21.0 + with: + name: bluetooth-classic-sim-test-logs + path: | + tests/bluetooth/classic/sim/test_logs/ + bt_classic_sim/ + comment_mode: off + + - name: Check test results + if: always() + run: | + if [[ -f tests/bluetooth/classic/sim/test_logs/junit.xml ]]; then + failures=$(grep -o 'failures="[0-9]*"' \ + tests/bluetooth/classic/sim/test_logs/junit.xml | \ + grep -o '[0-9]*') + if [[ "$failures" -gt 0 ]]; then + echo "::error::$failures test(s) failed" + exit 1 + fi + fi + + - name: Cleanup + if: always() + run: | + sudo pkill -9 zephyr.exe || true diff --git a/tests/bluetooth/classic/sim/ci.bt.classic.sh b/tests/bluetooth/classic/sim/ci.bt.classic.sh new file mode 100644 index 0000000000000..3d9e390d0dbfa --- /dev/null +++ b/tests/bluetooth/classic/sim/ci.bt.classic.sh @@ -0,0 +1,507 @@ +#!/usr/bin/env bash +# Copyright 2025 NXP +# SPDX-License-Identifier: Apache-2.0 + +# Script to run all test scripts in subdirectories and generate JUnit XML report +# +# Usage: +# 1. Source Zephyr environment: +# source zephyr/zephyr-env.sh +# +# 2. Run the script: +# ./ci.bt.classic.sh +# +# Environment Variables: +# ZEPHYR_BASE - Path to Zephyr root directory (required) +# +# Output: +# - Test logs: test_logs/*.log +# - Summary: test_logs/test_summary.log +# - JUnit XML: test_logs/junit.xml + +set -euo pipefail + +# Color definitions +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" >&2 +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" >&2 +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +log_section() { + echo -e "${BLUE}[====]${NC} $1" >&2 +} + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Test directories configuration file +TEST_DIRS_FILE="$SCRIPT_DIR/tests.native_sim.txt" + +# Create logs directory +LOGS_DIR="$SCRIPT_DIR/test_logs" +mkdir -p "$LOGS_DIR" + +# Log file for summary +SUMMARY_LOG="$LOGS_DIR/test_summary.log" +> "$SUMMARY_LOG" # Clear previous summary + +# JUnit XML file +JUNIT_XML="$LOGS_DIR/junit.xml" + +# Arrays to track test results +declare -a PASSED_TESTS=() +declare -a FAILED_TESTS=() +declare -a SKIPPED_TESTS=() +declare -A TEST_TIMES=() +declare -A TEST_EXIT_CODES=() +declare -A TEST_MESSAGES=() + +# Global variable to track if any test failed +HAS_FAILURE=false + +# Function to escape XML special characters +xml_escape() { + local text="$1" + text="${text//&/&}" + text="${text///>}" + text="${text//\"/"}" + text="${text//\'/'}" + echo "$text" +} + +# Global arrays for test directories and scripts +declare -a TEST_DIRECTORIES=() +declare -a TEST_SCRIPTS=() + +# Function to read test directories from file +read_test_directories() { + TEST_DIRECTORIES=() + + if [[ ! -f "$TEST_DIRS_FILE" ]]; then + log_error "Test directories file not found: $TEST_DIRS_FILE" + return 1 + fi + + log_info "Reading test directories from: $TEST_DIRS_FILE" + + # Read file line by line, skip comments and empty lines + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip empty lines + [[ -z "$line" ]] && continue + + # Skip comments (lines starting with #) + [[ "$line" =~ ^[[:space:]]*# ]] && continue + + # Trim whitespace + line=$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + + # Skip if empty after trimming + [[ -z "$line" ]] && continue + + # Convert relative path to absolute path + local abs_path + if [[ "$line" = /* ]]; then + abs_path="$line" + else + abs_path="$SCRIPT_DIR/$line" + fi + + # Check if directory exists + if [[ -d "$abs_path" ]]; then + TEST_DIRECTORIES+=("$abs_path") + log_info " Added directory: $abs_path" + else + log_warn " Directory not found (skipping): $abs_path" + fi + done < "$TEST_DIRS_FILE" + + if [[ ${#TEST_DIRECTORIES[@]} -eq 0 ]]; then + log_error "No valid directories found in $TEST_DIRS_FILE" + return 1 + fi + + log_info "Total directories to search: ${#TEST_DIRECTORIES[@]}" + return 0 +} + +# Function to find all test scripts +find_test_scripts() { + TEST_SCRIPTS=() + + log_info "Searching for test scripts in configured directories..." + + if [[ ${#TEST_DIRECTORIES[@]} -eq 0 ]]; then + log_error "No test directories configured" + return 1 + fi + + # Search for test scripts in each directory + for test_dir in "${TEST_DIRECTORIES[@]}"; do + log_info "Searching in: $test_dir" + + # Find all .sh files in tests_scripts subdirectories + while IFS= read -r -d '' script; do + # Check if file is executable + if [[ -x "$script" ]]; then + TEST_SCRIPTS+=("$script") + log_info " Found executable: $(basename "$script")" + else + log_warn " Found non-executable: $(basename "$script")" + + # Try to make it executable + if chmod +x "$script" 2>/dev/null; then + log_info " Made executable: $(basename "$script")" + TEST_SCRIPTS+=("$script") + else + log_error " Failed to make executable: $(basename "$script")" + local script_name=$(basename "$script") + local test_name=$(basename "$(dirname "$(dirname "$script")")") + SKIPPED_TESTS+=("$test_name/$script_name") + TEST_MESSAGES["$test_name/$script_name"]="Not executable (chmod failed)" + fi + fi + done < <(find "$test_dir" -type d -name "tests_scripts" \ + -exec find {} -maxdepth 1 -type f -name "*.sh" -print0 \;) + done + + # Sort scripts for consistent execution order + if [[ ${#TEST_SCRIPTS[@]} -gt 0 ]]; then + IFS=$'\n' TEST_SCRIPTS=($(sort <<<"${TEST_SCRIPTS[*]}")) + unset IFS + fi + + return 0 +} + +# Function to run a single test script +run_test_script() { + local script="$1" + local test_index="$2" + local total_tests="$3" + local script_name=$(basename "$script") + local script_dir=$(dirname "$script") + local test_name=$(basename "$(dirname "$script_dir")") + local test_id="$test_name/$script_name" + local log_file="$LOGS_DIR/${test_id//\//_}.log" + + # Check if ZEPHYR_BASE is set + if [[ -z "${ZEPHYR_BASE:-}" ]]; then + log_info "$test_index/$total_tests $test_id \033[31mFAIL\033[0m (ZEPHYR_BASE not set)" + FAILED_TESTS+=("$test_id") + TEST_MESSAGES["$test_id"]="ZEPHYR_BASE not set" + TEST_TIMES["$test_id"]=0 + TEST_EXIT_CODES["$test_id"]=1 + HAS_FAILURE=true + return 1 + fi + + # Check if ZEPHYR_BASE directory exists + if [[ ! -d "$ZEPHYR_BASE" ]]; then + log_info "$test_index/$total_tests $test_id \033[31mFAIL\033[0m (ZEPHYR_BASE dir not found)" + FAILED_TESTS+=("$test_id") + TEST_MESSAGES["$test_id"]="ZEPHYR_BASE directory not found: $ZEPHYR_BASE" + TEST_TIMES["$test_id"]=0 + TEST_EXIT_CODES["$test_id"]=1 + HAS_FAILURE=true + return 1 + fi + + # Save current directory + local original_dir=$(pwd) + + # Record start time + local start_time=$(date +%s) + + # Run the script and capture output + # Use 'set +e' to continue even if script fails + set +e + ( + # Change to ZEPHYR_BASE directory + if ! cd "$ZEPHYR_BASE"; then + echo "ERROR: Failed to change to ZEPHYR_BASE: $ZEPHYR_BASE" >&2 + exit 1 + fi + + # Execute the script and redirect all output to log file + "$script" > "$log_file" 2>&1 + exit $? + ) + local exit_code=$? + set -e + + # Return to original directory (don't fail if this doesn't work) + cd "$original_dir" 2>/dev/null || true + + # Record end time + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + # Store test time + TEST_TIMES["$test_id"]=$duration + TEST_EXIT_CODES["$test_id"]=$exit_code + + # Check result + if [[ $exit_code -eq 0 ]]; then + log_info "$test_index/$total_tests $test_id \033[32mPASS\033[0m (${duration}s)" + PASSED_TESTS+=("$test_id") + TEST_MESSAGES["$test_id"]="Test passed successfully" + echo "PASSED: $test_id (${duration}s)" >> "$SUMMARY_LOG" + else + log_info "$test_index/$total_tests $test_id \033[31mFAIL\033[0m (${duration}s)" + cat "$log_file" 2>/dev/null || echo "Failed to read log file: $log_file" + FAILED_TESTS+=("$test_id") + TEST_MESSAGES["$test_id"]="Test failed with exit code $exit_code" + echo "FAILED: $test_id (exit code: $exit_code, ${duration}s)" >> "$SUMMARY_LOG" + HAS_FAILURE=true + fi + + # Always return 0 to continue with other tests + return 0 +} + +# Function to generate JUnit XML report +generate_junit_xml() { + log_info "Generating JUnit XML report..." + + local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S") + local total_tests=$((${#PASSED_TESTS[@]} + ${#FAILED_TESTS[@]})) + local total_time=0 + + # Calculate total time + for test_id in "${!TEST_TIMES[@]}"; do + total_time=$((total_time + TEST_TIMES["$test_id"])) + done + + # Start XML document + cat > "$JUNIT_XML" < + + +EOF + + # Add passed tests + for test_id in "${PASSED_TESTS[@]}"; do + local time="${TEST_TIMES[$test_id]}" + local classname=$(echo "$test_id" | cut -d'/' -f1) + local testname=$(echo "$test_id" | cut -d'/' -f2) + + cat >> "$JUNIT_XML" < + Test passed successfully + +EOF + done + + # Add failed tests + for test_id in "${FAILED_TESTS[@]}"; do + local time="${TEST_TIMES[$test_id]}" + local exit_code="${TEST_EXIT_CODES[$test_id]}" + local message="${TEST_MESSAGES[$test_id]}" + local classname=$(echo "$test_id" | cut -d'/' -f1) + local testname=$(echo "$test_id" | cut -d'/' -f2) + local log_file="$LOGS_DIR/${test_id//\//_}.log" + + # Read last 50 lines of log for failure details + local log_content="" + if [[ -f "$log_file" ]]; then + log_content=$(cat "$log_file" 2>/dev/null || echo "Log file not found") + fi + + cat >> "$JUNIT_XML" < + +Exit Code: $exit_code +Test: $test_id +Log File: $log_file + +Complete log output: +$(xml_escape "$log_content") + + Test failed with exit code $exit_code + +EOF + done + + # Add skipped tests + for test_id in "${SKIPPED_TESTS[@]}"; do + local message="${TEST_MESSAGES[$test_id]}" + local classname=$(echo "$test_id" | cut -d'/' -f1) + local testname=$(echo "$test_id" | cut -d'/' -f2) + + cat >> "$JUNIT_XML" < + + +EOF + done + + # Close XML document + cat >> "$JUNIT_XML" < + +EOF + + log_info "JUnit XML report generated: $JUNIT_XML" +} + +# Function to print summary +print_summary() { + local total_tests=$((${#PASSED_TESTS[@]} + ${#FAILED_TESTS[@]})) + + echo "" + log_section "Test Summary" + echo "============================================" + echo "Total tests run: $total_tests" + echo "Passed: ${#PASSED_TESTS[@]}" + echo "Failed: ${#FAILED_TESTS[@]}" + echo "Skipped: ${#SKIPPED_TESTS[@]}" + echo "============================================" + + if [[ ${#PASSED_TESTS[@]} -gt 0 ]]; then + echo "" + echo -e "${GREEN}Passed tests:${NC}" + for test in "${PASSED_TESTS[@]}"; do + echo " ✅ $test (${TEST_TIMES[$test]}s)" + done + fi + + if [[ ${#FAILED_TESTS[@]} -gt 0 ]]; then + echo "" + echo -e "${RED}Failed tests:${NC}" + for test in "${FAILED_TESTS[@]}"; do + echo " ❌ $test (exit code: ${TEST_EXIT_CODES[$test]}, \ +${TEST_TIMES[$test]}s)" + done + fi + + if [[ ${#SKIPPED_TESTS[@]} -gt 0 ]]; then + echo "" + echo -e "${YELLOW}Skipped tests:${NC}" + for test in "${SKIPPED_TESTS[@]}"; do + echo " ⚠️ $test - ${TEST_MESSAGES[$test]}" + done + fi + + echo "" + echo "Detailed logs available in: $LOGS_DIR" + echo "Summary log: $SUMMARY_LOG" + echo "JUnit XML report: $JUNIT_XML" +} + +# Cleanup function +cleanup() { + log_info "CI BT Classic cleaning up..." + + # Kill any remaining processes (try with and without sudo) + pkill -9 btvirt 2>/dev/null || sudo pkill -9 btvirt 2>/dev/null || true + pkill -9 btmon 2>/dev/null || sudo pkill -9 btmon 2>/dev/null || true + pkill -9 zephyr.exe 2>/dev/null || sudo pkill -9 zephyr.exe 2>/dev/null || true + + # Generate JUnit XML report + generate_junit_xml + + # Print summary + print_summary + + # Return appropriate exit code + if [[ "$HAS_FAILURE" == true ]]; then + log_error "Some tests failed - exiting with error code 1" + exit 1 + else + log_info "All tests passed - exiting with code 0" + exit 0 + fi +} + +# Set cleanup on exit +trap cleanup EXIT + +# Main function +main() { + log_section "Bluetooth Classic Simulation Test Runner" + log_info "Script directory: $SCRIPT_DIR" + log_info "Test directories file: $TEST_DIRS_FILE" + log_info "Logs directory: $LOGS_DIR" + + # Check ZEPHYR_BASE early + if [[ -z "${ZEPHYR_BASE:-}" ]]; then + log_error "ZEPHYR_BASE environment variable is not set" + log_error "Please run: source zephyr/zephyr-env.sh" + HAS_FAILURE=true + return 1 + fi + + if [[ ! -d "$ZEPHYR_BASE" ]]; then + log_error "ZEPHYR_BASE directory does not exist: $ZEPHYR_BASE" + HAS_FAILURE=true + return 1 + fi + + log_info "ZEPHYR_BASE: $ZEPHYR_BASE" + + if ! read_test_directories; then + log_error "Failed to read test directories" + HAS_FAILURE=true + return 1 + fi + + if ! find_test_scripts; then + log_error "Failed to find test scripts" + HAS_FAILURE=true + return 1 + fi + + if [[ ${#TEST_SCRIPTS[@]} -eq 0 ]]; then + log_warn "No executable test scripts found in configured directories" + + if [[ ${#SKIPPED_TESTS[@]} -gt 0 ]]; then + HAS_FAILURE=true + fi + + return 0 + fi + + log_info "Found ${#TEST_SCRIPTS[@]} executable test script(s)" + + total_tests=${#TEST_SCRIPTS[@]} + test_index=1 + + for script in "${TEST_SCRIPTS[@]}"; do + run_test_script "$script" "$test_index" "$total_tests" + ((test_index++)) + done + + # Summary will be printed in cleanup function + # Exit code will be set based on HAS_FAILURE +} + +# Run main function +main "$@" diff --git a/tests/bluetooth/classic/sim/common/bt_sim_controller.py b/tests/bluetooth/classic/sim/common/bt_sim_controller.py new file mode 100644 index 0000000000000..6f9737e305a63 --- /dev/null +++ b/tests/bluetooth/classic/sim/common/bt_sim_controller.py @@ -0,0 +1,349 @@ +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import logging +import struct +from collections.abc import Callable + +import bt_sim_hci as sim_hci +import bt_sim_ll as sim_ll +from bumble import hci +from bumble.controller import Connection, ScoLink +from bumble.controller import Controller as controller +from bumble.core import PhysicalTransport + +logger = logging.getLogger(__name__) + + +class bt_classic_discovery: + def __init__( + self, + iac_lap, + count, + interval=1.25, + on_inquiry: Callable | None = None, + on_complete: Callable | None = None, + ): + self.flag = False + self.count = count + self.interval = interval + self._task = None + self.on_inquiry = on_inquiry + self.on_complete = on_complete + self._is_running = False + self.iac_lap = iac_lap + + def start( + self, + count, + interval=1.25, + on_inquiry: Callable | None = None, + on_complete: Callable | None = None, + ): + """Start periodic timeout callback""" + self.flag = True + self._is_running = True + self.count = count + self.interval = interval + if on_inquiry: + self.on_complete = on_inquiry + if on_complete: + self.on_complete = on_complete + if self._task: + self._task.cancel() + self._task = asyncio.create_task(self._periodic_callback(interval)) + + async def _periodic_callback(self, interval): + """Execute callback periodically""" + try: + while self._is_running: + await asyncio.sleep(interval) + if self._is_running and self.on_inquiry: + if asyncio.iscoroutinefunction(self.on_inquiry): + await self.on_inquiry() + else: + self.on_inquiry() + if self.count > 0: + self.count = self.count - 1 + if self.count == 0 and self.on_complete: + if asyncio.iscoroutinefunction(self.on_complete): + await self.on_complete() + else: + self.on_complete() + self.stop() + except asyncio.CancelledError: + pass + + def stop(self): + """Stop periodic execution""" + self.flag = False + self._is_running = False + if self._task: + self._task.cancel() + self._task = None + + +class bt_sim_controller(controller): + # Enable BR/EDR feature + lmp_features: bytes = bytes.fromhex( + '0000000040000000' + ) # BR/EDR Supported, LE Supported (Controller) + + inquiry_mode: int = 0 + page_timeout: int = 0x2000 + default_link_policy_settings: int = 0 + discovery: bt_classic_discovery = None + iac_lap: list = [hci.HCI_GENERAL_INQUIRY_LAP] + class_of_device: int = 0 + + def start_inquiry(self, lap) -> None: + logger.debug("[%s] >>> Inquiry LAP %s", self.name, lap) + if self.link: + self.link.start_inquiry(self, lap) + + def on_inquiry_complete(self) -> None: + logger.debug("Inquiry complete") + self.send_hci_packet(hci.HCI_Inquiry_Complete_Event(status=hci.HCI_SUCCESS)) + + def on_inquiry_timeout(self) -> None: + logger.debug("Inquiry timeout") + self.start_inquiry(self.discovery.iac_lap) + + def on_hci_write_page_timeout_command( + self, command: hci.HCI_Write_Page_Timeout_Command + ) -> bytes | None: + ''' + See Bluetooth spec Vol 4, Part E - 7.3.18 Write Page Timeout Command + ''' + self.page_timeout = command.page_timeout + return bytes([hci.HCI_SUCCESS]) + + def on_hci_write_inquiry_mode_command( + self, command: hci.HCI_Write_Inquiry_Mode_Command + ) -> bytes | None: + ''' + See Bluetooth spec Vol 4, Part E - 7.3.18 Write Inquiry Mode Command + ''' + self.inquiry_mode = command.inquiry_mode + return bytes([hci.HCI_SUCCESS]) + + def on_hci_read_default_link_policy_settings_command( + self, _command: sim_hci.HCI_Read_Default_Link_Policy_Settings_Command + ) -> bytes | None: + ''' + See Bluetooth spec 7.2.11 Read Default Link Policy Settings command + ''' + return struct.pack( + ' None: + self.send_hci_packet( + hci.HCI_Inquiry_Result_Event( + bd_addr=packet.bd_address, + page_scan_repetition_mode=packet.Page_Scan_Repetition_Mode, + reserved_0=packet.Reserved, + reserved_1=packet.Reserved, + class_of_device=packet.Class_Of_Device, + clock_offset=packet.Clock_Offset, + ) + ) + + def on_inquiry_result_with_rssi_ind(self, packet: sim_ll.InquiryRssiInd) -> None: + self.send_hci_packet( + hci.HCI_Inquiry_Result_With_RSSI_Event( + bd_addr=packet.bd_address, + page_scan_repetition_mode=packet.Page_Scan_Repetition_Mode, + reserved=packet.Reserved, + class_of_device=packet.Class_Of_Device, + clock_offset=packet.Clock_Offset, + rssi=packet.RSSI, + ) + ) + + def on_extended_inquiry_result_ind(self, packet: sim_ll.ExtendedInquiryInd) -> None: + self.send_hci_packet( + hci.HCI_Extended_Inquiry_Result_Event( + num_responses=packet.num_responses, + bd_addr=packet.bd_address, + page_scan_repetition_mode=packet.Page_Scan_Repetition_Mode, + reserved=packet.Reserved, + class_of_device=packet.Class_Of_Device, + clock_offset=packet.Clock_Offset, + rssi=packet.RSSI, + extended_inquiry_response=packet.Extended_Inquiry_Response, + ) + ) + + def on_inquiry_result_pdu(self, packet: sim_ll.InquiryPdu) -> None: + logger.debug("[%s] <<< Inquiry PDU: %s", self.name, packet) + if isinstance(packet, sim_ll.InquiryInd): + self.on_inquiry_result_ind(packet) + elif isinstance(packet, sim_ll.InquiryRssiInd): + self.on_inquiry_result_with_rssi_ind(packet) + elif isinstance(packet, sim_ll.ExtendedInquiryInd): + self.on_extended_inquiry_result_ind(packet) + else: + logger.warning("[%s] Unknown inquiry PDU type: %s", self.name, type(packet)) + + def on_hci_inquiry_command(self, command: hci.HCI_Inquiry_Command) -> None: + ''' + See Bluetooth spec 7.1.1 Inquiry command + ''' + logger.debug( + f'Inqury LAP {command.lap} length {command.inquiry_length} ' + f'num_responses {command.num_responses}' + ) + + self.discovery = bt_classic_discovery( + command.lap, + command.inquiry_length, + on_inquiry=self.on_inquiry_timeout, + on_complete=self.on_inquiry_complete, + ) + self.discovery.start(command.inquiry_length) + + self.send_hci_packet( + hci.HCI_Command_Status_Event( + status=hci.HCI_SUCCESS, + num_hci_command_packets=1, + command_opcode=command.op_code, + ) + ) + + self.start_inquiry(command.lap) + + def on_hci_inquiry_cancel_command( + self, _command: hci.HCI_Inquiry_Cancel_Command + ) -> bytes | None: + ''' + See Bluetooth spec 7.1.2 Inquiry Cancel command + ''' + if self.discovery is not None: + self.discovery.stop() + + return bytes([hci.HCI_SUCCESS]) + + def on_hci_write_current_iac_lap_command( + self, command: sim_hci.HCI_Write_Current_Iac_Lap_Command + ) -> bytes | None: + ''' + See Bluetooth spec 7.3.45 Write Current IAC LAP command + ''' + self.iac_lap = [] + + for iac in command.iac_lap: + self.iac_lap.append(iac) + return bytes([hci.HCI_SUCCESS]) + + def on_hci_read_class_of_device_command( + self, _command: hci.HCI_Read_Class_Of_Device_Command + ) -> bytes | None: + ''' + See Bluetooth spec Vol 4, Part E - 7.3.25 Read Class of Device Command + ''' + return struct.pack(' bytes | None: + ''' + See Bluetooth spec Vol 4, Part E - 7.3.26 Write Class of Device Command + ''' + self.class_of_device = command.class_of_device + return bytes([hci.HCI_SUCCESS]) + + def on_classic_connection_request(self, peer_address: hci.Address, link_type: int) -> None: + if link_type == hci.HCI_Connection_Complete_Event.LinkType.ACL: + self.classic_connections[peer_address] = Connection( + controller=self, + handle=0, + role=hci.Role.PERIPHERAL, + peer_address=peer_address, + link=self.link, + transport=PhysicalTransport.BR_EDR, + link_type=link_type, + classic_allow_role_switch=self.classic_allow_role_switch, + ) + else: + self.sco_links[peer_address] = ScoLink( + handle=0, + link_type=link_type, + peer_address=peer_address, + ) + self.send_hci_packet( + hci.HCI_Connection_Request_Event( + bd_addr=peer_address, + class_of_device=self.class_of_device, + link_type=link_type, + ) + ) + + def on_read_remote_supported_features_pdu( + self, lmp_features: bytes, connection_handle: int + ) -> None: + logger.debug("[%s] <<< Read Remote Supported Features PDU %s", self.name, lmp_features) + self.send_hci_packet( + hci.HCI_Read_Remote_Supported_Features_Complete_Event( + status=hci.HCI_SUCCESS, + connection_handle=connection_handle, + lmp_features=lmp_features, + ) + ) + + def read_remote_supported_features( + self, peer_address: hci.Address, connection_handle: int + ) -> None: + logger.debug("[%s] >>> Peer Address %s", self.name, peer_address) + if self.link: + self.link.read_remote_supported_features(self, peer_address, connection_handle) + else: + logger.warning("[%s] No link available", self.name) + + def on_hci_read_remote_supported_features_command( + self, command: hci.HCI_Read_Remote_Supported_Features_Command + ) -> None: + ''' + See Bluetooth spec 7.1.1 Inquiry command + ''' + logger.debug(f'Connect handle {command.connection_handle}') + + peer_address = None + + for address, connection in self.classic_connections.items(): + if connection.handle == command.connection_handle: + peer_address = address + break + + if peer_address is None: + status = hci.HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR + else: + status = hci.HCI_SUCCESS + + self.send_hci_packet( + hci.HCI_Command_Status_Event( + status=status, + num_hci_command_packets=1, + command_opcode=command.op_code, + ) + ) + + if peer_address is not None: + self.read_remote_supported_features(peer_address, command.connection_handle) + else: + logger.warning("[%s] Unknown connection handle", self.name) + + def on_hci_host_number_of_completed_packets_command( + self, command: sim_hci.HCI_Host_Number_Of_Completed_Packets_Command + ) -> None: + ''' + See Bluetooth spec 7.1.1 Inquiry command + ''' + logger.debug( + f'Connect handle {command.connection_handle}' + f'num_of_completed_packets {command.num_of_completed_packets}' + ) diff --git a/tests/bluetooth/classic/sim/common/bt_sim_hci.py b/tests/bluetooth/classic/sim/common/bt_sim_hci.py new file mode 100644 index 0000000000000..c6ccd0ae3efb4 --- /dev/null +++ b/tests/bluetooth/classic/sim/common/bt_sim_hci.py @@ -0,0 +1,43 @@ +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 + +import dataclasses +from collections.abc import Sequence +from dataclasses import field + +from bumble.hci import STATUS_SPEC, HCI_Command, metadata + + +@HCI_Command.command +@dataclasses.dataclass +class HCI_Read_Default_Link_Policy_Settings_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.2.11 Read Default Link Policy Settings command + ''' + + return_parameters_fields = [ + ('status', STATUS_SPEC), + ('Default_Link_Policy_Settings', 2), + ] + + +@HCI_Command.command +@dataclasses.dataclass +class HCI_Write_Current_Iac_Lap_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.3.45 Write Current IAC LAP command + ''' + + iac_lap: Sequence[int] = field(metadata=metadata(3, list_begin=True, list_end=True)) + + +@HCI_Command.command +@dataclasses.dataclass +class HCI_Host_Number_Of_Completed_Packets_Command(HCI_Command): + ''' + See Bluetooth spec @ 7.3.40 Host Number Of Completed Packets command + ''' + + connection_handle: Sequence[int] = field(metadata=metadata(2, list_begin=True, list_end=True)) + num_of_completed_packets: int = field(metadata=metadata(2)) diff --git a/tests/bluetooth/classic/sim/common/bt_sim_link.py b/tests/bluetooth/classic/sim/common/bt_sim_link.py new file mode 100644 index 0000000000000..fe8dff161c5d4 --- /dev/null +++ b/tests/bluetooth/classic/sim/common/bt_sim_link.py @@ -0,0 +1,74 @@ +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import logging + +import bt_sim_ll as sim_ll +import bumble.hci as hci +from bt_sim_controller import bt_sim_controller +from bumble.link import LocalLink + +logger = logging.getLogger(__name__) + + +class bt_sim_local_link(LocalLink): + def start_inquiry( + self, + sender_controller: bt_sim_controller, + lap, + ): + loop = asyncio.get_running_loop() + for c in self.controllers: + if c != sender_controller and lap in c.iac_lap and (c.classic_scan_enable & 0x01): + if sender_controller.inquiry_mode is hci.HCI_EXTENDED_INQUIRY_MODE: + inquiry_pdu = sim_ll.ExtendedInquiryInd( + num_responses=1, + bd_address=c.public_address, + Page_Scan_Repetition_Mode=0x00, + Reserved=0x00, + Class_Of_Device=c.class_of_device, + Clock_Offset=0x00, + RSSI=-60, + Extended_Inquiry_Response=b'\x00' * 240, + ) + elif sender_controller.inquiry_mode is hci.HCI_INQUIRY_WITH_RSSI_MODE: + inquiry_pdu = sim_ll.InquiryRssiInd( + num_responses=1, + bd_address=[c.public_address], + Page_Scan_Repetition_Mode=[0x00], + Reserved=[0x00], + Class_Of_Device=[c.class_of_device], + Clock_Offset=[0x00], + RSSI=[-60], + ) + else: + inquiry_pdu = sim_ll.InquiryInd( + num_responses=1, + bd_address=[c.public_address], + Page_Scan_Repetition_Mode=[0x00], + Reserved=[0x00], + Class_Of_Device=[c.class_of_device], + Clock_Offset=[0x00], + ) + loop.call_soon(sender_controller.on_inquiry_result_pdu, inquiry_pdu) + + def read_remote_supported_features( + self, + sender_controller: bt_sim_controller, + peer_address: hci.Address, + connection_handle: int, + ): + loop = asyncio.get_running_loop() + for c in self.controllers: + if c == sender_controller: + continue + + if c.public_address == peer_address: + loop.call_soon( + sender_controller.on_read_remote_supported_features_pdu, + c.lmp_features[:8], + connection_handle, + ) + return diff --git a/tests/bluetooth/classic/sim/common/bt_sim_ll.py b/tests/bluetooth/classic/sim/common/bt_sim_ll.py new file mode 100644 index 0000000000000..e6b76b79b53f9 --- /dev/null +++ b/tests/bluetooth/classic/sim/common/bt_sim_ll.py @@ -0,0 +1,79 @@ +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 + +import dataclasses +from collections.abc import Sequence + +from bumble import hci + + +class InquiryPdu: + """Base Inquiry Physical Channel PDU class. + + Currently these messages don't really follow the LL spec, because LL protocol is + context-aware and we don't have real physical transport. + """ + + +@dataclasses.dataclass +class InquiryInd(InquiryPdu): + num_responses: int + bd_address: Sequence[hci.Address] + Page_Scan_Repetition_Mode: Sequence[int] + Reserved: Sequence[int] + Class_Of_Device: Sequence[int] + Clock_Offset: Sequence[int] + + def __str__(self): + return ( + f"InquiryRssiInd(" + f"num_responses={self.num_responses}, " + f"bd_address={self.bd_address}, " + f"PSRM={self.Page_Scan_Repetition_Mode}, " + f"CoD={[hex(c) for c in self.Class_Of_Device]})" + ) + + +@dataclasses.dataclass +class InquiryRssiInd(InquiryPdu): + num_responses: int + bd_address: Sequence[hci.Address] + Page_Scan_Repetition_Mode: Sequence[int] + Reserved: Sequence[int] + Class_Of_Device: Sequence[int] + Clock_Offset: Sequence[int] + RSSI: Sequence[int] + + def __str__(self): + return ( + f"InquiryRssiInd(" + f"num_responses={self.num_responses}, " + f"bd_address={self.bd_address}, " + f"PSRM={self.Page_Scan_Repetition_Mode}, " + f"CoD={[hex(c) for c in self.Class_Of_Device]}, " + f"RSSI={self.RSSI})" + ) + + +@dataclasses.dataclass +class ExtendedInquiryInd(InquiryPdu): + num_responses: int + bd_address: hci.Address + Page_Scan_Repetition_Mode: int + Reserved: int + Class_Of_Device: int + Clock_Offset: int + RSSI: int + Extended_Inquiry_Response: bytes # Size is 240 + + def __str__(self): + return ( + f"ExtendedInquiryInd(" + f"num_responses={self.num_responses}, " + f"bd_address={self.bd_address}, " + f"PSRM={self.Page_Scan_Repetition_Mode}, " + f"CoD={hex(self.Class_Of_Device)}, " + f"RSSI={self.RSSI}," + f"Extended_Inquiry_Response={self.Extended_Inquiry_Response})" + ) diff --git a/tests/bluetooth/classic/sim/common/controllers.py b/tests/bluetooth/classic/sim/common/controllers.py new file mode 100644 index 0000000000000..41a2e10f6d6a1 --- /dev/null +++ b/tests/bluetooth/classic/sim/common/controllers.py @@ -0,0 +1,120 @@ +# Copyright 2021-2022 Google LLC +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import logging +import random +import sys + +import bumble.logging +from bt_sim_controller import bt_sim_controller as Controller +from bt_sim_link import bt_sim_local_link +from bumble.transport import open_transport + +logger = logging.getLogger(__name__) + + +def generate_unique_bd_address(existing_addresses: set) -> str: + """Generate a unique Bluetooth device address. + + Args: + existing_addresses: Set of already allocated BD addresses + + Returns: + A unique BD address string in format XX:XX:XX:XX:XX:XX + """ + prefix = '00:00:01' + max_attempts = 1000 + + for _ in range(max_attempts): + suffix = ':'.join([f'{random.randint(0, 255):02X}' for _ in range(3)]) + public_address = f'{prefix}:{suffix}' + if public_address not in existing_addresses: + existing_addresses.add(public_address) + return public_address + + raise RuntimeError("Failed to generate unique BD address after maximum attempts") + + +async def create_controller(index: int, transport_name: str, link, bd_addresses: set): + """Create and configure a single controller. + + Args: + index: Controller index number + transport_name: Transport connection string + link: Shared link object for controllers + bd_addresses: Set of allocated BD addresses + + Returns: + Tuple of (transport, controller) + """ + transport = await open_transport(transport_name) + public_address = generate_unique_bd_address(bd_addresses) + + controller = Controller( + f'HCI{index}', + host_source=transport.source, + host_sink=transport.sink, + link=link, + public_address=public_address, + ) + + port = transport_name.split(":")[-1] + logger.info(f"HCI{index} BD Address {public_address} port {port}") + + return transport, controller + + +async def async_sim_controllers(): + """Initialize and run Bluetooth simulation controllers.""" + if len(sys.argv) < 2: + logger.error("No transport provided. Usage: controllers.py [transport2] ...") + return + + link = bt_sim_local_link() + transports: list = [] + controllers: list = [] + bd_addresses: set = set() + + try: + # Create all controllers + for index, transport_name in enumerate(sys.argv[1:]): + transport, controller = await create_controller( + index, transport_name, link, bd_addresses + ) + transports.append(transport) + controllers.append(controller) + + logger.info(f"Successfully initialized {len(controllers)} controller(s)") + + # Keep running until interrupted + await asyncio.get_running_loop().create_future() + + except Exception as e: + logger.error(f"Error during controller initialization: {e}", exc_info=True) + finally: + # Cleanup transports + for transport in transports: + try: + transport.close() + except Exception as e: + logger.warning(f"Error closing transport: {e}") + + +def main(): + """Main entry point for the Bluetooth controller simulator.""" + bumble.logging.setup_basic_logging(default_level="DEBUG") + + try: + asyncio.run(async_sim_controllers()) + except KeyboardInterrupt: + logger.info("Shutting down controllers...") + except Exception as e: + logger.error(f"Fatal error: {e}", exc_info=True) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/tests/bluetooth/classic/sim/gap_discovery/CMakeLists.txt b/tests/bluetooth/classic/sim/gap_discovery/CMakeLists.txt new file mode 100644 index 0000000000000..e572d8d0c3409 --- /dev/null +++ b/tests/bluetooth/classic/sim/gap_discovery/CMakeLists.txt @@ -0,0 +1,17 @@ +# 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) + +if(CONFIG_TEST_GAP_PERIPHERAL) +FILE(GLOB app_sources src/test_peripheral.c) +endif () + +if(CONFIG_TEST_GAP_CENTRAL) +FILE(GLOB app_sources src/test_central.c) +endif() + +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/bluetooth/classic/sim/gap_discovery/Kconfig b/tests/bluetooth/classic/sim/gap_discovery/Kconfig new file mode 100644 index 0000000000000..c80342941a2ee --- /dev/null +++ b/tests/bluetooth/classic/sim/gap_discovery/Kconfig @@ -0,0 +1,39 @@ +# Copyright 2025 NXP +# +# SPDX-License-Identifier: Apache-2.0 + +source "Kconfig.zephyr" + +choice TEST_GAP_ROLE + prompt "GAP Role" + default TEST_GAP_PERIPHERAL + help + Select the GAP role for testing + +config TEST_GAP_PERIPHERAL + bool "GAP Role Peripheral" + help + Test GAP role as peripheral device + +config TEST_GAP_CENTRAL + bool "GAP Role Central" + help + Test GAP role as central device + +endchoice + +config TEST_GAP_CENTRAL_ADDRESS + string "Central Device Address" + default "00:11:22:33:44:55" + help + GAP central device Bluetooth address in format XX:XX:XX:XX:XX:XX + +config TEST_GAP_PERIPHERAL_ADDRESS + string "Peripheral Device Address" + default "AA:BB:CC:DD:EE:FF" + help + GAP peripheral device Bluetooth address in format XX:XX:XX:XX:XX:XX + +module = TEST_GAP_DISCOVERY +module-str = test_gap_discovery +source "subsys/logging/Kconfig.template.log_config" diff --git a/tests/bluetooth/classic/sim/gap_discovery/README.rst b/tests/bluetooth/classic/sim/gap_discovery/README.rst new file mode 100644 index 0000000000000..272d00701fe0d --- /dev/null +++ b/tests/bluetooth/classic/sim/gap_discovery/README.rst @@ -0,0 +1,78 @@ +.. _bluetooth_classic_gap_discovery_test: + +Bluetooth Classic GAP Discovery Test +##################################### + +Overview +******** + +This test verifies the Bluetooth Classic GAP (Generic Access Profile) discovery +functionality. It tests the device's ability to discover nearby Bluetooth Classic +devices through inquiry procedures. + +Requirements +************ + +* A board with Bluetooth Classic support +* Bluetooth Classic controller enabled in the configuration + +Building and Running +******************** + +This test can be built and executed on boards with Bluetooth Classic support. + +.. code-block:: console + + ./tests/bluetooth/classic/gap_discovery/tests_scripts/gap_discovery_run.sh + +If the error "Permission denied" occurs, the following command should be used to add permission for +the script. + +.. code-block:: console + + chmod +x ./tests/bluetooth/classic/gap_discovery/tests_scripts/gap_discovery_run.sh + +If the error "west: command not found" occurs, the following command should be used to activate the +virtual environment. Refer to :ref:`getting_started` for details. + +.. code-block:: console + + source ~/zephyrproject/.venv/bin/activate + +Sample Output +************* + +.. code-block:: console + + ------ TESTSUITE SUMMARY START ------ + + SUITE PASS - 100.00% [gap_central]: pass = 2, fail = 0, skip = 0, total = 2 duration = 46.640 seconds + - PASS - [gap_central.test_01_gap_central_general_discovery] duration = 5.180 seconds + - PASS - [gap_central.test_02_gap_central_limited_discovery] duration = 41.460 seconds + + ------ TESTSUITE SUMMARY END ------ + + ------ TESTSUITE SUMMARY START ------ + + SUITE PASS - 100.00% [gap_peripheral]: pass = 2, fail = 0, skip = 0, total = 2 duration = 48.620 seconds + - PASS - [gap_peripheral.test_01_gap_peripheral_general_discovery] duration = 7.180 seconds + - PASS - [gap_peripheral.test_02_gap_peripheral_limited_discovery] duration = 41.440 seconds + + ------ TESTSUITE SUMMARY END ------ + +Test Coverage +************* + +This test covers: + +* Bluetooth Classic initialization +* GAP inquiry start/stop procedures +* Device discovery callbacks +* Inquiry result handling + +Configuration Options +********************* + +See :file:`prj.conf` for the default configuration. + +Additional configuration options can be found in :file:`Kconfig`. diff --git a/tests/bluetooth/classic/sim/gap_discovery/prj.conf b/tests/bluetooth/classic/sim/gap_discovery/prj.conf new file mode 100644 index 0000000000000..888f590db74d9 --- /dev/null +++ b/tests/bluetooth/classic/sim/gap_discovery/prj.conf @@ -0,0 +1,12 @@ +CONFIG_BT=y +CONFIG_BT_CLASSIC=y +CONFIG_LOG=y +CONFIG_ZTEST=y + +CONFIG_BT_DEVICE_NAME="gap_discovery" + +CONFIG_BT_CREATE_CONN_TIMEOUT=30 +CONFIG_BT_PAGE_TIMEOUT=0xFFFF +CONFIG_BT_LIMITED_DISCOVERABLE_DURATION=31 + +CONFIG_TEST_GAP_DISCOVERY_LOG_LEVEL_DBG=y diff --git a/tests/bluetooth/classic/sim/gap_discovery/src/test_central.c b/tests/bluetooth/classic/sim/gap_discovery/src/test_central.c new file mode 100644 index 0000000000000..89531374909d1 --- /dev/null +++ b/tests/bluetooth/classic/sim/gap_discovery/src/test_central.c @@ -0,0 +1,254 @@ +/* + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE_NAME test_gap_discovery_central +LOG_MODULE_REGISTER(LOG_MODULE_NAME, CONFIG_TEST_GAP_DISCOVERY_LOG_LEVEL); + +static struct bt_br_discovery_param br_discover_param; +#define BR_DISCOVER_RESULT_COUNT 10 +static struct bt_br_discovery_result br_discover_result[BR_DISCOVER_RESULT_COUNT]; + +static K_SEM_DEFINE(br_discover_sem, 0, 1); + +static bt_addr_t peer_addr; +static ATOMIC_DEFINE(test_flags, 32); + +#define TEST_FLAG_DEVICE_FOUND 0 +#define TEST_FLAG_CONN_CONNECTED 1 +#define TEST_FLAG_CONN_DISCONNECTED 2 + +static void br_discover_timeout(const struct bt_br_discovery_result *results, size_t count) +{ + LOG_DBG("BR discovery done, found %zu devices", count); + + k_sem_give(&br_discover_sem); +} + +static void br_discover_recv(const struct bt_br_discovery_result *result) +{ + char br_addr[BT_ADDR_STR_LEN]; + + bt_addr_to_str(&result->addr, br_addr, sizeof(br_addr)); + + LOG_DBG("[DEVICE]: %s, RSSI %i, COD %u", br_addr, result->rssi, sys_get_le24(result->cod)); + + if (bt_addr_eq(&peer_addr, &result->addr)) { + atomic_set_bit(test_flags, TEST_FLAG_DEVICE_FOUND); + LOG_DBG(" Target %s is found", br_addr); + k_sem_give(&br_discover_sem); + } +} + +static struct bt_br_discovery_cb br_discover = { + .recv = br_discover_recv, + .timeout = br_discover_timeout, +}; + +static void br_connected(struct bt_conn *conn, uint8_t conn_err) +{ + LOG_DBG("connected: conn %p err 0x%02x", (void *)conn, conn_err); + if (conn_err == 0) { + k_sem_give(&br_discover_sem); + atomic_set_bit(test_flags, TEST_FLAG_CONN_CONNECTED); + } else { + LOG_ERR("Connection failed"); + } +} + +static void br_disconnected(struct bt_conn *conn, uint8_t reason) +{ + LOG_DBG("disconnected: conn %p reason 0x%02x", (void *)conn, reason); + k_sem_give(&br_discover_sem); + atomic_set_bit(test_flags, TEST_FLAG_CONN_DISCONNECTED); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = br_connected, + .disconnected = br_disconnected, +}; + +/* Multiplier for each discovery result to estimate timeout (seconds per device) */ +#define GAP_DISCOVERY_TIMEOUT_PER_DEVICE 1.25 +/* Base timeout added to the total (seconds) */ +#define GAP_DISCOVERY_TIMEOUT_BASE 5.0 +#define GAP_DISCOVERY_TIMEOUT(count) \ + ((double)(count) * GAP_DISCOVERY_TIMEOUT_PER_DEVICE + GAP_DISCOVERY_TIMEOUT_BASE) + +static bool peer_device_discovery(bool limited) +{ + int err; + double timeout; + + atomic_clear_bit(test_flags, TEST_FLAG_DEVICE_FOUND); + + LOG_DBG("Starting Bluetooth inquiry"); + + br_discover_param.length = BR_DISCOVER_RESULT_COUNT; + br_discover_param.limited = limited; + + memset(br_discover_result, 0, sizeof(br_discover_result)); + + err = bt_br_discovery_start(&br_discover_param, br_discover_result, + ARRAY_SIZE(br_discover_result)); + zassert_equal(err, 0, "Bluetooth inquiry failed (err %d)", err); + + /* Wait for all discovery results to be processed */ + timeout = GAP_DISCOVERY_TIMEOUT(ARRAY_SIZE(br_discover_result)); + LOG_DBG("Will wait for GAP discovery done (timeout %us)", (uint32_t)timeout); + err = k_sem_take(&br_discover_sem, K_SECONDS(timeout)); + zassert_equal(err, 0, "Failed to wait for discovery done (err %d)", err); + + err = bt_br_discovery_stop(); + if ((err != 0) && (err != -EALREADY)) { + LOG_ERR("Failed to stop GAP discovery procedure (err %d)", err); + } + + LOG_DBG("Bluetooth inquiry completed"); + + return atomic_test_bit(test_flags, TEST_FLAG_DEVICE_FOUND); +} + +static void peer_device_connect(void) +{ + int err; + struct bt_conn *conn; + + conn = bt_conn_create_br(&peer_addr, BT_BR_CONN_PARAM_DEFAULT); + zassert_true(conn != NULL, "BR connection creating failed"); + + err = k_sem_take(&br_discover_sem, K_SECONDS(30)); + zassert_equal(err, 0, "Connection timeout (err %d)", err); + zassert_true(atomic_test_bit(test_flags, TEST_FLAG_CONN_CONNECTED), "Connection failed"); + + k_sem_reset(&br_discover_sem); + + k_sleep(K_SECONDS(5)); + + err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + zassert_equal(err, 0, "Disconnection ACL failed (err %d)", err); + + err = k_sem_take(&br_discover_sem, K_SECONDS(30)); + zassert_equal(err, 0, "Disconnection timeout (err %d)", err); + zassert_true(atomic_test_bit(test_flags, TEST_FLAG_CONN_DISCONNECTED), + "Disconnection failed"); + + bt_conn_unref(conn); +} + +ZTEST(gap_central, test_01_gap_central_general_discovery) +{ + bool found; + + found = peer_device_discovery(false); + + zassert_true(found, "Peer device not found"); + + peer_device_connect(); +} + +#define BT_COD_MAJOR_SVC_CLASS_LIMITED_DISCOVER BIT(13) + +static bool is_limited_inquiry(void) +{ + ARRAY_FOR_EACH(br_discover_result, i) { + uint32_t cod; + + if (!bt_addr_eq(&br_discover_result[i].addr, &peer_addr)) { + continue; + } + + cod = sys_get_le24(br_discover_result[i].cod); + if ((cod & BT_COD_MAJOR_SVC_CLASS_LIMITED_DISCOVER) != 0) { + return true; + } + } + + return false; +} + +ZTEST(gap_central, test_02_gap_central_limited_discovery) +{ + bool found; + + found = peer_device_discovery(true); + + zassert_true(found, "Peer device not found"); + + if (!is_limited_inquiry()) { + /* There is a timing issue. Rediscovery to ensure limited bit is set. */ + found = peer_device_discovery(true); + + zassert_true(found, "Peer device not found"); + } + + zassert_true(is_limited_inquiry(), "Invalid COD (limited bit not set)"); + + k_sleep(K_SECONDS(CONFIG_BT_LIMITED_DISCOVERABLE_DURATION + 5)); + + found = peer_device_discovery(true); + if (found) { + zassert_true(!is_limited_inquiry(), "Invalid COD (limited bit is set)"); + } + + peer_device_connect(); +} + +static void *setup(void) +{ + int err; + + LOG_DBG("Initializing Bluetooth"); + + /* Initialize the Bluetooth Subsystem */ + err = bt_enable(NULL); + zassert_equal(err, 0, "Bluetooth init failed (err %d)", err); + + LOG_DBG("Bluetooth initialized"); + + err = bt_addr_from_str(CONFIG_TEST_GAP_PERIPHERAL_ADDRESS, &peer_addr); + zassert_equal(err, 0, "Invalid peer address (err %d)", err); + + LOG_DBG("Register discovery callback"); + + bt_br_discovery_cb_register(&br_discover); + + return NULL; +} + +static void teardown(void *f) +{ + int err; + + LOG_DBG("Disabling Bluetooth"); + + /* De-initialize the Bluetooth Subsystem */ + err = bt_disable(); + zassert_equal(err, 0, "Bluetooth de-init failed (err %d)", err); + + LOG_DBG("Bluetooth de-initialized"); +} + +static void before(void *f) +{ + atomic_clear_bit(test_flags, TEST_FLAG_DEVICE_FOUND); + atomic_clear_bit(test_flags, TEST_FLAG_CONN_CONNECTED); + atomic_clear_bit(test_flags, TEST_FLAG_CONN_DISCONNECTED); + + k_sem_reset(&br_discover_sem); +} + +static void after(void *f) +{ +} + +ZTEST_SUITE(gap_central, NULL, setup, before, after, teardown); diff --git a/tests/bluetooth/classic/sim/gap_discovery/src/test_peripheral.c b/tests/bluetooth/classic/sim/gap_discovery/src/test_peripheral.c new file mode 100644 index 0000000000000..c0ef878722d99 --- /dev/null +++ b/tests/bluetooth/classic/sim/gap_discovery/src/test_peripheral.c @@ -0,0 +1,141 @@ +/* + * Copyright 2025 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#define LOG_MODULE_NAME test_gap_discovery_peripheral +LOG_MODULE_REGISTER(LOG_MODULE_NAME, CONFIG_TEST_GAP_DISCOVERY_LOG_LEVEL); + +static K_SEM_DEFINE(br_discover_sem, 0, 1); + +static bt_addr_t peer_addr; +static ATOMIC_DEFINE(test_flags, 32); + +#define TEST_FLAG_CONN_CONNECTED 0 +#define TEST_FLAG_CONN_DISCONNECTED 1 + +static void br_connected(struct bt_conn *conn, uint8_t conn_err) +{ + LOG_DBG("connected: conn %p err 0x%02x", (void *)conn, conn_err); + if (conn_err == 0) { + k_sem_give(&br_discover_sem); + atomic_set_bit(test_flags, TEST_FLAG_CONN_CONNECTED); + } +} + +static void br_disconnected(struct bt_conn *conn, uint8_t reason) +{ + LOG_DBG("disconnected: conn %p reason 0x%02x", (void *)conn, reason); + k_sem_give(&br_discover_sem); + atomic_set_bit(test_flags, TEST_FLAG_CONN_DISCONNECTED); +} + +BT_CONN_CB_DEFINE(conn_callbacks) = { + .connected = br_connected, + .disconnected = br_disconnected, +}; + +ZTEST(gap_peripheral, test_01_gap_peripheral_general_discovery) +{ + int err; + + err = bt_br_set_connectable(true); + zassert_equal(err, 0, "Failed to set connectable (err %d)", err); + + err = bt_br_set_discoverable(true, false); + zassert_equal(err, 0, "Failed to set discoverable (err %d)", err); + + err = k_sem_take(&br_discover_sem, K_SECONDS(60)); + zassert_equal(err, 0, "Connection timeout (err %d)", err); + zassert_true(atomic_test_bit(test_flags, TEST_FLAG_CONN_CONNECTED), "Connection failed"); + + k_sem_reset(&br_discover_sem); + + err = k_sem_take(&br_discover_sem, K_SECONDS(30)); + zassert_equal(err, 0, "Disconnection timeout (err %d)", err); + zassert_true(atomic_test_bit(test_flags, TEST_FLAG_CONN_DISCONNECTED), + "Disconnection failed"); + + err = bt_br_set_discoverable(false, false); + zassert_equal(err, 0, "Failed to clear discoverable (err %d)", err); + + err = bt_br_set_connectable(false); + zassert_equal(err, 0, "Failed to clear connectable (err %d)", err); +} + +ZTEST(gap_peripheral, test_02_gap_peripheral_limited_discovery) +{ + int err; + + err = bt_br_set_connectable(true); + zassert_equal(err, 0, "Failed to set connectable (err %d)", err); + + err = bt_br_set_discoverable(true, true); + zassert_equal(err, 0, "Failed to set discoverable (err %d)", err); + + err = k_sem_take(&br_discover_sem, K_SECONDS(30 + CONFIG_BT_LIMITED_DISCOVERABLE_DURATION)); + zassert_equal(err, 0, "Connection timeout (err %d)", err); + zassert_true(atomic_test_bit(test_flags, TEST_FLAG_CONN_CONNECTED), "Connection failed"); + + k_sem_reset(&br_discover_sem); + + err = k_sem_take(&br_discover_sem, K_SECONDS(30)); + zassert_equal(err, 0, "Disconnection timeout (err %d)", err); + zassert_true(atomic_test_bit(test_flags, TEST_FLAG_CONN_DISCONNECTED), + "Disconnection failed"); + + err = bt_br_set_connectable(false); + zassert_equal(err, 0, "Failed to clear connectable (err %d)", err); +} + +static void *setup(void) +{ + int err; + + LOG_DBG("Initializing Bluetooth"); + + /* Initialize the Bluetooth Subsystem */ + err = bt_enable(NULL); + zassert_equal(err, 0, "Bluetooth init failed (err %d)", err); + + LOG_DBG("Bluetooth initialized"); + + err = bt_addr_from_str(CONFIG_TEST_GAP_CENTRAL_ADDRESS, &peer_addr); + zassert_equal(err, 0, "Invalid peer address (err %d)", err); + + return NULL; +} + +static void teardown(void *f) +{ + int err; + + LOG_DBG("Disabling Bluetooth"); + + /* De-initialize the Bluetooth Subsystem */ + err = bt_disable(); + zassert_equal(err, 0, "Bluetooth de-init failed (err %d)", err); + + LOG_DBG("Bluetooth de-initialized"); +} + +static void before(void *f) +{ + k_sem_reset(&br_discover_sem); + + atomic_clear_bit(test_flags, TEST_FLAG_CONN_CONNECTED); + atomic_clear_bit(test_flags, TEST_FLAG_CONN_DISCONNECTED); +} + +static void after(void *f) +{ +} + +ZTEST_SUITE(gap_peripheral, NULL, setup, before, after, teardown); diff --git a/tests/bluetooth/classic/sim/gap_discovery/testcase.yaml b/tests/bluetooth/classic/sim/gap_discovery/testcase.yaml new file mode 100644 index 0000000000000..1e70a796e8e35 --- /dev/null +++ b/tests/bluetooth/classic/sim/gap_discovery/testcase.yaml @@ -0,0 +1,11 @@ +tests: + bluetooth.classic.gap.discovery: + platform_allow: + - native_sim + integration_platforms: + - native_sim + tags: + - bluetooth + - gap + timeout: 120 + harness: bluetooth diff --git a/tests/bluetooth/classic/sim/gap_discovery/tests_scripts/gap_discovery_run.sh b/tests/bluetooth/classic/sim/gap_discovery/tests_scripts/gap_discovery_run.sh new file mode 100644 index 0000000000000..53f8559b53f68 --- /dev/null +++ b/tests/bluetooth/classic/sim/gap_discovery/tests_scripts/gap_discovery_run.sh @@ -0,0 +1,595 @@ +#!/usr/bin/env bash +# Copyright 2025 NXP +# SPDX-License-Identifier: Apache-2.0 + +# Smoketest for GAP Discovery + +set -euo pipefail + +# Create gap_discovery directory if it doesn't exist +mkdir -p ${ZEPHYR_BASE}/bt_classic_sim/gap_discovery + +# Redirect all output to log file while also displaying on terminal +exec > >(tee -a ${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/script_output.log) +exec 2>&1 + +# Color definitions +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +# Global variables +BUMBLE_CONTROLLER_PID="" +PERIPHERAL_PID="" +HCI_PORT_0="9000" +HCI_PORT_1="9001" +BD_ADDRESS_0="" +BD_ADDRESS_1="" + +# Check and kill existing controllers.py processes +check_and_kill_existing_controllers() { + log_info "Checking for existing controllers.py processes..." + + local controllers_script="${ZEPHYR_BASE}/tests/bluetooth/classic/sim/common/controllers.py" + + # Find all processes running controllers.py + local pids=$(pgrep -f "$controllers_script" 2>/dev/null || true) + + if [[ -z "$pids" ]]; then + log_info "No existing controllers.py processes found" + return 0 + fi + + log_warn "Found existing controllers.py processes: $pids" + + # Kill each process + for pid in $pids; do + log_info "Attempting to kill process $pid..." + + # Try graceful shutdown first + if kill -TERM "$pid" 2>/dev/null; then + log_info "Sent SIGTERM to process $pid" + + # Wait for graceful shutdown + local count=0 + while kill -0 "$pid" 2>/dev/null && [[ $count -lt 5 ]]; do + sleep 1 + ((count++)) + done + + # Force kill if still running + if kill -0 "$pid" 2>/dev/null; then + log_warn "Process $pid still running, force killing..." + kill -KILL "$pid" 2>/dev/null || true + sleep 1 + fi + fi + + # Check if process was killed + if kill -0 "$pid" 2>/dev/null; then + log_error "Failed to kill process $pid" + return 1 + else + log_info "Successfully killed process $pid" + fi + done + + log_info "All existing controllers.py processes have been terminated" + + # Wait a bit to ensure ports are released + sleep 2 + + return 0 +} + +# Start bumble controllers asynchronously +start_bumble_controllers() { + log_info "Starting bumble controllers asynchronously..." + + # Check if python is available + if ! command -v python &> /dev/null; then + log_error "python command not found" + return 1 + fi + + # Check if controllers.py exists + local controllers_script="${ZEPHYR_BASE}/tests/bluetooth/classic/sim/common/controllers.py" + if [[ ! -f "$controllers_script" ]]; then + log_error "controllers.py not found at $controllers_script" + return 1 + fi + + log_info "Starting bumble controllers on ports $HCI_PORT_0 and $HCI_PORT_1..." + + # Start bumble controllers asynchronously and redirect output to file + python "$controllers_script" \ + "tcp-server:_:$HCI_PORT_0" \ + "tcp-server:_:$HCI_PORT_1" \ + > ${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/bumble_controllers.log 2>&1 & + + BUMBLE_CONTROLLER_PID=$! + + log_info "Started bumble controllers with PID: $BUMBLE_CONTROLLER_PID" + + # Wait briefly to check if process started successfully + sleep 2 + + # Check if process is still running + if ! kill -0 "$BUMBLE_CONTROLLER_PID" 2>/dev/null; then + log_error "Bumble controllers process failed to start or exited immediately" + cat ${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/bumble_controllers.log + return 1 + fi + + log_info "Bumble controllers started successfully" + + # Wait for controllers to initialize + log_info "Waiting for bumble controllers to initialize..." + sleep 3 + + # Extract BD addresses from log file + extract_bd_addresses + + return 0 +} + +# Extract BD addresses from bumble controller log +extract_bd_addresses() { + log_info "Extracting BD addresses from bumble controller log..." + + local log_file="${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/bumble_controllers.log" + + if [[ ! -f "$log_file" ]]; then + log_error "Bumble controller log file not found" + return 1 + fi + + # Remove ANSI color codes using sed and extract BD addresses + # Use a more robust pattern to match BD addresses (XX:XX:XX:XX:XX:XX format) + BD_ADDRESS_0=$(grep "HCI0 BD Address" "$log_file" | sed -r 's/\x1B\[[0-9;]*[mK]//g' | \ + grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1) + BD_ADDRESS_1=$(grep "HCI1 BD Address" "$log_file" | sed -r 's/\x1B\[[0-9;]*[mK]//g' | \ + grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1) + + if [[ -z "$BD_ADDRESS_0" ]] || [[ -z "$BD_ADDRESS_1" ]]; then + log_error "Failed to extract BD addresses from log file" + log_info "Log file content:" + cat "$log_file" + log_info "Attempting alternative extraction method..." + + # Alternative method: use perl for better ANSI code removal + BD_ADDRESS_0=$(grep "HCI0 BD Address" "$log_file" | perl -pe 's/\e\[[0-9;]*m//g' | \ + grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1) + BD_ADDRESS_1=$(grep "HCI1 BD Address" "$log_file" | perl -pe 's/\e\[[0-9;]*m//g' | \ + grep -oE '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' | head -1) + + if [[ -z "$BD_ADDRESS_0" ]] || [[ -z "$BD_ADDRESS_1" ]]; then + log_error "Alternative extraction method also failed" + return 1 + fi + fi + + log_info "HCI0 BD Address: $BD_ADDRESS_0 (port $HCI_PORT_0)" + log_info "HCI1 BD Address: $BD_ADDRESS_1 (port $HCI_PORT_1)" + + return 0 +} + +# Kill bumble controllers process +kill_bumble_controllers() { + if [[ -n "$BUMBLE_CONTROLLER_PID" ]]; then + log_info "Stopping bumble controllers (PID: $BUMBLE_CONTROLLER_PID)..." + + # Check if process is still running + if ! kill -0 "$BUMBLE_CONTROLLER_PID" 2>/dev/null; then + log_info "Bumble controllers process already stopped" + BUMBLE_CONTROLLER_PID="" + return 0 + fi + + # Try graceful shutdown first + if kill -TERM "$BUMBLE_CONTROLLER_PID" 2>/dev/null; then + log_info "Sent SIGTERM to bumble controllers" + + # Wait for graceful shutdown + local count=0 + while kill -0 "$BUMBLE_CONTROLLER_PID" 2>/dev/null && [[ $count -lt 5 ]]; do + sleep 1 + ((count++)) + done + + # Force kill if still running + if kill -0 "$BUMBLE_CONTROLLER_PID" 2>/dev/null; then + log_warn "Bumble controllers still running, force killing..." + if kill -KILL "$BUMBLE_CONTROLLER_PID" 2>/dev/null; then + sleep 1 + fi + fi + fi + + # Final check + if kill -0 "$BUMBLE_CONTROLLER_PID" 2>/dev/null; then + log_warn "Bumble controllers process still running after kill attempts" + # Don't return error, just warn + else + log_info "Bumble controllers stopped successfully" + fi + + BUMBLE_CONTROLLER_PID="" + fi + + return 0 +} + +# Display controller information +display_controller_info() { + echo "" + echo "Bumble Controller Information:" + echo "==============================" + echo "HCI0: tcp-server:127.0.0.1:$HCI_PORT_0" + echo " BD Address: $BD_ADDRESS_0" + echo "" + echo "HCI1: tcp-server:127.0.0.1:$HCI_PORT_1" + echo " BD Address: $BD_ADDRESS_1" + echo "" +} + +# Build peer device +build_peer_device() { + log_info "Building peer device (peripheral)..." + + if [[ -z "$BD_ADDRESS_1" ]]; then + log_error "BD_ADDRESS_1 not available for peripheral build" + return 1 + fi + + local build_dir="${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/peripheral" + + log_info "Executing build command for peripheral device" + + # Execute the build command directly + if ! west build -b native_sim \ + ${ZEPHYR_BASE}/tests/bluetooth/classic/sim/gap_discovery \ + -d "$build_dir" \ + -DCONFIG_TEST_GAP_PERIPHERAL=y \ + -DCONFIG_TEST_GAP_CENTRAL_ADDRESS=\"${BD_ADDRESS_1}\"; then + log_error "Build failed for peer device" + return 1 + fi + + log_info "Build completed successfully" + + # Check if zephyr.exe was generated + local zephyr_exe="$build_dir/zephyr/zephyr.exe" + + log_info "Checking for generated executable: $zephyr_exe" + + if [[ ! -f "$zephyr_exe" ]]; then + log_error "zephyr.exe not found at $zephyr_exe" + return 1 + fi + + log_info "zephyr.exe found successfully at $zephyr_exe" + + # Optional: Check if the file is executable + if [[ ! -x "$zephyr_exe" ]]; then + log_warn "zephyr.exe exists but is not executable" + chmod +x "$zephyr_exe" + log_info "Made zephyr.exe executable" + fi + + log_info "Peer device build verification completed" + return 0 +} + +# Run peer device asynchronously +run_peer_device() { + log_info "Running peripheral device asynchronously..." + + local peripheral_exe="${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/peripheral/zephyr/zephyr.exe" + + # Check if peripheral executable exists + if [[ ! -f "$peripheral_exe" ]]; then + log_error "Peripheral executable not found: $peripheral_exe" + return 1 + fi + + log_info "Starting peripheral device asynchronously" + log_info "Using HCI transport: 127.0.0.1:$HCI_PORT_0" + log_info "BD Address: $BD_ADDRESS_0" + + # Execute the test command asynchronously + "$peripheral_exe" --bt-dev="127.0.0.1:$HCI_PORT_0" & + PERIPHERAL_PID=$! + + log_info "Peripheral device started with PID: $PERIPHERAL_PID" + + # Wait a moment to check if process started successfully + sleep 2 + + # Check if process is still running + if ! kill -0 "$PERIPHERAL_PID" 2>/dev/null; then + log_error "Peripheral device process failed to start or exited immediately" + PERIPHERAL_PID="" + return 1 + fi + + log_info "Peripheral device is running successfully" + return 0 +} + +# Kill peripheral device process +kill_peripheral_device() { + if [[ -n "$PERIPHERAL_PID" ]]; then + log_info "Stopping peripheral device (PID: $PERIPHERAL_PID)..." + + # Check if process is still running + if ! kill -0 "$PERIPHERAL_PID" 2>/dev/null; then + log_info "Peripheral device process already stopped" + PERIPHERAL_PID="" + return 0 + fi + + # Try graceful shutdown first + if kill -TERM "$PERIPHERAL_PID" 2>/dev/null; then + log_info "Sent SIGTERM to peripheral device" + + # Wait for graceful shutdown + local count=0 + while kill -0 "$PERIPHERAL_PID" 2>/dev/null && [[ $count -lt 10 ]]; do + sleep 1 + ((count++)) + done + + # Force kill if still running + if kill -0 "$PERIPHERAL_PID" 2>/dev/null; then + log_warn "Peripheral device still running, force killing..." + if kill -KILL "$PERIPHERAL_PID" 2>/dev/null; then + sleep 1 + fi + fi + fi + + # Final check + if kill -0 "$PERIPHERAL_PID" 2>/dev/null; then + log_warn "Peripheral device process still running after kill attempts" + # Don't return error, just warn + else + log_info "Peripheral device stopped successfully" + fi + + PERIPHERAL_PID="" + fi + + return 0 +} + +# Build central device +build_central_device() { + log_info "Building central device..." + + if [[ -z "$BD_ADDRESS_0" ]]; then + log_error "BD_ADDRESS_0 not available for central build" + return 1 + fi + + local central_dir="${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/central" + + log_info "Executing build command for central device" + log_info "Peripheral address configured as: $BD_ADDRESS_0" + + # Execute the build command directly + if ! west build -b native_sim \ + ${ZEPHYR_BASE}/tests/bluetooth/classic/sim/gap_discovery \ + -d "$central_dir" \ + -DCONFIG_TEST_GAP_CENTRAL=y \ + -DCONFIG_TEST_GAP_PERIPHERAL_ADDRESS=\"$BD_ADDRESS_0\"; then + log_error "Build failed for central device" + return 1 + fi + + log_info "Build completed successfully" + + # Check if zephyr.exe was generated + local zephyr_exe="$central_dir/zephyr/zephyr.exe" + + log_info "Checking for generated executable: $zephyr_exe" + + if [[ ! -f "$zephyr_exe" ]]; then + log_error "zephyr.exe not found at $zephyr_exe" + return 1 + fi + + log_info "zephyr.exe found successfully at $zephyr_exe" + + # Optional: Check if the file is executable + if [[ ! -x "$zephyr_exe" ]]; then + log_warn "zephyr.exe exists but is not executable" + chmod +x "$zephyr_exe" + log_info "Made zephyr.exe executable" + fi + + log_info "Central device build verification completed" + return 0 +} + +# Run central device +run_central_device() { + log_info "Running central device..." + + local central_exe="${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/central/zephyr/zephyr.exe" + + # Check if central executable exists + if [[ ! -f "$central_exe" ]]; then + log_error "Central executable not found: $central_exe" + return 1 + fi + + log_info "Executing central device test" + log_info "Using HCI transport: 127.0.0.1:$HCI_PORT_1" + log_info "BD Address: $BD_ADDRESS_1" + + # Execute the test command and capture exit code + set +e + "$central_exe" --bt-dev="127.0.0.1:$HCI_PORT_1" + local exit_code=$? + set -e + + if [[ $exit_code -ne 0 ]]; then + log_error "Central device execution failed with exit code: $exit_code" + return 1 + fi + + log_info "Central device execution completed successfully" + return 0 +} + +# Wait for peripheral device to complete +wait_peripheral_device() { + local timeout=${1:-30} # Default timeout is 30 seconds + + if [[ -z "$PERIPHERAL_PID" ]]; then + log_warn "No peripheral device PID to wait for" + return 0 + fi + + log_info "Waiting for peripheral device to complete (PID: $PERIPHERAL_PID, max ${timeout}s)..." + + local wait_count=0 + + while kill -0 "$PERIPHERAL_PID" 2>/dev/null && [[ $wait_count -lt $timeout ]]; do + sleep 1 + ((wait_count++)) + + # Log progress every 5 seconds + if [[ $((wait_count % 5)) -eq 0 ]]; then + log_info "Still waiting... ($wait_count/$timeout seconds)" + fi + done + + if kill -0 "$PERIPHERAL_PID" 2>/dev/null; then + log_warn "Peripheral device still running after ${timeout}s timeout" + return 1 + else + log_info "Peripheral device completed after ${wait_count}s" + PERIPHERAL_PID="" # Clear the PID since process has finished + return 0 + fi +} + +# Global variable to track test result +TEST_RESULT=0 + +# Cleanup function +cleanup() { + local exit_code=$? + + log_info "Cleaning up..." + + # Temporarily disable exit on error for cleanup + set +e + + # Kill peripheral device if running + kill_peripheral_device + + # Kill bumble controllers + kill_bumble_controllers + + # Re-enable exit on error + set -e + + # Determine final exit code + if [[ $TEST_RESULT -ne 0 ]]; then + log_error "Script completed with errors" + log_info "=========Sim controller log=========" + cat ${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/bumble_controllers.log + exit $TEST_RESULT + elif [[ $exit_code -ne 0 ]]; then + log_error "Script completed with errors (exit code: $exit_code)" + log_info "=========Sim controller log=========" + cat ${ZEPHYR_BASE}/bt_classic_sim/gap_discovery/bumble_controllers.log + exit $exit_code + else + log_info "Script completed successfully" + exit 0 + fi +} + +# Set cleanup on exit +trap cleanup EXIT + +# Main function +main() { + log_info "Starting GAP Discovery test with Bumble controllers..." + + # 0. Check and kill existing controllers.py processes + if ! check_and_kill_existing_controllers; then + log_error "Failed to clean up existing controllers.py processes" + TEST_RESULT=1 + return 1 + fi + + # 1. Start bumble controllers + if ! start_bumble_controllers; then + log_error "Failed to start bumble controllers" + TEST_RESULT=1 + return 1 + fi + + # 2. Display controller information + display_controller_info + + # 3. Build peer device + if ! build_peer_device; then + log_error "Failed to build peer device" + TEST_RESULT=1 + return 1 + fi + + # 4. Build central device + if ! build_central_device; then + log_error "Failed to build central device" + TEST_RESULT=1 + return 1 + fi + + # 5. Run peer device + if ! run_peer_device; then + log_error "Failed to run peer device" + TEST_RESULT=1 + return 1 + fi + + # 6. Run central device + if ! run_central_device; then + log_error "Failed to run central device" + TEST_RESULT=1 + return 1 + fi + + # 7. Wait for peripheral device to complete + if ! wait_peripheral_device 30; then + log_warn "Peripheral device did not complete within timeout" + fi + + log_info "PASSED" + TEST_RESULT=0 + return 0 +} + +# Run main function +main "$@" diff --git a/tests/bluetooth/classic/sim/tests.native_sim.txt b/tests/bluetooth/classic/sim/tests.native_sim.txt new file mode 100644 index 0000000000000..d4cea555930e5 --- /dev/null +++ b/tests/bluetooth/classic/sim/tests.native_sim.txt @@ -0,0 +1,17 @@ +# Copyright 2025 NXP +# SPDX-License-Identifier: Apache-2.0 +# +# Bluetooth Classic Simulation Test Directories +# Lines starting with # are comments +# Empty lines are ignored +# Paths can be relative (to this script's directory) or absolute + +# GAP Discovery tests +gap_discovery + +# Add more test directories here +# another_test +# some_other_test + +# You can also use absolute paths +# /absolute/path/to/test/directory