Skip to content

Factory Tools

The factory_tools module is a collection of Python host-side scripts that sit inside each board’s Factory/tools/ directory. These scripts are the final step in the manufacturing and development workflow: they take the binary artefacts produced by the build system and push them onto the physical hardware over USB-serial using esptool.

The module contains two canonical scripts that every board shares, plus one board-specific generator:

ScriptPurpose
flash_all.pyMulti-mode flash utility — recovery, full, or firmware-only
flash_factory.pyTargeted flash of the factory (recovery) partition only
gen_ota_initial.pyGenerates a valid ota_data_initial.bin.bkp (ESP32-S3-WROOM-CAM only)

The factory_tools scripts operate within a three-layer factory ecosystem. Understanding this context is essential before using the tools.

Flash Memory Layout

Host (PC)

produces binaries

produces ota_data_initial.bin.bkp

esptool write_flash

esptool write_flash

esptool write_flash

esptool write_flash

esptool write_flash

build_system

build_one.py / build_mgr.py

factory_tools

flash_all.py / flash_factory.py

factory_tools

gen_ota_initial.py

0x1000 - Bootloader

(custom_bootloader)

0xC000 - Partition Table

0x10000 - OTA Data

(otadata)

0x20000 - App0

(main firmware)

0x660000+ - Flash FS

(LittleFS / FAT)

0x7A0000+ - Factory App

(recovery)

  • factory_bootloader — The custom bootloader binary (bootloader.bin) that flash_all.py --recovery flashes at 0x1000. It contains OTA-data backup/restore logic and the button-hold-to-recover mechanism.
  • factory_app — The factory application binary (factory.bin) flashed to the factory partition. It provides the on-device menu for SD-card updates, partition selection, and hardware self-test.
  • build_system — Produces all .bin artefacts that land in each board’s Factory/installer/ directory before flash_all.py is invoked.

Every supported board has its own copy of the tools directory. The scripts are functionally identical across boards; board-specific differences (flash chip, partition layout) are handled automatically via the partition-table CSV parser and the --chip flag passed to esptool.

Board directoryflash_allflash_factorygen_ota_initial
boards/ESP32_S3_WROOM_CAM/Factory/tools/——✓
boards/esp32_2432s028r/Factory/tools/✓✓—
boards/esp32_3248s035c/Factory/tools/✓✓—
boards/esp32_3248s035r/Factory/tools/✓✓—
boards/esp32s3_4827s043c/Factory/tools/✓✓—
boards/esp32s3_8048_touch_lcd_7/Factory/tools/✓✓—
boards/esp32s3_8048s043c/Factory/tools/✓✓—
boards/esp32s3_8048s050c/Factory/tools/✓✓—
boards/esp32s3_8048s070c/Factory/tools/✓✓—
boards/esp32s3_bzm_tft35_gt911/Factory/tools/✓✓—
boards/esp32s3_hmi43v3/Factory/tools/✓✓—
boards/esp32s3_zx3d50ce02s_usrc_4832/Factory/tools/✓✓—
boards/pibot_pendant_v1_0/Factory/tools/✓✓—

Note: ESP32_S3_WROOM_CAM only ships gen_ota_initial.py; its flash operations are handled through the global tools/flash_scripts/flash_mgr.py.


All scripts must agree with the partition table. The fixed addresses are defined as constants at the top of each flash_all.py:

SymbolAddressBinary
BOOTLOADER_OFFSET0x1000bootloader.bin
PARTITIONS_OFFSET0xC000partitions.bin
OTA data0x10000ota_data_initial.bin
APP0_OFFSET0x20000main firmware
Factory offsetauto-detectedfactory.bin — parsed from CSV, default 0x7A0000

SPI Flash - 8 MB typical layout

0x1000 Bootloader (bootloader.bin)

0xC000 Partition table (partitions.bin)

0x10000 OTA data (ota_data_initial.bin)

0x20000 App0 / main firmware

0x660000 Flash filesystem (LittleFS / FAT)

0x7A0000 Factory app (factory.bin)


flash_all.py — Multi-Mode Flashing Utility

Section titled “flash_all.py — Multi-Mode Flashing Utility”

flash_all.py is the primary production and development flash tool. It supports three mutually exclusive modes selected via a required command-line argument.

ModeWhat is flashedTypical use
--recoverybootloader + partitions + factoryFirst-time board setup; restore recovery infrastructure
--full --fw <bin>bootloader + partitions + factory + firmwareComplete factory programming of a new unit
--fw <bin>firmware only at app0Development: fast iteration without touching recovery

--recovery

--full --fw bin

--fw bin

flash_all.py CLI

Mode?

bootloader.bin @ 0x1000

partitions.bin @ 0xC000

factory.bin @ factory_offset

bootloader.bin @ 0x1000

partitions.bin @ 0xC000

factory.bin @ factory_offset

firmware.bin @ 0x20000

firmware.bin @ 0x20000

esptool write_flash

flash_all.py --recovery [--port PORT] [--baud RATE]
flash_all.py --full --fw FIRMWARE.bin [--port PORT] [--baud RATE]
flash_all.py --fw FIRMWARE.bin [--port PORT] [--baud RATE]
ArgumentDefaultDescription
--recovery—Flash bootloader + partitions + factory
--full—Flash everything (requires --fw)
--fw FILE / --firmware FILE—Flash firmware only to app0
--fw-file FILE—Alternate firmware path for use with --full
--port, -pauto-detectSerial port (e.g., /dev/ttyUSB0, COM3)
--baud460800Baud rate for esptool

No

Yes

No

Yes

No

Yes

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Start

Parse CLI arguments

Mode valid?

Print error, exit 1

Port specified?

Auto-detect via pyserial

Keywords: CP210x / CH340

CH9102 / FTDI / USB Serial

Use specified port

Found?

Print error, exit 1

Build file list

recovery or full?

find_factory_offset

parse partitions CSV

check_file for

bootloader / partitions / factory

All exist?

Print error, exit 1

Append base files to list

full or fw mode?

check_file firmware

Exists?

Print error, exit 1

Append firmware to list

flash: invoke esptool subprocess

Success?

Exit 0

Exit 1

The script uses serial.tools.list_ports (pyserial) and matches USB-serial adapter descriptions against known ESP32 chip identifier strings:

CP210x • CH340 • CH9102 • FTDI • USB Serial • USB-SERIAL

If no keyword match is found, the first available COM port is used as a fallback. If no ports exist at all, the script exits and instructs the user to pass --port explicitly.

find_factory_offset() searches for partitions*.csv files using these glob patterns:

partitions*.csv
boards/*/partitions*.csv

It reads each CSV line by line, looking for a row whose third column (subtype) equals factory (case-insensitive), then returns the fourth column (offset string). If no match is found in any CSV, the hard-coded default 0x7A0000 is returned — the standard factory offset for an 8 MB flash layout.

The scripts expect this directory structure relative to the board’s Factory/ directory:

boards/<board>/Factory/
├── installer/
│ ├── bootloader.bin ← Custom bootloader binary
│ ├── partitions.bin ← Compiled partition table
│ └── factory.bin ← Factory recovery app binary
└── tools/
├── flash_all.py
└── flash_factory.py

flash_factory.py — Factory Partition Flash Tool

Section titled “flash_factory.py — Factory Partition Flash Tool”

flash_factory.py is a focused, single-purpose tool for flashing only the factory (recovery) partition. It is useful when:

  • The factory app needs to be updated without disturbing the bootloader or main firmware slot.
  • During manufacturing QA, a defective recovery image must be replaced in isolation.
flash_factory.py [--port PORT] [--bin FILE] [--baud RATE] [--offset HEX]
ArgumentDefaultDescription
--port, -pauto-detectSerial port
--bin, -binstaller/factory.binPath to factory binary
--baud460800Baud rate
--offset, -oauto-detectManual factory partition offset; overrides CSV lookup

No

Yes

Yes

No

No

Yes

No

Yes

Yes

No

Start

factory.bin exists?

Exit 1

--offset given?

Use manual offset

find_factory_offset

Patterns:

partitions*.csv

boards/*/partitions*.csv

boards/*/*/partitions*.csv

Found?

Print error, suggest --offset

Exit 1

Port specified?

Auto-detect via pyserial

Use specified port

Print flash summary

binary / offset / port / baud

esptool write_flash offset factory.bin

Success?

Exit 0

Exit with esptool return code

flash_factory.py uses a wider CSV search pattern than flash_all.py — it adds a third depth level (boards/*/*/partitions*.csv) to accommodate boards that keep their partition table nested under a sub-directory.


Board-specific: This script is only present for boards/ESP32_S3_WROOM_CAM/. All other boards rely on the factory_bootloader to manage OTA data at boot time (backup, erase, and restore).

ESP-IDF’s OTA system uses a special otadata partition (8 KB = two 4 KB sectors at 0x10000) to track which application slot (ota_0 / ota_1) should boot. A freshly erased partition is all 0xFF, which ESP-IDF interprets as “no valid OTA entry — fall back to factory”. To direct the device to boot ota_0 (the main firmware slot) after initial programming, a valid entry must be pre-written.

gen_ota_initial.py generates ota_data_initial.bin.bkp containing exactly one valid OTA entry pointing to ota_0. The postbuild.cmake script copies this file into the installer package as ota_data_initial_16MB.bin, which is flashed at 0x10000.

OTA Entry Binary Format (esp_ota_select_entry_t, 32 bytes)

Section titled “OTA Entry Binary Format (esp_ota_select_entry_t, 32 bytes)”
BytesFieldValueMeaning
0–3ota_seq1 (uint32 LE)Odd sequence → slot 0 = ota_0
4–23seq_label[20]0xFF × 20Unused
24–27ota_state0xFFFFFFFFOTA_IMG_VALID
28–31crc32computedCRC32 (Ethernet poly 0xEDB88320) over bytes 0–27

Layout note: ESP-IDF 5.x places ota_state at offset 24, overlapping the field labelled unused[4] in older SDK documentation. The script comments reflect this discrepancy explicitly.

ota_seq valueSelected slot
1 (odd)ota_0 — app0 at 0x20000
2 (even)ota_1
0xFFFFFFFF (erased)Fall through to factory partition
ota_data_initial.bin.bkp (8192 bytes total)
├── Sector 1 (4096 bytes)
│ ├── Bytes 0–31 valid esp_ota_select_entry_t (seq=1 → ota_0)
│ └── Bytes 32–4095 0xFF padding
└── Sector 2 (4096 bytes)
└── Bytes 0–4095 0xFF (no second entry required)

python tools/gen_ota_initial.py

Resolve paths:

script_dir / factory_dir

output = factory_dir/ota_data_initial.bin.bkp

make_ota_entry(seq=1)

struct.pack_into:

ota_seq=1 at offset 0

ota_state=0xFFFFFFFF at offset 24

CRC32(bytes[0:28]) at offset 28

Build sectors:

sector1[0:32] = entry

sector1[32:] = 0xFF

sector2 = 0xFF x 4096

Write 8192 bytes to

ota_data_initial.bin.bkp

Print path, size, slot, CRC

Exit

The script implements the standard Ethernet CRC32 (polynomial 0xEDB88320) inline, with no external dependencies beyond the Python standard library:

def crc32(data: bytes) -> int:
crc = 0xFFFFFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xEDB88320
else:
crc >>= 1
return crc ^ 0xFFFFFFFF
Terminal window
cd boards/ESP32_S3_WROOM_CAM/Factory
python tools/gen_ota_initial.py
# Generated: .../ota_data_initial.bin.bkp (8192 bytes)
# Entry: seq=1 -> ota_0 (app0)
# CRC32: 0xXXXXXXXX

This file must exist before running idf.py build. The postbuild.cmake hook fails if the file is absent.


PackageRole
esptoolInvoked as python -m esptool via subprocess. Must be installed in the active Python environment.
pyserialserial.tools.list_ports for COM port auto-detection.
argparseStandard library — CLI argument parsing.
structStandard library — binary packing (gen_ota_initial.py only).
glob, csv, os, sys, subprocessStandard library — file discovery, process invocation.
Terminal window
pip install esptool pyserial

1. First-Time Board Programming (Production)

Section titled “1. First-Time Board Programming (Production)”
Terminal window
cd boards/esp32s3_8048s043c/Factory
# Binaries must be in installer/ first (see build_system docs)
python tools/flash_all.py --full --fw ../../build/firmware.bin

Use when the main firmware slot will be programmed separately (e.g., via OTA in the field):

Terminal window
python tools/flash_all.py --recovery

Only update the application slot; leave the recovery infrastructure untouched:

Terminal window
python tools/flash_all.py --fw build/firmware.bin --port /dev/ttyUSB0
Terminal window
python tools/flash_factory.py --bin installer/factory.bin --port COM5

When working with a non-standard partition layout:

Terminal window
python tools/flash_factory.py --offset 0x780000 --port /dev/ttyUSB0

6. Generate OTA Data (ESP32-S3-WROOM-CAM Only)

Section titled “6. Generate OTA Data (ESP32-S3-WROOM-CAM Only)”
Terminal window
cd boards/ESP32_S3_WROOM_CAM/Factory
python tools/gen_ota_initial.py # must run before idf.py build

ConditionScript behaviour
Binary file not foundPrints Error: <name> not found at <path> and exits 1
No serial port detectedPrints detection error, directs user to --port; exits 1
Factory offset not in any CSVflash_factory.py exits 1 with --offset hint; flash_all.py falls back to default 0x7A0000
--full without --fwArgument parser catches this immediately and exits 2
esptool not installedFileNotFoundError caught; prints pip install esptool hint; exits 1
esptool write failureCalledProcessError caught; exits with esptool’s return code

factory_tools (this module)

produces installer/*.bin

bootloader @ 0x1000

partitions @ 0xC000

factory @ 0x7A0000+

firmware @ 0x20000

ota_data_initial.bin.bkp

via postbuild.cmake

On-Device Factory Ecosystem

factory_bootloader

OTA backup / restore

Button-hold recovery

factory_app

SD update menu

Hardware self-test

Snapshot

build_system

build_one.py / variants.py

flash_all.py

recovery / full / fw

flash_factory.py

factory partition only

gen_ota_initial.py

OTA data generator

ESP32 Hardware

SPI Flash

Related moduleDocumentation
Custom bootloader (OTA backup, button-hold recovery)factory_bootloader.md
Factory application (on-device menu, GFX, SD update)factory_app.md