Updating the firmware¶
Once the board is installed, a new version of the firmware can go on in two ways, neither of which needs the board's buttons or its auto-reset circuit:
- Over Wi-Fi, from a phone or a computer: the easy way once the board is on your network or you have joined its own access point.
- Over serial, through the console on the USB-C port: when there is no Wi-Fi.
Either way the new image goes into the application slot that is not running, and the board checks the whole of it before it can boot. When it goes wrong, the image that was running still boots.
Updating over Wi-Fi¶
Open the board's web app (see The board over Wi-Fi for how to reach it) and go to Update. There you can:
- Install the latest release. The page shows the newest release next to what the board runs. Download and install has the board fetch the release from this site itself, checked against the release's SHA-256.
- Upload a file. Choose
holo-player-fw-vX.Y.Z.binfrom a release, orbuild/holo-player-fw.binfrom your own build. Before sending, the page reads the file and says which firmware and version it is. It warns about a downgrade, or about a file for another project. The bootloader and partition-table files from a release are not firmware, and the page says so. - Switch slots. The other slot still holds the previous firmware. Switch to this restarts into it.
After an upload or download, the new firmware is staged: checked and ready, but not yet what the board boots. Restart into it makes it the boot image and restarts. The page then waits for the board to come back and reports what it is running. Leave Restart into it once the board has checked it ticked, and an upload does all of this in one go.
A 1.6 MB image uploads in about 15 seconds over Wi-Fi, a third of the time it takes over serial.
From a script¶
The web app is a front end for the board's HTTP API, so the same steps automate. Upload, activate and restart in one call:
curl -T build/holo-player-fw.bin "http://holo-2db0.local/api/v1/ota/image?activate=1&reboot=1"
Or have the board fetch the latest release, and install it only once it is staged:
curl -X POST -H "Content-Type: application/json" -d '{"channel":"latest"}' http://holo-2db0.local/api/v1/ota/pull
curl http://holo-2db0.local/api/v1/ota/image # until "state" is "staged"
curl -X POST -H "Content-Type: application/json" -d '{}' http://holo-2db0.local/api/v1/ota/activate
From the console¶
The board can fetch an update from its console too: a release channel, a release's manifest, or an
image's URL. It downloads the image, checks it, makes it the boot image and restarts, as ota put
does:
ota pull latest
| Flag | Does |
|---|---|
-s |
Stage it only; ota activate makes it the boot image later |
-n |
Make it the boot image, but do not restart |
-f |
Accept an image built for another project |
--sha256 <hex> |
Refuse the image unless it has this SHA-256 |
ota activate makes the staged image the boot image and restarts. ota activate ota_1 does the
same for the image already in a slot, which is how to go back to the firmware before.
Updating over serial¶
Installing over USB needs the USB-C port and its auto-reset circuit. Once the board is mounted inside a droid that may not be reachable, so the firmware can also replace itself through the console it is already talking on.
python external/esp-console-kit/tools/fs_xfer.py -p <port> ota build/holo-player-fw.bin
A 1.26 MB image takes about 44 seconds. As with any host-side tool here, close idf.py monitor
first — only one program can hold the serial port.
What happens¶
The flash carries two application slots, ota_0 and ota_1, of 2.25 MB each.
- The board erases the slot that is not running and receives the image over XMODEM-1K at 460800 baud, writing each block as it arrives.
- ESP-IDF checks the complete image — its header, that it was built for this chip, and its SHA-256 — before it is allowed to become the boot image.
- The board makes that slot the boot image and restarts into it.
- The tool waits for the console to come back and confirms that the new slot is the one running.
When it goes wrong¶
A failed transfer, a truncated image, or an image built for something else changes nothing: the image that was running still boots. There is no state in which the board is left with no working firmware, because the slot being written is never the slot being run.
Rollback¶
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is on, so a newly installed image boots on trial. It
confirms itself once the application is up and the console is running. A reset before that point
boots the previous image instead.
Rollback lives in the bootloader
The trial-and-confirm logic is part of the bootloader, and the bootloader is only written by
installing over USB — from the browser, with esptool, or with
idf.py flash. A board that has never been installed over USB with this firmware does not
have it.
Checking the slots¶
ota
lists both application slots: the partition label, where it is, how big it is, which one is running and which boots next, and for each one the project name, version and build date of the image it holds, plus its state.
This is where the release version becomes visible on the board:
ota_0 0x010000 2304 KB running, boots holo-player-fw v0.1.0, built Sep 20 2026 14:02:11
An image built from an untagged tree reports the commit it came from instead of a tag. See RELEASING.md for how the version gets there.
Options¶
ota put is the on-board half, and fs_xfer.py ota drives it. Driving it by hand:
ota put [-b baud] [-n] [-d] <size>
| Flag | Does |
|---|---|
-b <baud> |
Transfer at this rate instead of the default |
-n |
Make the image the boot image, but do not restart |
-d |
A link test: receive and hash the image, write nothing |
-d is the useful one when a transfer is failing and you want to know whether the problem is the
link or the flash. fs_xfer.py ota --dry-run does the same from the host.
Going back¶
Installing over USB always writes to ota_0 and boots it, whichever
slot was running before — whether from the browser, with esptool, or with idf.py flash.
That is the way back if an update leaves the board in a state you would rather leave behind.