This is an automated email from the ASF dual-hosted git repository.

acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git


The following commit(s) were added to refs/heads/master by this push:
     new 59e92e968 testing/bluetooth: Add BSWorld integration tests and CI
59e92e968 is described below

commit 59e92e9687d29a0dea52accfd57acba31df09d7a
Author: Lingao Meng <[email protected]>
AuthorDate: Thu Oct 22 12:03:45 2026 +0800

    testing/bluetooth: Add BSWorld integration tests and CI
    
    Build-only checks miss runtime failures in Host initialization, timer
    delivery and Controller capability handling, as demonstrated by NuttX
    PR #20879. As AI-assisted code generation becomes more common, stricter
    code review, static checks and runtime regression testing are needed.
    
    Add tests for BTSak advertising/scanning and NimBLE discovery, connection,
    GATT discovery and subscription against Python BSControllers through
    BabbleSim. Reuse the stock bt, blecent and bleprph commands, with one
    firmware image per Host stack and roles selected through NSH.
    
    Provide configuration fragments, separate preparation/build scripts and
    local test instructions. Add a native Linux workflow that pins BSWorld,
    rebuilds the current NuttX/apps sources, runs both tests with two workers
    and uploads build logs, JUnit results, device logs and HCI/radio captures.
    Cache only simulation dependencies and support companion PR dependencies.
    
    Testing:
    
      Host: Ubuntu 22.04 x86_64, GCC 14.3.0, Python 3.12.3
      Board/configs: sim:bthcisock and sim:nimble with SIM_BSIM_TIME=y
      NuttX includes the fixes in apache/nuttx#20879.
    
      Both firmware builds and both integration tests passed. The workflow's
      build and pytest steps were also executed locally. HCI snoops, Ellisys
      traces and firmware configurations matched the previous validated run.
    
      ShellCheck, actionlint, Black, isort, Flake8, YAML validation and NuttX
      checkpatch passed. The GitHub-hosted workflow has not been run yet.
    
    Depends-On: https://github.com/apache/nuttx/pull/20879
    Assisted-by: OpenAI Codex
    Signed-off-by: Lingao Meng <[email protected]>
---
 .github/workflows/bluetooth.yml                  | 214 +++++++++++++++++++++++
 testing/bluetooth/bsworld/README.md              |  43 +++++
 testing/bluetooth/bsworld/build.sh               |  94 ++++++++++
 testing/bluetooth/bsworld/configs/bthcisock.conf |   7 +
 testing/bluetooth/bsworld/configs/nimble.conf    |  12 ++
 testing/bluetooth/bsworld/prepare.sh             |  88 ++++++++++
 testing/bluetooth/bsworld/pytest.ini             |   5 +
 testing/bluetooth/bsworld/test_bluetooth.py      |  98 +++++++++++
 8 files changed, 561 insertions(+)

diff --git a/.github/workflows/bluetooth.yml b/.github/workflows/bluetooth.yml
new file mode 100644
index 000000000..ca789faaa
--- /dev/null
+++ b/.github/workflows/bluetooth.yml
@@ -0,0 +1,214 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+---
+name: Bluetooth
+
+on:
+  pull_request:
+    paths-ignore:
+      - "**/*.md"
+      - "Documentation/**"
+      - "**/CODEOWNERS"
+  push:
+    branches:
+      - master
+      - "releases/*"
+  workflow_dispatch:
+    inputs:
+      companion_ref:
+        description: >-
+          Branch, tag, SHA or refs/pull/N/head in the companion repository
+        default: master
+        required: true
+        type: string
+
+permissions:
+  contents: read
+  pull-requests: read
+
+defaults:
+  run:
+    shell: bash
+
+concurrency:
+  group: bluetooth-${{ github.event.pull_request.number || github.ref }}
+  cancel-in-progress: true
+
+env:
+  BSWORLD_REVISION: bf94e2fcc0c876e244dafcea4fb0ea5ed3a1c157
+  NUTTX_REPOSITORY: apache/nuttx
+  APPS_REPOSITORY: ${{ github.repository }}
+  BUILD_JOBS: 4
+  PYTHONDONTWRITEBYTECODE: 1
+
+jobs:
+  bluetooth:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 90
+    steps:
+      - name: Checkout NuttX
+        uses: actions/checkout@v7
+        with:
+          repository: ${{ env.NUTTX_REPOSITORY }}
+          ref: >-
+            ${{ env.NUTTX_REPOSITORY == github.repository && github.sha
+              || inputs.companion_ref || github.base_ref
+              || (startsWith(github.ref, 'refs/heads/releases/')
+                && github.ref_name)
+              || 'master' }}
+          path: nuttx
+          persist-credentials: false
+
+      - name: Checkout applications
+        uses: actions/checkout@v7
+        with:
+          repository: ${{ env.APPS_REPOSITORY }}
+          ref: >-
+            ${{ env.APPS_REPOSITORY == github.repository && github.sha
+              || inputs.companion_ref || github.base_ref
+              || (startsWith(github.ref, 'refs/heads/releases/')
+                && github.ref_name)
+              || 'master' }}
+          path: apps
+          persist-credentials: false
+
+      - name: Apply Depends-On changes
+        if: github.event_name == 'pull_request' && github.base_ref == 'master'
+        env:
+          GH_TOKEN: ${{ github.token }}
+          PR_BODY: ${{ github.event.pull_request.body }}
+          PR_NUMBER: ${{ github.event.pull_request.number }}
+        shell: bash
+        run: |
+          # Reuse the parser used by the main build workflow.
+          if body=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
+              --jq '.body // ""'); then
+            export PR_BODY="$body"
+          else
+            echo "::warning::Could not re-read the PR description;" \
+              "using the event payload."
+          fi
+          python3 nuttx/.github/scripts/depends_on.py --print-state \
+            > "$RUNNER_TEMP/bluetooth-dependencies"
+          read -r state < "$RUNNER_TEMP/bluetooth-dependencies"
+          if [ "$state" = invalid ]; then
+            echo "::warning::Invalid Depends-On declaration;" \
+              "using the normal source selection."
+            exit 0
+          fi
+          git config --global user.name github-actions
+          git config --global user.email [email protected]
+          while read -r dependency; do
+            repo=${dependency%/pull/*}
+            number=${dependency##*/}
+            case "$repo" in
+              apache/nuttx) tree=nuttx ;;
+              apache/nuttx-apps) tree=apps ;;
+              *) exit 1 ;;
+            esac
+            query='[.state, .merged, (.merge_commit_sha // "-")] | @tsv'
+            if info=$(gh api "repos/$repo/pulls/$number" --jq "$query"); then
+              read -r status merged merge_sha <<< "$info"
+            else
+              echo "::warning::Could not read the state of $dependency;" \
+                "applying it."
+              status=unknown
+              merged=false
+              merge_sha=-
+            fi
+            if [ "$status" = closed ] && [ "$merged" != true ]; then
+              echo "::error::Dependency $dependency is closed without merging"
+              exit 1
+            fi
+            if [ -f "$tree/.git/shallow" ]; then
+              git -C "$tree" fetch --unshallow origin
+            fi
+            git -C "$tree" fetch "https://github.com/$repo.git"; \
+              "pull/$number/head"
+            dependency_sha=$(git -C "$tree" rev-parse FETCH_HEAD)
+            if [ "$merged" = true ] &&
+                git -C "$tree" merge-base --is-ancestor "$merge_sha" HEAD; then
+              continue
+            fi
+            git -C "$tree" merge-base HEAD "$dependency_sha" > /dev/null
+            if ! commit_list=$(git -C "$tree" rev-list \
+                --reverse "HEAD..$dependency_sha"); then
+              echo "::error::Could not list commits for $dependency"
+              exit 1
+            fi
+            if [ -n "$commit_list" ]; then
+              mapfile -t commits <<< "$commit_list"
+              git -C "$tree" cherry-pick "${commits[@]}"
+            fi
+          done < <(tail -n +2 "$RUNNER_TEMP/bluetooth-dependencies")
+
+      - name: Checkout the validated BSWorld revision
+        uses: actions/checkout@v7
+        with:
+          repository: XiaoMi/bsworld
+          ref: ${{ env.BSWORLD_REVISION }}
+          path: bsworld
+          persist-credentials: false
+
+      - name: Install native build prerequisites
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y --no-install-recommends \
+            build-essential gcc-multilib g++-multilib lib32z1-dev \
+            python3.12-dev python3.12-venv repo rsync \
+            autoconf automake bison flex gettext texinfo gperf \
+            libtool pkg-config \
+            libncurses-dev xxd genromfs kconfig-frontends \
+            wget unzip xz-utils
+
+      - name: Cache the fixed simulation infrastructure
+        uses: actions/cache@v6
+        with:
+          path: .bluetooth-ci/dependencies
+          key: >-
+            bluetooth-v1-ubuntu24.04-python3.12-${{ runner.arch }}-${{
+            env.BSWORLD_REVISION }}-${{
+            hashFiles('apps/testing/bluetooth/bsworld/prepare.sh') }}
+
+      - name: Prepare BabbleSim and the Python Controller
+        run: |
+          mkdir -p bluetooth-results
+          bash apps/testing/bluetooth/bsworld/prepare.sh \
+            "$PWD/bsworld" "$PWD/.bluetooth-ci/dependencies" \
+            2>&1 | tee bluetooth-results/prepare.log
+
+      - name: Build the current NuttX and applications sources
+        run: |
+          bash apps/testing/bluetooth/bsworld/build.sh \
+            "$PWD/nuttx" "$PWD/apps" "$PWD/.bluetooth-ci/dependencies" \
+            "$RUNNER_TEMP/bluetooth-build" "$PWD/bluetooth-results"
+
+      - name: Run Bluetooth integration tests
+        run: |
+          source .bluetooth-ci/dependencies/env.sh
+          firmware="$PWD/bluetooth-results/firmware"
+          export BSWORLD_NUTTX_BTSAK_BIN="$firmware/btsak"
+          export BSWORLD_NUTTX_NIMBLE_BIN="$firmware/nimble"
+          python -m pytest -c apps/testing/bluetooth/bsworld/pytest.ini \
+            apps/testing/bluetooth/bsworld -v -n 2 -p no:cacheprovider \
+            --out-dir "$PWD/bluetooth-results/runtime" \
+            --junitxml="$PWD/bluetooth-results/bluetooth.xml" \
+            2>&1 | tee bluetooth-results/bluetooth.log
+
+      - name: Upload Bluetooth results
+        if: always()
+        uses: actions/[email protected]
+        with:
+          name: bluetooth-results
+          path: bluetooth-results/
diff --git a/testing/bluetooth/bsworld/README.md 
b/testing/bluetooth/bsworld/README.md
new file mode 100644
index 000000000..28c60baf2
--- /dev/null
+++ b/testing/bluetooth/bsworld/README.md
@@ -0,0 +1,43 @@
+# Bluetooth integration tests
+
+Run NuttX Hosts against Python BSControllers through BabbleSim.
+
+| Firmware | Commands | Checks |
+|---|---|---|
+| BTSak | `bt` | Discover the expected advertiser |
+| NimBLE | `blecent`, `bleprph` | Discovery, connection, GATT discovery, 
subscription |
+
+Each firmware runs as two processes with roles selected through NSH commands.
+Configuration overrides: `configs/bthcisock.conf` and `configs/nimble.conf`.
+
+## Run on Linux
+
+Requires Linux/x86_64 and Python 3.12. Install the packages listed in
+[bluetooth.yml](../../../.github/workflows/bluetooth.yml), and check out 
BSWorld
+at its `BSWORLD_REVISION`.
+
+From a directory containing sibling `nuttx`, `apps` and `bsworld` checkouts:
+
+```sh
+bash apps/testing/bluetooth/bsworld/prepare.sh \
+  "$PWD/bsworld" "$PWD/.bluetooth-ci/dependencies"
+bash apps/testing/bluetooth/bsworld/build.sh \
+  "$PWD/nuttx" "$PWD/apps" "$PWD/.bluetooth-ci/dependencies" \
+  "$PWD/.bluetooth-ci/build" "$PWD/bluetooth-results"
+
+source .bluetooth-ci/dependencies/env.sh
+export BSWORLD_NUTTX_BTSAK_BIN="$PWD/bluetooth-results/firmware/btsak"
+export BSWORLD_NUTTX_NIMBLE_BIN="$PWD/bluetooth-results/firmware/nimble"
+python -m pytest -c apps/testing/bluetooth/bsworld/pytest.ini \
+  apps/testing/bluetooth/bsworld -v -n 2 -p no:cacheprovider \
+  --out-dir "$PWD/bluetooth-results/runtime" \
+  --junitxml="$PWD/bluetooth-results/bluetooth.xml"
+```
+
+`prepare.sh` installs dependencies. `build.sh` builds both images in source
+copies. Set `BUILD_JOBS` to limit compilation parallelism.
+To rerun tests, execute only the environment setup and pytest commands.
+Use a new dependency directory when changing the BSWorld revision.
+
+Results in `bluetooth-results/`: firmware/configs, build logs, JUnit report,
+device logs, HCI snoops and Ellisys traces.
diff --git a/testing/bluetooth/bsworld/build.sh 
b/testing/bluetooth/bsworld/build.sh
new file mode 100755
index 000000000..9bcc1c79d
--- /dev/null
+++ b/testing/bluetooth/bsworld/build.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+############################################################################
+# apps/testing/bluetooth/bsworld/build.sh
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+############################################################################
+
+set -euo pipefail
+
+usage()
+{
+  cat <<'EOF'
+Usage: build.sh NUTTX_DIR APPS_DIR DEPENDENCY_DIR WORK_DIR RESULT_DIR
+
+Build BTSak and NimBLE firmware in separate source copies, preserving the
+original configurations and build outputs. Run prepare.sh first.
+BUILD_JOBS defaults to nproc. Run pytest separately as described in README.md.
+EOF
+}
+
+if [ "${1:-}" = -h ] || [ "${1:-}" = --help ]; then
+  usage
+  exit 0
+fi
+if [ "$#" -ne 5 ]; then
+  usage
+  exit 2
+fi
+
+copy_source()
+{
+  local source_dir=$1 target_dir=$2
+  mkdir -p "$target_dir"
+  # Do not copy old configurations, objects or downloaded package trees.
+  git -C "$source_dir" ls-files --cached --others --exclude-standard -z |
+    rsync -a --from0 --files-from=- "$source_dir/" "$target_dir/"
+}
+
+nuttx=$(realpath "$1")
+apps=$(realpath "$2")
+deps=$(realpath "$3")
+mkdir -p "$4" "$5"
+work=$(realpath "$4")
+results=$(realpath "$5")
+# shellcheck disable=SC1091
+source "$deps/env.sh"
+export PYTHONDONTWRITEBYTECODE=1
+test -x "$BSIM_OUT_PATH/bin/bs_2G4_phy_v1"
+mkdir -p "$results/firmware"
+
+{
+  printf 'NuttX: '; git -C "$nuttx" rev-parse HEAD
+  printf 'apps: '; git -C "$apps" rev-parse HEAD
+  printf 'BSWorld: '; cat "$deps/bsworld-revision"
+  gcc --version
+  python --version
+  git -C "$nuttx" diff HEAD
+  git -C "$apps" diff HEAD
+} > "$results/environment.txt"
+cp "$deps/python-packages.txt" "$deps/babblesim-manifest.xml" "$results/"
+
+for config in bthcisock nimble; do
+  build=$(mktemp -d "$work/$config.XXXXXX")
+  copy_source "$nuttx" "$build/nuttx"
+  copy_source "$apps" "$build/apps"
+  (
+    cd "$build/nuttx"
+    ./tools/configure.sh -l -a ../apps "sim:$config"
+    # Kconfig applies the last assignment when loading a fragment.
+    cat "$build/apps/testing/bluetooth/bsworld/configs/$config.conf" >> .config
+    make olddefconfig
+    make -j"${BUILD_JOBS:-$(nproc)}"
+  ) 2>&1 | tee "$results/build-$config.log"
+  binary_name=$config
+  if [ "$config" = bthcisock ]; then binary_name=btsak; fi
+  cp "$build/nuttx/nuttx" "$results/firmware/$binary_name"
+  cp "$build/nuttx/.config" "$results/firmware/$binary_name.config"
+done
diff --git a/testing/bluetooth/bsworld/configs/bthcisock.conf 
b/testing/bluetooth/bsworld/configs/bthcisock.conf
new file mode 100644
index 000000000..656e6b48b
--- /dev/null
+++ b/testing/bluetooth/bsworld/configs/bthcisock.conf
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: Apache-2.0
+# Overrides for the sim:bthcisock advertising/scanning test.
+
+CONFIG_SIM_BSIM_TIME=y
+CONFIG_BTSAK=y
+# CONFIG_BLUETOOTH_SMP_SELFTEST is not set
+# CONFIG_EXAMPLES_HELLO is not set
diff --git a/testing/bluetooth/bsworld/configs/nimble.conf 
b/testing/bluetooth/bsworld/configs/nimble.conf
new file mode 100644
index 000000000..2084872d8
--- /dev/null
+++ b/testing/bluetooth/bsworld/configs/nimble.conf
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: Apache-2.0
+# Overrides for the sim:nimble Central/Peripheral test.
+
+CONFIG_SIM_BSIM_TIME=y
+CONFIG_EXAMPLES_NIMBLE_BLECENT=y
+CONFIG_EXAMPLES_NIMBLE_BLEPRPH=y
+CONFIG_NIMBLE_ROLE_CENTRAL=y
+CONFIG_NIMBLE_ROLE_OBSERVER=y
+CONFIG_NIMBLE_ROLE_PERIPHERAL=y
+# CONFIG_EXAMPLES_HELLO is not set
+# CONFIG_EXAMPLES_NIMBLE is not set
+# CONFIG_NIMBLE_PORTING_EXAMPLE is not set
diff --git a/testing/bluetooth/bsworld/prepare.sh 
b/testing/bluetooth/bsworld/prepare.sh
new file mode 100755
index 000000000..6ceca6203
--- /dev/null
+++ b/testing/bluetooth/bsworld/prepare.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+############################################################################
+# apps/testing/bluetooth/bsworld/prepare.sh
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+############################################################################
+
+set -euo pipefail
+
+usage()
+{
+  cat <<'EOF'
+Usage: prepare.sh BSWORLD_DIR DEPENDENCY_DIR
+
+Prepare BSWorld, its Python Controller and BabbleSim on Linux/x86_64.
+BSWorld must be checked out at the revision pinned in bluetooth.yml.
+BUILD_JOBS defaults to nproc. See README.md for build prerequisites.
+EOF
+}
+
+if [ "${1:-}" = -h ] || [ "${1:-}" = --help ]; then
+  usage
+  exit 0
+fi
+if [ "$#" -ne 2 ]; then
+  usage
+  exit 2
+fi
+
+bsworld=$(realpath "$1")
+mkdir -p "$2"
+deps=$(realpath "$2")
+revision=$(git -C "$bsworld" rev-parse HEAD)
+
+if [ -f "$deps/bsworld-revision" ]; then
+  if [ "$(cat "$deps/bsworld-revision")" != "$revision" ]; then
+    echo "BSWorld revision changed; use a new dependency directory." >&2
+    exit 1
+  fi
+fi
+
+python3.12 -m venv "$deps/venv"
+# shellcheck disable=SC1091
+source "$deps/venv/bin/activate"
+python -m pip install -e "$bsworld" -e "$bsworld/components/bscontroller" 
kconfiglib
+
+export BSIM_OUT_PATH="$deps/babblesim/build"
+export BSIM_COMPONENTS_PATH="$deps/babblesim/components"
+export BSIM_BUILD_FAIL_ASAP=1
+mkdir -p "$deps/babblesim"
+(
+  cd "$deps/babblesim"
+  repo init --standalone-manifest --depth=1 \
+    -u "file://$bsworld/components/babblesim.xml"
+  repo sync -c --no-tags
+  make -j"${BUILD_JOBS:-$(nproc)}" everything
+)
+
+test -x "$BSIM_OUT_PATH/bin/bs_2G4_phy_v1"
+python -c 'from bsworld.devices import BSController'
+printf '%s\n' "$revision" > "$deps/bsworld-revision"
+python -m pip freeze > "$deps/python-packages.txt"
+(
+  cd "$deps/babblesim"
+  repo manifest -r
+) > "$deps/babblesim-manifest.xml"
+{
+  printf 'source %q\n' "$deps/venv/bin/activate"
+  printf 'export BSWORLD_ROOT=%q\n' "$bsworld"
+  printf 'export BSIM_OUT_PATH=%q\n' "$BSIM_OUT_PATH"
+  printf 'export BSIM_COMPONENTS_PATH=%q\n' "$BSIM_COMPONENTS_PATH"
+} > "$deps/env.sh"
diff --git a/testing/bluetooth/bsworld/pytest.ini 
b/testing/bluetooth/bsworld/pytest.ini
new file mode 100644
index 000000000..6def9e967
--- /dev/null
+++ b/testing/bluetooth/bsworld/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+asyncio_mode = auto
+asyncio_default_fixture_loop_scope = function
+required_plugins = bsworld pytest-asyncio pytest-xdist
+addopts = -ra
diff --git a/testing/bluetooth/bsworld/test_bluetooth.py 
b/testing/bluetooth/bsworld/test_bluetooth.py
new file mode 100644
index 000000000..4d6a24f62
--- /dev/null
+++ b/testing/bluetooth/bsworld/test_bluetooth.py
@@ -0,0 +1,98 @@
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.  The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Exercise NuttX Bluetooth Hosts against a simulated radio environment."""
+
+import os
+import re
+from pathlib import Path
+
+import pytest
+from bsworld.devices import BSController, ProcessInstance
+
+
[email protected]
+def host(topo):
+    """Attach a newly built NuttX simulator to its own Python Controller."""
+
+    def load(binary_env, label, address):
+        value = os.environ.get(binary_env)
+        if not value:
+            pytest.fail(f"{binary_env} must identify the NuttX binary under 
test")
+        binary = Path(value).resolve()
+        if not binary.is_file() or not os.access(binary, os.X_OK):
+            pytest.fail(f"NuttX binary is missing or not executable: {binary}")
+
+        controller = topo.add_controller(
+            controller_type=BSController, public_address=address
+        )
+
+        def command():
+            return [
+                binary,
+                f"--sim-bsim-sid={controller.sim_id}",
+                f"--sim-bsim-pid={controller.phy_id}",
+                f"--sim-bsim-dev={controller.application_dev_id}",
+                f"--bt-dev={controller.hci_sock}",
+            ]
+
+        return topo.add_process(
+            ProcessInstance(
+                command=command,
+                controller=controller,
+                label=label,
+                shell=True,
+                log_subdir=label,
+            )
+        )
+
+    return load
+
+
+async def test_btsak_discovers_advertiser(topo, host):
+    """Use BTSak commands to advertise and discover a specific peer."""
+    address = "C0:FF:EE:10:00:01"
+    advertiser = host("BSWORLD_NUTTX_BTSAK_BIN", "advertiser", address)
+    scanner = host("BSWORLD_NUTTX_BTSAK_BIN", "scanner", "C0:FF:EE:10:00:02")
+    await topo.start()
+    await advertiser.shell.run_many(("ifup bnep0", "bt bnep0 advertise start"))
+    commands = ["ifup bnep0", "bt bnep0 scan start"]
+    for _ in range(20):
+        commands.extend(("sleep 1", "bt bnep0 scan get"))
+    await scanner.shell.run_many(commands)
+    await scanner.wait_log(rf"(?i)addr:\s+{re.escape(address)}\s+type: 0$", 
timeout=30)
+
+
+async def test_nimble_connects_and_subscribes(topo, host):
+    """Exercise the stock blecent/bleprph flow used by BSWorld's NuttX 
tests."""
+    central = host("BSWORLD_NUTTX_NIMBLE_BIN", "central", "C0:FF:EE:20:01:01")
+    peripheral = host("BSWORLD_NUTTX_NIMBLE_BIN", "peripheral", 
"C0:FF:EE:20:00:01")
+    await topo.start()
+    await central.shell.run_many(("ifup bnep0", "blecent"))
+    await peripheral.shell.run_many(("ifup bnep0", "bleprph"))
+    await central.wait_log(r"name\(complete\)=NuttX NimBLE PRPH", timeout=30)
+    await central.wait_log(r"Connection established handle=\d+", timeout=30)
+    await peripheral.wait_log(
+        r"connection established; status=0 handle=\d+", timeout=30
+    )
+    await central.wait_log(
+        r"Service discovery complete; status=0 conn_handle=\d+", timeout=30
+    )
+    await central.wait_log(
+        r"Subscribe complete; status=0 conn_handle=\d+ attr_handle=\d+",
+        timeout=30,
+    )

Reply via email to