Skip to content

Commit 84fb467

Browse files
robkerseyclaude
andcommitted
Initial commit: Bert BLE profile compliance tester
Verifies a physical BLE peripheral against a Bluetooth SIG profile spec. Acts as the central/Collector via Bumble (pure-Python BT stack) over an nRF52840 USB Dongle running Zephyr hci_uart, with a second nRF52840 running Nordic's nRF Sniffer to capture over-the-air traffic. Reports host + OTA observations on a unified timeline with JUnit/Markdown/HTML output. Highlights: - pydantic IR for SIG profile specs, with parse → review → validate flow - bundled HRP, Battery Service, Device Information profiles - pluggable @testcase procedures + entry-point discovery - automated bert flash-firmware (interactive nrfutil DFU) with manifest- driven SHA256-verified GitHub Releases download for prebuilt firmware - bert.adapters.firmware_fetch + bert firmware list/download/verify CLI - GitHub Actions workflow (.github/workflows/build-firmware.yml) that builds Zephyr hci_uart in a container and publishes to releases - 59 unit tests passing; integration tests scaffolded Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 parents  commit 84fb467

77 files changed

Lines changed: 6882 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: build-firmware
2+
3+
# Builds Zephyr's hci_uart sample for the nRF52840 USB Dongle and uploads
4+
# the .hex as a GitHub Release asset. Triggered on:
5+
# - manual dispatch (workflow_dispatch),
6+
# - any tag matching `firmware-*` (the canonical way to publish).
7+
#
8+
# After a successful run, update src/bert/firmware/manifest.json with the
9+
# release tag + SHA256 from the workflow output, commit, and cut a Bert
10+
# release. Bert will then download this firmware on demand at flash time.
11+
12+
on:
13+
workflow_dispatch:
14+
inputs:
15+
release_tag:
16+
description: "Release tag to publish under (e.g. firmware-2026.05.06)"
17+
required: true
18+
zephyr_revision:
19+
description: "Zephyr revision to build against"
20+
required: true
21+
default: "v3.7.0"
22+
push:
23+
tags:
24+
- "firmware-*"
25+
26+
permissions:
27+
contents: write # needed to create releases
28+
29+
jobs:
30+
build:
31+
runs-on: ubuntu-latest
32+
container:
33+
image: ghcr.io/zephyrproject-rtos/ci:v0.27.4
34+
options: --user root
35+
env:
36+
ZEPHYR_REVISION: ${{ inputs.zephyr_revision || 'v3.7.0' }}
37+
steps:
38+
- name: Checkout Bert (only for context; not strictly needed to build hci_uart)
39+
uses: actions/checkout@v4
40+
41+
- name: Set up Zephyr workspace
42+
run: |
43+
set -euxo pipefail
44+
mkdir -p /workspace
45+
cd /workspace
46+
west init -m https://github.com/zephyrproject-rtos/zephyr --mr "$ZEPHYR_REVISION" .
47+
west update --narrow -o=--depth=1
48+
west zephyr-export
49+
pip install -r zephyr/scripts/requirements.txt
50+
51+
- name: Build hci_uart for nRF52840 USB Dongle
52+
working-directory: /workspace
53+
run: |
54+
set -euxo pipefail
55+
west build \
56+
-b nrf52840dongle/nrf52840 \
57+
-d build-hci-dongle \
58+
zephyr/samples/bluetooth/hci_uart
59+
mkdir -p $GITHUB_WORKSPACE/firmware-out
60+
cp build-hci-dongle/zephyr/zephyr.hex \
61+
$GITHUB_WORKSPACE/firmware-out/hci_uart_nrf52840dongle.hex
62+
63+
- name: Hash + size
64+
id: meta
65+
working-directory: ${{ github.workspace }}/firmware-out
66+
run: |
67+
set -euxo pipefail
68+
SHA=$(sha256sum hci_uart_nrf52840dongle.hex | awk '{print $1}')
69+
SIZE=$(stat -c%s hci_uart_nrf52840dongle.hex 2>/dev/null || stat -f%z hci_uart_nrf52840dongle.hex)
70+
echo "sha256=$SHA" >> $GITHUB_OUTPUT
71+
echo "size=$SIZE" >> $GITHUB_OUTPUT
72+
echo "## hci_uart_nrf52840dongle.hex" >> $GITHUB_STEP_SUMMARY
73+
echo "- sha256: \`$SHA\`" >> $GITHUB_STEP_SUMMARY
74+
echo "- size: $SIZE bytes" >> $GITHUB_STEP_SUMMARY
75+
echo "- zephyr: $ZEPHYR_REVISION" >> $GITHUB_STEP_SUMMARY
76+
77+
- name: Upload build artefact (always)
78+
uses: actions/upload-artifact@v4
79+
with:
80+
name: hci_uart_nrf52840dongle
81+
path: firmware-out/hci_uart_nrf52840dongle.hex
82+
if-no-files-found: error
83+
84+
- name: Determine release tag
85+
id: tag
86+
run: |
87+
if [ -n "${{ inputs.release_tag }}" ]; then
88+
echo "tag=${{ inputs.release_tag }}" >> $GITHUB_OUTPUT
89+
else
90+
echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
91+
fi
92+
93+
- name: Create / update release
94+
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
95+
uses: softprops/action-gh-release@v2
96+
with:
97+
tag_name: ${{ steps.tag.outputs.tag }}
98+
name: ${{ steps.tag.outputs.tag }}
99+
files: firmware-out/hci_uart_nrf52840dongle.hex
100+
fail_on_unmatched_files: true
101+
body: |
102+
Pre-built firmware images for Bert.
103+
104+
| file | sha256 | size | zephyr |
105+
|---|---|---|---|
106+
| `hci_uart_nrf52840dongle.hex` | `${{ steps.meta.outputs.sha256 }}` | ${{ steps.meta.outputs.size }} bytes | `${{ env.ZEPHYR_REVISION }}` |
107+
108+
Update `src/bert/firmware/manifest.json` to point at this release:
109+
110+
```json
111+
"release_tag": "${{ steps.tag.outputs.tag }}",
112+
"base_url": "https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}",
113+
"firmware": {
114+
"hci": {
115+
"filename": "hci_uart_nrf52840dongle.hex",
116+
"sha256": "${{ steps.meta.outputs.sha256 }}",
117+
"size_bytes": ${{ steps.meta.outputs.size }}
118+
}
119+
}
120+
```

.gitignore

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
dist/
9+
*.egg-info/
10+
*.egg
11+
.pytest_cache/
12+
.mypy_cache/
13+
.ruff_cache/
14+
.coverage
15+
htmlcov/
16+
.tox/
17+
18+
# Environments
19+
.venv/
20+
venv/
21+
.env
22+
23+
# uv
24+
.uv/
25+
26+
# IDE
27+
.vscode/
28+
.idea/
29+
*.swp
30+
31+
# OS
32+
.DS_Store
33+
Thumbs.db
34+
35+
# Bert runtime
36+
runs/
37+
*.pcapng
38+
*.pcap
39+
40+
# Draft IR (only reviewed YAML is committed)
41+
*.draft.yaml
42+
*.draft.yml
43+
44+
# Cache
45+
.cache/
46+
47+
# Local overrides
48+
.bert.local.toml
49+
50+
# Claude Code per-user settings (project-shared settings live in settings.json)
51+
.claude/settings.local.json

LICENSE

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Apache License
2+
Version 2.0, January 2004
3+
http://www.apache.org/licenses/
4+
5+
Copyright 2026 Bert contributors
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.

0 commit comments

Comments
 (0)