diff --git a/.github/workflows/treadmill-ci-ble-test.yml b/.github/workflows/treadmill-ci-ble-test.yml new file mode 100644 index 0000000..0b2884f --- /dev/null +++ b/.github/workflows/treadmill-ci-ble-test.yml @@ -0,0 +1,73 @@ +# Licensed under the Apache License, Version 2.0 or the MIT License. +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright Tock Contributors 2024. +# This workflow contains Treadmill-based hardware CI for nightly BLE testing. +# +# Treadmill is a distributed hardware testbed developed within the Tock OS +# project. For more information on Treadmill, have a look at its documentation +# [1] or repository [2]. +# +# This workflow specifically targets BLE advertising and scanning tests on multiple +# boards attached to a specific supervisor. +# +# [1]: https://book.treadmill.ci/ +# [2]: https://github.com/treadmill-tb/treadmill +# [3]: https://book.treadmill.ci/user-guide/github-actions-integration.html +# TEST +name: treadmill-ci-ble-test +env: + TERM: xterm # Makes tput work in actions output +# Controls when the action will run. +on: + # Manual trigger + workflow_dispatch: + inputs: + tock-kernel-ref: + description: 'Ref (revision/branch/tag) of the upstream Tock repo to test' + required: true + default: 'master' + libtock-c-ref: + description: 'Ref (revision/branch/tag) of the upstream libtock-c repo to test' + required: true + default: 'master' + # Add push trigger for your branch + push: + branches: + - dev/mult-board-treadmill +permissions: + contents: read +jobs: + prepare-ble-test: + runs-on: ubuntu-latest + outputs: + # This is a fixed list containing just the BLE advertising and scanning test + hwci-tests-json: ${{ steps.prepare-test.outputs.hwci-tests-json }} + steps: + - name: Prepare BLE test + id: prepare-test + run: | + # Instead of analyzing changes, we specifically select the BLE test + echo 'hwci-tests-json=["tests/ble_advertising_scanning_test.py"]' >> "$GITHUB_OUTPUT" + echo "Selected test: tests/ble_advertising_scanning_test.py" + run-treadmill-ci: + needs: [prepare-ble-test] + uses: ./.github/workflows/treadmill-ci.yml + with: + # Only run on a specific repository + repository-filter: 'tock/tock-hardware-ci' + # Provide access to the required Treadmill secrets + job-environment: 'treadmill-ci' + # This workflow tests the tock-hardware-ci scripts itself, so take the + # current GITHUB_SHA: + tock-hardware-ci-ref: ${{ github.sha }} + # Use the provided upstream Tock kernel / userspace components: + tock-kernel-ref: ${{ inputs.tock-kernel-ref }} + libtock-c-ref: ${{ inputs.libtock-c-ref }} + # Pass our fixed test JSON + tests-json: ${{ needs.prepare-ble-test.outputs.hwci-tests-json }} + # Specify that this is for the BLE test which needs multiple boards + multi-board: 'true' + supervisor-id: 'fb1384d5-e1a5-469c-beb4-0d4d215c9793' + board-descriptors: >- + board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050202501.yml board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050244773.yml + secrets: inherit diff --git a/.github/workflows/treadmill-ci.yml b/.github/workflows/treadmill-ci.yml index 3a11331..a652bd2 100644 --- a/.github/workflows/treadmill-ci.yml +++ b/.github/workflows/treadmill-ci.yml @@ -1,7 +1,6 @@ # Licensed under the Apache License, Version 2.0 or the MIT License. # SPDX-License-Identifier: Apache-2.0 OR MIT # Copyright Tock Contributors 2024. - # This workflow contains all Treadmill-based hardware CI jobs. # # Treadmill is a distributed hardware testbed developed within the Tock OS @@ -17,12 +16,9 @@ # [2]: https://github.com/treadmill-tb/treadmill # [3]: https://book.treadmill.ci/user-guide/github-actions-integration.html # - name: treadmill-ci - env: TERM: xterm # Makes tput work in actions output - on: workflow_call: inputs: @@ -45,23 +41,30 @@ on: required: false type: string default: '["tests/c_hello.py"]' # Default to single test for backward compatibility - + # New inputs for multi-board tests + multi-board: + required: false + type: string + default: 'false' + supervisor-id: + required: false + type: string + default: '' + board-descriptors: + required: false + type: string + default: '' jobs: test-prepare: runs-on: ubuntu-latest - # Do not run job on forks, as they will not have the correct environment set up if: github.repository == inputs.repository-filter - environment: ${{ inputs.job-environment }} - outputs: tml-job-ids: ${{ steps.treadmill-job-launch.outputs.tml-job-ids }} tml-jobs: ${{ steps.treadmill-job-launch.outputs.tml-jobs }} - steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 - - name: Checkout Treadmill repository uses: actions/checkout@v4 with: @@ -69,52 +72,47 @@ jobs: # treadmill-tb/treadmill main as of Oct 1, 2024, 3:05 PM EDT ref: 'c82f4d7ebddd17f8275ba52139e64e04623f30cb' path: treadmill - - name: Cache Treadmill CLI compilation artifacts id: cache-tml-cli uses: actions/cache@v4 with: path: treadmill/target key: ${{ runner.os }}-tml-cli - - name: Compile the Treadmill CLI binary run: | pushd treadmill cargo build --package tml-cli popd echo "$PWD/treadmill/target/debug" >> "$GITHUB_PATH" - - name: Generate a token to register new just-in-time runners id: generate-token uses: actions/create-github-app-token@v1 with: app-id: ${{ vars.TREADMILL_GH_APP_CLIENT_ID }} private-key: ${{ secrets.TREADMILL_GH_APP_PRIVATE_KEY }} - - name: Create GitHub just-in-time runners and enqueue Treadmill jobs id: treadmill-job-launch env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} TML_API_TOKEN: ${{ secrets.TREADMILL_API_TOKEN }} - # Currently, all tests run only on hosts attached to an nRF52840DK DUT_BOARD: nrf52840dk - # A Raspberry Pi OS netboot (NBD) image with a GitHub Actions # self-hosted runner pre-configured. # # For the available images see # https://book.treadmill.ci/treadmillci-deployment/images.html IMAGE_ID: f94b8f8edd54321e6370d898f87ccbd2659a67ed0300fda2adc8099cdd157790 - # Limit the supervisors to hosts that are compatible with this # image. This is a hack until we introduce "image sets" which define # multiple images for various supervisor hosts, but otherwise behave # identically: HOST_TYPE: nbd-netboot HOST_ARCH: arm64 - TESTS_JSON: ${{ inputs.tests-json }} + # Support for specific supervisor targeting for multi-board tests + MULTI_BOARD: ${{ inputs.multi-board }} + SUPERVISOR_ID: ${{ inputs.supervisor-id }} run: | # When we eventually launch tests on multiple hardware platforms in # parallel, we need to supply different SUB_TEST_IDs here: @@ -152,11 +150,22 @@ jobs: }" echo "Enqueueing treadmill job:" - TML_JOB_ID_JSON="$(tml job enqueue \ - "$IMAGE_ID" \ - --tag-config "board:$DUT_BOARD;host-type:$HOST_TYPE;host-arch:$HOST_ARCH" \ - --parameters "$TML_JOB_PARAMETERS" \ - )" + + # If this is a multi-board test targeting a specific supervisor + if [ "$MULTI_BOARD" = "true" ] && [ -n "$SUPERVISOR_ID" ]; then + TML_JOB_ID_JSON="$(tml job enqueue \ + "$IMAGE_ID" \ + --tag-config "supervisor:$SUPERVISOR_ID;host-type:$HOST_TYPE;host-arch:$HOST_ARCH" \ + --parameters "$TML_JOB_PARAMETERS" \ + )" + else + # Default job enqueue for single board tests + TML_JOB_ID_JSON="$(tml job enqueue \ + "$IMAGE_ID" \ + --tag-config "board:$DUT_BOARD;host-type:$HOST_TYPE;host-arch:$HOST_ARCH" \ + --parameters "$TML_JOB_PARAMETERS" \ + )" + fi TML_JOB_ID="$(echo "$TML_JOB_ID_JSON" | jq -r .job_id)" echo "Enqueued Treadmill job with ID $TML_JOB_ID" @@ -179,9 +188,12 @@ jobs: |------|-------|-----| GITHUB_STEP_SUMMARY echo "$TESTS_JSON" | jq -r -c '.[]' | while read TEST; do - echo "| \`$TEST\` | \`$DUT_BOARD\` | [\`$TML_JOB_ID\`](#tml-job-summary-$TML_JOB_ID) |" >>"$GITHUB_STEP_SUMMARY" + BOARD_INFO="$DUT_BOARD" + if [ "$MULTI_BOARD" = "true" ]; then + BOARD_INFO="Multiple boards on supervisor $SUPERVISOR_ID" + fi + echo "| \`$TEST\` | \`$BOARD_INFO\` | [\`$TML_JOB_ID\`](#tml-job-summary-$TML_JOB_ID) |" >>"$GITHUB_STEP_SUMMARY" done - test-execute: needs: test-prepare strategy: @@ -192,7 +204,7 @@ jobs: - name: Print Treadmill Job Context and Debug Information run: | echo "Treadmill job id: ${{ matrix.tml-job-id }}" - echo "GitHub Actions Runner ID: ${{ fromJSON(needs.test-prepare.outputs.tml-jobs)[matrix.tml-job-id] }}" + echo "GitHub Actions Runner ID: ${{ fromJSON(needs.test-prepare.outputs.tml-jobs)[matrix.tml-job-id].runner-id }}" echo "Host ID: $(cat /run/tml/host-id)" echo "===== Parameters: =====" ls /run/tml/parameters @@ -207,16 +219,13 @@ jobs: ls -lh /dev/ttyACM* 2>/dev/null || true ls -lh /dev/ttyUSB* 2>/dev/null || true ls -lh /dev/bus/usb/*/* 2>/dev/null || true - - name: Disable wget progress output run: | echo "verbose = off" >> $HOME/.wgetrc - - uses: actions-rust-lang/setup-rust-toolchain@v1 with: # Avoid overwriting the RUSTFLAGS environment variable rustflags: '' - # This is required for the actions/checkout steps to perform a # proper git clone that also supports checking out submodules: - name: Install git @@ -228,20 +237,17 @@ jobs: # part out. sudo DEBIAN_FRONTEND=noninteractive apt update || true sudo DEBIAN_FRONTEND=noninteractive apt install -y git - - name: Checkout the Tock Hardware CI scripts uses: actions/checkout@v4 with: repository: tock/tock-hardware-ci ref: ${{ inputs.tock-hardware-ci-ref }} - - name: Checkout the Tock kernel repository uses: actions/checkout@v4 with: path: hwci/repos/tock repository: tock/tock ref: ${{ inputs.tock-kernel-ref }} - - name: Checkout the libtock-c repository uses: actions/checkout@v4 with: @@ -251,24 +257,28 @@ jobs: fetch-depth: 0 submodules: false persist-credentials: true - - name: Run setup script run: | cd ./hwci/ ./setup.sh - - name: Run tests env: JSON_TEST_ARRAY: ${{ toJSON(fromJSON(needs.test-prepare.outputs.tml-jobs)[matrix.tml-job-id].tests) }} + MULTI_BOARD: ${{ inputs.multi-board }} + SUPERVISOR_ID: ${{ inputs.supervisor-id }} + BOARD_DESCRIPTORS: ${{ inputs.board-descriptors }} run: | + # MODIFY BEFORE merge vvvvv cd ./hwci + ls source ./.venv/bin/activate + cd .. STEP_FAIL=0 # Generate a summary of all the tests executed: cat <>"$GITHUB_STEP_SUMMARY" - ### Tests executed on board \`nrf52840dk\`, job ID ${{ matrix.tml-job-id }} + ### Tests executed on job ID ${{ matrix.tml-job-id }} | Result | Test | |--------|------| @@ -293,7 +303,27 @@ jobs: echo "===== RUNNING TEST $TEST =====" FAIL=0 set -o pipefail - python3 core/main.py --board boards/nrf52dk.py --test "$TEST" 2>&1 | tee ./job-output.txt || FAIL=1 + + # Check if this is a multi-board test with specific board descriptors + if [ "$MULTI_BOARD" = "true" ] && [ -n "$BOARD_DESCRIPTORS" ]; then + echo "Running multi-board test with specified board descriptors" + BOARD_DESC_ARGS="--board-descriptors" + for DESC in $BOARD_DESCRIPTORS; do + BOARD_DESC_ARGS+=" $DESC" + done + + pwd + + echo "python3 hwci/core/main.py $BOARD_DESC_ARGS --test hwci/$TEST 2>&1 | tee ./job-output.txt || FAIL=1" + + # Run test with multiple board descriptors + python3 hwci/core/main.py $BOARD_DESC_ARGS --test "hwci/$TEST" 2>&1 | tee ./job-output.txt || FAIL=1 + + else + # Run standard single-board test + python3 hwci/core/main.py --board hwci/boards/nrf52dk.py --test "hwci/$TEST" 2>&1 | tee ./job-output.txt || FAIL=1 + fi + set +o pipefail # Insert the result into the markdown table: @@ -328,11 +358,9 @@ jobs: echo "At least one test failed, exiting with error." exit 1 fi - - name: Request shutdown after successful job completion run: | sudo touch /run/github-actions-shutdown - - name: Provide connection information on job failure if: failure() run: | diff --git a/hwci/target_spec.yaml b/board_descriptors/0679be07-6106-48aa-8057-b1d4f2e18a99/board-nrf52840dk-SERIAL.yml similarity index 73% rename from hwci/target_spec.yaml rename to board_descriptors/0679be07-6106-48aa-8057-b1d4f2e18a99/board-nrf52840dk-SERIAL.yml index e58aafb..eafb600 100644 --- a/hwci/target_spec.yaml +++ b/board_descriptors/0679be07-6106-48aa-8057-b1d4f2e18a99/board-nrf52840dk-SERIAL.yml @@ -1,6 +1,10 @@ model: nrf52840dk -hw_rev: '3.2' -serial_number: 0xfoobar +serial_number: '0679be07-6106-48aa-8057-b1d4f2e18a99' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: true pin_mappings: P0.13: io_interface: raspberrypi5gpio @@ -26,11 +30,3 @@ pin_mappings: target_pin_function: BUTTON2 target_pin_mode: input target_pin_active: low - P1.01: - io_interface: raspberrypi5gpio - io_pin_spec: 16 - target_pin_function: GPIO0 - P1.02: - io_interface: raspberrypi5gpio - io_pin_spec: 13 - target_pin_function: GPIO1 diff --git a/board_descriptors/0af84b36-1d44-4e0e-9046-1f3fd8ec1cbf/board-nrf52840dk-SERIAL.yml b/board_descriptors/0af84b36-1d44-4e0e-9046-1f3fd8ec1cbf/board-nrf52840dk-SERIAL.yml new file mode 100644 index 0000000..48d89d9 --- /dev/null +++ b/board_descriptors/0af84b36-1d44-4e0e-9046-1f3fd8ec1cbf/board-nrf52840dk-SERIAL.yml @@ -0,0 +1,8 @@ +model: nrf52840dk +serial_number: '' +board_module: boards/nrf52dk.py +host_type: QEMU +features: + ble: true + debugger: jlink + gpio: false diff --git a/board_descriptors/1bdc10a7-9bea-4da5-9e9c-02c046223dfb/board-nrf52840dk-SERIAL.yml b/board_descriptors/1bdc10a7-9bea-4da5-9e9c-02c046223dfb/board-nrf52840dk-SERIAL.yml new file mode 100644 index 0000000..86d9056 --- /dev/null +++ b/board_descriptors/1bdc10a7-9bea-4da5-9e9c-02c046223dfb/board-nrf52840dk-SERIAL.yml @@ -0,0 +1,8 @@ +model: nrf52840dk +serial_number: '1bdc10a7-9bea-4da5-9e9c-02c046223dfb' +board_module: hwci/boards/nrf52dk.py +host_type: QEMU +features: + ble: true + debugger: jlink + gpio: false diff --git a/board_descriptors/25b97cf7-cf3c-4955-8f33-a8ea938c4f5b/board-nrf52840dk-SERIAL.yml b/board_descriptors/25b97cf7-cf3c-4955-8f33-a8ea938c4f5b/board-nrf52840dk-SERIAL.yml new file mode 100644 index 0000000..eec17aa --- /dev/null +++ b/board_descriptors/25b97cf7-cf3c-4955-8f33-a8ea938c4f5b/board-nrf52840dk-SERIAL.yml @@ -0,0 +1,8 @@ +model: nrf52840dk +serial_number: '25b97cf7-cf3c-4955-8f33-a8ea938c4f5b' +board_module: hwci/boards/nrf52dk.py +host_type: QEMU +features: + ble: true + debugger: jlink + gpio: false diff --git a/board_descriptors/524aa422-3ea7-47be-99d3-b78430449589/board-nrf52840dk-SERIAL.yml b/board_descriptors/524aa422-3ea7-47be-99d3-b78430449589/board-nrf52840dk-SERIAL.yml new file mode 100644 index 0000000..eed8fcd --- /dev/null +++ b/board_descriptors/524aa422-3ea7-47be-99d3-b78430449589/board-nrf52840dk-SERIAL.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '524aa422-3ea7-47be-99d3-b78430449589' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: true +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/board_descriptors/56f98833-da16-4ba0-9f38-2b02cfd01ddd/board-nrf52840dk-SERIAL.yml b/board_descriptors/56f98833-da16-4ba0-9f38-2b02cfd01ddd/board-nrf52840dk-SERIAL.yml new file mode 100644 index 0000000..bfeecf1 --- /dev/null +++ b/board_descriptors/56f98833-da16-4ba0-9f38-2b02cfd01ddd/board-nrf52840dk-SERIAL.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '56f98833-da16-4ba0-9f38-2b02cfd01ddd' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: true +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/board_descriptors/64e5e94d-67e9-4276-9a0c-509a6789b372/board-nrf52840dk-SERIAL.yml b/board_descriptors/64e5e94d-67e9-4276-9a0c-509a6789b372/board-nrf52840dk-SERIAL.yml new file mode 100644 index 0000000..d241174 --- /dev/null +++ b/board_descriptors/64e5e94d-67e9-4276-9a0c-509a6789b372/board-nrf52840dk-SERIAL.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '64e5e94d-67e9-4276-9a0c-509a6789b372' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: true +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/board_descriptors/8723bd6d-88d4-4605-94f1-331b8d54a202/board-nrf52840dk-SERIAL.yml b/board_descriptors/8723bd6d-88d4-4605-94f1-331b8d54a202/board-nrf52840dk-SERIAL.yml new file mode 100644 index 0000000..a14eed6 --- /dev/null +++ b/board_descriptors/8723bd6d-88d4-4605-94f1-331b8d54a202/board-nrf52840dk-SERIAL.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '8723bd6d-88d4-4605-94f1-331b8d54a202' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: true +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/board_descriptors/8ff22e8e-ead7-433a-a921-c7206face09d/board-nrf52840dk-SERIAL.yml b/board_descriptors/8ff22e8e-ead7-433a-a921-c7206face09d/board-nrf52840dk-SERIAL.yml new file mode 100644 index 0000000..85c41f4 --- /dev/null +++ b/board_descriptors/8ff22e8e-ead7-433a-a921-c7206face09d/board-nrf52840dk-SERIAL.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '8ff22e8e-ead7-433a-a921-c7206face09d' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: true +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050202501.yml b/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050202501.yml new file mode 100644 index 0000000..478b95a --- /dev/null +++ b/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050202501.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '1050202501' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: false +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050244773.yml b/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050244773.yml new file mode 100644 index 0000000..f26d9e8 --- /dev/null +++ b/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050244773.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '1050244773' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: false +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050289195.yml b/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050289195.yml new file mode 100644 index 0000000..20aa453 --- /dev/null +++ b/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050289195.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '1050289195' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: false +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050292283.yml b/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050292283.yml new file mode 100644 index 0000000..ce74af4 --- /dev/null +++ b/board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050292283.yml @@ -0,0 +1,32 @@ +model: nrf52840dk +serial_number: '1050292283' +board_module: hwci/boards/nrf52dk.py +features: + ble: true + debugger: jlink + gpio: false +pin_mappings: + P0.13: + io_interface: raspberrypi5gpio + io_pin_spec: 20 + target_pin_function: LED1 + target_pin_mode: output + target_pin_active: low + P0.14: + io_interface: raspberrypi5gpio + io_pin_spec: 19 + target_pin_function: LED2 + target_pin_mode: output + target_pin_active: low + P0.11: + io_interface: raspberrypi5gpio + io_pin_spec: 21 + target_pin_function: BUTTON1 + target_pin_mode: input + target_pin_active: low + P0.12: + io_interface: raspberrypi5gpio + io_pin_spec: 26 + target_pin_function: BUTTON2 + target_pin_mode: input + target_pin_active: low diff --git a/hwci/boards/nrf52dk.py b/hwci/boards/nrf52dk.py index 9384b85..7a7dcb0 100644 --- a/hwci/boards/nrf52dk.py +++ b/hwci/boards/nrf52dk.py @@ -10,41 +10,100 @@ from boards.tockloader_board import TockloaderBoard from utils.serial_port import SerialPort from gpio.gpio import GPIO -import yaml -import os class Nrf52dk(TockloaderBoard): def __init__(self): super().__init__() self.arch = "cortex-m4" - self.kernel_path = os.path.join( - self.base_dir, "repos/tock") + + # Path to Tock's root directory + self.kernel_path = os.path.join(self.base_dir, "repos", "tock") + + # Path to the nrf52840dk board folder in Tock self.kernel_board_path = os.path.join( - self.kernel_path, "boards/nordic/nrf52840dk") + self.kernel_path, "boards", "nordic", "nrf52840dk" + ) + self.uart_port = self.get_uart_port() self.uart_baudrate = self.get_uart_baudrate() - self.openocd_board = "nrf52dk" + + # Tockloader board info + self.openocd_board = "nrf52dk" # Matches `--board nrf52dk` in Tockloader self.board = "nrf52dk" + self.serial = self.get_serial_port() self.gpio = self.get_gpio_interface() def get_uart_port(self): logging.info("Getting list of serial ports") ports = list(serial.tools.list_ports.comports()) - for port in ports: - if "J-Link" in port.description: - logging.info(f"Found J-Link port: {port.device}") - return port.device + print(ports) + + # First, check if we have a serial_number to match + board_serial = getattr(self, "serial_number", None) + if board_serial: + logging.info(f"Looking for serial port with J-Link SN: {board_serial}") + + # Find ports that match our serial number + matching_ports = [] + for port in ports: + # Check if our serial number is in the port's serial_number or hwid + if ( + hasattr(port, "serial_number") + and board_serial == port.serial_number + ): + matching_ports.append(port) + elif hasattr(port, "hwid") and board_serial in port.hwid: + matching_ports.append(port) + + # Sort matching ports by device name to pick the lower-numbered one + if matching_ports: + matching_ports.sort(key=lambda p: p.device) + selected_port = matching_ports[0].device + logging.info( + f"Found matching J-Link port for SN {board_serial}: {selected_port}" + ) + return selected_port + + logging.warning(f"No serial port found matching J-Link SN: {board_serial}") + + # If no serial number is provided or we couldn't find a match, + # just return the first J-Link port for now (this will be replaced later) + jlink_ports = [p for p in ports if "J-Link" in p.description] + + if jlink_ports: + selected_port = jlink_ports[0].device + logging.info( + f"No serial number provided or match found. Using first J-Link port: {selected_port}" + ) + return selected_port + + # If no J-Link ports were found at all if ports: - logging.info(f"Automatically selected port: {ports[0].device}") + logging.warning( + f"No J-Link ports found. Using first available port: {ports[0].device}" + ) return ports[0].device - else: - logging.error("No serial ports found") - raise Exception("No serial ports found") + + logging.error("No serial ports found") + raise Exception("No serial ports found") + + def update_serial_port(self): + """Update the serial port based on the current serial_number attribute.""" + if hasattr(self, "serial_number") and self.serial_number: + old_port = self.uart_port + self.uart_port = self.get_uart_port() + if old_port != self.uart_port: + logging.info(f"Updated serial port from {old_port} to {self.uart_port}") + # Close the old serial port if it exists + if self.serial: + self.serial.close() + # Create a new serial port object + self.serial = self.get_serial_port() def get_uart_baudrate(self): - return 115200 # Default baudrate for the board + return 115200 def get_serial_port(self): logging.info( @@ -53,11 +112,15 @@ def get_serial_port(self): return SerialPort(self.uart_port, self.uart_baudrate) def get_gpio_interface(self): - # Load the target spec from a YAML file - target_spec = load_target_spec() - # Initialize GPIO with the target spec - gpio = GPIO(target_spec) - return gpio + # If there is no 'pin_mappings' attribute, skip GPIO config + if not hasattr(self, "pin_mappings"): + logging.info( + "No pin_mappings found in board descriptor; skipping GPIO init." + ) + return None + + target_spec = {"pin_mappings": self.pin_mappings} + return GPIO(target_spec) def cleanup(self): if self.gpio: @@ -67,54 +130,188 @@ def cleanup(self): self.serial.close() def flash_kernel(self): + """ + Flash the Tock OS kernel using tockloader directly with correct board config. + """ logging.info("Flashing the Tock OS kernel") if not os.path.exists(self.kernel_path): - logging.error(f"Tock directory {self.kernel_path} not found") raise FileNotFoundError(f"Tock directory {self.kernel_path} not found") - # Run make flash-openocd from the board directory + # Make sure the kernel is built and ready subprocess.run( - ["make", "flash-openocd"], cwd=self.kernel_board_path, check=True + ["make"], + cwd=self.kernel_board_path, + check=True, + ) + + # Path to the kernel binary + kernel_bin = os.path.join( + self.kernel_path, "target/thumbv7em-none-eabi/release/nrf52840dk.bin" + ) + + # Get the serial number of this board + serial_number = getattr(self, "serial_number", None) + if not serial_number: + logging.warning( + "No serial number specified for board. Using first available board." + ) + + # Build the tockloader command with the correct order of arguments + tockloader_cmd = ["tockloader", "flash"] + + # Order matters! These flags must come AFTER the 'flash' command + if serial_number: + tockloader_cmd.extend(["--openocd-serial-number", serial_number]) + + # Add the rest of the arguments + tockloader_cmd.extend( + [ + "--openocd", + "--openocd-board", + "nordic_nrf52_dk.cfg", + "--address", + "0x00000", + "--board", + "nrf52dk", + kernel_bin, + ] + ) + + # Run the tockloader command + logging.info(f"Running tockloader command: {' '.join(tockloader_cmd)}") + subprocess.run( + tockloader_cmd, + check=True, ) def erase_board(self): + """ + Issue an nrf52_recover over SWD, specifying 'adapter serial ' if available. + This uses OpenOCD directly to mass erase/unlock the chip. + """ logging.info("Erasing the board") - command = [ - "openocd", - "-c", - "adapter driver jlink; transport select swd; source [find target/nrf52.cfg]; init; nrf52_recover; exit", - ] + jlink_serial = getattr(self, "serial_number", None) + + if jlink_serial: + cmd_string = ( + f"adapter driver jlink; transport select swd; source [find target/nrf52.cfg]; " + f"adapter serial {jlink_serial}; " + "init; nrf52_recover; exit" + ) + else: + cmd_string = ( + "adapter driver jlink; transport select swd; source [find target/nrf52.cfg]; " + "init; nrf52_recover; exit" + ) + + command = ["openocd", "-c", cmd_string] + logging.info(f"Running OpenOCD command: {command}") subprocess.run(command, check=True) def reset(self): - logging.info("Performing a target reset via JTAG") - command = [ - "openocd", - "-c", - "adapter driver jlink; transport select swd; source [find target/nrf52.cfg]; init; reset; exit", - ] + """ + Hard reset the board using OpenOCD, specifying the J-Link serial if we have one. + """ + logging.info("Performing a target reset via JTAG/SWD") + jlink_serial = getattr(self, "serial_number", None) + + if jlink_serial: + cmd_string = ( + f"adapter driver jlink; transport select swd; source [find target/nrf52.cfg]; " + f"adapter serial {jlink_serial}; " + "init; reset; exit" + ) + else: + cmd_string = ( + "adapter driver jlink; transport select swd; source [find target/nrf52.cfg]; " + "init; reset; exit" + ) + + command = ["openocd", "-c", cmd_string] + logging.info(f"Running OpenOCD command: {command}") subprocess.run(command, check=True) - # The flash_app method is inherited from TockloaderBoard + def flash_app(self, app): + if type(app) == str: + app_path = app + app_name = os.path.basename(app_path) + tab_file = os.path.join("build", f"{app_name}.tab") + else: + app_path = app["path"] + app_name = app["name"] + tab_file = app["tab_file"] # relative to "path" + + logging.info(f"Flashing app: {app_name}") + libtock_c_dir = os.path.join(self.base_dir, "repos", "libtock-c") + if not os.path.exists(libtock_c_dir): + logging.error(f"libtock-c directory {libtock_c_dir} not found") + raise FileNotFoundError(f"libtock-c directory {libtock_c_dir} not found") + + app_dir = os.path.join(libtock_c_dir, "examples", app_path) + if not os.path.exists(app_dir): + logging.error(f"App directory {app_dir} not found") + raise FileNotFoundError(f"App directory {app_dir} not found") + + # Build the app using absolute paths + logging.info(f"Building app: {app_name}") + if app_name != "lua-hello": + subprocess.run( + ["make", f"TOCK_TARGETS={self.arch}"], cwd=app_dir, check=True + ) + else: + # if the app is lua-hello, we need to build the libtock-c submodule first + with self.change_directory(libtock_c_dir): + subprocess.run( + ["make", f"TOCK_TARGETS={self.arch}"], cwd=app_dir, check=True + ) + + tab_path = os.path.join(app_dir, tab_file) + if not os.path.exists(tab_path): + logging.error(f"Tab file {tab_path} not found") + raise FileNotFoundError(f"Tab file {tab_path} not found") + + logging.info(f"Installing app: {app_name}") + + # Get the serial number of this board + serial_number = getattr(self, "serial_number", None) + + # Build the tockloader command with the correct order of arguments + tockloader_cmd = ["tockloader", "install"] + + # Add serial number if available + if serial_number: + tockloader_cmd.extend(["--openocd-serial-number", serial_number]) + + # Add the rest of the arguments + tockloader_cmd.extend( + [ + "--openocd", + "--openocd-board", + "nordic_nrf52_dk.cfg", + "--board", + self.board, + tab_path, + ] + ) + + # Run the tockloader command + logging.info(f"Running tockloader command: {' '.join(tockloader_cmd)}") + subprocess.run( + tockloader_cmd, + check=True, + ) @contextmanager def change_directory(self, new_dir): - previous_dir = os.getcwd() + old_dir = os.getcwd() os.chdir(new_dir) logging.info(f"Changed directory to: {os.getcwd()}") try: yield finally: - os.chdir(previous_dir) + os.chdir(old_dir) logging.info(f"Reverted to directory: {os.getcwd()}") -def load_target_spec(): - # Assume the target spec file is in a fixed location - target_spec_path = os.path.join(os.getcwd(), "target_spec.yaml") - with open(target_spec_path, "r") as f: - target_spec = yaml.safe_load(f) - return target_spec - - +# Global board object used by the test harness board = Nrf52dk() diff --git a/hwci/core/main.py b/hwci/core/main.py index c704b63..7bbf575 100644 --- a/hwci/core/main.py +++ b/hwci/core/main.py @@ -6,11 +6,24 @@ import logging import importlib.util import sys +import os +import yaml from pathlib import Path +# python3 hwci/core/main.py \ +# --board-descriptors \ +# board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050202501.yml \ +# board_descriptors/fb1384d5-e1a5-469c-beb4-0d4d215c9793/board-nrf52840dk-001050244773.yml \ +# --test hwci/tests/ble_advertising_scanning_test.py + + def main(): parser = argparse.ArgumentParser(description="Run tests on Tock OS") - parser.add_argument("--board", required=True, help="Path to the board module") + parser.add_argument( + "--board-descriptors", + nargs="+", + help="Paths to YAML board descriptor files (e.g. board-descriptors/.../*.yml).", + ) parser.add_argument("--test", required=True, help="Path to the test module") args = parser.parse_args() @@ -20,39 +33,92 @@ def main(): format="%(asctime)s - %(levelname)s - %(message)s", ) - # Ensure that imported modules can find the top-level hwci modules - # (appends the hwci root to the PYTHONPATH): + # Ensure that Python can find 'hwci' modules sys.path.append(str(Path(__file__).parent.parent)) - # 1. Load board module - board_spec = importlib.util.spec_from_file_location("board_module", args.board) - board_module = importlib.util.module_from_spec(board_spec) - board_spec.loader.exec_module(board_module) - if hasattr(board_module, "board"): - board = board_module.board - else: - logging.error("No board class found in the specified board module") + # 1. Parse each board descriptor + boards = [] + if not args.board_descriptors: + logging.error( + "No board descriptors were provided. Use --board-descriptors *.yml" + ) sys.exit(1) - # 5. Load test module, run test function - test_spec = importlib.util.spec_from_file_location("test_module", args.test) + logging.info(f"Loading {len(args.board_descriptors)} board descriptors") + logging.info(f"Loading test module: {args.test}") + logging.info(f"Python path: {sys.path}") + logging.info(f"Board descriptors: {args.board_descriptors}") + + for descriptor_path in args.board_descriptors: + logging.info(f"Loading board descriptor: {descriptor_path}") + with open(descriptor_path, "r") as f: + yaml_content = f.read() + # Log the raw YAML content + logging.info(f"YAML content:\n{yaml_content}") + # Parse the YAML + board_info = yaml.safe_load(yaml_content) + + if not board_info: + logging.error(f"Board descriptor file {descriptor_path} is empty or invalid.") + sys.exit(1) + + # We expect something like: board_module: boards/nrf52dk.py + board_module_path = board_info.get("board_module") + if not board_module_path: + logging.error(f"Missing 'board_module' in descriptor {descriptor_path}") + sys.exit(1) + + # 2. Import that Python module dynamically + if os.path.isfile(board_module_path): + # If board_module is an actual file path (e.g. boards/nrf52dk.py) + spec = importlib.util.spec_from_file_location( + "board_module", board_module_path + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + else: + # Alternatively, if it's a dotted name like "boards.nrf52dk" + mod = importlib.import_module(board_module_path) + + # 3. Grab the 'board' object from the module + if not hasattr(mod, "board"): + logging.error(f"No 'board' object found in {board_module_path}") + sys.exit(1) + + board_instance = getattr(mod, "board") + + # Optionally store descriptor metadata on the board instance + board_instance.model = board_info.get("model") + board_instance.serial_number = board_info.get("serial_number") + board_instance.features = board_info.get("features", {}) + if hasattr(board_instance, "update_serial_port"): + board_instance.update_serial_port() + + boards.append(board_instance) + + # 4. Load the test module + test_path = args.test + test_spec = importlib.util.spec_from_file_location("test_module", test_path) test_module = importlib.util.module_from_spec(test_spec) test_spec.loader.exec_module(test_module) - if hasattr(test_module, "test"): - test = test_module.test - else: + + if not hasattr(test_module, "test"): logging.error("No test variable found in the specified test module") sys.exit(1) - # Run the test + test = test_module.test + + # 5. Run the test, passing our list of boards try: - test.test(board) - logging.info("Test completed successfully") + test.test(boards) + logging.info("Test completed successfully!") except Exception as e: logging.exception("An error occurred during test execution") sys.exit(1) finally: - board.cleanup() + # Cleanup each board + for b in boards: + b.cleanup() if __name__ == "__main__": diff --git a/hwci/core/test_harness.py b/hwci/core/test_harness.py index 3f0447f..5d4098e 100644 --- a/hwci/core/test_harness.py +++ b/hwci/core/test_harness.py @@ -3,6 +3,9 @@ # Copyright Tock Contributors 2024. -class TestHarness: - def test(self, board): +class TestHarness(object): + """Base class for all tests. By default it does nothing with the boards.""" + + def test(self, boards): + """Entry point: a list of BoardHarness objects is passed.""" pass diff --git a/hwci/requirements-frozen.txt b/hwci/requirements-frozen.txt index 7e187b7..937213d 100644 --- a/hwci/requirements-frozen.txt +++ b/hwci/requirements-frozen.txt @@ -11,7 +11,7 @@ pycryptodome==3.21.0 pyserial==3.5 questionary==2.0.1 siphash==0.0.1 -tockloader==1.13.0 toml==0.10.2 tqdm==4.66.6 wcwidth==0.2.13 +tockloader==1.14.0 diff --git a/hwci/requirements.txt b/hwci/requirements.txt index cfee161..c2f76e1 100644 --- a/hwci/requirements.txt +++ b/hwci/requirements.txt @@ -4,7 +4,7 @@ pexpect pyserial -tockloader pyyaml gpiozero lgpio +tockloader diff --git a/hwci/setup.sh b/hwci/setup.sh index 49819e6..116a371 100755 --- a/hwci/setup.sh +++ b/hwci/setup.sh @@ -37,7 +37,6 @@ set -e -x # of its own build process: type rustup || (echo "rustup is not installed, aborting."; exit 1) -# Ensure that `elf2tab` is installed: if ! type elf2tab; then # We may not have a rustup default toolchain selected. In this case, # select the stable toolchain for elf2tab. diff --git a/hwci/tests/ble_advertising_scanning_test.py b/hwci/tests/ble_advertising_scanning_test.py new file mode 100644 index 0000000..d7d6482 --- /dev/null +++ b/hwci/tests/ble_advertising_scanning_test.py @@ -0,0 +1,174 @@ +# hwci/tests/ble_advertising_scanning_test.py +# +# Multi‑board integration test: +# • Board 0 runs the ble_advertising example +# • Board 1 runs the ble_passive_scanning example +# +# We verify that the advertiser prints its “start advertising” message and that +# the scanner eventually prints at least one advertisement whose +# manufacturer‑specific data contains our company identifier {0x13, 0x37}. +# +# The identifier is transmitted little‑endian on the air interface, so the +# scanner can show either “… ff 13 37 …” or “… ff 03 37 …”. + +import logging +import time +import re +from core.test_harness import TestHarness + + +class BleAdvertisingScanningTest(TestHarness): + """ + Multi‑board test: + 1. Advertiser reports it is advertising with the expected device name. + 2. Scanner prints at least one advertisement that contains our company ID + (0x13, 0x37) inside the manufacturer‑specific field. + """ + + EXPECTED_DEVICE_NAME = "TockOS" + + # Human‑readable form for error messages / logging only + # MANUFACTURER_DATA = "13 37" + + # Regex that matches either byte order, with flexible whitespace and case + # MANUFACTURER_DATA_RE = re.compile(r"\bff\s+(?:13\s+37|03\s+37)\b", re.IGNORECASE) + + MANUFACTURER_DATA = "06 00" + MANUFACTURER_DATA_RE = re.compile(r"\bff\s+06\s+00\b", re.IGNORECASE) + + def test(self, boards): + if len(boards) < 2: + raise ValueError( + "Need at least 2 boards for BLE advertising/scanning test!" + ) + + advertiser, scanner = boards[0], boards[1] + + # Safety: ensure distinct UARTs + if advertiser.uart_port == scanner.uart_port: + raise ValueError( + f"Both boards are using the same serial port: {advertiser.uart_port}. " + f"Each board must have a unique serial port. " + f"Board 1 SN: {getattr(advertiser, 'serial_number', 'unknown')}, " + f"Board 2 SN: {getattr(scanner, 'serial_number', 'unknown')}" + ) + + logging.info( + f"Advertiser (SN: {advertiser.serial_number}) using port: {advertiser.uart_port}" + ) + logging.info( + f"Scanner (SN: {scanner.serial_number}) using port: {scanner.uart_port}" + ) + + # Clean slate + advertiser.erase_board() + scanner.erase_board() + advertiser.serial.flush_buffer() + scanner.serial.flush_buffer() + + advertiser.flash_kernel() + scanner.flash_kernel() + + advertiser.flash_app("ble_advertising") + scanner.flash_app("ble_passive_scanning") + + logging.info( + "Flashed ble_advertising -> board0, ble_passive_scanning -> board1." + ) + + adv_done = False # advertiser start message seen + scan_done = False # manufacturer data seen + + scanner_output: list[str] = [] + advertiser_output: list[str] = [] + + start_time = time.time() + TIMEOUT = 30 # seconds + + while True: + if time.time() - start_time > TIMEOUT: + break + + # ---------- Advertiser ---------- + line_adv = advertiser.serial.expect( + ".+\r?\n", timeout=1, timeout_error=False + ) + if line_adv: + text_adv = line_adv.decode("utf-8", errors="replace").strip() + advertiser_output.append(text_adv) + logging.debug(f"[Advertiser] {text_adv}") + + if re.search( + rf"(Now advertising .*'{self.EXPECTED_DEVICE_NAME}'|" + rf"Begin advertising!? *{self.EXPECTED_DEVICE_NAME})", + text_adv, + ): + adv_done = True + logging.info( + f"Advertiser started advertising as '{self.EXPECTED_DEVICE_NAME}'" + ) + + # ---------- Scanner ---------- + line_scan = scanner.serial.expect(".+\r?\n", timeout=1, timeout_error=False) + if line_scan: + text_scan = line_scan.decode("utf-8", errors="replace").strip() + scanner_output.append(text_scan) + logging.debug(f"[Scanner] {text_scan}") + + if self.MANUFACTURER_DATA_RE.search(text_scan): + logging.info( + f"Scanner detected our expected manufacturer data ({self.MANUFACTURER_DATA})" + ) + scan_done = True + + if adv_done and scan_done: + break + + # Final pass over accumulated output + full_scan_out = "\n".join(scanner_output) + logging.info(f"Collected {len(scanner_output)} lines from scanner") + + if not scan_done and self.MANUFACTURER_DATA_RE.search(full_scan_out): + logging.info("Found manufacturer data in combined scanner output") + scan_done = True + + # Assemble error report if needed + errors = [] + + if not adv_done: + errors.append( + f"Advertiser never printed its start‑advertising line containing " + f"'{self.EXPECTED_DEVICE_NAME}'." + ) + + if not scan_done: + data_fields = re.findall(r"Data:\s*([0-9a-fA-F ]+)", full_scan_out) + + err = ( + "Scanner board never detected an advertisement with the expected " + f"manufacturer data ({self.MANUFACTURER_DATA}).\n" + ) + if data_fields: + err += "\nData fields seen in advertisements:\n" + for i, data in enumerate(data_fields[:10], 1): + err += f" {i:2d}: {data}\n" + if len(data_fields) > 10: + err += f" … and {len(data_fields) - 10} more\n" + else: + err += "No advertisement ‘Data: …’ lines were captured!\n" + + sample = "\n".join(scanner_output[:20]) + err += "\nSample scanner output:\n" + sample + if len(scanner_output) > 20: + err += f"\n… and {len(scanner_output) - 20} more lines" + + errors.append(err) + + if errors: + raise Exception("\n\n".join(errors)) + + logging.info("BLE advertising + scanning test passed successfully!") + + +# For manual invocation +test = BleAdvertisingScanningTest() diff --git a/hwci/utils/test_helpers/oneshot.py b/hwci/utils/test_helpers/oneshot.py index ff7ba47..931e1aa 100644 --- a/hwci/utils/test_helpers/oneshot.py +++ b/hwci/utils/test_helpers/oneshot.py @@ -6,14 +6,22 @@ class OneshotTest(TestHarness): def __init__(self, apps=[]): self.apps = apps - def test(self, board): + def test(self, boards): logging.info("Starting OneshotTest") - board.erase_board() - board.serial.flush_buffer() - board.flash_kernel() + if len(boards) != 1: + raise ValueError( + f"OneshotTest requires exactly 1 board. Got {len(boards)} boards." + ) + single_board = boards[0] + + # Normal single-board flow: + single_board.erase_board() + single_board.serial.flush_buffer() + single_board.flash_kernel() for app in self.apps: - board.flash_app(app) - self.oneshot_test(board) + single_board.flash_app(app) + + self.oneshot_test(single_board) logging.info("Finished OneshotTest") def oneshot_test(self, board):