Using the Seeed Studio XIAO ESP32-S3 Silicon Fingerprint as a Unique BLE Device ID

Flash the same firmware onto ten Seeed Studio XIAO ESP32-S3 boards and every one of them advertises the same BLE device name. A phone scanning nearby sees ten identical entries with no way to tell them apart. This is a real problem in multi-device deployments, but you can fix that easily by embedding each board’s unique silicon fingerprint into the BLE device ID.

What the Silicon Fingerprint Is

Every ESP32-S3 leaves the factory with a 48-bit MAC address burned into a block of one-time-programmable memory called eFuse. These bits can only be written once and can never be changed or erased, not even by a full flash wipe. Espressif draws MAC addresses from the IEEE Organizationally Unique Identifier (OUI) pool, so the values are globally unique by construction. No two chips carry the same address.

Espressif derives four interface-specific addresses from that single base value, each offset by a fixed increment:

InterfaceOffset from base
Wi-Fi Station+0 (base eFuse MAC)
Wi-Fi AP+1
Bluetooth+2
Ethernet+3

Bytes 0 through 2 hold the OUI and bytes 3 through 5 hold the host-specific octets. Six hex characters from those last three bytes produce a short, readable suffix that is unique in any deployment you are likely to run.

The ESP-IDF system API exposes each slot through esp_read_mac(mac, type), where type selects the interface. For BLE work, the correct type is ESP_MAC_BT because it returns the Bluetooth-derived address that the radio actually places on air.

Hardware and Development Environment

The board used in this tutorial is the Seeed Studio XIAO ESP32-S3, a thumb-sized development board with a dual-core Xtensa LX7 processor running at 240 MHz, 8 MB of flash, 8 MB of PSRAM, and a BLE 5.0 radio. Its compact form factor makes it well-suited for embedded tracking and sensing applications where board space is scarce.

Development runs through VS Code with the ESP-IDF extension by Espressif, which bundles the Xtensa compiler toolchain, the NimBLE stack, and build, flash, and monitor tooling into a single installation. After running the extension’s guided setup wizard, set the chip target to esp32s3 by clicking the chip label in the VS Code status bar. Then open the SDK Configuration Editor and enable the NimBLE BLE stack by searching for nimble and enabling “NimBLE - BLE only stack”. That configuration change is all that is needed before writing code.

Reading the Fingerprint and Constructing the BLE Name

The NimBLE host stack handles all BLE advertising. Before advertising starts, you read the Bluetooth MAC, format it into a device name string, and pass that string to ble_svc_gap_device_name_set(). The NimBLE host picks it up when constructing the advertising payload.

#include <string.h>
#include "esp_log.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"

static const char *TAG = "XIAO_BLE";
static char s_device_name[16];

static void start_advertising(void) {
    struct ble_hs_adv_fields fields = {0};
    fields.flags            = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
    fields.name             = (uint8_t *)s_device_name;
    fields.name_len         = strlen(s_device_name);
    fields.name_is_complete = 1;
    ble_gap_adv_set_fields(&fields);

    struct ble_gap_adv_params params = {0};
    params.conn_mode = BLE_GAP_CONN_MODE_UND;
    params.disc_mode = BLE_GAP_DISC_MODE_GEN;
    ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL,
                      BLE_HS_FOREVER, &params, NULL, NULL);
    ESP_LOGI(TAG, "Advertising as: %s", s_device_name);
}

static void on_sync(void)   { start_advertising(); }
static void on_reset(int r) { ESP_LOGE(TAG, "BLE reset: %d", r); }

static void nimble_host_task(void *p) {
    nimble_port_run();
    nimble_port_freertos_deinit();
}

void app_main(void) {
    // 1. Read the Bluetooth MAC (eFuse base + 2) — matches the address
    //    the radio places on air and what BLE scanners report
    uint8_t mac[6] = {0};
    esp_read_mac(mac, ESP_MAC_BT);
    snprintf(s_device_name, sizeof(s_device_name),
             "XIAO-%02X%02X%02X", mac[3], mac[4], mac[5]);
    ESP_LOGI(TAG, "BT MAC: %02X:%02X:%02X:%02X:%02X:%02X  Name: %s",
             mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], s_device_name);

    // 2. NimBLE requires NVS for BLE key storage
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
        ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        nvs_flash_erase();
        nvs_flash_init();
    }

    // 3. Initialize and start the NimBLE host
    nimble_port_init();
    ble_hs_cfg.sync_cb  = on_sync;
    ble_hs_cfg.reset_cb = on_reset;
    ble_svc_gap_device_name_set(s_device_name);
    ble_svc_gap_init();
    ble_svc_gatt_init();
    nimble_port_freertos_init(nimble_host_task);
}

esp_read_mac() fills mac[0] through mac[5] in network byte order. Bytes 3, 4, and 5 carry the host-specific octets, so snprintf uses those to build a six-character hex suffix. On any XIAO ESP32-S3, this produces a name like XIAO-A3F823. The next board you flash produces a different suffix without any extra configuration.

In main/CMakeLists.txt, be sure to declare the dependencies on the NimBLE, NVS, and system libraries by adding esp_system nvs_flash bt to the REQUIRES field of idf_component_register. The build system then pulls in the correct headers and links the right libraries automatically.

Verifying the Result

Build and flash using ESP-IDF. After the board resets, the IDF monitor should confirm the Bluetooth MAC was read and the name was set:

ESP-IDF monitor log showing the Bluetooth MAC read from eFuse and the resulting fingerprint-derived device name

Open a Bluetooth scanner mobile app, like TI SimpleLink Connect or nRF Connect for Mobile, and start scanning for devices. Your board should appear with its fingerprint-derived name, and the device address shown in the scanner should match the last three bytes of the BT MAC printed in the monitor.

Mobile BLE scanner app showing the XIAO board advertising with its fingerprint-derived device name

Why This Approach Works at Scale

Naming schemes that rely on counters, random seeds, or configuration files all share the same weakness: they require a separate provisioning step after the firmware image is written. A counter increments only if the device has already been programmed with one. A random value changes on every factory reset. A configuration file has to be written to flash independently of the firmware build.

The eFuse-derived Bluetooth MAC bypasses all of that. The identifier exists before your firmware does. You produce one firmware image and every board in a production batch self-identifies correctly without any extra tooling on the programming bench.

This technique pairs naturally with Hubble Network’s terrestrial infrastructure, which receives standard BLE advertisements through its global gateway network and identifies devices by their registered encryption key. The fingerprint-derived device name acts as a human-readable label in your scanning tools and during development, while Hubble handles the production identity layer on the cloud side. The two layers do not conflict: one addresses discoverability during testing, and the other addresses secure, cloud-connected identity in deployment.


Ready to connect your devices anywhere on Earth? Get started with Hubble Network for free →