View this page on MeshCore Docs.
LoRa OTA example: Station G2 on a Pi to an Ikoka Stick
For the automatic version using only the Pi's USB-connected source radio, start with the guided LoRa updater.
Update the Ikoka Stick's bootloader to OTAFIX 2.4.12, then install 1.18.0.1 Full Repeater firmware. The Station G2 stays connected to the Pi by USB and serves the files. Use the app's authenticated Repeater Admin console for the Ikoka Stick, through your existing Companion.
| Board | Job |
|---|---|
| Raspberry Pi | Runs the file server |
| Station G2 Repeater | USB-connected LoRa seeder |
| Ikoka Stick 33 dBm Repeater | Receives and installs both updates |
| Companion | Keeps the app connected to the Ikoka Stick's admin console |
This example requires a direct LoRa link between the Station G2 and Ikoka Stick. It keeps their existing main-channel settings during a four-hour TempRadio window. The Companion stays on that same channel. Only the Station G2 needs a USB connection to the Pi. For a route through intermediate repeaters, every relay also needs an overlapping TempRadio window; see the scripted update guide.
1. Check the Ikoka Stick
Run these in Ikoka Stick — Repeater Admin and save the replies:
ver
get bootloader.ver
ota self
ota qspi
ota bootloader status
ota config
ota key list
get radio
get radio.rxps.config
The example starts with these reported values:
| Check | Required value for this example |
|---|---|
| Application hardware / target | ikoka_stick_nrf_33dbm / E5331F02 |
| Installed bootloader | OTAFIX 2.4.11 |
| Bootloader identity | Board and target 28860044, device XIAO_DFU |
| Bootloader support | abi=3 caps=0E, QSPI apply OK, codecs=0x5 |
| Storage | QSPI store:2048K, working ota qspi probe |
These values support a remote bootloader upgrade. A stock bootloader or missing QSPI apply support needs the matching local installation first. The Stick uses its onboard flash; no additional storage is needed.
If get radio.rxps.config is unavailable, record get radio.rxps instead.
Keep each board's original RXPS and ota config replies for cleanup.
2. Install the Pi tools
On Raspberry Pi — terminal, install the host dependencies:
sudo apt update
sudo apt install -y build-essential pkg-config libudev-dev libssl-dev curl unzip pipx python3-venv
pipx ensurepath
pipx install 'meshcore-cli>=1.6.0'
Install stable Rust if it is not already installed:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/meshcore-rustup.sh
sh /tmp/meshcore-rustup.sh -y --profile minimal
Open a new terminal so the Pi finds meshcli and Cargo. Check that both Rust
and Cargo are 1.86 or newer, then install the supported motatool revision:
rustc --version
cargo --version
cargo install --git https://github.com/mikecarper/motatool.git \
--rev 8c38369e7d35ad50cf74261869676d52dd24adf7 --locked --bin motatool
meshcli -v
motatool --version
mkdir -p ~/ikoka-stick-ota/downloads ~/ikoka-stick-ota/inputs ~/ikoka-stick-ota/serve
cd ~/ikoka-stick-ota
3. Prepare the two update files
On Raspberry Pi — terminal, download the signed bootloader bundle:
BOOT_URL=https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/v0.11.0-OTAFIX2.4.12
curl -fL "$BOOT_URL/OTAFIX-2.4.12-bootloader-mota.zip" -o downloads/bootloader-2.4.12.zip
unzip -q downloads/bootloader-2.4.12.zip -d inputs/bootloader-2.4.12
cp inputs/bootloader-2.4.12/mota/update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.12.mota serve/
BOOT_MOTA=serve/update-xiao_nrf52840_ble_bootloader-v0.11.0-OTAFIX2.4.12.mota
OTAFIX_PUBLIC_KEY=272564CC588D3D122285A15E6E2566D2ABE7177BB7EA1D1E41B23B29F0F85D2D
BOOT_KEY_FILE=inputs/bootloader-2.4.12/OTAFIX_MOTA_SIGNING_PUBLIC_KEY.txt
test "$(tr -d '[:space:]' < "$BOOT_KEY_FILE")" = "$OTAFIX_PUBLIC_KEY"
motatool verify "$BOOT_MOTA" --pub "$BOOT_KEY_FILE"
motatool inspect "$BOOT_MOTA"
The bootloader package must be signed, format 3, for XIAO nRF52840 BLE /
XIAO_DFU, target 28860044, version 2.4.12. The key comparison and package
verification must both pass before continuing.
Download the exact ordinary Ikoka Stick 33 dBm Full Repeater application DFU ZIP on Raspberry Pi — terminal:
APP_URL=https://github.com/mikecarper/MeshCore/releases/download/lora-ota-2-v1.18.0.1-halo-keymind-cascade-dev-03214a16
curl -fL "$APP_URL/ikoka_stick_nrf_33dbm_repeater-full-ota-v1.18.0.1-halo-keymind-cascade-dev-03214a16.zip" \
-o downloads/ikoka-stick-33dbm-repeater.zip
For a later release, use the firmware picker to select the same board, Repeater role, Full profile, and application DFU ZIP.
On Raspberry Pi — terminal, extract the application binary named by the DFU manifest and check its firmware identity trailer:
python3 - <<'PY'
import hashlib, json, struct, zipfile
from pathlib import Path
with zipfile.ZipFile('downloads/ikoka-stick-33dbm-repeater.zip') as archive:
app = json.loads(archive.read('manifest.json'))['manifest']['application']
image = archive.read(app['bin_file'])
trailer = image[-56:]
assert len(trailer) == 56 and trailer[:4] == b'EndF', 'Missing EndF'
assert struct.unpack_from('<I', trailer, 4)[0] == len(image) - 56
assert hashlib.sha256(image[:-56]).digest()[:8] == trailer[8:16], 'Body hash mismatch'
assert struct.unpack_from('<I', trailer, 20)[0] == 0xE5331F02, 'Wrong application target'
assert trailer[24:].rstrip(b'\0') == b'ikoka_stick_nrf_33dbm', 'Wrong board'
Path('inputs/ikoka-stick-33dbm-repeater.bin').write_bytes(image)
print('Expected post-install base_hash:', trailer[8:16].hex().upper())
PY
Save the printed base_hash. Stop if extraction or any identity check fails.
Then make a full application package from that binary:
motatool build --fw inputs/ikoka-stick-33dbm-repeater.bin \
--out serve/ikoka-stick-33dbm-repeater.mota
motatool verify serve/ikoka-stick-33dbm-repeater.mota
motatool inspect serve/ikoka-stick-33dbm-repeater.mota
Check application target E5331F02, hardware ikoka_stick_nrf_33dbm, and the
expected newer firmware version. Save its eight-character merkle_root as the application
manifest ID. The package's image_hash covers the complete image; the
post-install base_hash is the body hash printed above. A full QSPI
application update needs no copy of the old firmware.
This locally packaged application is unsigned and uses explicit manual
installation; the separate bootloader package remains signed.
Optional: differential firmware upgrade (smaller download)
A differential update sends a patch from the Ikoka Stick's exact running application to the new application. It can reduce LoRa transfer time. The bootloader update in step 7 still uses the signed full bootloader package.
Prepare the new binary and full package above first. Use this option only when you also have the original application DFU ZIP for the firmware currently running on the Ikoka Stick. A matching version label or a rebuild of the same commit is not enough. If that original file is unavailable, use the full update above.
On Ikoka Stick — Repeater Admin, get its current identity:
ver
ota self
Copy the original application DFU ZIP onto Raspberry Pi as
downloads/ikoka-stick-33dbm-repeater-running.zip. In the Pi terminal, paste
the 16-character base_hash from that fresh ota self reply when prompted:
read -r -p 'Current Ikoka Stick base_hash (16 hex characters): ' IKOKA_BASE_HASH
export IKOKA_BASE_HASH
python3 - <<'PY'
import hashlib, json, os, re, struct, zipfile
from pathlib import Path
expected = os.environ['IKOKA_BASE_HASH'].strip().upper()
assert re.fullmatch(r'[0-9A-F]{16}', expected), 'Invalid base_hash'
with zipfile.ZipFile('downloads/ikoka-stick-33dbm-repeater-running.zip') as archive:
app = json.loads(archive.read('manifest.json'))['manifest']['application']
image = archive.read(app['bin_file'])
trailer = image[-56:]
assert len(trailer) == 56 and trailer[:4] == b'EndF', 'Missing EndF'
assert struct.unpack_from('<I', trailer, 4)[0] == len(image) - 56
assert hashlib.sha256(image[:-56]).digest()[:8] == trailer[8:16], 'Body hash mismatch'
assert struct.unpack_from('<I', trailer, 20)[0] == 0xE5331F02, 'Wrong application target'
assert trailer[24:].rstrip(b'\0') == b'ikoka_stick_nrf_33dbm', 'Wrong board'
assert trailer[8:16].hex().upper() == expected, 'This is not the running firmware'
Path('inputs/ikoka-stick-33dbm-repeater-running.bin').write_bytes(image)
print('Exact running application confirmed:', expected)
PY
Stop if any check fails. Then build and check the in-place nRF52 delta:
motatool build \
--base inputs/ikoka-stick-33dbm-repeater-running.bin \
--fw inputs/ikoka-stick-33dbm-repeater.bin \
--patch-type in-place \
--out inputs/ikoka-stick-33dbm-repeater-delta.mota
motatool verify inputs/ikoka-stick-33dbm-repeater-delta.mota
motatool inspect inputs/ikoka-stick-33dbm-repeater-delta.mota
ls -lh serve/ikoka-stick-33dbm-repeater.mota inputs/ikoka-stick-33dbm-repeater-delta.mota
Check codec_id is 2 (detools-in-place), base_hash matches the live Ikoka
Stick, target is E5331F02, and the new version/hardware match the full
package. Let motatool derive the safe workspace from the firmware; do not
override its memory or target checks. Compare package sizes and keep the full
update if the delta is not smaller. verify checks the delta container's
integrity; it does not prove that you selected the running base image.
To serve the delta, replace the full application package in the serving folder before starting motatool:
cp inputs/ikoka-stick-33dbm-repeater-delta.mota serve/ikoka-stick-33dbm-repeater.mota
Continue with steps 4–9. In step 8, choose the application, delta row and
use its own eight-character manifest ID with ota pull <APP_MID8> flash,
then ota install when ready. The target's base_hash must still match
immediately before that pull. Updating only the bootloader in step 7 leaves
the running application unchanged, so it does not invalidate the delta base.
After installation, check the same new firmware version and expected
post-install body hash printed earlier in step 3.
4. Identify the Station G2's USB port
On Raspberry Pi — terminal:
ls -l /dev/serial/by-id/
Select the Station G2's actual entry. Set G2_PORT to that complete path;
/dev/ttyUSB0 below is only an example:
G2_PORT=/dev/ttyUSB0
meshcli -r -s "$G2_PORT" -b 115200 "board"
meshcli -r -s "$G2_PORT" -b 115200 "ver"
meshcli -r -s "$G2_PORT" -b 115200 "ota status"
meshcli -r -s "$G2_PORT" -b 115200 "get radio"
meshcli -r -s "$G2_PORT" -b 115200 "get radio.rxps.config"
meshcli -r -s "$G2_PORT" -b 115200 "ota config"
Confirm that this is the Station G2 and that its firmware includes LoRa OTA.
Save its radio, RXPS, and OTA settings. If access is denied, add your Pi login
to dialout with sudo usermod -aG dialout "$(id -un)", then log out and in.
Close other programs using that USB port.
5. Open the OTA windows and trust the bootloader signer
The commands below assume the Station G2, Ikoka Stick, and Companion already use 910.525 MHz, BW 62.5 kHz, SF7, CR5. If yours differ, use their current shared tuple in every TempRadio command below. Preserve an explicit preamble if one is configured by adding it after the duration. This keeps admin access on the existing channel; TempRadio is still required to enable LoRa OTA.
On Ikoka Stick — Repeater Admin:
ota key add 272564CC588D3D122285A15E6E2566D2ABE7177BB7EA1D1E41B23B29F0F85D2D
ota key list
ota config autofetch off
ota config autoinstall off
ota config hops 0
set radio.rxps off
get radio.rxps
tempradio 910.525,62.5,7,5,240
get tempradio
On Raspberry Pi — Station G2 USB CLI:
meshcli -r -s "$G2_PORT" -b 115200 "ota config autofetch off"
meshcli -r -s "$G2_PORT" -b 115200 "ota config autoinstall off"
meshcli -r -s "$G2_PORT" -b 115200 "ota config hops 0"
meshcli -r -s "$G2_PORT" -b 115200 "set radio.rxps off"
meshcli -r -s "$G2_PORT" -b 115200 "get radio.rxps"
meshcli -r -s "$G2_PORT" -b 115200 "tempradio 910.525,62.5,7,5,240"
meshcli -r -s "$G2_PORT" -b 115200 "get tempradio"
Both receivers must report RXPS off and an active temporary tuple. Wait if
get tempradio says pending; do not start serving while it says off.
The hops 0 setting is for this direct link. These configuration changes are
saved; the TempRadio window is temporary.
6. Start the Station G2 seeder
On Raspberry Pi — terminal, leave this running in the foreground:
motatool serve --dir ./serve --serial "$G2_PORT" --baud 115200 -v
It attaches the folder and the Station G2 advertises the two packages. Wait for a successful folder attachment/catalog count. While motatool owns USB, send later commands through Ikoka Stick — Repeater Admin. Keep the Pi and both repeaters powered through each transfer and installation.
7. Update the Ikoka Stick bootloader
On Ikoka Stick — Repeater Admin:
ota ls
Discovery is asynchronous. Wait a few seconds and repeat it. Select the
bootloader row for XIAO target 28860044, version 2.4.12. Copy its
eight-character manifest ID into BOOT_MID8 below; placeholders must be
replaced with the actual values, without angle brackets:
ota pull <BOOT_MID8> flash
ota status
Check progress until it reports ready to install, then run:
ota bootloader status
Confirm staged:ready. Copy that reply's exact mid and 16-character hash:
ota bootloader install <BOOT_MID8> <BOOT_HASH16>
Wait for the reboot, reconnect in the app, and check:
get bootloader.ver
ota bootloader status
ota status
ver
Expect OTAFIX 2.4.12, the same XIAO_DFU identity, ABI 3 / capabilities
0E, and blup:C8. The application version should still be the old version.
8. Update the Ikoka Stick firmware
The bootloader reboot cleared the Ikoka Stick's temporary window. On Ikoka Stick — Repeater Admin, reopen it before downloading firmware:
get radio.rxps
tempradio 910.525,62.5,7,5,240
get tempradio
Wait until get tempradio shows the active tuple, repeating the query if it
says pending. Then run:
ota ls
Keep the Station G2 seeder running. Select the application, full row with
target E5331F02 and the version inspected in step 3. Use its stable manifest
ID, not its list position or the bootloader ID:
ota pull <APP_MID8> flash
ota status
Repeat ota status until ready to install, then run:
ota install
After the application reboots, reconnect and check:
ver
get bootloader.ver
ota self
ota status
Confirm the selected firmware version, bootloader 2.4.12, working QSPI
apply support, and the base_hash recorded from the application package.
The Ikoka Stick has returned to its saved main radio settings.
9. Stop serving and restore settings
On Raspberry Pi — terminal, press Ctrl+C and wait for motatool to detach the folder and return to the shell. Then run:
meshcli -r -s "$G2_PORT" -b 115200 "ota folder"
meshcli -r -s "$G2_PORT" -b 115200 "normalradio"
meshcli -r -s "$G2_PORT" -b 115200 "get tempradio"
Confirm the host folder detached. Restore each board's original RXPS, hop limit, autofetch, and autoinstall settings from steps 1 and 4. For the example starting values, on Ikoka Stick — Repeater Admin:
set radio.rxps level 8 preamble 16
ota config hops 3
ota config autofetch off
ota config autoinstall off
get radio.rxps.config
ota config
get tempradio
Use the Station G2's own recorded values through meshcli -r as in step 5.
An original RXPS value of off stays off; for an automatic preamble (0),
restore with set radio.rxps level N; fixed manual timings use
set radio.rxps RX_US SLEEP_US. Retain the official signing key for future
bootloader updates. Both boards should now report get tempradio as off.
If a transfer stops
- No update rows: check the seeder attachment, matching radio settings, direct reachability, and active TempRadio windows on both boards.
- Four-hour window expired: stop motatool before reopening the G2 USB CLI, renew both windows, restart the same seeder, and pull the same MID. Keep the same package; current firmware can resume committed blocks.
- Signature/key error: verify the signed package against its trusted publisher key. Do not substitute an arbitrary key from an update row.
- Download failed or wrong selection: inspect
ota status; useota cancelto discard that download before selecting another package. - No completed install: keep the running firmware until the destination reports a verified, ready package. See the bootloader guide or firmware guide for the reported error.