Factory NVS
The factory NVS partition holds the node's permanent identity: the MQTT host it connects to, the X.509 client certificate and private key it authenticates with, its node ID, and optionally a codesign certificate for OTA signature verification.
This page covers pre-flashing that identity at manufacture. It is not the default route.
With assisted claiming — on automatically whenever BLE is enabled — the node generates its own key and obtains a certificate at first setup, and you do not pre-flash anything. Read this page when you have opted out with CONFIG_ESP_RMAKER_ASSISTED_CLAIM=n, or when you cannot claim: SoftAP provisioning, no BLE, POSIX builds, or a factory flow that needs node IDs up front.
Whichever route you use, the partition itself must exist and the credentials it holds are the node's permanent identity: a node with no certificate will not connect, and no reset will restore one — resets deliberately leave this partition alone.
Which tool
| You have | Use |
|---|---|
| BLE provisioning and a claiming-capable app | Assisted claiming — no flashing step at all |
| Key, cert, MQTT host, and node ID already in hand as JSON | factory_nvs_gen — this page |
| A browser and a few test nodes to make | Dashboard batch flow |
| A batch to create, register, and emit artifacts for in one step | CLI batch flow |
The batch flows also register the node with the cloud, which factory_nvs_gen does not — it only turns credentials you already have into a flashable image.
What goes in it
The credentials provider reads these fields:
| Field | Required | Purpose |
|---|---|---|
mqtt_host | Yes | The AWS IoT endpoint for your deployment |
client_cert | Yes | X.509 client certificate — PEM or DER |
client_key | Yes | Private key — PEM, DER, or a raw 32-byte big-endian NIST P-256 key |
node_id | Yes | The node ID, also the AWS IoT Thing name and MQTT client ID |
client_user, client_pass | No | Username and password, where the deployment uses them |
random | Auto-generated | Random material used by the SDK |
codesign_cert | Only for OTA signature verification | Verifies OTA image signatures — see OTA |
A factory_nvs_gen input file looks like this:
{
"client_key": "path/to/client_key.pem",
"client_cert": "path/to/client_cert.pem",
"codesign_cert": "path/to/codesign_cert.pem",
"mqtt_host": "mqtt.example.com",
"node_id": "node_id",
"client_user": "username",
"client_pass": "password"
}
For the complete input schema, see tools/factory_nvs_gen/README.md in the SDK.
The factory partition is the default source, not the only one. esp_rmaker_credentials_provider_override() lets you supply any of these fields from your own code — a secure element, an encrypted blob, a host MCU. Call it before esp_rmaker_pre_prov_init() and esp_rmaker_node_init(); leave a field NULL to keep the default provider for it.
Generate a factory image
From tools/factory_nvs_gen/:
python factory_nvs_gen.py <factory_partition_label> <factory_namespace> <json_input_file>
The first two positional arguments must match your firmware's Kconfig:
| Argument | Kconfig option | Default |
|---|---|---|
factory_partition_label | CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAME | fctry |
factory_namespace | CONFIG_ESP_RMAKER_FACTORY_NAMESPACE | rmaker_creds |
A mismatch here produces a partition the firmware cannot read, and the symptom is indistinguishable from having no partition at all. Check menuconfig and partitions.csv before generating.
Output layout
out/<json_basename>/
├── esp-idf/*.bin # flash to the factory NVS partition
└── posix/nvs_persistent/*.bin # POSIX equivalent
ESP-IDF: partition table and flashing
Partition table
Declare an NVS partition for the factory data. From examples/light/partitions.csv:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
ota_data, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
ota_0, app, ota_0, 0x20000, 1920K,
ota_1, app, ota_1, , 1920K,
fctry, data, nvs, 0x3E0000, 0x6000,
Rules:
- Name must match
CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAMEand the label you passed to the generator. - Type
data, subtypenvs. - Size: the examples use 24 KB (
0x6000). Pre-flashed credentials fit in the three-sector NVS minimum of 12 KB (0x3000), but assisted claiming writes the key, certificate, node ID, and random bytes at runtime and needs the headroom. Keep 24 KB unless you have setCONFIG_ESP_RMAKER_ASSISTED_CLAIM=n. - This table assumes 4 MB of flash. On a 2 MB part, shrink the app slots or drop
ota_1— but note that dropping the second slot disables OTA rollback.
Enable it with CONFIG_PARTITION_TABLE_CUSTOM=y. See the ESP-IDF partition tables guide.
Flash the binary
Write it at the factory partition's offset:
esptool.py --chip <chip> -p <port> write_flash <factory_offset> <path-to-factory.bin>
For the light example that offset is 0x3E0000. Read it out of your own partitions.csv — do not copy it from the docs.
idf.py flash does not write this partition, which is why a board that has been flashed with idf.py flash alone still has no identity.
Merged full-flash image
For production you usually want one file that contains everything idf.py flash would write plus the per-device factory blob, flashable in a single operation at 0x0:
cdinto the project's ESP-IDF build directory — the one containingflash_argsafteridf.py build.- Run
esptool.py merge_binwith your chip, an output path,@flash_args(which pulls in every default image and offset from the build), then the factory partition offset in hex and the path to that device's factory.bin.
cd /path/to/your/project/build
esptool.py --chip <target> merge_bin -o <merged_output.bin> @flash_args <fctry_offset_hex> <path-to-factory.bin>
Flash the result at offset 0x0:
esptool.py write_flash 0x0 <merged_output.bin>
Since the factory blob differs per device, this produces one image per unit. For batches, factory_attach.py automates it — see CLI batch flow.
POSIX: where to put NVS files
Copy the generated nvs_persistent directory so it sits next to the running executable — the same directory as the mock bootloader binary:
build/
├── light # mock bootloader — run this
├── nvs_persistent/ # ← the generated directory goes here
│ └── *.bin
└── partitions/
├── ota_0
└── ota_1
Then run from inside build/ so the relative paths resolve:
cd build && ./light
Security
Factory credentials are per-device private keys. Treat the outputs accordingly:
- Never commit real private keys, factory binaries, or
registration.jsonfrom production runs. - Use throwaway test credentials in CI.
- Keep manufacturing outputs in secure storage with restricted access, and account for who ran each batch.
- A leaked
client_keylets anyone impersonate that node. It cannot be revoked from the device side — only by deregistering the node in the cloud.
Troubleshooting
| Symptom | Cause |
|---|---|
| Node boots, never connects, no TLS attempt | No factory partition, or the label/namespace does not match Kconfig |
| Credentials load but TLS fails | Wrong mqtt_host, or the certificate is not registered in this deployment |
| Node connects but the cloud does not recognise it | The node was never registered — factory_nvs_gen does not register; use a batch flow |
Works after idf.py flash, breaks after a full-flash erase | The factory partition was erased and not rewritten |
| Worked, then stopped after a firmware update | The new build changed CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAME or the partition table offsets |
Related
- Device credentials — how identity, network credentials, and user association differ
- Dashboard batch flow
- CLI batch flow
- Firmware specifications → Provisioning