Flashing firmware from a release¶
How to get a published RaceLink firmware .bin onto a factory-fresh
gateway dongle or WLED node — on Windows and on Linux, with the
esptool command line or with a browser-based web flasher.
Audience. Anyone commissioning hardware for the first time, or recovering a device that no longer boots. Once a device runs RaceLink firmware you normally never touch this page again — subsequent updates go over the air from the host WebUI, see
RaceLink_Host/firmware-updates.md.Building firmware yourself is a different task; that lives in
RaceLink_Gateway/README.mdandRaceLink_WLED/README.md.
The release artifacts¶
Both firmware repositories publish pre-built binaries on their GitHub releases page. Nothing has to be compiled to commission a device.
- Gateway — https://github.com/PSi86/RaceLink_Gateway/releases
- WLED nodes — https://github.com/PSi86/RaceLink_WLED/releases
Since gateway v0.1.7 and RaceLink_WLED v0.1.9 each release ships
two files per build environment, named
<env>-<version>-<what-it-is>.bin. Which one you need depends on the
route; the route table decides for you.
Version numbers in this guide are examples
Commands and filenames below are written against gateway 0.1.7 and
RaceLink_WLED 0.1.9 — the releases where the current asset format
began — so that they stay stable and can be compared against the
format description. Always take the newest version from the two
releases pages above, or let the web flasher preselect it. Nothing in
the layout has changed since; only the number in the filename does.
| Suffix | Offset | What it is |
|---|---|---|
-ota.bin |
0x10000 |
The application alone. What OTA and the host's firmware dialog take. |
-usbflash.bin |
0x0 |
Bootloader, partition table, OTA selector and application in one image. Commissions a blank chip in a single write. |
Plus, once per release, a -assets.json index naming each file's kind,
flash offset, size and digest — that is what the
web flasher reads, so it never has to
interpret a filename.
Older releases look different
Gateway v0.1.6 and RaceLink_WLED v0.1.8 published five files
per environment, with much longer names: -app.bin instead of
-ota.bin, -factory-usb-serial-only.bin instead of -usbflash.bin,
plus -bootloader.bin, -partitions.bin and -boot_app0.bin
separately and a -sha256.txt. The three pre-application files are
now merged into the factory image only — writing -usbflash.bin at
0x0 reaches exactly the same state, and it is the same command on
every SoC. Their offsets and digests are still on the record, as the
parts list in -assets.json.
-usbflash means exactly that
The factory image is written over USB serial, at offset 0x0, and
never through an OTA path. Uploaded to WLED's /update page or
picked in the host's OTA dialog it is normally rejected — but with
Ignore firmware validation ticked it lands in the inactive OTA
slot and bricks the device until someone re-flashes over serial.
It also rewrites the partition table, so the filesystem is reformatted
on the next boot: WLED presets, cfg.json, the master binding and the
group id are gone and the node returns as unconfigured. It commissions
hardware. It is not an update.
Gateway¶
<env> |
Target hardware |
|---|---|
WirelessStickV3-ESP32S3 |
Heltec Wireless Stick V3 — ESP32-S3 + SX1262, 0.49″ 64×32 OLED |
HeltecWiFiLoRa32V4-ESP32S3 |
Heltec WiFi LoRa 32 V4 — ESP32-S3 + SX1262, 0.96″ 128×64 OLED |
Manufacturer links and the full pin map for both boards are in
reference/hardware.md.
So the application image for the first board is
WirelessStickV3-ESP32S3-0.1.7-ota.bin.
Both builds are DEV_TYPE=1 (RaceLink_Gateway_v4) and differ only in
OLED panel geometry. Flashing the wrong one of the two gives you a working
gateway with a garbled display — annoying, not fatal.
Gateway factory images need ≥ 8 MB of flash
The gateway pins the Arduino 8 MB partition table, which a 4 MB ESP32-S3 module cannot hold. Both supported boards have 8 MB.
WLED nodes¶
Node assets are named the same way as the gateway's —
<env>-<version>-ota.bin — for example
RaceLink_Node_v4_s3_llcc68-0.1.9-ota.bin.
A node build is two projects, so it has two versions: the RaceLink release
in the filename, and the upstream WLED release the usermod was compiled
against. The second is not in the name; it is stated once per release
as wled_ref in -assets.json, along with each environment's DEV_TYPE.
Repeating both in thirty filenames is what made them unreadable.
<env> |
SoC | Transport | DEV_TYPE |
WLED_RELEASE_NAME |
|---|---|---|---|---|
RaceLink_Node_v1_c3_ct62 |
ESP32-C3 (HT-CT62 module) | SX1262 | 10 | RaceLink_Node_V1_TYPE_10 |
RaceLink_Node_v3_s2_llcc68 |
ESP32-S2 | LLCC68 | 11 | RaceLink_Node_V3_TYPE_11 |
RaceLink_Node_v3_s2_llcc68_epaper |
ESP32-S2 | LLCC68 | 50 | RaceLink_Node_V3_TYPE_50 |
RaceLink_Node_v4_s3_llcc68 |
ESP32-S3 | LLCC68 | 12 | RaceLink_Node_V4_TYPE_12 |
RaceLink_Node_v5_s3_eth |
ESP32-S3 | W5500 Ethernet — no radio | 13 | RaceLink_Node_V5_TYPE_13 |
RaceLink_Node_v6_s3_heltec_wpaper |
ESP32-S3 (Heltec Wireless Paper) | SX1262 | 51 | RaceLink_Node_V6_HeltecWPaper_TYPE_51 |
Picking the wrong <env> is the one mistake that hurts
The SoC families are not interchangeable. An ESP32-S3 image on an
ESP32-C3 does not boot, and the radio pin map, LED pin map and
chip family are all compile-time. Match the <env> in the
filename to your board before you flash anything. The per-profile
hardware detail — which board each <env> targets, who makes it and
how it is wired — is in
reference/hardware.md §"Nodes".
The published binaries start on 867.7 MHz — but the frequency is configurable
Every shipped profile compiles in RACELINK_FREQ_HZ=867700000
(see RaceLink_Gateway/operator-setup.md
§"Radio defaults"), so a freshly flashed device comes up on the EU
default. That is a default, not a limit: the firmware accepts any
frequency from 863 to 928 MHz at runtime, which covers EU868 and
US915 alike, and stores it in NVS. Operators outside the EU do not
need to build from source. Three ways to set it:
- At flash time — the web flasher writes the channel into the image it flashes, so the device is never on the wrong frequency, not even for one boot.
- From the host —
GW_CMD_SET_RF_CONFIGover USB for a gateway,OPC_RF_CONFIGover LoRa for a node. Seereference/channels.md. - Not at all, if EU868 is what you want.
What is not configurable is the band the board was built for. Some boards are sold in 433 MHz as well as 868/915 MHz variants, and Heltec ships separate hardware revisions for 863–870 and 902–928 MHz. Nothing in software moves a board between those, nothing over USB can detect which one you are holding, and the firmware rejects 433 MHz outright. Match the hardware to the region before you flash.
Nodes are the reason to set the frequency while flashing
A gateway can always be corrected over USB. A node cannot: it has no serial command interface, so its frequency arrives over LoRa — from a gateway that has to be on the node's current frequency to talk to it at all. Flash a node on the EU default for a US setup and you have a device nothing can reach until it is re-flashed.
Which route do I need?¶
| Situation | Route |
|---|---|
| Anything, if you have Chrome, Edge or Opera on a desktop | The web flasher — pick the model, pick a version, click. It writes the factory image, so the state of the chip beforehand does not matter, and it can set the radio channel at the same time. |
| Working WLED or RaceLink firmware, and you only want a newer version | No flashing at all — go over the air, Route A |
| A chip that already holds a compatible bootloader and partition table, and you prefer a terminal | Route B — write -ota.bin alone at 0x10000. Identical command for every asset. |
| A blank chip, and you prefer a terminal | Route C — write -usbflash.bin at 0x0. Identical command for every asset. |
If you are unsure and using a terminal, try Route B first: an application-only write cannot damage the bootloader, so a wrong guess costs you nothing but a reboot. If the board then stays dark, go to Route C.
The numbers, per release asset¶
You do not have to read this table to flash a device — neither Route B nor Route C needs any of it, because both write a single file. It is here so that the layout the release binaries assume is on the record, and for the case where you build the firmware yourself.
<env> |
SoC | Bootloader offset | App offset | App partition | Partition table |
|---|---|---|---|---|---|
WirelessStickV3-ESP32S3 |
ESP32-S3 | 0x0 |
0x10000 |
3264 KiB | Arduino default_8MB.csv |
HeltecWiFiLoRa32V4-ESP32S3 |
ESP32-S3 | 0x0 |
0x10000 |
3264 KiB | Arduino default_8MB.csv |
RaceLink_Node_v1_c3_ct62 |
ESP32-C3 | 0x0 |
0x10000 |
1536 KiB | WLED WLED_ESP32_4MB_1MB_FS.csv |
RaceLink_Node_v3_s2_llcc68 |
ESP32-S2 | 0x1000 |
0x10000 |
1536 KiB | WLED WLED_ESP32_4MB_1MB_FS.csv |
RaceLink_Node_v3_s2_llcc68_epaper |
ESP32-S2 | 0x1000 |
0x10000 |
1536 KiB | WLED WLED_ESP32_4MB_1MB_FS.csv |
RaceLink_Node_v4_s3_llcc68 |
ESP32-S3 | 0x0 |
0x10000 |
1536 KiB | WLED WLED_ESP32_4MB_1MB_FS.csv |
RaceLink_Node_v5_s3_eth |
ESP32-S3 | 0x0 |
0x10000 |
1536 KiB | WLED WLED_ESP32_4MB_1MB_FS.csv |
RaceLink_Node_v6_s3_heltec_wpaper |
ESP32-S3 | 0x0 |
0x10000 |
1536 KiB | WLED WLED_ESP32_4MB_1MB_FS.csv |
These are also the offsets baked into each release's -usbflash.bin, and
the -assets.json index states them per environment in its parts list —
so nothing here has to be transcribed by hand.
Reading the table:
- The app offset is
0x10000everywhere. Both partition tables in play put the first application partition (app0) there, so Route B never needs a per-board decision. - The bootloader offset is the only per-SoC value. ESP32-S3 and
ESP32-C3 boot from
0x0; ESP32-S2 boots from0x1000. It never matters when flashing a release: it is inside-usbflash.bin, not on the command line. It matters only when you merge a build of your own. - Two further offsets are fixed by the SoC, not by the build:
partition table at
0x8000, OTA-selector (otadata) at0xe000. Both partition tables above agree on0xe000. - All six node profiles pin the 4 MB WLED table via
board_build.partitions = ${esp32.default_partitions}— including the ESP32-S3 ones, which thereby override WLED's own 8 MB S3 default. So every node image expects the same layout regardless of SoC:nvs 0x9000,otadata 0xe000,app0 0x10000(1536 KiB),app1 0x190000,spiffs 0x310000(960 KiB) — 4 MB in total. On a node with a larger flash chip (the v6 Heltec Wireless Paper has 8 MB) everything above 4 MB simply stays unpartitioned. - The gateway uses the Arduino 8 MB default table — its
platformio.inisets noboard_build.partitions, and theesp32-s3-devkitc-1board definition declares an 8 MB chip:app0 0x10000(3264 KiB),app1 0x340000,spiffs 0x670000,coredump 0x7F0000. - You never need
--flash-mode/--flash-freq/--flash-size. All three default tokeep, so esptool preserves the values the compiled bootloader already carries — which is exactly what the build intended. Those flags matter only when merging an image, see the note at the end of Route C.
A classic-ESP32 profile is in the pipeline
RaceLink_Node_v7_classic_esp32_emac (internal-EMAC Ethernet,
board esp32dev, device type 14 — the GLEDOPTO GL-C-616WL) lives on
the feat/eth-emac-gledopto branch of RaceLink_WLED. It is not on
main and not part of any release, so it has no asset row above
and the web flasher does not offer it. Being a classic ESP32 it
would boot from 0x1000, like the S2 profiles.
Route A — no cable needed¶
If the device already runs RaceLink firmware, use the host's OTA
workflow: Firmware Update (OTA) in the WebUI pushes a node .bin
over the node's Wi-Fi AP, one device or a whole fleet at a time. That
is the sanctioned upgrade path and it is documented in
RaceLink_Host/firmware-updates.md.
A node running stock WLED (not yet RaceLink) can also be converted over the air — see §"Bootstrapping a blank node through WLED".
The gateway has no OTA path. Every gateway update goes over USB.
Prerequisites¶
Windows — driver and COM port¶
Most supported boards reach the PC through a USB-to-serial bridge chip rather than the SoC's native USB:
| Bridge chip | Typical board | Driver |
|---|---|---|
| Silicon Labs CP210x | Heltec Wireless Stick V3 / Wireless Paper | Silicon Labs CP210x VCP |
| WCH CH340 / CH341 | generic ESP32 dev boards, many clones | WCH CH341SER |
| FTDI FT232 | older dev boards | FTDI VCP |
| native USB-Serial-JTAG | Heltec WiFi LoRa 32 V4, bare ESP32-S3 / C3 wired straight to USB | in-box on Windows 10/11 — appears as USB Serial Device |
The WiFi LoRa 32 V4 has no bridge chip
Unlike every other supported Heltec board, the V4 carries no CP2102 —
its USB socket is wired to the ESP32-S3's own USB-Serial/JTAG
peripheral. So it needs no driver on Windows 10/11, it appears under
a different Device Manager heading than a V3 sitting next to it, and
on Linux it is /dev/ttyACM0 rather than /dev/ttyUSB0.
This is also why the gateway firmware builds the V4 environment with
ARDUINO_USB_CDC_ON_BOOT=1 and the V3 without it. A V4 flashed with
a build that lacks the flag comes up with a working radio and a dead
USB port: the OLED shows the 30-second SYNC, and the host never sees
the gateway at all. Fixed in gateway v0.1.10 — on an earlier
build, re-flash rather than debug the cable.
Find the port in Device Manager → Ports (COM & LPT), or from PowerShell:
Run it once with the board unplugged and once with it connected — the entry that appears the second time is yours.
Linux — port and permissions¶
The board shows up as /dev/ttyUSB0 (CP210x, CH340, FTDI) or
/dev/ttyACM0 (native USB-Serial-JTAG). Confirm which:
Serial access needs group membership, not sudo:
Log out and back in, then check with groups. Flashing as root works
but leaves root-owned state behind and is not needed.
Ubuntu / Debian: brltty steals CH34x adapters
The braille-terminal daemon claims some CH340/CH341 adapters the
moment they are plugged in, so /dev/ttyUSB0 appears and vanishes
again within a second. If dmesg shows the port being released
right after it is created, remove or mask the daemon:
Verify the download before you flash¶
A truncated download flashes happily and then fails to boot, which is a
miserable thing to debug. Every release states a SHA-256 per file in its
-assets.json; compare it against what you downloaded.
# Linux — check one file against the digest the release states for it
file=WirelessStickV3-ESP32S3-0.1.7-usbflash.bin
python -c "import json,sys; print(next(a['sha256'] for e in json.load(open(sys.argv[1]))['environments'] for a in e['assets'] if a['file']==sys.argv[2]))" \
RaceLink_Gateway-0.1.7-assets.json "$file"
sha256sum "$file"
Releases before gateway v0.1.7 / RaceLink_WLED v0.1.9
Those shipped a -sha256.txt as well, which sha256sum -c reads
directly. It was dropped because it restated the digests
-assets.json already carries.
Installing esptool¶
esptool is Espressif's official flasher and works identically on
Windows and Linux. It needs Python 3.10 or newer; a virtual
environment keeps it out of the way of your system Python.
python -m venv $HOME\esptool-env
& $HOME\esptool-env\Scripts\Activate.ps1
pip install esptool
esptool version
If activation is refused, the execution-policy note in
RaceLink_Host/standalone-install.md
§"Windows installation and usage" applies here too.
pipx install esptool works equally well and keeps the tool on
PATH permanently.
Espressif publishes standalone builds for Windows, Linux and macOS on the esptool releases page. Unpack and call the executable directly — every command below works unchanged.
esptool vs esptool.py, hyphens vs underscores
esptool v5 renamed the entry point from esptool.py to esptool
and every subcommand from write_flash to write-flash. The old
forms still work but print a deprecation warning and are scheduled
for removal. This page uses the current spelling; if you are on
esptool v4, substitute esptool.py write_flash and so on.
Entering download mode¶
ESP32 boards flash from a ROM bootloader that must be active. Most dev
boards enter it automatically — esptool toggles DTR/RTS and the board's
auto-reset circuit does the rest. When that fails (Failed to connect
… No serial data received), do it by hand:
- Hold BOOT (also labelled
PRG,IO0orGPIO0). - Tap RESET (also labelled
RSTorEN). - Release BOOT.
- Run the esptool command within a few seconds.
- Tap RESET again afterwards to leave download mode.
Board-specific notes:
- Heltec Wireless Stick V3 / WiFi LoRa 32 V4 / Wireless Paper —
both buttons are present (
PRGandRST); auto-reset normally works, so the manual sequence is a fallback. On the V4 the reset is driven over native USB, so the caveat below about leaving download mode applies to it as well. - HT-CT62 module (
RaceLink_Node_v1_c3_ct62) — a bare module with no buttons. PullGPIO0toGNDwhile applying power, and flash over a USB-UART adapter on the module'sTX/RXpads. The build profile records that these modules are unhappy at high rates: start at--baud 115200and drop to19200(some units need9600) if the connection attempt fails. - Native-USB boards (Heltec WiFi LoRa 32 V4, bare ESP32-S3 / C3) —
after a manual download-mode entry, esptool cannot always reset the
chip back out again, because the USB peripheral is part of the ROM
bootloader. Add
--after watchdog-reset, or just power-cycle the board when the write finishes.
Route B — esptool, application image only¶
This is the normal case for a retail dev board or any board that has
run an Arduino-based sketch before. The existing bootloader and
partition table stay untouched; only the application partition at
0x10000 is rewritten.
Node images work exactly the same way — only the filename changes:
esptool --port /dev/ttyUSB0 --baud 460800 write-flash 0x10000 \
./RaceLink_Node_v4_s3_llcc68-0.1.9-ota.bin
There is nothing to decide here. 0x10000 is correct for all eight
release assets, on every SoC, and no --chip or flash-parameter flag is
needed — esptool detects the chip itself and leaves the existing
bootloader's flash settings alone.
Do read the chip line esptool prints before it writes:
If that does not name the SoC your .bin was built for
(table above), stop and re-check which
file you picked — that mismatch is the one way to waste an afternoon.
If the board previously took an OTA update
A board that has done an OTA runs from app1, and its OTA-data
partition still points there — so your fresh write to app0 is
ignored and the old firmware boots. Reset the OTA selector once:
With an empty OTA-data partition the bootloader falls back to
app0.
Route C — full flash (blank chip)¶
A bare module on a custom PCB, or a chip you erased with erase-flash,
has no bootloader. One file covers it:
esptool --chip esp32s3 --port /dev/ttyUSB0 write-flash 0x0 \
WirelessStickV3-ESP32S3-0.1.7-usbflash.bin
Substitute your own --chip and --port. The merged image carries the
per-SoC bootloader offset internally, so this command is the same for
every board — the S2's 0x1000 is inside the file, not on the command
line. That is the whole route.
What is inside that file¶
Four blocks. The release states each one's offset, size and digest in the
parts list of its -assets.json, so none of this has to be transcribed:
| Offset | Block | Role |
|---|---|---|
0x0 or 0x1000 — per SoC |
bootloader | second-stage bootloader |
0x8000 |
partitions | partition table |
0xe000 |
boot_app0 | OTA selector, points the bootloader at app0 |
0x10000 |
application | the same bytes as -ota.bin |
Up to gateway v0.1.6 and RaceLink_WLED v0.1.8 the first three were published as separate files too, and this route had a four-command variant. It is gone: it reached exactly the same state with more steps and one number — the bootloader offset — that had to be looked up per SoC and could be got wrong.
Writing the blocks separately — for a build of your own¶
You only need this if you built the firmware yourself; a release hands you
the merged image. After pio run -e <env>, bootloader.bin and
partitions.bin are in .pio/build/<env>/, and boot_app0.bin is in
~/.platformio/packages/framework-arduinoespressif32/tools/partitions/
(%USERPROFILE%\.platformio\… on Windows). Put all four in one folder and
work there.
Pick the block for your SoC by the SoC column of the
asset table. The --chip value and
the bootloader offset are the only things that differ; everything else
is identical.
Bootloader at 0x0.
Bootloader at 0x0.
# Linux
esptool --chip esp32c3 --port /dev/ttyUSB0 --baud 115200 write-flash \
0x0 ./bootloader.bin \
0x8000 ./partitions.bin \
0xe000 ./boot_app0.bin \
0x10000 ./firmware.bin
# Windows
esptool --chip esp32c3 --port COM5 --baud 115200 write-flash `
0x0 .\bootloader.bin `
0x8000 .\partitions.bin `
0xe000 .\boot_app0.bin `
0x10000 .irmware.bin
The lower baud rate is deliberate — see the HT-CT62 note under §"Entering download mode".
Substitute your own port; leave the offsets alone. firmware.bin is the
application your build produced, in .pio/build/<env>/.
Merging a build of your own¶
This is what the release workflow does to produce -usbflash.bin. The four
blocks collapse into one image written at offset 0:
# ESP32-S3 gateway
esptool --chip esp32s3 merge-bin -o my-gateway-factory.bin \
0x0 bootloader.bin 0x8000 partitions.bin \
0xe000 boot_app0.bin 0x10000 firmware.bin
# ESP32-S2 node — bootloader part stays at 0x1000, output still starts at 0x0
esptool --chip esp32s2 merge-bin -o my-node-factory.bin \
0x1000 bootloader.bin 0x8000 partitions.bin \
0xe000 boot_app0.bin 0x10000 firmware.bin
Write it with write-flash 0x0 <factory>.bin, or hand it to a web
flasher at address 0x0.
--flash-mode, --flash-freq and --flash-size are not needed:
like write-flash, they default to keep, so the compiled bootloader's
own header survives the merge — for the gateway that is qio / 80 MHz /
8 MB, for the node profiles qio (v3: 40 MHz, v4–v6: 80 MHz). Pass them
only if you are deliberately overriding the build. Full option set in
the esptool
documentation.
A factory image resets the device, not just its firmware
Writing a factory image rewrites the partition table, so anything
the old layout held becomes unreachable and WLED reformats its
filesystem on the next boot: presets, cfg.json, the RaceLink
master-MAC binding and the group id are all gone. That is correct
for commissioning and wrong for an update — for updates use
Route B or OTA.
A factory image is a cable-only artifact — never send it over OTA
A merged image is not a valid OTA payload, and the checks that normally stop it are weaker than you would hope:
- WLED's
/updaterejects it, but withThis firmware file is missing compatibility metadata.— not with a release-name mismatch. WLED looks for its metadata structure in a ~768-byte window at the very start of the upload (METADATA_OFFSET = 256,METADATA_SEARCH_RANGE = 512inwled00/ota_update.cpp). In a merged image that window holds bootloader bytes; the metadata sits ~0x10100into the stream, far outside it. - On ESP32-S2 the upload dies even earlier: the merged image
starts with
0x1000bytes of0xFFpadding, and the ESP32Updatelibrary aborts on the first chunk because byte 0 is not the image magic0xE9. On ESP32-S3 / C3 that check passes — byte 0 is the bootloader's own0xE9— so the S2 safety net does not exist there. - With "Ignore firmware validation" ticked, nothing stops it.
That checkbox pre-sets
releaseCheckPassed, so the metadata check is skipped entirely and every chunk is written into the inactive OTA slot. The slot then begins with a bootloader image instead of an application, passes the library's bootable-partition check (again0xE9), and gets marked as the boot target. Recovery means a serial re-flash.
Keep the two artifacts clearly apart: the release .bin is the only
thing that ever goes to /update or into the host's OTA dialog; a
-factory.bin only ever goes over USB.
Web flashers — no installation¶
Chromium-based browsers expose the Web Serial API, which is enough to
run a full flasher in the browser. Desktop Chrome, Edge or Opera is
required; Firefox and Safari do not implement Web Serial, and neither
does any browser on iOS. The Linux port permissions above still apply —
the browser needs the same access to /dev/ttyUSB0 as esptool does.
The web flasher (fw.racelink.dev)¶
The RaceLink flasher. It knows the models and the releases, so there is no offset to type and no file to pick:
- Choose what the device should become — gateway, node or startblock.
- Choose the board. This is the one choice nothing can check for you: it refuses an image built for a different chip, but five models share ESP32-S3 and those are indistinguishable over USB.
- Choose a version. The newest is preselected; the last five are kept.
- Choose what you are doing — see the two modes below.
- When commissioning: choose the region and channel for a board with a radio, and the LED output for a node. Both are written into the image before flashing, so the device starts configured rather than coming up on the compile-time defaults and being corrected afterwards.
- Connect the board, click Flash, pick the port in the browser's chooser.
The firmware is checked against the digest published with the release before anything is written, and what was written is verified afterwards.
Commission a device, or update its firmware¶
| writes | keeps | |
|---|---|---|
| Commission a device | the whole factory image, from offset 0x0 |
nothing — every stored setting is replaced |
| Update the firmware only | the application partition alone | radio channel, LED output, Wi-Fi, presets, master binding |
Commissioning is the mode for a new board, or one that should start over. It cannot preserve settings even with the erase option off: the factory image spans NVS, so the radio channel is rewritten whatever that option says. The option governs the rest of the chip, which is why the WLED filesystem survived an un-erased flash but the channel never did.
Update the firmware only is the same thing an over-the-air update does, over the cable — for a device with no Wi-Fi in reach, or one whose firmware no longer boots far enough to accept one. It reads the device's partition table first and refuses if this version's layout differs, since an application image written against a different table lands in the wrong place. It also clears the OTA selector, so a device that last updated over the air boots what was just written rather than the slot it was still pointing at.
The selection stays put once a device is done, so a batch is unplug, plug in the next, click — and each device's MAC is recorded, with the session exportable as CSV if you need a commissioning record.
esptool-js — the generic alternative¶
https://espressif.github.io/esptool-js/ is Espressif's official in-browser build of esptool. It knows nothing about RaceLink, so you supply the file and the offset yourself — useful for a build the flasher does not carry, such as one you compiled.
- Click Connect and pick the board's serial port from the browser's device chooser.
- Set Flash Address —
0x10000for a-ota.bin,0x0for a-usbflash.bin. - Choose the file.
- Start the Program run and wait for the console to report completion.
- Power-cycle the board.
ESP Launchpad (DIY tab) — several files at once¶
https://espressif.github.io/esp-launchpad/ has a DIY mode that
accepts several local files, each with its own flash offset. A release
needs none of this — -usbflash.bin at 0x0 is one file — but it is the
browser equivalent of the multi-block command for a build of your own,
when you have the pieces, no merged image and no Python environment:
File from .pio/build/<env>/ |
Offset |
|---|---|
bootloader.bin |
0x0 on ESP32-S3 / C3, 0x1000 on ESP32-S2 |
partitions.bin |
0x8000 |
boot_app0.bin (from the framework package) |
0xe000 |
firmware.bin |
0x10000 |
install.wled.me — not for RaceLink firmware¶
The WLED web installer only serves the official WLED builds from
wled/WLED. It cannot flash a RaceLink node image, and pointing it at
a RaceLink node overwrites the usermod with stock WLED — the node then
disappears from Discover Devices until RaceLink firmware is flashed
back.
It used to be the most convenient way to get a bootloader onto a blank
node, which is what the next section is about. Since the releases carry
factory images that detour is no longer necessary: the
web flasher or a single write-flash
0x0 commissions a blank node directly, without routing anyone through
WLED's Ignore firmware validation checkbox.
Bootstrapping a blank node through WLED¶
Superseded — kept for the record
This detour existed because releases published no bootloader. Since
RaceLink_WLED v0.1.8 they do, so a blank node is commissioned
directly by the web flasher or by
one write-flash 0x0 with the factory image. Prefer either: the
route below ends at Ignore firmware validation, which disables
every payload check WLED has, and it is not a control to teach
someone on their first device.
Use this only for a node already running stock WLED that you want to convert without a cable.
For a node whose flash is blank and where you would rather not set up PlatformIO, stock WLED can supply the missing bootloader and partition table, and WLED's own OTA page can then install the RaceLink build. Gateways have no equivalent path — they need Route C.
- Flash stock WLED with https://install.wled.me in desktop Chrome or Edge. The installer detects the SoC and writes a complete image, bootloader and partition table included.
- Join the node's access point and open its web UI — after a stock
install that is
WLED-AP/wled1234athttp://4.3.2.1. (The RaceLink builds rename the AP toWLED_RaceLink_AP; seeRaceLink_WLED/operator-setup.md§"Default factory state".) - Open the manual OTA form — Settings → Security & Updates →
Manual OTA Update, i.e.
http://4.3.2.1/update. - Tick "Ignore firmware validation", choose the release
.binfor your node, and press Update WLED!. - The node reboots into RaceLink firmware. Confirm with the checks in §"First boot".
Why the checkbox is needed — and what it disables
WLED's /update handler compares the WLED_RELEASE_NAME baked
into the uploaded image against the running build's and rejects a
mismatch with HTTP 500 (Firmware release name mismatch). Stock
WLED reports e.g. ESP32-S3, the RaceLink build reports
RaceLink_Node_V4_TYPE_12 — always a mismatch, so the upload only
succeeds with validation off. Full gate breakdown in
reference/wled-ota-gates.md.
The checkbox does not disable one gate, it disables all payload
validation for that upload (releaseCheckPassed is pre-set in
beginOTA). With it ticked, you are the only guard against two
things WLED would otherwise refuse:
- An image for the wrong SoC family, which does not boot.
Verify the
<env>in the filename against the asset table first. - A merged
-factory.binfrom Route C. Upload the plain release.binhere — never a factory image; see the danger box in that section for what happens if you do.
The partition table written by the WLED installer stays in place; the RaceLink application reads the table at runtime, so the layout difference between stock WLED and the RaceLink profiles is harmless.
First boot¶
Gateway¶
- The OLED lights up and shows TX / RX counters.
- Plug it into the host machine, start the host, and check that the
master pill in the WebUI header turns cyan (
IDLE) within a second of page load. - Full walkthrough:
RaceLink_Gateway/operator-setup.md§"First-time connection".
WLED node¶
- The strip lights up in a single solid colour — red, green or blue, rolled once on the very first boot and then persisted.
- Five-click the boot button to check the radio path, or triple-click
it to open the Wi-Fi AP at
4.3.2.1. http://<node-ip>/json/inforeports the build; thereleasefield must read theWLED_RELEASE_NAMEfrom the table at the top of this page.- Run Discover Devices from the host WebUI to pair the node to your gateway. The node answers the first gateway that broadcasts, so a freshly flashed node needs no preparation.
- Full walkthrough:
RaceLink_WLED/operator-setup.md.
Erasing and recovery¶
erase-flash wipes the entire chip — bootloader, partition table,
application, WLED settings, presets and the RaceLink master-MAC
binding. Afterwards the device can only be revived through
Route C. Reach for it only when a
device is in an unknown bad state.
Softer options, in increasing severity:
- Forget master MAC from the host WebUI — clears the gateway binding, keeps everything else. The canonical way to move a node between gateways.
- Reset to RaceLink defaults in the Device Options dialog — clears
host-set overrides, see
RaceLink_WLED/operator-setup.md§"Reset to RaceLink defaults". - WLED factory reset (Security & Updates → Reset) — WLED settings only.
- Filesystem erase (
pio run -t uploadfs) — wipes presets, segments and settings, keeps the firmware.
Troubleshooting¶
Failed to connect to ESP32-S3: No serial data received¶
The chip is not in download mode, or nothing is listening on that port.
Work through: correct port selected → driver installed (Windows) →
dialout membership (Linux) → data-capable USB cable, not
charge-only → manual BOOT/RESET sequence →
--baud 115200.
esptool reports a different chip than expected¶
Stop. You are about to write an image for the wrong SoC family. Re-read
the <env> in the filename against the table at the top of this page.
The write succeeds but the device stays dark¶
Almost always Route B on a chip that has no matching bootloader — or an
OTA selector still pointing at app1. Try the
erase-region 0xe000 0x2000
fix first; if that changes nothing, do a full
Route C flash.
The COM port disappears right after flashing¶
Expected on boards using the SoC's native USB: the ROM bootloader's USB
device goes away when the application starts, and the application
re-enumerates (or does not, if ARDUINO_USB_CDC_ON_BOOT=0).
Which case you are in depends on the build, and the two supported native-USB targets differ:
- Heltec WiFi LoRa 32 V4 gateway — builds with CDC-on-boot on, so the port must come back within a second or two of the reset. If it does not, the flash landed but the build was an older one without the flag; see the warning under §"Windows — driver and COM port".
- Every node profile — builds with CDC-on-boot off, because those boards reach the PC through an external bridge chip whose port never depends on the application. A vanished port on a bare ESP32-S3 or C3 running a node build is therefore expected, not a fault; the device is running.
Permission denied: '/dev/ttyUSB0' on Linux¶
Group membership has not taken effect. groups must list dialout; if
it does not, log out and back in. See
§"Linux — port and permissions".
The browser flasher shows no ports¶
Web Serial needs desktop Chrome or Edge 89+ over HTTPS. Firefox and
Safari do not implement it. On Linux the browser needs the same
dialout access as esptool.
OTA says This firmware file is missing compatibility metadata¶
WLED could not find its metadata structure in the first ~768 bytes of
the upload. The usual cause is uploading a merged -factory.bin
instead of the plain release .bin — in a merged image that window
holds bootloader bytes. Upload the release asset instead; a factory
image is USB-only. The other cause is a binary that is not a WLED build
at all.
Do not work around this with Ignore firmware validation — that lets the merged image through into the OTA slot and costs you a serial re-flash. Detail in the danger box under Route C.
OTA returns HTTP 500 Firmware release name mismatch¶
Expected whenever the uploaded image's WLED_RELEASE_NAME differs from
the running build's. Tick Ignore firmware validation — and re-check
the SoC first, see the warning in
§"Bootstrapping a blank node through WLED".
The host's OTA dialog exposes the same escape hatch as Skip
firmware-name validation; details in
reference/wled-ota-gates.md.
The node pairs but the gateway never hears it¶
Radio parameters are compile-time on both sides and must match. Check
the gateway's defaults in
RaceLink_Gateway/operator-setup.md
§"Radio defaults" against the node profile's RACELINK_* flags. A
mismatched RACELINK_SYNC_WORD, spreading factor or frequency produces
exactly this silence.
For everything after commissioning, the operator-side index is
troubleshooting.md.
Where the binaries come from¶
Both releases are produced by a manually dispatched GitHub Actions
workflow that builds the shipping PlatformIO environments and uploads
the application images plus a SHA-256 manifest. The gateway workflow
builds two environments from its own repository; the WLED workflow
checks out upstream wled/WLED, stages the RaceLink usermod and each
shipping build profile into it, and builds every [env:…] the profile
declares. Version numbering follows the cross-component policy in
versioning.md.
The release flows themselves are documented in
RaceLink_WLED/README.md §"GitHub release
workflow"; the source-build path for both components is in the
respective repository README.