Skip to main content

Getting Started with ESP-Matter: Debugging Commissioning Failures on ESP32

 You have successfully compiled the lighting-app example from the esp-matter SDK. You flash the ESP32, monitor the UART, and see the QR code URL. You pull out your iPhone, scan the code in Apple Home, and wait. The spinner rotates for 30 seconds, followed by the dreaded, non-descript error: "Unable to Add Accessory."

The UART logs on the ESP32 side show a BLE connection, a brief exchange, and then a sudden disconnect with an error like SecureChannel: error 0 or CASESession: error.

This is the most common hurdle in professional Matter development. It is rarely a code syntax error; it is almost always a failure in the Device Attestation Procedure or Storage Partitioning.

The Root Cause: Device Attestation and Factory Data

Matter relies on a rigorous security handshake known as PASE (Passcode Authenticated Session Establishment). During commissioning, the Commissioner (the phone/hub) challenges the Accessory (ESP32) to prove its identity.

To pass this check, the ESP32 must:

  1. Possess a DAC (Device Attestation Certificate) and a PAI (Product Attestation Intermediate) certificate.
  2. Sign a challenge nonce using the DAC's private key.
  3. Verify the Spake2+ verifier (pairing code parameters).

In the default "Hello World" examples, these certificates are often hardcoded in the firmware (test_attestation_credentials). However, as soon as you modify the partition table or move toward a custom board configuration, the device frequently loses access to these credentials or fails to initialize the secure storage provider responsible for fetching them.

If the ESP32 cannot read the DAC from flash or the partition table overlaps with the NVS (Non-Volatile Storage) where fabric secrets are stored, the handshake fails silently. The phone assumes the device is illegitimate and terminates the BLE connection.

The Fix: Implementing a Dedicated Factory Data Partition

To robustly solve commissioning failures, we must move away from hardcoded credentials and implement a dedicated Factory Data Partition. This ensures that certificates, discriminators, and passcodes are stored in a protected region of flash, separate from the firmware application logic.

1. Configure the Partition Table

Standard ESP32 partition tables are insufficient for Matter due to the size of the IPv6/Thread stack and the requirement for a dedicated factory partition.

Create a file named partitions.csv in your project root. We need a factory partition (for certificates) and a large nvs partition (for storing active Fabric data).

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     ,        0x4000,
otadata,  data, ota,     ,        0x2000,
phy_init, data, phy,     ,        0x1000,
factory,  data, nvs,     ,        0x10000,
factory_data, 0x40, 0x01, ,       0x10000,
# Main app partition
ota_0,    app,  ota_0,   ,        0x1C0000,
ota_1,    app,  ota_1,   ,        0x1C0000,

Note: The factory partition (standard NVS) is different from factory_data (Matter credentials). We define factory_data with custom type 0x40.

2. Enable Factory Data in sdkconfig

Modify your sdkconfig.defaults or configure via idf.py menuconfig to enable the factory data provider and tell the Matter stack to look for it.

# Enable ESP32 Factory Data Provider
CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y
CONFIG_ESP_MATTER_FACTORY_DATA_ENABLED=y
CONFIG_ESP_MATTER_FACTORY_DATA_PROVIDER_IMPL=y

# Ensure mbedTLS uses hardware acceleration where possible for speed
CONFIG_MBEDTLS_HARDWARE_AES=y
CONFIG_MBEDTLS_HARDWARE_MPI=y

3. Initialize the Provider in C++

This is the critical step often missed in documentation. You must explicitly tell the Matter stack to use the partition-based provider instead of the default test provider.

Modify your main.cpp:

#include <esp_err.h>
#include <esp_log.h>
#include <nvs_flash.h>

#include <esp_matter.h>
#include <esp_matter_console.h>
#include <esp_matter_ota.h>
#include <esp_matter_providers.h>

// Specific headers for factory data
#include <platform/ESP32/ESP32FactoryDataProvider.h>
#include <credentials/DeviceAttestationCredsProvider.h>

static const char *TAG = "app_main";

// Define the global factory data provider instance
chip::DeviceLayer::ESP32FactoryDataProvider sFactoryDataProvider;

extern "C" void app_main()
{
    // 1. Initialize NVS
    // We must initialize NVS first because the Matter stack and Factory Data rely on it.
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);

    // 2. Setup Factory Data Provider
    // This connects the chip::Credentials subsystem to our flash partition
    chip::Credentials::SetDeviceAttestationCredentialsProvider(&sFactoryDataProvider);
    
    // Initialize the specific ESP32 implementation data
    // This loads the DAC, PAI, and CD from the 'factory_data' partition defined in CSV
    ESP_ERROR_CHECK(sFactoryDataProvider.Init());

    // 3. Initialize Matter Node
    esp_matter::node::config_t node_config;
    esp_matter::node_t *node = esp_matter::node::create(&node_config, app_attribute_update_callback, app_identification_callback);
    
    if (!node) {
        ESP_LOGE(TAG, "Failed to create Matter node");
        abort();
    }

    // 4. Start the Matter Stack
    err = esp_matter::start(app_event_callback);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to start Matter stack: %d", err);
        abort();
    }

    ESP_LOGI(TAG, "Matter started successfully using Factory Data Provider");
}

4. Generate and Flash the Factory Binary

You cannot simply run idf.py flash. You must generate a binary containing the certificates and pairing codes, then flash it to the address specified in your partition table.

Espressif provides a script in the connectedhomeip SDK path: esp_matter/connectedhomeip/connectedhomeip/src/tools/esp32_factory_data_mfg_tool.py.

Run the generation tool (adjust paths based on your environment):

# Generate the factory data bin
# discriminators and passcodes here must match what you use for pairing
./mfg_tool.py \
  --passcode 20202021 \
  --discriminator 3840 \
  -n 1 \
  --vendor-id 0xFFF1 \
  --product-id 0x8000 \
  --output ./factory_bin

This generates a file usually named 1.bin (for the first device). Now, look at your partitions.csv generation log to find the offset for factory_data. If you let the build system handle partitions, check the build log. Assuming the offset is 0x110000:

esptool.py -p /dev/ttyUSB0 -b 460800 write_flash 0x110000 factory_bin/bin/1.bin

The Explanation

Why does this fix the "Unable to Add Accessory" error?

  1. Cryptographic Integrity: By explicitly initializing sFactoryDataProvider.Init(), the ESP32 loads the DAC (Device Attestation Certificate) from the binary we just flashed.
  2. Pointer Registration: chip::Credentials::SetDeviceAttestationCredentialsProvider redirects the Matter core (CHIP stack) to call your provider when the iPhone requests identity verification. If you skip this, the stack uses a stub provider that returns empty or invalid certificates.
  3. Storage Isolation: Moving credentials to a dedicated partition prevents NVS fragmentation. When the device commissions, it writes operational credentials (Fabric ID, Node ID) to the standard NVS. If the factory data lived in the same NVS namespace, high-write cycles during debugging could corrupt the certificates.

Conclusion

Matter commissioning failures are rarely about the protocol itself and almost always about how the device proves its identity. By enforcing a strict partition layout and manually injecting the ESP32FactoryDataProvider, you eliminate the ambiguity of the default test credentials.

Before retrying the commissioning process, always perform a full erase to clear any cached BLE bonds or partial fabric data:

idf.py -p /dev/ttyUSB0 erase-flash

Flash your app, flash your factory data bin, and the device will commission reliably.