Skip to content

Manual Flashing

Ravi Singh edited this page May 23, 2026 · 1 revision

Manual Flashing

For when you'd rather not use the Browser Flasher — typically because you're developing, debugging with idf.py monitor, flashing a custom build, or running on a system where WebSerial doesn't work (Firefox, Safari, mobile, headless server).

Three paths, in order of effort:


Path A — esptool.py against a published binary

You want to flash a specific released version from GitHub without building from source.

  1. Download the .bin from the Releases page. Pick the asset matching your board:

    • Hub: tanksync-receiver-rx-vX.Y.Z.bin
    • TX: tanksync-transmitter-tx-vX.Y.Z.bin
  2. Install esptool:

    pip install esptool
    # or: pipx install esptool   (recommended — isolated env)
  3. Find your serial port:

    • Linux: ls /dev/ttyUSB* (DevKit) or /dev/ttyACM* (C3)
    • macOS: ls /dev/cu.usbserial-* or /dev/cu.usbmodem*
    • Windows: Device Manager → COM ports
  4. Flash:

    # Hub (ESP32 DevKit)
    esptool.py --chip esp32 --port /dev/ttyUSB0 -b 460800 \
      write_flash 0x10000 tanksync-receiver-rx-v2.8.5.bin
    
    # Transmitter (ESP32-C3 SuperMini)
    esptool.py --chip esp32c3 --port /dev/ttyACM0 -b 460800 \
      write_flash 0x10000 tanksync-transmitter-tx-v2.0.13.bin
  5. Power-cycle the board. The new firmware starts. Verify via the OLED / web UI footer that the displayed version matches what you flashed.

Why 0x10000? That's the offset of the OTA app partition in TankSync's partition table. The bootloader + partition table + NVS at lower addresses are untouched, so paired transmitters, calibration, and Wi-Fi creds survive the flash.


Path B — Full clean flash (wipes NVS too)

When you want a factory state — paired transmitters cleared, Wi-Fi creds gone, hub back to AP setup mode.

# Erase EVERYTHING first
esptool.py --chip esp32 --port /dev/ttyUSB0 erase_flash

# Then flash bootloader + partition table + app
# (these come from a build, not from the Releases page —
#  use Path C if you don't have these files)
esptool.py --chip esp32 --port /dev/ttyUSB0 -b 460800 \
  write_flash \
    0x1000  bootloader.bin \
    0x8000  partition-table.bin \
    0x10000 tanksync-receiver-rx-vX.Y.Z.bin

This wipes everything. Don't run this just to update — Path A is enough.


Path C — Build from source with ESP-IDF

For development, custom config, or debugging serial output.

Prerequisites

  • ESP-IDF v5.4 or later. Install per docs.espressif.com.
  • About 1 GB disk for the toolchain + build artifacts.

Build + flash

# Setup IDF environment
source ~/esp/esp-idf-v5.5.2/export.sh    # path varies by install

# Receiver Hub
cd firmware/Receiver-ESP32-DevKit
idf.py set-target esp32
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

# Transmitter
cd firmware/Transmitter-IDF
idf.py set-target esp32c3
idf.py build
idf.py -p /dev/ttyACM0 flash monitor

idf.py monitor opens a live serial console — useful for watching the boot log, debugging LoRa packets, observing the sleep cycle. Press Ctrl+] to exit.

Per-target build (multi-target hub support)

The hub firmware supports both ESP32 DevKit and ESP32-S3 SuperMini from the same source tree. scripts/build_all.sh builds all targets:

cd firmware/Receiver-ESP32-DevKit
scripts/build_all.sh   # produces build_esp32/ and build_esp32s3/

Pick the right binary for your hub board.


Versioning — how do I know what I flashed?

After flashing, check esp_app_desc.version (compiled into the binary at flash offset 0x30):

  • Hub OLED: scroll the carousel to the System screen — shows the version + short SHA.

  • Hub web UI: footer of every page.

  • PWA: Settings → Firmware → "current: vX.Y.Z".

  • From the command line on the binary itself:

    # Read the version directly out of the .bin
    xxd -s 0x30 -l 32 tanksync-receiver-rx-vX.Y.Z.bin

The version field is null-padded ASCII. If what you read doesn't match the filename, the binary was built before the rx-v2.8.5 single-VERSION-file fix — re-build from current main.


Troubleshooting

  • "Failed to connect to ESP32: ... Timed out waiting for packet header" — board isn't in download mode. Hold BOOT, tap RESET, release BOOT, retry.
  • "No serial data received" — wrong port or cable is power-only.
  • Flashes but boots into a loop — usually a config mismatch (sdkconfig vs hardware). Check idf.py monitor for the panic string.
  • Permission denied on /dev/ttyUSB0 — add yourself to dialout group (sudo usermod -aG dialout $USER + log out and back in).

For board-specific tips and the version-stamping caveat, see Firmware Versions.

Clone this wiki locally