Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 108 additions & 100 deletions .github/workflows/arduino.yml
Original file line number Diff line number Diff line change
@@ -1,139 +1,147 @@
name: Build and Release

on:
pull_request:
push:
branches:
- master
- dev
tags:
- v*
- "v*"

permissions:
contents: read

env:
ARDUINO_ESP32_VERSION: 3.3.11
FQBN: esp32:esp32:m5stack_atom

jobs:
native-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run native tests
run: make -C tests runtests

build:
strategy:
matrix:
board: [tinypico, pico32]
include:
- board: tinypico
arduino-platform: esp32:esp32
arduino-platform-version: 2.0.15
arduino-board: esp32:esp32:tinypico
- board: pico32
arduino-platform: esp32:esp32
arduino-platform-version: 2.0.15
arduino-board: esp32:esp32:pico32
needs: native-tests
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
pip3 install pyserial
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Arduino CLI
uses: arduino/setup-arduino-cli@v1
- name: Install core
uses: arduino/setup-arduino-cli@v2

- name: Install ESP32 core
run: |
arduino-cli core update-index
arduino-cli core install --additional-urls "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json" ${{matrix.arduino-platform}}@${{matrix.arduino-platform-version}}
arduino-cli core update-index --additional-urls "https://espressif.github.io/arduino-esp32/package_esp32_index.json"
arduino-cli core install --additional-urls "https://espressif.github.io/arduino-esp32/package_esp32_index.json" "esp32:esp32@${ARDUINO_ESP32_VERSION}"

- name: Install libraries
run: |
arduino-cli lib install "TinyPICO Helper Library"@1.4.0
arduino-cli lib install "FreeRTOS"@11.0.1-5
arduino-cli lib install "M5Unified"@0.2.19
arduino-cli lib install "FastLED"@3.10.5
arduino-cli lib install "ArduinoQueue"@1.2.5
arduino-cli lib install "ArduinoLog"@1.1.1
arduino-cli lib install "AUnit"@1.7.1
- name: Compile
run: |
arduino-cli compile --fqbn ${{matrix.arduino-board}} --export-binaries src/bb-link/bb-link.ino
- name: Define version
run: |
VERSION=${GITHUB_REF#refs/tags/}
VERSION=${VERSION#v}
echo "VERSION=${VERSION}" >> $GITHUB_ENV
- name: Rename binary
run: |
fqbnpath=$(echo ${{matrix.arduino-board}} | tr ':' '.')
cp src/bb-link/build/$fqbnpath/bb-link.ino.bin src/bb-link/build/bb-link-${{matrix.board}}-${{env.VERSION}}.bin
- name: Calculate checksum

- name: Compile ATOM Lite firmware
run: |
sha256sum src/bb-link/build/bb-link-${{matrix.board}}-${{env.VERSION}}.bin | cut -d ' ' -f 1 > src/bb-link/build/bb-link-${{matrix.board}}-${{env.VERSION}}.bin.sha256.txt
- name: Create OTA manifest
arduino-cli compile \
--fqbn "${FQBN}" \
--build-path build/atomlite \
--build-property "compiler.cpp.extra_flags=-DBB_LINK_ALLOW_UNSIGNED_OTA_WITH_PHYSICAL_ACCESS=1" \
--warnings all \
src/bb-link

- name: Prepare release artifacts
if: startsWith(github.ref, 'refs/tags/v')
run: |
mkdir -p src/bb-link/build/ota/${{matrix.board}}
echo "{
\"version\": \"${{env.VERSION}}\",
\"url\": \"https://github.com/islandmagic/bb-link/releases/download/v${{env.VERSION}}/bb-link-${{matrix.board}}-${{env.VERSION}}.bin\",
\"sha256\": \"$(cat src/bb-link/build/bb-link-${{matrix.board}}-${{env.VERSION}}.bin.sha256.txt)\"
}" > src/bb-link/build/ota/${{matrix.board}}/beta.json
- name: Upload binaries artifacts
VERSION="${GITHUB_REF_NAME#v}"
BINARY="bb-link-atomlite-${VERSION}.bin"
cp build/atomlite/bb-link.ino.bin "build/atomlite/${BINARY}"
sha256sum "build/atomlite/${BINARY}" | cut -d ' ' -f 1 > "build/atomlite/${BINARY}.sha256.txt"
mkdir -p build/ota/atomlite
SHA256="$(cat "build/atomlite/${BINARY}.sha256.txt")"
printf '{\n "version": "%s",\n "url": "https://github.com/%s/releases/download/v%s/%s",\n "sha256": "%s"\n}\n' \
"${VERSION}" "${GITHUB_REPOSITORY}" "${VERSION}" "${BINARY}" "${SHA256}" \
> build/ota/atomlite/beta.json

- name: Upload firmware
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4
with:
name: bin-${{matrix.board}}
path: src/bb-link/build/bb-link-${{matrix.board}}*
name: firmware-atomlite
path: |
build/atomlite/bb-link-atomlite-*.bin
build/atomlite/bb-link-atomlite-*.sha256.txt
if-no-files-found: error
- name: Upload ota artifacts

- name: Upload OTA manifest
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4
with:
name: ota-${{matrix.board}}
path: src/bb-link/build/ota/*/beta.json
name: ota-atomlite
path: build/ota/atomlite/beta.json
if-no-files-found: error

release:
if: startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: true
prerelease: true

- name: Download artifacts
- name: Download firmware
uses: actions/download-artifact@v4
with:
name: firmware-atomlite
path: artifacts
pattern: bin-*
merge-multiple: true
- name: Upload Release Assets
id: upload_release_assets
uses: dwenegar/upload-release-assets@v1

- name: Create prerelease
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
release_id: ${{ steps.create_release.outputs.id }}
assets_path: "./artifacts/*"
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" artifacts/* \
--prerelease \
--title "Release ${GITHUB_REF_NAME}"

publish-ota:
if: startsWith(github.ref, 'refs/tags/v')
needs: release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: ota-*
merge-multiple: true
- name: Setup git user/email
run: |
git config --global user.name github-actions
git config --global user.email github-actions@github.com
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ota-firmware
path: ota-firmware
- name: Publish OTA
fetch-depth: 0

- name: Download OTA manifest
uses: actions/download-artifact@v4
with:
name: ota-atomlite
path: ${{ runner.temp }}/ota-artifacts/atomlite

- name: Publish beta manifest
run: |
cp -r artifacts/* ota-firmware/
cd ota-firmware
[ -z "$(git status -s -- .)" ] && echo "No changes to be committed. Exiting." && exit 0
git add -- .
git commit -m "Add new beta firmware"
git push origin ota-firmware
echo "Done."
git config user.name github-actions
git config user.email github-actions@github.com
if git ls-remote --exit-code --heads origin ota-firmware; then
git fetch origin ota-firmware
git switch --force-create ota-firmware origin/ota-firmware
else
git switch --orphan ota-firmware
git rm -r --force .
fi
mkdir -p atomlite
cp "${RUNNER_TEMP}/ota-artifacts/atomlite/beta.json" atomlite/beta.json
git add atomlite/beta.json
if git diff --cached --quiet; then
exit 0
fi
git commit -m "Publish ${GITHUB_REF_NAME} beta firmware"
git push origin HEAD:ota-firmware
28 changes: 13 additions & 15 deletions .github/workflows/ota-publisher.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
name: OTA publish
name: Promote OTA beta
on:
workflow_dispatch:

jobs:
promote-beta-to-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ota-firmware

- name: Setup git user/email
run: |
git config --global user.name github-actions
git config --global user.email github-actions@github.com

- name: Checkout
uses: actions/checkout@v3
with:
ref: ota-firmware
path: ota-firmware
git config user.name github-actions
git config user.email github-actions@github.com

- name: Promote beta to release
run: |
cd ota-firmware
for dir in */; do
if [[ -f "${dir}beta.json" ]]; then
cp "${dir}beta.json" "${dir}latest.json"
cp "${dir}beta.json" "${dir}latest.json"
fi
done
[ -z "$(git status -s -- .)" ] && echo "No changes to be committed. Exiting." && exit 0
git add -- .
git add -- */latest.json
if git diff --cached --quiet; then
exit 0
fi
git commit -m "Promoted beta to release."
git push origin ota-firmware
echo "Done."
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ Thumbs.db
.vscode/

src/bb-link/build/
tests/build/

*.o
*.out
sketch.yaml
sketch.yaml
54 changes: 38 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,16 @@ To power the adapter from an iPhone or iPad:
The firmware keeps the CPU clock, Bluetooth transmit power, and status LED brightness low by default so the adapter draws less current from mobile devices. This assumes the board is powered through the ATOM-Lite USB-C port; USB-C source detection itself is handled by the board hardware, not firmware.

## Build
1. Install Arduino IDE [https://www.arduino.cc](https://www.arduino.cc)
2. Add additional board manager URLs in Arduino IDE Settings: `https://espressif.github.io/arduino-esp32/package_esp32_index.json`
3. Install the ESP32 board library by Espressif Systems
4. Install the M5Unified library
5. Install the FastLED library
6. Install the FreeRTOS library
7. Install the ArduinoQueue library
8. Install the ArduinoLog library
9. Clone this repository
10. Flash the code to the M5Stack ATOM-Lite board
11. Download the [B.B. Link Configurator](https://apps.apple.com/us/app/b-b-link-configurator/id6476163710) app on your phone
12. Download RadioMail on your phone [https://radiomail.app](https://radiomail.app)

1. Install [Arduino IDE](https://www.arduino.cc/en/software).
2. Add `https://espressif.github.io/arduino-esp32/package_esp32_index.json` to the additional Board Manager URLs.
3. Install **esp32 by Espressif Systems 3.3.11**.
4. Install M5Unified 0.2.19, FastLED 3.10.5, ArduinoQueue 1.2.5, and ArduinoLog 1.1.1.
5. Select **M5Atom** as the board (`esp32:esp32:m5stack_atom` in Arduino CLI).
6. Open `src/bb-link/bb-link.ino`, compile, and flash the ATOM Lite.
7. Install [B.B. Link Configurator](https://apps.apple.com/us/app/b-b-link-configurator/id6476163710) and [RadioMail](https://radiomail.app) on the iPhone or iPad.

The firmware advertises the compatibility name `B.B. Link` by default. A custom name can be supplied at build time with `ADAPTER_NAME`, but application-side rig-control detection must be verified if the name is changed.

### Rig Control

Expand Down Expand Up @@ -83,10 +81,9 @@ By default, the adapter sets the radio to KISS mode and automatically responds t

### Buttons
- **Main Button**:
- Long press: Power on/off
- Short press: Reconnect to radio and/or iOS device
- **Side Button**:
- Long press: Reboot the adapter
- Short press: Disconnect and retry the saved radio connection.
- Hold for 2 seconds while running: enter deep sleep. Press once to wake.
- Hold for 3 seconds during startup: request the five-minute BLE firmware-update window. The service is exposed only when the build permits signed OTA, or when unsigned physical-access OTA was explicitly enabled at build time.

### LED Indicators

Expand Down Expand Up @@ -120,6 +117,31 @@ Alternatively, you can reset the adapter by connecting it to a computer:
5. Monitor the response in the Serial Monitor, which will confirm the clearing of previously paired devices.
6. After the process is complete, disconnect the adapter from your PC.

## Firmware Update Security

The BLE OTA service is not advertised during normal operation. It requires a three-second physical button hold during startup, uses an encrypted BLE characteristic, closes after five minutes, and checks every ESP-IDF OTA operation for failure. New firmware is marked valid only after the bridge, Bluetooth Classic, BLE, preferences, button, and status indicator have initialized successfully.

Unsigned OTA is disabled in source builds by default. Production OTA builds should use ESP-IDF signed-app verification (`CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT`) and bootloader rollback (`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE`). Developers who accept the reduced security of a physically authorized unsigned update may explicitly build with `BB_LINK_ALLOW_UNSIGNED_OTA_WITH_PHYSICAL_ACCESS=1`.

ATOM Lite uses hardware board ID `3`. Its update manifests are published under `atomlite/` on the `ota-firmware` branch. The corresponding Configurator mapping is:

```swift
case 0x03:
return URL(string: "https://raw.githubusercontent.com/halka/bb-link/ota-firmware/atomlite/\(channel).json")
```

The release workflow builds only the ATOM Lite target and explicitly enables the physical-access unsigned OTA profile. It creates `bb-link-atomlite-<version>.bin`, its SHA-256 checksum, and the beta manifest. The separate **Promote OTA beta** workflow promotes `beta.json` to `latest.json` after validation. For higher-assurance deployments, replace that profile with a custom ESP-IDF core configured for signed-app verification and rollback.

## Tests

The KISS stream parser tests run without Arduino or EpoxyDuino:

```sh
make -C tests runtests
```

They cover offset frames, malformed and truncated commands, escaped data, multiple frames in one BLE write, and commands split across writes. The legacy hardware-abstraction tests remain available as `epoxy-tests` and accept `EPOXYDUINO_DIR` instead of relying on a fixed home-directory path.

## Troubleshooting

If the adapter connects to the radio but the radio does not transmit, check the TNC settings. Go to Menu > Configuration > Interface > KISS (983) and set it to Bluetooth.
Expand Down
Loading
Loading