Skip to content

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.md and RaceLink_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.

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 hostGW_CMD_SET_RF_CONFIG over USB for a gateway, OPC_RF_CONFIG over LoRa for a node. See reference/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 0x10000 everywhere. 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 from 0x1000. 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) at 0xe000. Both partition tables above agree on 0xe000.
  • 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.ini sets no board_build.partitions, and the esp32-s3-devkitc-1 board 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 to keep, 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:

[System.IO.Ports.SerialPort]::GetPortNames()

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:

ls /dev/ttyUSB* /dev/ttyACM*
sudo dmesg | tail -20

Serial access needs group membership, not sudo:

sudo usermod -aG dialout "$USER"   # some distros use `uucp`

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:

sudo systemctl mask brltty.path brltty.service

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.

# Windows
Get-FileHash .\WirelessStickV3-ESP32S3-0.1.7-usbflash.bin -Algorithm SHA256
# 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.

python3 -m venv ~/esptool-env
source ~/esptool-env/bin/activate
pip install esptool
esptool version

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:

  1. Hold BOOT (also labelled PRG, IO0 or GPIO0).
  2. Tap RESET (also labelled RST or EN).
  3. Release BOOT.
  4. Run the esptool command within a few seconds.
  5. 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 (PRG and RST); 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. Pull GPIO0 to GND while applying power, and flash over a USB-UART adapter on the module's TX/RX pads. The build profile records that these modules are unhappy at high rates: start at --baud 115200 and drop to 19200 (some units need 9600) 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.

esptool --port COM5 --baud 460800 write-flash 0x10000 `
  .\WirelessStickV3-ESP32S3-0.1.7-ota.bin
esptool --port /dev/ttyUSB0 --baud 460800 write-flash 0x10000 \
  ./WirelessStickV3-ESP32S3-0.1.7-ota.bin

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:

Detecting chip type... ESP32-S3

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:

esptool --port /dev/ttyUSB0 erase-region 0xe000 0x2000

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 0x1000per 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.

# Linux
esptool --chip esp32s3 --port /dev/ttyUSB0 --baud 460800 write-flash \
  0x0     ./bootloader.bin \
  0x8000  ./partitions.bin \
  0xe000  ./boot_app0.bin \
  0x10000 ./firmware.bin
# Windows
esptool --chip esp32s3 --port COM5 --baud 460800 write-flash `
  0x0     .\bootloader.bin `
  0x8000  .\partitions.bin `
  0xe000  .\boot_app0.bin `
  0x10000 .irmware.bin

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".

Bootloader at 0x1000, not 0x0.

# Linux
esptool --chip esp32s2 --port /dev/ttyUSB0 --baud 460800 write-flash \
  0x1000  ./bootloader.bin \
  0x8000  ./partitions.bin \
  0xe000  ./boot_app0.bin \
  0x10000 ./firmware.bin
# Windows
esptool --chip esp32s2 --port COM5 --baud 460800 write-flash `
  0x1000  .\bootloader.bin `
  0x8000  .\partitions.bin `
  0xe000  .\boot_app0.bin `
  0x10000 .irmware.bin

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 /update rejects it, but with This 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 = 512 in wled00/ota_update.cpp). In a merged image that window holds bootloader bytes; the metadata sits ~0x10100 into the stream, far outside it.
  • On ESP32-S2 the upload dies even earlier: the merged image starts with 0x1000 bytes of 0xFF padding, and the ESP32 Update library aborts on the first chunk because byte 0 is not the image magic 0xE9. On ESP32-S3 / C3 that check passes — byte 0 is the bootloader's own 0xE9 — 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 (again 0xE9), 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)

https://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:

  1. Choose what the device should become — gateway, node or startblock.
  2. 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.
  3. Choose a version. The newest is preselected; the last five are kept.
  4. Choose what you are doing — see the two modes below.
  5. 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.
  6. 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.

  1. Click Connect and pick the board's serial port from the browser's device chooser.
  2. Set Flash Address0x10000 for a -ota.bin, 0x0 for a -usbflash.bin.
  3. Choose the file.
  4. Start the Program run and wait for the console to report completion.
  5. 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

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.

  1. 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.
  2. Join the node's access point and open its web UI — after a stock install that is WLED-AP / wled1234 at http://4.3.2.1. (The RaceLink builds rename the AP to WLED_RaceLink_AP; see RaceLink_WLED/operator-setup.md §"Default factory state".)
  3. Open the manual OTA formSettings → Security & Updates → Manual OTA Update, i.e. http://4.3.2.1/update.
  4. Tick "Ignore firmware validation", choose the release .bin for your node, and press Update WLED!.
  5. 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.bin from Route C. Upload the plain release .bin here — 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/info reports the build; the release field must read the WLED_RELEASE_NAME from 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

esptool --port /dev/ttyUSB0 erase-flash

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.