The PicoCalc's radio recovers from a broken frame, and its flash path works as the guide says
The XIAO stopped answering on 10-07 ("No response from meshcore
node" for many starts in a row). MeshCore's serial parser reads '<'
and a two-byte length, then waits for that many bytes with no time
limit, so one lost or damaged byte can make it swallow up to 65,535
bytes, and MeshTerm sends only 16 per start. Reproduced on the
PicoCalc: a half frame blocked two APP_STARTs until 600 filler bytes
arrived. meshcore-frame-timeout.patch drops a frame after 200 ms of
silence on the line (bytes queued while the loop was busy still
count), and build-firmware.sh applies it after meshcore-uart1.patch.
Upstream dev and main still have the old parser.
Flashing the XIAO with the new firmware, following the guide as
written, found these problems, now corrected:
- Step 11: `pip install platformio` failed (no pip outside a venv on
this PC), and it fails on externally managed Pythons or lands in an
active venv. The guide now uses the official installer only, and
build-firmware.sh and flash.py find pio in ~/.platformio/penv, so
nobody edits the PATH. uf2conv runs on PlatformIO's Python, because
a bare `python` is missing on many Linux systems.
- Step 12: the script's last line said to double-tap reset first;
flash.py touches the XIAO itself, and a wired XIAO's reset button is
inside the case.
- Step 13: this XIAO's bootloader has only a serial port, and flash.py
printed a pio command for the user to run "from your MeshCore
checkout", with pio not on the PATH. flash.py now does the serial
DFU upload itself (54 s here). Without pyserial, it runs itself
again with PlatformIO's Python instead of skipping the touch.
- A wired XIAO's USB power goes backward into the Lyra through the
3V3 wire (JP confirmed it on the bench). Step 13 now applies only
before wiring, and a new Day-to-day procedure updates the firmware
of a wired radio: poweroff, switch off, open, batteries out, flash.
After the flash, the radio kept its identity, MeshTerm's info command
connected, and an APP_START after a half frame and 0.5 s of silence
got its reply at once.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2bb563605f
commit
0d08e4bb5e
6 files changed
+320
-72
No files matched your search
@@ -52,6 +52,7 @@ the machine, and why each step is as it is. On the Lyra, only one program can ho
|
||||
| [`xiao-radio/build-firmware.sh`](../../scripts/picocalc-lyra/xiao-radio/build-firmware.sh) | development machine | Clones and patches MeshCore, builds the `Xiao_nrf52_companion_radio_serial` environment, and makes the `.uf2` file. |
|
||||
| [`xiao-radio/flash.py`](../../scripts/picocalc-lyra/xiao-radio/flash.py) | development machine | Flashes the `.uf2` file to the XIAO through its UF2 bootloader. It refuses the wrong board or SoftDevice. When the bootloader gives no drive, it prints the serial-DFU command. |
|
||||
| [`xiao-radio/meshcore-uart1.patch`](../../scripts/picocalc-lyra/xiao-radio/meshcore-uart1.patch) | none | The two firmware hunks: the companion on `Serial1` of the nRF52, and I²C moved off the UART pads. It is not upstream yet. |
|
||||
| [`xiao-radio/meshcore-frame-timeout.patch`](../../scripts/picocalc-lyra/xiao-radio/meshcore-frame-timeout.patch) | none | The serial parser of the companion drops a frame whose bytes stop for 200 ms. Thus one lost byte cannot make the radio ignore each command after it. It is not upstream yet. |
|
||||
| [`xiao-radio/lyra-setup.sh`](../../scripts/picocalc-lyra/xiao-radio/lyra-setup.sh) | the Lyra (root) | A UART1 → GP4/GP5 mux service that stays after a reboot, membership of `dialout`, and a default MeshTerm serial profile for `/dev/ttyS1`. |
|
||||
| [`xiao-radio/uart1-mux.py`](../../scripts/picocalc-lyra/xiao-radio/uart1-mux.py) | the Lyra (root) | The matrix-IO register poke that routes UART1 to the header pads. The service above runs it once at each boot. |
|
||||
| [`uconsole/meshterm-spi-bridge`](../../scripts/uconsole/meshterm-spi-bridge) | the radio host (your user) | The **always-on** option. It runs a mesh node in software over an SX1262 on an SPI bus (uConsole AIO, Waveshare HATs). It serves the companion protocol on `127.0.0.1:5000`. Thus the node stays on the mesh while MeshTerm is closed. It has a setup menu, a preflight check, and an optional `systemd --user` service. It runs on `openhop_core` (preferred) or on its predecessor `pymc_core`. The manual describes the venv that it needs on a bookworm uConsole. Most users must use `meshterm --spi` instead. It needs no script and no service, and MeshTerm drives the chip itself while MeshTerm runs. |
|
||||
|
||||
@@ -611,21 +611,27 @@ mode. Come back to this phase when you have the soldering iron out.
|
||||
|
||||
### Step 11: Install PlatformIO
|
||||
|
||||
On your PC, install PlatformIO:
|
||||
PlatformIO builds the firmware and loads it onto the XIAO. If you already have it (for
|
||||
example, the PlatformIO extension of VS Code), go to Step 12.
|
||||
|
||||
On your PC, install PlatformIO with its
|
||||
[official installer script](https://docs.platformio.org/en/latest/core/installation/methods/installer-script.html).
|
||||
The script puts PlatformIO in a Python environment of its own, `~/.platformio/penv`, and
|
||||
does not change your system Python:
|
||||
|
||||
```bash
|
||||
# on the PC
|
||||
pip install platformio
|
||||
# on the PC (Git Bash or WSL on Windows)
|
||||
curl -fsSL -o get-platformio.py https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py
|
||||
python3 get-platformio.py # on Windows: py get-platformio.py
|
||||
```
|
||||
|
||||
If you do not want to change the system Python, use the
|
||||
[official installer script](https://docs.platformio.org/en/latest/core/installation/methods/installer-script.html)
|
||||
instead. Then make sure that `pio` is on your PATH:
|
||||
The installer ends with the full path of `platformio`, and it tells you to add its directory
|
||||
to your PATH. You do not have to. `build-firmware.sh` and `flash.py` look for PlatformIO in
|
||||
`~/.platformio/penv` when it is not on the PATH.
|
||||
|
||||
```bash
|
||||
# on the PC
|
||||
pio --version
|
||||
```
|
||||
Do not use `pip install platformio`. Many systems refuse a `pip install` into the system
|
||||
Python (Debian, Ubuntu, and Homebrew mark it "externally managed"). Also, in a shell with an
|
||||
active virtual environment, the package goes into that environment.
|
||||
|
||||
The first firmware build downloads the nRF52 toolchain, which is several hundred
|
||||
megabytes. Do this where you have a good connection.
|
||||
@@ -633,16 +639,18 @@ megabytes. Do this where you have a good connection.
|
||||
### Step 12: Build the firmware
|
||||
|
||||
```bash
|
||||
# on the PC (Git Bash or WSL on Windows)
|
||||
# on the PC (Git Bash or WSL on Windows), from your MeshTerm checkout
|
||||
cd scripts/picocalc-lyra/xiao-radio
|
||||
sh build-firmware.sh
|
||||
```
|
||||
|
||||
The script clones MeshCore, applies a patch, and builds. It ends with this output:
|
||||
The script clones MeshCore, applies two patches, and builds. The first build takes several
|
||||
minutes. The script ends with this output:
|
||||
|
||||
```
|
||||
DONE. Firmware: /path/to/scripts/picocalc-lyra/xiao-radio/meshcore-xiao-radio.uf2
|
||||
Next: put the XIAO in bootloader (double-tap reset) and run: python flash.py
|
||||
Next: connect the USB-C port of the XIAO to this machine, and run: python3 flash.py
|
||||
(on Windows: py flash.py)
|
||||
```
|
||||
|
||||
These are the actions of the script. You can do them by hand if you must:
|
||||
@@ -650,15 +658,15 @@ These are the actions of the script. You can do them by hand if you must:
|
||||
1. Clone [MeshCore](https://github.com/meshcore-dev/MeshCore) into
|
||||
`scripts/picocalc-lyra/xiao-radio/_meshcore-build`.
|
||||
2. Check out the pinned commit `e9edfc8e` on the `dev` branch. This is the commit that the
|
||||
patch applies to and builds against without a problem.
|
||||
3. Apply `meshcore-uart1.patch`.
|
||||
patches apply to and build against without a problem.
|
||||
3. Apply `meshcore-uart1.patch`, then `meshcore-frame-timeout.patch`.
|
||||
4. Build the `Xiao_nrf52_companion_radio_serial` PlatformIO environment:
|
||||
`pio run -e Xiao_nrf52_companion_radio_serial`.
|
||||
5. Convert the resulting `.hex` file to a `.uf2` file with MeshCore's own converter.
|
||||
Use the UF2 family id of the nRF52840:
|
||||
`python bin/uf2conv/uf2conv.py firmware.hex -c -f 0xADA52840 -o meshcore-xiao-radio.uf2`.
|
||||
|
||||
#### What the patch changes, and why it is still necessary
|
||||
#### What the patches change, and why they are still necessary
|
||||
|
||||
`meshcore-uart1.patch` changes two files. Both changes are necessary:
|
||||
|
||||
@@ -687,6 +695,20 @@ still has no `_serial` companion environment. To build with the patch is the nor
|
||||
here, not a temporary measure. When the patch is merged, you can delete the `git apply`
|
||||
step and the patch file.
|
||||
|
||||
`meshcore-frame-timeout.patch` changes the serial parser of the companion,
|
||||
`src/helpers/ArduinoSerialInterface.cpp` and its header. Each protocol frame starts with `<`
|
||||
and a length of two bytes, and the parser then reads that number of bytes. Without the
|
||||
patch, the parser has no time limit. If one byte is lost or damaged on the wire, the parser
|
||||
can read a wrong length of up to 65,535 bytes. Then it ignores each command until that many
|
||||
bytes come in. MeshTerm sends one short request when it connects. Thus the radio does not
|
||||
answer for many starts in sequence, and only a power cycle of the XIAO clears the fault.
|
||||
|
||||
With the patch, the parser drops a frame when its bytes stop for 200 ms, and the next
|
||||
command starts a new frame. Bytes that wait in the receive buffer while the firmware is
|
||||
busy still belong to the frame. Only silence on the wire resets the parser. All the serial
|
||||
companions of MeshCore have this parser, thus the patch is also a candidate for upstream.
|
||||
We have not submitted it yet.
|
||||
|
||||
#### Building by hand
|
||||
|
||||
If you want to follow a newer MeshCore and not the pinned commit, run:
|
||||
@@ -697,13 +719,16 @@ git clone https://github.com/meshcore-dev/MeshCore.git
|
||||
cd MeshCore
|
||||
git checkout dev # or MESHCORE_COMMIT=dev sh build-firmware.sh
|
||||
git apply /path/to/scripts/picocalc-lyra/xiao-radio/meshcore-uart1.patch
|
||||
git apply /path/to/scripts/picocalc-lyra/xiao-radio/meshcore-frame-timeout.patch
|
||||
pio run -e Xiao_nrf52_companion_radio_serial
|
||||
python bin/uf2conv/uf2conv.py .pio/build/Xiao_nrf52_companion_radio_serial/firmware.hex \
|
||||
-c -f 0xADA52840 -o meshcore-xiao-radio.uf2
|
||||
```
|
||||
|
||||
If `git apply` fails, upstream has changed and the context of the patch does not match. The
|
||||
two hunks are small, so you can make them by hand:
|
||||
If `git apply` fails, upstream has changed and the context of the patch does not match. If
|
||||
`meshcore-frame-timeout.patch` fails, open it and make its changes by hand in the two files
|
||||
that it names. The two hunks of `meshcore-uart1.patch` are small, so you can also make them
|
||||
by hand:
|
||||
|
||||
1. In `examples/companion_radio/main.cpp`, find the line
|
||||
`HardwareSerial companion_serial(1);` and replace it with:
|
||||
@@ -745,22 +770,25 @@ two hunks are small, so you can make them by hand:
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
```
|
||||
|
||||
Only the pinned commit (`e9edfc8e`) is known to build without a problem with this patch. A
|
||||
Only the pinned commit (`e9edfc8e`) is known to build without a problem with these patches. A
|
||||
newer `dev` can need small changes.
|
||||
|
||||
### Step 13: Flash the XIAO
|
||||
|
||||
CAUTION: Make sure that the XIAO is the board that you flash and that it has the S140 v7
|
||||
bootloader. A flash to the wrong board, or to the wrong SoftDevice, can leave a board that
|
||||
does not start. `flash.py` checks both before it writes.
|
||||
does not start. `flash.py` checks both before it writes to a UF2 drive. Through serial DFU,
|
||||
it accepts only the USB ids of the XIAO bootloader.
|
||||
|
||||
1. Connect the **USB-C port of the XIAO itself** to your PC. It does not matter yet if the
|
||||
XIAO is also wired to the Lyra.
|
||||
2. Run the flash tool:
|
||||
1. Connect the **USB-C port of the XIAO itself** to your PC. Do this before you wire the
|
||||
XIAO in Step 14. To flash a XIAO that is already wired, follow "Updating the radio
|
||||
firmware" in [Day-to-day](#day-to-day) instead. The USB power of a wired XIAO goes
|
||||
backward into the Lyra.
|
||||
2. Run the flash tool, in the same directory as Step 12:
|
||||
|
||||
```bash
|
||||
# on the PC
|
||||
python flash.py
|
||||
# on the PC (Git Bash or WSL on Windows)
|
||||
python3 flash.py # on Windows: py flash.py
|
||||
```
|
||||
|
||||
The tool first tries a 1200-baud "touch" over USB serial. This resets the XIAO into its
|
||||
@@ -768,7 +796,8 @@ bootloader. The touch targets only the USB vendor id of Seeed. Thus it does not
|
||||
another nRF52840 board on the same bench. If the touch does not work, **double-tap the reset
|
||||
button of the XIAO** and run the tool again.
|
||||
|
||||
When the tool finds the bootloader, it checks two things before it writes anything:
|
||||
The bootloader then shows a UF2 drive, or only a serial port. When it shows a drive, the
|
||||
tool checks two things before it writes anything:
|
||||
|
||||
- **The `Model` field of `INFO_UF2.TXT`** must say a XIAO. A drive letter is not an
|
||||
identity. Another board can have the letter that your XIAO just gave up.
|
||||
@@ -782,17 +811,17 @@ If you are certain, `--force` overrides both checks. A good flash ends with this
|
||||
DONE. XIAO flashed and rebooting into the radio firmware.
|
||||
```
|
||||
|
||||
**If no UF2 drive ever shows**, this is not always an error. Some XIAO bootloaders show
|
||||
only a serial port and no mass-storage drive. `flash.py` knows this case. It prints the
|
||||
fallback command and does not fail silently:
|
||||
**When the bootloader shows only a serial port**, the tool loads the firmware through serial
|
||||
DFU itself. The XIAO that we tested has this type of bootloader. The tool runs the
|
||||
PlatformIO upload in `_meshcore-build`, which sends the firmware that Step 12 built. This
|
||||
takes approximately one minute. The USB ids of the port identify the XIAO bootloader. The
|
||||
upload ends with these lines:
|
||||
|
||||
```bash
|
||||
# on the PC, from your MeshCore checkout
|
||||
pio run -e Xiao_nrf52_companion_radio_serial -t upload --upload-port <bootloader port>
|
||||
```
|
||||
|
||||
This command ends with `Device programmed.` and the XIAO restarts itself into the radio
|
||||
firmware.
|
||||
Device programmed.
|
||||
...
|
||||
DONE. XIAO flashed and rebooting into the radio firmware.
|
||||
```
|
||||
|
||||
After the flash, the USB serial port of the XIAO is **silent** to companion frames. The
|
||||
companion protocol is now on D6/D7, not on USB. This silence is correct. It does not mean
|
||||
@@ -938,6 +967,24 @@ If an update adds new glyphs to the console font, build the font again afterward
|
||||
sh /home/meshterm/MeshTerm/scripts/picocalc-lyra/calculinux-console-font-6x12.sh
|
||||
```
|
||||
|
||||
**Updating the radio firmware.** A new firmware for the XIAO goes in through its USB-C port,
|
||||
as in Step 13. But the 3V3 wire of Step 14 connects the XIAO to the Lyra. When the XIAO gets
|
||||
USB power, that wire powers the Lyra backward.
|
||||
|
||||
1. On the PC, build the firmware first ([Step 12](#step-12-build-the-firmware)). Then the
|
||||
cable stays connected only for the flash.
|
||||
2. On the PicoCalc, run `sudo poweroff`. Then set the power switch to off.
|
||||
3. Remove the back of the shell. Then remove the batteries.
|
||||
|
||||
CAUTION: Remove the batteries before you connect the USB-C port of the XIAO. Through the
|
||||
3V3 wire, the USB power of the XIAO goes backward into the Lyra and the circuits next to
|
||||
it.
|
||||
|
||||
4. Connect the USB-C port of the XIAO to your PC.
|
||||
5. Run `flash.py` ([Step 13](#step-13-flash-the-xiao)).
|
||||
6. When the tool shows `DONE`, disconnect the cable.
|
||||
7. Put the batteries and the back of the shell in again. Then turn the PicoCalc on.
|
||||
|
||||
**Changing Wi-Fi.** Run `uwific` as root, as in [Step 7](#step-7-join-wi-fi). The kick at
|
||||
boot time finds the new network by itself. It needs only that `iwd` knows the network.
|
||||
|
||||
@@ -974,7 +1021,7 @@ You do not need to do anything to keep the shell's own font.
|
||||
| The port opens but the radio never answers | Flash again with the **`_serial`** environment, not `_ble` or `_usb`. Only `_serial` defines `SERIAL_RX`/`SERIAL_TX`. When the XIAO runs the radio firmware, its USB serial must be silent. This is expected and is not a fault. |
|
||||
| Still silent, and the wiring is correct | Make sure that the firmware has the I²C remap (`PIN_WIRE_SCL=16`, `PIN_WIRE_SDA=17`). A build without the second hunk of the patch looks the same until you check this. |
|
||||
| The flash was good, but the XIAO never comes back | The board or the SoftDevice is wrong. `INFO_UF2.TXT` must report a XIAO and **S140 v7**, not `6.1.1`. `flash.py` refuses to flash with either mismatch unless you use `--force`. |
|
||||
| `flash.py` finds no UF2 drive | This is expected on a bootloader that has only CDC. Use the `pio … -t upload --upload-port` command that the tool prints. |
|
||||
| `flash.py` finds no UF2 drive | This is expected on a bootloader that has only a serial port. The tool then loads the firmware through serial DFU itself. If it says that there is no build, run `build-firmware.sh` first. |
|
||||
| Nothing on `/dev/ttyS1` after a reboot | Run `systemctl status uart1-radio-mux`, then `journalctl -b -u uart1-radio-mux`. The pin-mux poke must run at each boot. If the oneshot fails, the log tells you why. |
|
||||
|
||||
---
|
||||
@@ -992,6 +1039,18 @@ checked these items by hand:
|
||||
- the nRF52840 UF2 family id
|
||||
- the XIAO bootloader ids, against the board files of Adafruit
|
||||
|
||||
On 2026-10-08, the author flashed the radio again, with the frame timeout, and checked these
|
||||
items on the bench:
|
||||
|
||||
- the build and the flash with the commands of this guide, on a Windows PC that did not
|
||||
have the PlatformIO command-line tool
|
||||
- that the bootloader of this XIAO shows only a serial port, and that `flash.py` then loads
|
||||
the firmware through serial DFU
|
||||
- that the USB power of a wired XIAO goes backward into the Lyra
|
||||
- that the radio keeps its identity through the flash
|
||||
- that, after the flash, a frame that breaks off no longer blocks the link: the next
|
||||
`APP_START` after 0.5 s of silence gets its reply
|
||||
|
||||
The other facts are the findings of one bench. A second device can give other results:
|
||||
|
||||
- **The RK3506 register map** in `uart1-mux.py`. This covers the two register blocks, the
|
||||
|
||||
@@ -10,8 +10,8 @@ It has the parts, the firmware build and an explanation of the patch, the flashi
|
||||
wiring, the setup of the Lyra, and what we proved on only one bench. The header of
|
||||
`uart1-mux.py` explains what the register write does. This page is the bench card.
|
||||
|
||||
**Licensing.** The firmware is MeshCore, under the MIT License. The `meshcore-uart1.patch`
|
||||
changes the source of MeshCore, and `build-firmware.sh` makes a MeshCore binary. The licence
|
||||
**Licensing.** The firmware is MeshCore, under the MIT License. The two patches
|
||||
(`meshcore-uart1.patch` and `meshcore-frame-timeout.patch`) change the source of MeshCore, and `build-firmware.sh` makes a MeshCore binary. The licence
|
||||
is [`LICENSE.MeshCore`](LICENSE.MeshCore) in this directory.
|
||||
|
||||
There are three steps on two machines:
|
||||
@@ -20,8 +20,10 @@ There are three steps on two machines:
|
||||
# 1. dev machine: build (needs git + PlatformIO)
|
||||
cd scripts/picocalc-lyra/xiao-radio && sh build-firmware.sh
|
||||
|
||||
# 2. dev machine: flash, with the XIAO's own USB-C plugged in
|
||||
python flash.py # double-tap reset if the touch doesn't take
|
||||
# 2. dev machine: flash, with the XIAO's own USB-C plugged in. If the XIAO is already
|
||||
# wired, shut the PicoCalc down and remove its batteries first: the USB power of the
|
||||
# XIAO goes backward into the Lyra.
|
||||
python3 flash.py # on Windows: py flash.py
|
||||
|
||||
# 3. the Lyra, as root
|
||||
sh scripts/picocalc-lyra/xiao-radio/lyra-setup.sh # MT_USER=meshterm by default
|
||||
|
||||
@@ -11,15 +11,20 @@
|
||||
# declares a HardwareSerial for ESP32 only, and nRF52 needs Serial1)
|
||||
# * add a Xiao_nrf52_companion_radio_serial env: the companion is on D6/D7, and I2C
|
||||
# moves off those pads (to internal pins 16/17), so that I2C cannot take them
|
||||
# 3. builds that environment
|
||||
# 4. converts the .hex to .uf2
|
||||
# 3. applies meshcore-frame-timeout.patch, which makes the link recover by itself: the
|
||||
# serial parser drops a frame whose bytes stop coming, so one lost byte cannot make
|
||||
# the companion ignore every command after it
|
||||
# 4. builds that environment
|
||||
# 5. converts the .hex to .uf2
|
||||
#
|
||||
# The patch has the form that upstream needs, and we submitted it to MeshCore. After it is
|
||||
# merged, delete the `git apply` step below. The env is then part of the firmware, and
|
||||
# nothing needs a patch.
|
||||
# Each patch has the form that upstream needs. When upstream merges one, remove it from
|
||||
# PATCHES below. When both are merged, the env is part of the firmware, and nothing needs
|
||||
# a patch.
|
||||
#
|
||||
# Prerequisites: git, and the PlatformIO CLI on PATH (`pio`). Install pio with:
|
||||
# pip install platformio
|
||||
# Prerequisites: git, and the PlatformIO CLI (`pio`). Install it with the official installer
|
||||
# script (refer to Step 11 of docs/devices/picocalc-lyra.md). The script finds `pio` on the
|
||||
# PATH, or else in ~/.platformio/penv, where the installer and the PlatformIO extension of
|
||||
# VS Code put it. Thus you do not have to change the PATH.
|
||||
#
|
||||
# Usage:
|
||||
# sh build-firmware.sh # clone+build into ./_meshcore-build
|
||||
@@ -28,30 +33,50 @@
|
||||
set -eu
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
PATCH="$HERE/meshcore-uart1.patch"
|
||||
PATCHES="meshcore-uart1.patch meshcore-frame-timeout.patch"
|
||||
MC="${MESHCORE_DIR:-$HERE/_meshcore-build}"
|
||||
ENV=Xiao_nrf52_companion_radio_serial
|
||||
# The commit that this patch is made against (a `dev` commit, because `dev` is the branch
|
||||
# The commit that the patches are made against (a `dev` commit, because `dev` is the branch
|
||||
# where MeshCore takes new work). A newer MeshCore can need the edits to be applied again by
|
||||
# hand (refer to the README, "Updating"). Set MESHCORE_COMMIT= to follow upstream.
|
||||
# hand (refer to the guide, "Building by hand"). Set MESHCORE_COMMIT= to follow upstream.
|
||||
PIN="${MESHCORE_COMMIT:-e9edfc8e}"
|
||||
|
||||
command -v git >/dev/null 2>&1 || { echo "ERROR: git not found"; exit 1; }
|
||||
command -v pio >/dev/null 2>&1 || command -v platformio >/dev/null 2>&1 || {
|
||||
echo "ERROR: PlatformIO CLI (pio) not found. Install with: pip install platformio"; exit 1; }
|
||||
PIO=$(command -v pio 2>/dev/null || command -v platformio)
|
||||
PIO=$(command -v pio 2>/dev/null || command -v platformio 2>/dev/null) || PIO=
|
||||
if [ -z "$PIO" ]; then
|
||||
# The installer does not put pio on the PATH. Look where it does put it: bin/ on Linux
|
||||
# and macOS, Scripts/ on Windows.
|
||||
for c in "$HOME/.platformio/penv/bin/pio" "$HOME/.platformio/penv/Scripts/pio.exe"; do
|
||||
if [ -x "$c" ]; then PIO=$c; break; fi
|
||||
done
|
||||
fi
|
||||
[ -n "$PIO" ] || {
|
||||
echo "ERROR: PlatformIO (pio) not found, on the PATH or in ~/.platformio/penv."
|
||||
echo "Install it with the official installer script (Step 11 of the PicoCalc guide)."
|
||||
exit 1; }
|
||||
echo ">> using $PIO"
|
||||
# The Python that runs uf2conv.py. PlatformIO's own Python comes first, because it is
|
||||
# always there when pio is. A bare `python` is absent on many Linux systems, and on Windows
|
||||
# `python3` can be a stub that opens the Microsoft Store.
|
||||
PY=
|
||||
for c in "$(dirname -- "$PIO")/python" "$(dirname -- "$PIO")/python.exe"; do
|
||||
if [ -x "$c" ]; then PY=$c; break; fi
|
||||
done
|
||||
[ -n "$PY" ] || PY=$(command -v python3 2>/dev/null || command -v python)
|
||||
|
||||
if [ ! -d "$MC/.git" ]; then
|
||||
echo ">> cloning MeshCore into $MC"
|
||||
git clone https://github.com/meshcore-dev/MeshCore.git "$MC"
|
||||
fi
|
||||
|
||||
echo ">> checkout $PIN + apply patch"
|
||||
echo ">> checkout $PIN + apply patches"
|
||||
git -C "$MC" fetch --quiet --tags origin 2>/dev/null || true
|
||||
git -C "$MC" checkout -f "$PIN" 2>/dev/null || {
|
||||
echo " (pinned commit not found; using current checkout)"; }
|
||||
git -C "$MC" apply "$PATCH"
|
||||
echo " applied meshcore-uart1.patch"
|
||||
for p in $PATCHES; do
|
||||
git -C "$MC" apply "$HERE/$p"
|
||||
echo " applied $p"
|
||||
done
|
||||
|
||||
echo ">> building $ENV (first build downloads the nRF52 toolchain; be patient)"
|
||||
( cd "$MC" && "$PIO" run -e "$ENV" )
|
||||
@@ -60,8 +85,9 @@ HEX="$MC/.pio/build/$ENV/firmware.hex"
|
||||
UF2CONV="$MC/bin/uf2conv/uf2conv.py"
|
||||
OUT="$HERE/meshcore-xiao-radio.uf2"
|
||||
echo ">> converting to uf2"
|
||||
python "$UF2CONV" "$HEX" -c -f 0xADA52840 -o "$OUT"
|
||||
"$PY" "$UF2CONV" "$HEX" -c -f 0xADA52840 -o "$OUT"
|
||||
|
||||
echo ""
|
||||
echo "DONE. Firmware: $OUT"
|
||||
echo "Next: put the XIAO in bootloader (double-tap reset) and run: python flash.py"
|
||||
echo "Next: connect the USB-C port of the XIAO to this machine, and run: python3 flash.py"
|
||||
echo " (on Windows: py flash.py)"
|
||||
@@ -6,22 +6,94 @@ This script runs on your development machine. The USB-C port of the XIAO must be
|
||||
to that machine. It works on Windows, Linux, and macOS.
|
||||
|
||||
The XIAO goes into bootloader mode in one of two ways. The first way is a physical
|
||||
**double-tap of its reset button**. Then it mounts as the ``XIAO-SENSE`` drive. The second
|
||||
way is for a XIAO that runs app firmware with a serial port. This script first tries an
|
||||
automatic 1200-baud "touch" on that port.
|
||||
**double-tap of its reset button**. The second way is for a XIAO that runs app firmware
|
||||
with a serial port. This script first tries an automatic 1200-baud "touch" on that port.
|
||||
|
||||
A XIAO bootloader shows one of two things. Some bootloaders mount a UF2 drive
|
||||
(``XIAO-SENSE``), and the script copies the .uf2 file to it. Other bootloaders show only a
|
||||
serial port. Then the script loads the firmware through serial DFU itself: it runs the
|
||||
PlatformIO upload in the MeshCore checkout that ``build-firmware.sh`` built.
|
||||
|
||||
The script needs ``pyserial`` for the touch and for the search of the serial port. When the
|
||||
Python that runs the script does not have it, the script runs itself again with the Python
|
||||
of PlatformIO, which always has it.
|
||||
|
||||
Usage:
|
||||
python flash.py # flash ./meshcore-xiao-radio.uf2
|
||||
python flash.py path/to/firmware.uf2 # flash a specific file
|
||||
python3 flash.py # flash ./meshcore-xiao-radio.uf2
|
||||
python3 flash.py path/to/firmware.uf2 # flash a specific file
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
#: The PlatformIO environment that ``build-firmware.sh`` builds.
|
||||
ENV = "Xiao_nrf52_companion_radio_serial"
|
||||
|
||||
#: The seconds that the script waits for a UF2 drive after the bootloader port shows. A
|
||||
#: bootloader with a drive mounts it within this time. A bootloader without one never does.
|
||||
DRIVE_GRACE_S = 5
|
||||
|
||||
|
||||
def platformio_penv():
|
||||
"""Return the directory of the PlatformIO environment, or None.
|
||||
|
||||
The official installer and the PlatformIO extension of VS Code both put it in
|
||||
``~/.platformio/penv``. The programs are in ``Scripts`` on Windows and in ``bin`` on
|
||||
other systems.
|
||||
"""
|
||||
base = os.path.join(os.path.expanduser("~"), ".platformio", "penv")
|
||||
for sub in ("Scripts", "bin"):
|
||||
path = os.path.join(base, sub)
|
||||
if os.path.isdir(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def find_pio():
|
||||
"""Return the path of the PlatformIO CLI: on the PATH first, else in its environment."""
|
||||
found = shutil.which("pio") or shutil.which("platformio")
|
||||
if found:
|
||||
return found
|
||||
penv = platformio_penv()
|
||||
if penv:
|
||||
for name in ("pio.exe", "pio"):
|
||||
path = os.path.join(penv, name)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def ensure_pyserial():
|
||||
"""Make sure that ``pyserial`` imports, or run this script again with a Python that has it.
|
||||
|
||||
Without ``pyserial``, the touch and the search of the bootloader port cannot run. In the
|
||||
past, both stopped without a message, and the script then asked for a double-tap that
|
||||
could not help.
|
||||
"""
|
||||
try:
|
||||
import serial # noqa: F401 - only a check that the module exists
|
||||
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
penv = platformio_penv()
|
||||
if penv:
|
||||
for name in ("python.exe", "python"):
|
||||
python = os.path.join(penv, name)
|
||||
same = os.path.abspath(python) == os.path.abspath(sys.executable)
|
||||
if os.path.isfile(python) and not same:
|
||||
print(f" (no pyserial here, so the script runs again with {python})", flush=True)
|
||||
sys.exit(subprocess.call([python, os.path.abspath(__file__)] + sys.argv[1:]))
|
||||
sys.exit(
|
||||
"ERROR: flash.py needs pyserial. Install PlatformIO (Step 11), or run: pip install pyserial"
|
||||
)
|
||||
|
||||
|
||||
def find_uf2_drive():
|
||||
"""Return the mount path of a UF2 bootloader volume, or None."""
|
||||
@@ -123,6 +195,33 @@ def check_drive_is_ours(drive, force=False):
|
||||
return True
|
||||
|
||||
|
||||
def flash_serial_dfu(port, uf2_given=False):
|
||||
"""Load the firmware through the serial DFU of a bootloader that shows no drive.
|
||||
|
||||
The PlatformIO upload makes the DFU package from the build and sends it. Thus it loads
|
||||
the firmware that ``build-firmware.sh`` built in the MeshCore checkout, and not a .uf2
|
||||
file. The checkout is ``MESHCORE_DIR``, or else ``_meshcore-build`` next to this script,
|
||||
as in ``build-firmware.sh``.
|
||||
"""
|
||||
checkout = os.environ.get("MESHCORE_DIR") or os.path.join(HERE, "_meshcore-build")
|
||||
command = ["run", "-e", ENV, "-t", "upload", "--upload-port", port]
|
||||
pio = find_pio()
|
||||
print(f">> the bootloader on {port} has no UF2 drive: loading through serial DFU")
|
||||
if uf2_given:
|
||||
print(" note: serial DFU loads the build in the MeshCore checkout, not the file you gave")
|
||||
if not pio or not os.path.isdir(os.path.join(checkout, ".pio", "build", ENV)):
|
||||
sys.exit(
|
||||
f"ERROR: no {'PlatformIO' if not pio else 'build in ' + checkout} for serial DFU.\n"
|
||||
"Run build-firmware.sh first. To load by hand, run in the MeshCore checkout:\n"
|
||||
f" pio {' '.join(command)}"
|
||||
)
|
||||
sys.stdout.flush() # keep this script's lines before the lines of the upload
|
||||
status = subprocess.call([pio] + command, cwd=checkout)
|
||||
if status != 0:
|
||||
sys.exit(f"ERROR: the serial DFU upload failed (exit {status}).")
|
||||
print("DONE. XIAO flashed and rebooting into the radio firmware.")
|
||||
|
||||
|
||||
def main():
|
||||
"""Put the XIAO in its bootloader and copy the firmware onto the drive that it shows.
|
||||
|
||||
@@ -136,32 +235,39 @@ def main():
|
||||
if not os.path.exists(uf2):
|
||||
sys.exit(f"ERROR: firmware not found: {uf2}\n(run build-firmware.sh first)")
|
||||
|
||||
ensure_pyserial()
|
||||
print(">> looking for XIAO bootloader drive ...")
|
||||
drive = find_uf2_drive()
|
||||
before = drive
|
||||
port = None
|
||||
if not drive:
|
||||
touch_1200()
|
||||
port_since = None
|
||||
for _ in range(30):
|
||||
drive = find_uf2_drive()
|
||||
if drive:
|
||||
break
|
||||
port = bootloader_port()
|
||||
if port:
|
||||
# A bootloader that mounts a drive does it within a few seconds of its port.
|
||||
# After that, it is a bootloader with only a serial port.
|
||||
port_since = port_since or time.monotonic()
|
||||
if time.monotonic() - port_since >= DRIVE_GRACE_S:
|
||||
break
|
||||
time.sleep(1)
|
||||
if drive and drive == before:
|
||||
# The letter was there before we touched anything, so it can belong to a different
|
||||
# board. Check what the drive reports. Do not trust the letter.
|
||||
print(f" note: {drive} was already mounted before the touch")
|
||||
if not drive:
|
||||
port = bootloader_port()
|
||||
port = port or bootloader_port()
|
||||
if port:
|
||||
sys.exit(
|
||||
f"ERROR: the XIAO is in its bootloader on {port} but exposes no UF2 drive.\n"
|
||||
"This bootloader presents a serial port only, so copy-to-drive can't work.\n"
|
||||
"Flash it over serial DFU instead, from your MeshCore checkout:\n"
|
||||
f" pio run -e Xiao_nrf52_companion_radio_serial -t upload --upload-port {port}"
|
||||
)
|
||||
flash_serial_dfu(port, uf2_given=bool(args))
|
||||
return
|
||||
sys.exit(
|
||||
"ERROR: no UF2 bootloader drive and no XIAO bootloader serial port found.\n"
|
||||
"Double-tap the XIAO's reset button, then re-run."
|
||||
"ERROR: no XIAO found: no UF2 bootloader drive, and no XIAO serial port.\n"
|
||||
"Connect the USB-C port of the XIAO. If it is connected, double-tap its reset\n"
|
||||
"button, then run this again."
|
||||
)
|
||||
|
||||
check_drive_is_ours(drive, force=force)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp
|
||||
index a01fa58..49f98aa 100644
|
||||
--- a/src/helpers/ArduinoSerialInterface.cpp
|
||||
+++ b/src/helpers/ArduinoSerialInterface.cpp
|
||||
@@ -5,6 +5,13 @@
|
||||
#define RECV_STATE_LEN1_FOUND 2
|
||||
#define RECV_STATE_LEN2_FOUND 3
|
||||
|
||||
+// A frame whose bytes stop coming is dropped after this long. Without it, one lost or
|
||||
+// damaged byte can leave the parser waiting for a length of up to 65535 bytes, and every
|
||||
+// command after it is swallowed until that many bytes have arrived.
|
||||
+#ifndef RECV_FRAME_TIMEOUT_MILLIS
|
||||
+ #define RECV_FRAME_TIMEOUT_MILLIS 200
|
||||
+#endif
|
||||
+
|
||||
void ArduinoSerialInterface::enable() {
|
||||
_isEnabled = true;
|
||||
_state = RECV_STATE_IDLE;
|
||||
@@ -37,9 +44,17 @@ size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
}
|
||||
|
||||
size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) {
|
||||
+ // Only silence on the line counts: nothing waiting, and nothing read for a while. Bytes
|
||||
+ // that queued up while the loop was busy still belong to the frame in progress.
|
||||
+ if (_state != RECV_STATE_IDLE && !_serial->available()
|
||||
+ && millis() - _last_rx > RECV_FRAME_TIMEOUT_MILLIS) {
|
||||
+ _state = RECV_STATE_IDLE; // the frame broke off: look for the next '<'
|
||||
+ }
|
||||
+
|
||||
while (_serial->available()) {
|
||||
int c = _serial->read();
|
||||
if (c < 0) break;
|
||||
+ _last_rx = millis();
|
||||
|
||||
switch (_state) {
|
||||
case RECV_STATE_IDLE:
|
||||
diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h
|
||||
index c408635..b6b8be3 100644
|
||||
--- a/src/helpers/ArduinoSerialInterface.h
|
||||
+++ b/src/helpers/ArduinoSerialInterface.h
|
||||
@@ -8,11 +8,12 @@ class ArduinoSerialInterface : public BaseSerialInterface {
|
||||
uint8_t _state;
|
||||
uint16_t _frame_len;
|
||||
uint16_t rx_len;
|
||||
+ unsigned long _last_rx;
|
||||
Stream* _serial;
|
||||
uint8_t rx_buf[MAX_FRAME_SIZE];
|
||||
|
||||
public:
|
||||
- ArduinoSerialInterface() { _isEnabled = false; _state = 0; }
|
||||
+ ArduinoSerialInterface() { _isEnabled = false; _state = 0; _last_rx = 0; }
|
||||
|
||||
void begin(Stream& serial) {
|
||||
_serial = &serial;
|
||||
Reference in new issue
Block a user