Skip to main content

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.

Claiming is the default

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 haveUse
BLE provisioning and a claiming-capable appAssisted claiming — no flashing step at all
Key, cert, MQTT host, and node ID already in hand as JSONfactory_nvs_gen — this page
A browser and a few test nodes to makeDashboard batch flow
A batch to create, register, and emit artifacts for in one stepCLI 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:

FieldRequiredPurpose
mqtt_hostYesThe AWS IoT endpoint for your deployment
client_certYesX.509 client certificate — PEM or DER
client_keyYesPrivate key — PEM, DER, or a raw 32-byte big-endian NIST P-256 key
node_idYesThe node ID, also the AWS IoT Thing name and MQTT client ID
client_user, client_passNoUsername and password, where the deployment uses them
randomAuto-generatedRandom material used by the SDK
codesign_certOnly for OTA signature verificationVerifies 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.

Override the provider instead

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:

ArgumentKconfig optionDefault
factory_partition_labelCONFIG_ESP_RMAKER_FACTORY_PARTITION_NAMEfctry
factory_namespaceCONFIG_ESP_RMAKER_FACTORY_NAMESPACErmaker_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_NAME and the label you passed to the generator.
  • Type data, subtype nvs.
  • 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 set CONFIG_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:

  1. cd into the project's ESP-IDF build directory — the one containing flash_args after idf.py build.
  2. Run esptool.py merge_bin with 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.json from 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_key lets anyone impersonate that node. It cannot be revoked from the device side — only by deregistering the node in the cloud.

Troubleshooting

SymptomCause
Node boots, never connects, no TLS attemptNo factory partition, or the label/namespace does not match Kconfig
Credentials load but TLS failsWrong mqtt_host, or the certificate is not registered in this deployment
Node connects but the cloud does not recognise itThe node was never registered — factory_nvs_gen does not register; use a batch flow
Works after idf.py flash, breaks after a full-flash eraseThe factory partition was erased and not rewritten
Worked, then stopped after a firmware updateThe new build changed CONFIG_ESP_RMAKER_FACTORY_PARTITION_NAME or the partition table offsets