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
104 changes: 104 additions & 0 deletions tools/flash/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Flashing tools

Keep the earables powered by USB or a charged battery. J-Link scripts require
SEGGER's J-Link software and Nordic's `nrfjprog` on `PATH`; list probes with
`nrfjprog --ids`. Run the existing single-device scripts from the repository
root.

| Script | Purpose | Images / connection |
| --- | --- | --- |
| `flash_fota.sh` | Flash one device with FOTA support | `build_fota/`, one J-Link |
| `flash_fota.ps1` | PowerShell equivalent of `flash_fota.sh` | `build_fota/`, one J-Link |
| `flash.sh` | Flash one device with the standard build | `build/`, one J-Link |
| `recover.sh` | Erase/unlock both nRF5340 cores; does not install firmware | One J-Link |
| `mcumgr_upload.sh` | Upload and test signed application/network images, then reset | Serial SMP connection; no J-Link |
| `flash_pair.sh` | Flash two devices concurrently and provision a left/right pair | `build_fota/`, two J-Links |

The J-Link flashers require both merged HEX files:
`merged_openearable_v2_nrf5340_cpuapp.hex` and
`merged_openearable_v2_nrf5340_cpunet.hex`. They also accept the older pair of
names, `merged.hex` and `merged_CPUNET.hex`. Build with merged images enabled
(`SB_CONFIG_MERGED_HEX_FILES=y`). A FOTA ZIP alone is not a complete J-Link image.

## One device: `flash_fota.sh`, `flash.sh`, `flash_fota.ps1`

```bash
# Retain the existing UICR side, hardware and pair configuration:
bash tools/flash/flash_fota.sh --snr 123456789
# Configure a left device explicitly:
bash tools/flash/flash_fota.sh --snr 123456789 --left --hw 2.0.1
```

| Bash option | PowerShell option | Meaning |
| --- | --- | --- |
| `--snr SERIAL` | `-Snr SERIAL` | Required J-Link serial |
| `--left` / `--right` | `-Left` / `-Right` | Choose one side; omit both to back up and restore UICR |
| `--hw x.y.z` | `-Hw x.y.z` | Requires a side; defaults to `2.0.0` when a side is supplied |
| `--standalone` | `-Standalone` | Requires a side; configure independent operation |
| — | `-Chip NRF53` | Chip family; default `NRF53` |
| — | `-Clockspeed 8000` | SWD speed in kHz; default `8000` |

All three erase phone bonds. Supplying a side skips UICR restoration, so existing
pair configuration is erased. Omitting both side flags restores UICR, but does
not preserve phone bonds. The single-device scripts share
`tools/flash/uicr_backup.hex`; do not run them concurrently. The FOTA scripts use
a pin reset, a five-second wait and an application reset; `flash.sh` uses a
single system reset.

## Recovery and serial updates

`bash tools/flash/recover.sh --snr SERIAL` accepts only `--snr`. It recovers both
cores and erases their firmware, UICR configuration and bonds. Reflash afterwards
with the desired side and hardware revision. Inspect both recovery command
results; this helper does not stop automatically if the first command fails.

`bash tools/flash/mcumgr_upload.sh` takes no command-line options. It requires
`mcumgr` and firmware exposing serial SMP. Edit its hard-coded serial port,
connection name, baud rate/MTU and signed-image paths before use. It uploads
image 0 and image 1, extracts hashes from `image list`, marks both for a test
boot, then resets. Check the hash selection against your device's image list;
this helper assumes a particular output order and does not stop on errors.

## Two devices: `flash_pair.sh`

This script can run from any directory and requires Bash 3.2 or later
(macOS/Linux), with no Python dependency.

```bash
bash tools/flash/flash_pair.sh --left-snr 123456789 --right-snr 987654321 --hw 2.0.1
```

| Option | Meaning |
| --- | --- |
| `--left-snr SERIAL` | Required J-Link serial for the left device |
| `--right-snr SERIAL` | Required, different J-Link serial for the right device |
| `--hw x.y.z` | Hardware revision for both devices; default `2.0.1`, each component 0–255 |
| `--build-dir DIR` | Directory containing the merged images; default is the repository's `build_fota/` |
| `--dry-run` | Validate arguments/image paths and print the plan; no hardware access |
| `-h`, `--help` | Print usage |

Set `NRFJPROG=/path/to/nrfjprog` to select a particular executable or wrapper.
The script programs the files in the selected directory; it does not download
firmware, compile it, or change its embedded firmware version.

Both devices are fully erased, including phone bonds and previous pairing
configuration. Separate original UICR backups are saved first. The new pair ID
is the XOR of the low 32 bits of the two factory chip IDs, matching normal
earable discovery. The script writes left/right, that shared ID and the hardware
revision; standalone mode remains disabled. This provisions the coordinated-set
identity, not phone bonding keys. Stock 2.3.0 ignores this stored identity for
CSIP; use the 2.3.1 fix in [#369](https://github.com/OpenEarable/open-earable-2/pull/369)
or firmware that honors the UICR pair identity.

Each run saves per-device logs, UICR backups, chip IDs, `pair.conf` and successful
verification stages under the selected build directory's `pair-logs/`. Both
devices must pass image verification and configuration readback before final
resets. A specific Nordic worker-process crash is retried once; other failures
stop the operation. A successful exit confirms programming and reset commands,
not runtime Bluetooth pairing or charging behavior.

Per-probe lock directories in `${TMPDIR:-/tmp}` block overlapping pair-script
runs by the same user. On interruption, the script waits for the current workers
before releasing locks. If a force-killed process leaves a lock, check its `pid`
file and ensure no flashing process is running before removing it. Do not run
other programming tools on these probes concurrently.
218 changes: 218 additions & 0 deletions tools/flash/flash_pair.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#!/bin/bash
# Provision a left/right nRF5340 pair with two J-Links (Bash 3.2+, macOS/Linux).
set -euo pipefail

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
BUILD_DIR="$SCRIPT_DIR/../../build_fota"
LEFT_SNR=
RIGHT_SNR=
HW_VERSION=2.0.1
DRY_RUN=false
LOCKS=()
WORKERS=()
RUN_DIR=

usage() {
cat <<'EOF'
Usage: flash_pair.sh --left-snr SERIAL --right-snr SERIAL [options]
--left-snr SERIAL J-Link attached to the left earable (required)
--right-snr SERIAL J-Link attached to the right earable (required)
--hw x.y.z Hardware revision for both devices (default: 2.0.1)
--build-dir DIR Merged FOTA images (default: repository's build_fota)
--dry-run Validate inputs and show the plan without accessing hardware
-h, --help Show this help

Requires nrfjprog on PATH, or NRFJPROG=/path/to/nrfjprog.
Both devices are fully erased, including phone bonds. Their pair identity is
derived from their chip IDs, and both are configured for paired operation.
EOF
}

die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}

while [[ $# -gt 0 ]]; do
case "$1" in
--left-snr|--right-snr|--hw|--build-dir)
[[ $# -ge 2 && -n "$2" ]] || die "Missing value for $1"
case "$1" in
--left-snr) LEFT_SNR=$2 ;;
--right-snr) RIGHT_SNR=$2 ;;
--hw) HW_VERSION=$2 ;;
--build-dir) BUILD_DIR=$2 ;;
esac
shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown option: $1 (use --help)" ;;
esac
done

# Normalize decimal serials without arithmetic overflow or octal interpretation.
normalize_serial() {
local value=$1
[[ "$value" =~ ^[0-9]+$ ]] || die "J-Link serial must be a positive number"
while [[ "$value" == 0* ]]; do value=${value#0}; done
[[ -n "$value" ]] || die "J-Link serial must be a positive number"
printf '%s\n' "$value"
}
LEFT_SNR=$(normalize_serial "$LEFT_SNR")
RIGHT_SNR=$(normalize_serial "$RIGHT_SNR")
[[ "$LEFT_SNR" != "$RIGHT_SNR" ]] || die "Left and right must use different J-Links"
[[ "$HW_VERSION" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || die "Hardware version must be x.y.z (components 0..255)"
IFS=. read -r HW_MAJOR HW_MINOR HW_PATCH <<< "$HW_VERSION"
HW_MAJOR=$((10#$HW_MAJOR)); HW_MINOR=$((10#$HW_MINOR)); HW_PATCH=$((10#$HW_PATCH))
(( HW_MAJOR <= 255 && HW_MINOR <= 255 && HW_PATCH <= 255 )) || die "Hardware version components must be 0..255"
printf -v HW_WORD '%02X%02X%02X00' "$HW_MAJOR" "$HW_MINOR" "$HW_PATCH"

[[ -d "$BUILD_DIR" ]] || die "Build directory does not exist: $BUILD_DIR"
BUILD_DIR=$(cd -- "$BUILD_DIR" && pwd -P)
NET_HEX="$BUILD_DIR/merged_openearable_v2_nrf5340_cpunet.hex"
APP_HEX="$BUILD_DIR/merged_openearable_v2_nrf5340_cpuapp.hex"
if [[ ! -f "$NET_HEX" || ! -f "$APP_HEX" ]]; then
NET_HEX="$BUILD_DIR/merged_CPUNET.hex"
APP_HEX="$BUILD_DIR/merged.hex"
fi
[[ -s "$NET_HEX" && -s "$APP_HEX" ]] || die "Missing merged application/network images in $BUILD_DIR"
printf 'Left: %s; right: %s; hardware: %s\nImages: %s\n' "$LEFT_SNR" "$RIGHT_SNR" "$HW_VERSION" "$BUILD_DIR"
printf '%s\n' 'Both devices will be erased, including phone bonds, and provisioned as a new pair.'
if "$DRY_RUN"; then
printf '%s\n' 'Plan: read chip IDs -> back up UICR -> flash both cores in parallel -> write/read back pair settings -> reset both. No hardware accessed.'
exit 0
fi
NRFJPROG=$(command -v "${NRFJPROG:-nrfjprog}") || die "nrfjprog not found; add it to PATH or set NRFJPROG"

# Keep locks until both workers have stopped, including on interruption. A
# SIGKILL can leave a lock behind; never remove another process's lock here.
cleanup() {
local status=$? pid lock
trap - EXIT
trap '' INT TERM
# The guarded expansions also support empty arrays with Bash 3.2 nounset.
for pid in ${WORKERS[@]+"${WORKERS[@]}"}; do wait "$pid" || :; done
for lock in ${LOCKS[@]+"${LOCKS[@]}"}; do
rm -f -- "$lock/pid"
rmdir -- "$lock" || :
done
if [[ $status -ne 0 && -n "$RUN_DIR" ]]; then
printf 'Pair operation incomplete. Inspect logs before retrying: %s\n' "$RUN_DIR" >&2
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

# Per-probe locks also prevent two versions/build directories racing each other.
for snr in "$LEFT_SNR" "$RIGHT_SNR"; do
lock="${TMPDIR:-/tmp}/openearable-jlink-$UID-$snr.lock"
mkdir -m 700 -- "$lock" 2>/dev/null || die "Probe is locked: $lock (check its pid file before removing a stale lock)"
LOCKS+=("$lock")
printf '%s\n' "$$" > "$lock/pid"
done
AVAILABLE=$("$NRFJPROG" --ids) || die "Could not enumerate connected J-Links"
for snr in "$LEFT_SNR" "$RIGHT_SNR"; do
printf '%s\n' "$AVAILABLE" | awk -v snr="$snr" '$1 == snr && NF == 1 { found=1 } END { exit !found }' || die "J-Link $snr is not connected"
done
mkdir -p -- "$BUILD_DIR/pair-logs"
RUN_DIR=$(mktemp -d "$BUILD_DIR/pair-logs/$(date +%Y%m%d-%H%M%S).XXXXXX")
printf 'Logs and original UICR backups: %s\n' "$RUN_DIR"

# Run one probe command, retaining its output for memory parsing and diagnosis.
# Only the known Nordic worker-process crash is retried, once with identical args.
run() {
local side=$1 snr=$2 attempt status
shift 2
for attempt in 1 2; do
{
printf '\n%s $ ' "$(date +%Y-%m-%dT%H:%M:%S%z)"
printf '%q ' "$NRFJPROG" "$@" -f NRF53 --snr "$snr" --clockspeed 8000
printf '\n'
} >> "$RUN_DIR/$side.log"
status=0
"$NRFJPROG" "$@" -f NRF53 --snr "$snr" --clockspeed 8000 > "$RUN_DIR/$side.last-output" 2>&1 || status=$?
cat "$RUN_DIR/$side.last-output" >> "$RUN_DIR/$side.log"
if [[ $status -eq 0 ]]; then return 0; fi
if [[ $attempt -eq 1 && $status -eq 63 ]] && grep -q 'Worker process is dead' "$RUN_DIR/$side.last-output"; then
printf '%s: Nordic worker exited; retrying this command once.\n' "$side"
sleep 1
else
die "$side: command failed ($status); see $RUN_DIR/$side.log"
fi
done
}

# Extract exactly count hexadecimal words from the requested nrfjprog row.
read_words() {
local side=$1 address=$2 count=$3
awk -v address="$address:" -v count="$count" '
toupper($1) == toupper(address) {
for (i=2; i<=count+1; i++) {
if (length($i) != 8 || $i !~ /^[0-9a-fA-F]+$/) exit 1
printf "%s%s", toupper($i), (i==count+1 ? "\n" : " ")
}
found=1; exit
}
END { if (!found) exit 1 }
' "$RUN_DIR/$side.last-output"
}

# Start one worker per probe and wait for BOTH before advancing the phase.
# Do not call this function in a conditional: workers rely on Bash errexit.
parallel() {
local step=$1 left_pid right_pid left_status=0 right_status=0
(trap - EXIT INT TERM; "$step" left "$LEFT_SNR" 0) &
left_pid=$!
WORKERS=("$left_pid")
(trap - EXIT INT TERM; "$step" right "$RIGHT_SNR" 1) &
right_pid=$!
WORKERS+=("$right_pid")
wait "$left_pid" || left_status=$?
wait "$right_pid" || right_status=$?
WORKERS=()
[[ $left_status -eq 0 && $right_status -eq 0 ]] || die "$step failed (left=$left_status, right=$right_status)"
}

identify() {
local side=$1 snr=$2
run "$side" "$snr" --memrd 0x00FF0204 --n 8 --coprocessor CP_APPLICATION
read_words "$side" 0x00FF0204 2 > "$RUN_DIR/$side.chip-id"
run "$side" "$snr" --readuicr "$RUN_DIR/$side-original-uicr.hex" --coprocessor CP_APPLICATION
[[ -s "$RUN_DIR/$side-original-uicr.hex" ]] || die "$side: UICR backup is missing or empty"
}
parallel identify
read -r LEFT_LOW LEFT_HIGH < "$RUN_DIR/left.chip-id"
read -r RIGHT_LOW RIGHT_HIGH < "$RUN_DIR/right.chip-id"
[[ "$LEFT_LOW $LEFT_HIGH" != "$RIGHT_LOW $RIGHT_HIGH" ]] || die "Both probes report the same chip identity"
PAIR_ID=$((16#$LEFT_LOW ^ 16#$RIGHT_LOW))
(( PAIR_ID != 0 && PAIR_ID != 0xFFFFFFFF )) || die "Derived pair identity is reserved"
printf -v PAIR_WORD '%08X' "$PAIR_ID"
printf 'LEFT_SNR=%s\nRIGHT_SNR=%s\nPAIR_ID=%s\nHARDWARE_WORD=%s\n' "$LEFT_SNR" "$RIGHT_SNR" "$PAIR_WORD" "$HW_WORD" > "$RUN_DIR/pair.conf"
printf 'Pair identity: %s. Programming both devices...\n' "$PAIR_WORD"

program() {
local side=$1 snr=$2 channel=$3 actual expected
run "$side" "$snr" --program "$NET_HEX" --chiperase --verify --coprocessor CP_NETWORK
run "$side" "$snr" --program "$APP_HEX" --chiperase --verify --coprocessor CP_APPLICATION
run "$side" "$snr" --memwr 0x00FF80F4 --val "$channel" --coprocessor CP_APPLICATION
run "$side" "$snr" --memwr 0x00FF80F8 --val "0x$PAIR_WORD" --coprocessor CP_APPLICATION
run "$side" "$snr" --memwr 0x00FF8100 --val "0x$HW_WORD" --coprocessor CP_APPLICATION
run "$side" "$snr" --memrd 0x00FF80F4 --n 16 --coprocessor CP_APPLICATION
actual=$(read_words "$side" 0x00FF80F4 4)
printf -v expected '%08X %s FFFFFFFF %s' "$channel" "$PAIR_WORD" "$HW_WORD"
[[ "$actual" == "$expected" ]] || die "$side: configuration mismatch ($actual != $expected)"
printf '%s: both images and pair configuration verified.\n' "$side"
}
parallel program
printf 'IMAGES_VERIFIED=true\nPAIR_CONFIGURATION_VERIFIED=true\n' > "$RUN_DIR/verification.conf"

pin_reset() { run "$1" "$2" --pinreset; }
app_reset() { run "$1" "$2" --reset --coprocessor CP_APPLICATION; }
parallel pin_reset
# Let the bootloader finish before setting the application's SREQ reset reason.
sleep 5
parallel app_reset
printf 'RESET_COMMANDS_SUCCEEDED=true\nBLUETOOTH_RUNTIME_VERIFIED=false\n' >> "$RUN_DIR/verification.conf"
printf '%s\n' 'Both devices flashed and provisioned as left/right; resets succeeded. Verify Bluetooth startup on your phone.'
Loading