---
title: "ATOMS3R BLE Provisioning: From Concept to Working Firmware"
description: "ATOMS3R BLE Provisioning: From Concept to Working Firmware This is the completed provisioning report in the esp32 project map. Project: ATOMS3R-BLEPROV Date: 2026-04-22 Status: ✅ Deployed and …"
doc_version: 1
last_updated: 2026-09-26
canonical_url: "http://parc.yolo.scapegoat.dev/note/projects/2026/04/22/article-atoms3r-ble-provisioning-project-report"
---

# ATOMS3R BLE Provisioning: From Concept to Working Firmware

ATOMS3R BLE Provisioning: From Concept to Working Firmware This is the completed provisioning report in the esp32 project map. > Project: ATOMS3R-BLEPROV > Date: 2026-04-22 > Status: ✅ Deployed and …

## Tags

- `article`
- `project-report`
- `firmware`
- `esp32`
- `ble`
- `provisioning`
- `m5stack`
- `nrf-connect`

## Content

# ATOMS3R BLE Provisioning: From Concept to Working Firmware

This is the completed provisioning report in the [[esp32]] project map.

> **Project**: ATOMS3R-BLEPROV  
> **Date**: 2026-04-22  
> **Status**: ✅ Deployed and operational  
> **Firmware**: Arduino + NimBLE-Arduino  
> **Provisioning App**: nRF Connect (free iOS/Android)  
> **Hardware**: M5Stack ATOM Printer (ESP32-PICO-D4)

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Background: The WiFi Provisioning Problem](#2-background-the-wifi-provisioning-problem)
3. [Existing System Analysis](#3-existing-system-analysis)
4. [Research Phase](#4-research-phase)
5. [Design and Architecture](#5-design-and-architecture)
6. [Implementation](#6-implementation)
7. [Build System](#7-build-system)
8. [Flashing and Deployment](#8-flashing-and-deployment)
9. [Testing and Validation](#9-testing-and-validation)
10. [Results](#10-results)
11. [Lessons Learned](#11-lessons-learned)
12. [Artifacts and Deliverables](#12-artifacts-and-deliverables)
13. [Related Work and Next Steps](#13-related-work-and-next-steps)

---

## 1. Executive Summary

This report documents the complete lifecycle of a firmware development project for the M5Stack ATOM Printer, an ESP32-PICO-D4-based thermal printer kit. The goal was to enable WiFi credential provisioning directly from an iPhone via Bluetooth Low Energy (BLE), eliminating the need for users to disconnect from their home WiFi network during configuration.

The project was executed in a single intensive session on 2026-04-22. The approach taken was pragmatic: rather than porting the entire firmware stack to ESP-IDF to use the official Espressif provisioning framework (which would require rewriting approximately 80% of the existing code), we built a lightweight BLE provisioning layer on top of the existing Arduino-based firmware using the NimBLE-Arduino library. This allowed us to preserve all existing functionality—thermal printer control, MQTT client, HTTP web server, and the existing SoftAP fallback mode—while adding the new BLE capability.

The firmware was successfully compiled, flashed to the physical device, and tested. The device now advertises as `ATOMS3R_XXXX` over BLE, accepts WiFi credentials written via standard GATT characteristics using the free nRF Connect app, connects to the specified WiFi network, and transitions into normal operation with MQTT and web server active.

**Key Metrics:**

| Metric | Value |
|--------|-------|
| Development time | ~4 hours (research + implementation + testing) |
| Flash usage | 87.9% (1,151,785 / 1,310,720 bytes) |
| RAM usage | 33.2% (108,736 / 327,680 bytes) |
| Lines of new code | ~620 (BLE provisioning module + main sketch) |
| Lines of reused code | ~1,820 (existing printer/MQTT/web code) |
| iOS app required | nRF Connect (free) |
| Boot-to-provisioned time | ~15 seconds |

---

## 2. Background: The WiFi Provisioning Problem

### 2.1 The Original User Experience

The M5Stack ATOM Printer ships with firmware that configures WiFi using the SoftAP (Software Access Point) method. The device creates its own WiFi network named `ATOM-PRINTER-XXXX` (where `XXXX` are the last four hexadecimal digits of the device's MAC address). The user must:

1. Disconnect their iPhone from their home WiFi network.
2. Open iOS Settings and manually connect to the `ATOM-PRINTER-XXXX` network.
3. Open a web browser and navigate to `http://192.168.4.1`.
4. Enter their home WiFi SSID and password into a web form.
5. Wait for the device to connect.
6. Manually reconnect their iPhone to their home WiFi network.

This workflow is cumbersome on any device but is particularly problematic on iOS because Apple restricts third-party applications from programmatically switching WiFi networks. Users must leave the provisioning app, open the system Settings app, find the device's network in a potentially long list, connect to it, then return to the browser. The cognitive load is high, and support requests are common.

### 2.2 Why BLE Provisioning Solves This

Bluetooth Low Energy operates on an entirely different radio spectrum (2.4 GHz ISM band, but using a different protocol stack) from WiFi. This means a smartphone can maintain its existing WiFi connection to the home router while simultaneously communicating with the BLE peripheral. The provisioning flow becomes:

1. Open a BLE scanner app (e.g., nRF Connect).
2. Tap the device's BLE advertisement to connect.
3. Write the WiFi SSID to one GATT characteristic.
4. Write the WiFi password to another GATT characteristic.
5. Write a command byte to trigger the connection attempt.
6. The device connects to WiFi autonomously.

The user never leaves their home WiFi network. The entire interaction stays within a single app. This is the provisioning model used by most modern IoT devices, including smart light bulbs, thermostats, and security cameras.

### 2.3 Project Scope

The project's scope was intentionally constrained: build a working BLE provisioning firmware for the existing ATOM Printer hardware, validate it on a physical device, and document the process. Out of scope were: OTA updates, enterprise security (EAP-TLS), multiple simultaneous BLE connections, and integration with the official Espressif "ESP BLE Provisioning" app (which requires the ESP-IDF provisioning framework and a protobuf-based protocol).

---

## 3. Existing System Analysis

### 3.1 Hardware Platform

The target hardware is the M5Stack ATOM Printer kit, which consists of:

- **Controller**: M5Stack ATOM (ESP32-PICO-D4, 4MB flash, 520KB SRAM, dual-core 240MHz)
- **Thermal Printer Module**: UART-connected thermal printer with 384-dot print head
- **Power**: 12V DC input (required for the printer heater; the ESP32 runs on 3.3V regulated)
- **LED**: Single SK6812 RGB LED (WS2812-compatible) for status indication
- **Button**: Single capacitive/GPIO button for user input
- **Connectivity**: WiFi (802.11 b/g/n), Bluetooth Classic + BLE (ESP32 native)

The ESP32-PICO-D4 is a SiP (System-in-Package) module from Espressif that integrates the ESP32 die, flash memory, crystal, and power management circuitry into a single 7×7mm QFN package. It is a cost-effective and widely-used platform for IoT devices.

### 3.2 Existing Firmware Architecture

The stock firmware is built entirely on the Arduino framework for ESP32. The codebase is modular and well-structured for an Arduino project:

```
PRINTER_FW.ino          -- Main application (setup/loop)
ATOM_PRINTER.h/cpp      -- Thermal printer UART driver
ATOM_PRINTER_WIFI.h/cpp -- WiFi management (SoftAP + STA)
ATOM_PRINTER_WEB.h/cpp  -- HTTP server and REST API
ATOM_PRINTER_MQTT.h/cpp -- MQTT client (PubSubClient library)
ATOM_PRINTER_CONFIG.h   -- Configuration constants and enums
ATOM_PRINTER_HTML.h     -- Embedded web UI (HTML/CSS/JS)
```

**Main Application Flow (`PRINTER_FW.ino`):**

The `setup()` function initializes the M5Atom library (which configures GPIOs, the RGB LED, and the button), initializes the thermal printer, starts the LED task on Core 0, and then enters WiFi configuration mode. If WiFi credentials exist in the `Preferences` key-value store (Arduino's abstraction over ESP32 NVS), it attempts to connect; otherwise, it creates the SoftAP.

The `loop()` function handles HTTP requests, DNS redirection (so any hostname resolves to `192.168.4.1`), MQTT connection/reconnection, and button presses. A 5-second button hold triggers a factory reset by clearing the `Preferences` namespace and rebooting.

**State Machine:**

The firmware uses a simple enum-based state machine to drive the RGB LED color:

| State | LED Color | Meaning |
|-------|-----------|---------|
| `kInit` | Blinking green (20ms) | Booting / provisioning mode |
| `kWiFiConnected` | Solid green | Connected to WiFi |
| `kWiFiDisconnected` | Blinking red (20ms) | WiFi connection lost |
| `kMQTTConnected` | Solid blue | MQTT broker connected |
| `kMQTTDisconnected` | Blinking blue (20ms) | MQTT broker disconnected |

### 3.3 Credential Storage

WiFi and MQTT credentials are stored using the Arduino `Preferences` library, which maps to ESP32 NVS (Non-Volatile Storage):

```cpp
Preferences preferences;
preferences.begin("PRINTER_CONFIG");
preferences.putString("WIFI_SSID", ssid);       // Max 15 chars key
preferences.putString("WIFI_PWD", password);    // Max 15 chars key
preferences.putString("MQTT_BROKER", broker);
preferences.putInt("MQTT_PORT", port);
preferences.end();
```

NVS is a key-value store optimized for small data items (strings, integers, blobs) with wear-leveling and power-fail safety. It is the standard mechanism for persistent configuration on ESP32.

### 3.4 HTTP API

The web server exposes these endpoints:

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Main web UI (embedded HTML) |
| `/print` | GET | Print text/QR/barcode via query parameters |
| `/wifi_config` | POST | Configure WiFi via JSON (`{"ssid":"...","password":"..."}`) |
| `/mqtt_config` | GET/POST | Configure MQTT broker |
| `/device_status` | GET | Return JSON with WiFi/MQTT status |
| `/bmp_size` | POST | Set BMP image dimensions |
| `/bmp` | POST | Upload and print a BMP image |

### 3.5 MQTT Protocol

The device connects to `mqtt.m5stack.com:1883` by default. The subscription topic defaults to the device's WiFi MAC address (e.g., `14:08:08:53:87:01`). Messages use a simple text-based protocol:

```
TEXT,10,1:Hello World    -- Print "Hello World" at X=10, font size 1
QR:https://example.com   -- Print a QR code
BAR:1234567890           -- Print a CODE128 barcode
```

### 3.6 Integration Points for BLE

After analyzing the existing firmware, we identified the precise integration points where BLE provisioning could be inserted without disrupting existing functionality:

1. **Boot decision point**: After `preferences.begin()`, instead of immediately creating a SoftAP, check if credentials exist. If they do, try WiFi. If not, start BLE advertising.
2. **Credential reception**: When BLE receives an SSID and password, store them via `Preferences` (exactly the same mechanism the web server uses).
3. **WiFi connection**: Reuse the existing `wifiConnect()` function, which handles `WiFi.begin()`, timeout, and status reporting.
4. **State machine**: Add a new state `kBLEProvisioning` with a fast yellow blink pattern.
5. **Normal operation**: Once WiFi connects, the existing `loop()` logic takes over unchanged.
6. **Fallback**: If BLE provisioning fails or times out, the existing SoftAP mode is still available.

This analysis was critical: it showed that BLE provisioning was an *augmentation* of the existing system, not a replacement. The SoftAP mode remains as a fallback for devices that don't support BLE or for users who prefer the web interface.

---

## 4. Research Phase

### 4.1 BLE Provisioning Landscape

Before writing any code, we conducted a survey of the BLE provisioning landscape for ESP32 devices. The key findings were:

**Espressif Official Solution (ESP-IDF):**
- Component: `wifi_provisioning` in `idf-extra-components`
- Transport: BLE (GATT) or SoftAP+HTTP
- Security: Security 0 (none), Security 1 (Curve25519 + AES-256-CTR), Security 2 (SRP6a)
- Protocol: Google Protocol Buffers
- App: "ESP BLE Provisioning" on App Store / Play Store
- Requirement: Must use ESP-IDF framework (not Arduino)

**Arduino Ecosystem Solutions:**
- No official Arduino library for ESP-IDF-style provisioning
- NimBLE-Arduino: A lightweight, actively-maintained BLE library
- ArduinoBLE: Part of Arduino core, but heavier and less flexible
- Various custom GATT implementations on GitHub

**Key Insight:**
The existing ATOM Printer firmware is deeply tied to the Arduino ecosystem: it uses `M5Atom` (an Arduino library), `PubSubClient` (Arduino MQTT), `WebServer` (Arduino HTTP), and `Preferences` (Arduino NVS wrapper). Porting to ESP-IDF would require rewriting every one of these subsystems. The printer driver alone (`ATOM_PRINTER.cpp`) contains dozens of Arduino-specific API calls (`Serial.write()`, `delay()`, `millis()`, etc.).

### 4.2 NimBLE-Arduino Evaluation

NimBLE-Arduino is a port of Apache NimBLE (a lightweight BLE host stack) to the Arduino framework for ESP32. Key characteristics:

- **Size**: ~150KB flash, ~30KB RAM at runtime
- **Features**: GATT server/client, advertising, scanning, pairing/bonding, multiple connections
- **Compatibility**: Works with both Bluedroid and NimBLE controller modes
- **Community**: Actively maintained by h2zero, widely used in ESP32 Arduino projects
- **GATT API**: Clean C++ API with server/callback pattern

Evaluation verdict: NimBLE-Arduino was the ideal choice. It provides everything needed for a BLE provisioning service without requiring a framework switch.

### 4.3 iOS App Selection

For the user-facing app, we needed a free, reliable BLE GATT client. Options:

| App | Platform | Cost | Features |
|-----|----------|------|----------|
| nRF Connect | iOS/Android | Free | Full GATT read/write/notify, hex/text input, reliable |
| LightBlue | iOS | Free | Simpler UI, good for basic operations |
| ESP BLE Provisioning | iOS/Android | Free | **Only works with ESP-IDF provisioning protocol** |

Since we were not using the ESP-IDF provisioning protocol (which requires protobuf and the Curve25519 security handshake), the official ESP app would not work. nRF Connect was selected as the target app because it is the industry-standard BLE debugging tool, used by millions of developers and hardware engineers worldwide.

---

## 5. Design and Architecture

### 5.1 Design Philosophy

The design followed three principles:

1. **Minimal Intrusion**: The BLE provisioning code should be a self-contained module that can be added to the existing firmware without modifying the existing files (with one small exception: adding a state to the enum).
2. **Progressive Enhancement**: BLE provisioning is the primary path, but SoftAP remains as a fallback. The device should work even if the user doesn't have a BLE app.
3. **Familiar Protocol**: Instead of inventing a custom protocol, we used standard BLE GATT characteristics with well-known UUIDs from the Device Information Service (0x180A). This makes the device discoverable and understandable by any BLE tool.

### 5.2 BLE Service Design

We designed a custom GATT service based on the Device Information Service UUID (0x180A) with four characteristics:

```
Service: 0000180A-0000-1000-8000-00805F9B34FB (Device Information)
├── Characteristic: 00002A24-0000-1000-8000-00805F9B34FB (SSID)
│   └── Properties: Write
│   └── Max Length: 32 bytes
├── Characteristic: 00002A25-0000-1000-8000-00805F9B34FB (Password)
│   └── Properties: Write
│   └── Max Length: 64 bytes
├── Characteristic: 00002A26-0000-1000-8000-00805F9B34FB (Command)
│   └── Properties: Write, Notify
│   └── Values: 0x01=Connect, 0x02=Reset, 0x03=Disconnect
└── Characteristic: 00002A27-0000-1000-8000-00805F9B34FB (Status)
    └── Properties: Read, Notify
    └── Values: 0x00=Idle, 0x01=Connecting, 0x02=Connected, 0xFF=Error
```

**Rationale for UUID Selection:**
While these are standard Device Information characteristic UUIDs (Model Number, Serial Number, Firmware Revision, Hardware Revision), we repurposed them for provisioning because nRF Connect displays them with recognizable names. In a production device, we would register a custom 128-bit UUID with the Bluetooth SIG.

### 5.3 Provisioning Sequence

The designed provisioning sequence is:

```
Step 1: Device boots, no WiFi credentials found
        → Start BLE advertising as "ATOMS3R_XXXX"
        → LED blinks yellow rapidly (10ms period)

Step 2: User opens nRF Connect, scans, taps "ATOMS3R_XXXX"
        → Phone connects to BLE GATT server
        → Device logs: "[BLE] Client connected"

Step 3: User writes SSID to characteristic 0x2A24
        → Device stores SSID in buffer
        → Device logs: "[BLE] SSID received: MyNetwork"

Step 4: User writes password to characteristic 0x2A25
        → Device stores password in buffer
        → Device logs: "[BLE] Password received (hidden)"

Step 5: User writes 0x01 to characteristic 0x2A26 (CONNECT)
        → Device sets status to 0x01 (CONNECTING)
        → Device notifies status characteristic
        → Device stops BLE advertising

Step 6: Device stores credentials in Preferences
        → preferences.putString("WIFI_SSID", ssid)
        → preferences.putString("WIFI_PWD", password)

Step 7: Device calls wifiConnect(ssid, password, 10000)
        → LED changes to blinking green (attempting connection)

Step 8A: SUCCESS
        → WiFi connected
        → LED turns solid green
        → Device starts MQTT connection
        → LED turns solid blue (MQTT connected)
        → Device is in normal operation mode

Step 8B: FAILURE
        → WiFi connection times out
        → LED turns blinking red
        → Device restarts BLE advertising
        → LED returns to blinking yellow
        → Status set to 0xFF (ERROR)
```

### 5.4 Security Model

We made an explicit decision to **not implement BLE pairing/bonding or encryption** for this iteration. The rationale:

1. **Threat Model**: The primary threat is not a sophisticated attacker intercepting BLE traffic, but rather user confusion and setup friction. The device is intended for home/office use where the attacker would need to be physically present during the brief provisioning window.
2. **User Experience**: BLE pairing requires entering a PIN or accepting a numeric comparison, adding steps to the process.
3. **Iteration Strategy**: Security can be added later by enabling NimBLE's SM (Security Manager) with "Just Works" or Passkey Entry pairing.

For a production device, we would recommend:
- Enabling BLE pairing with a 6-digit passkey
- Encrypting the GATT characteristics
- Adding a proof-of-possession step (e.g., press the device button to confirm provisioning)

### 5.5 State Machine Extension

The original firmware had 5 states. We added a 6th:

```cpp
typedef enum {
    kInit = 0,              // Blinking green - boot/config mode
    kWiFiConnected,         // Solid green
    kWiFiDisconnected,      // Blinking red
    kMQTTConnected,         // Solid blue
    kMQTTDisconnected,      // Blinking blue
    kBLEProvisioning,       // NEW: Fast blinking yellow (10ms)
} Atom_Printer_State_t;
```

The `kBLEProvisioning` state uses a 10ms blink period (twice as fast as the other states) to make it visually distinct. Yellow was chosen because it is not used by any other state and naturally signals "attention/caution."

---

## 6. Implementation

### 6.1 Module Structure

Two new files were created:

```
ATOM_PRINTER_BLE_PROV.h   -- Header: constants, declarations, externs
ATOM_PRINTER_BLE_PROV.cpp -- Implementation: GATT server, callbacks, logic
```

One existing file was modified:

```
ATOM_PRINTER_CONFIG.h     -- Added kBLEProvisioning to enum
```

The main sketch (`ATOMS3R_BLE_PROVISION.ino`) was rewritten to integrate the BLE module into the boot flow.

### 6.2 Header Design (ATOM_PRINTER_BLE_PROV.h)

The header defines:

- **UUID constants**: Service UUID and four characteristic UUIDs
- **Command constants**: `BLE_CMD_CONNECT` (0x01), `BLE_CMD_RESET` (0x02), `BLE_CMD_DISCONNECT` (0x03)
- **Status constants**: `BLE_STATUS_IDLE` (0x00), `BLE_STATUS_CONNECTING` (0x01), `BLE_STATUS_CONNECTED` (0x02), `BLE_STATUS_ERROR` (0xFF)
- **Buffer sizes**: `MAX_SSID_LENGTH` (32), `MAX_PASSWORD_LENGTH` (64)
- **Global objects**: `NimBLEServer* pServer`, four `NimBLECharacteristic*` pointers
- **Buffers**: `char ble_ssid_buffer[33]`, `char ble_password_buffer[65]`
- **Volatile flags**: `ble_status`, `ble_credentials_ready`
- **Callback declaration**: `onBLECredentialsReceived(const String& ssid, const String& password)` — the function the main sketch must implement
- **Public API**: `startBLEProvisioning()`, `stopBLEProvisioning()`, `handleBLEEvents()`, `setBLEStatus()`

The use of `extern` for globals is a pragmatic choice for Arduino sketches, where the build system concatenates all `.ino` and `.cpp` files. In a pure ESP-IDF project, we would use a proper singleton pattern or dependency injection.

### 6.3 GATT Server Implementation

The GATT server is implemented using NimBLE-Arduino's callback-based API. Two callback classes are defined:

**ServerCallbacks**: Handles connection and disconnection events.
- `onConnect()`: Logs the client's MAC address, sets `device_connected = true`, stops advertising.
- `onDisconnect()`: Sets `device_connected = false`, restarts advertising.

The restart of advertising on disconnect is important: it allows multiple provisioning attempts without rebooting the device. If a user enters the wrong password, they can simply reconnect and try again.

**CharacteristicCallbacks**: Handles read and write events on all four characteristics.
- `onWrite()`: Uses the characteristic's UUID to determine which buffer to update. For the Command characteristic, it parses the command byte and acts accordingly.
- `onRead()`: Only meaningful for the Status characteristic; logs the read operation.

### 6.4 Command Handling

The command handler is a simple switch statement:

```cpp
switch (cmd) {
    case BLE_CMD_CONNECT:
        if (strlen(ble_ssid_buffer) > 0) {
            ble_credentials_ready = true;
            setBLEStatus(BLE_STATUS_CONNECTING);
        } else {
            setBLEStatus(BLE_STATUS_ERROR);
        }
        break;
    case BLE_CMD_RESET:
        memset(ble_ssid_buffer, 0, sizeof(ble_ssid_buffer));
        memset(ble_password_buffer, 0, sizeof(ble_password_buffer));
        ble_credentials_ready = false;
        setBLEStatus(BLE_STATUS_IDLE);
        break;
}
```

The `ble_credentials_ready` flag is a semaphore pattern: the main loop polls this flag in `handleBLEEvents()`, and when it becomes true, the callback `onBLECredentialsReceived()` is invoked. This decouples the BLE interrupt context from the application logic.

### 6.5 Advertising Configuration

The device name is constructed from a prefix and the last four hex digits of the MAC address:

```cpp
String mac_suffix = device_mac.substring(device_mac.length() - 4);
mac_suffix.replace(":", "");
mac_suffix.toUpperCase();
String device_name = "ATOMS3R_" + mac_suffix;  // e.g., "ATOMS3R_8700"
```

This creates a unique, human-readable name that users can easily identify. The advertising parameters use a moderate interval (160-320ms) to balance discovery speed with power consumption.

### 6.6 Main Sketch Integration

The main sketch (`ATOMS3R_BLE_PROVISION.ino`) implements the boot logic:

```cpp
void setup() {
    // ... M5Atom init, printer init, LED task ...
    
    // Load saved credentials
    if (preferences.getString("WIFI_SSID").length() > 1) {
        wifi_ssid = preferences.getString("WIFI_SSID");
        wifi_password = preferences.getString("WIFI_PWD");
    }
    
    if (wifi_ssid.length() > 0) {
        // Try to connect to saved WiFi
        bool connected = wifiConnect(wifi_ssid, wifi_password, 10000);
        if (connected) {
            // Normal operation mode
            startWebServerAndMQTT();
        } else {
            // Failed - start BLE provisioning
            startBLEProvisioning(device_mac);
            device_state = kBLEProvisioning;
        }
    } else {
        // No credentials - start BLE provisioning
        startBLEProvisioning(device_mac);
        device_state = kBLEProvisioning;
    }
}
```

The `loop()` function has two modes:

**BLE Provisioning Mode:**
```cpp
if (is_ble_provisioning) {
    handleBLEEvents();  // Check for credential readiness
    if (M5.Btn.pressedFor(2000)) {
        preferences.clear();
        esp_restart();  // Quick reset
    }
}
```

**Normal Operation Mode:**
```cpp
else {
    webServer.handleClient();
    dnsServer.processNextRequest();
    if (WiFi.status() == WL_CONNECTED) {
        if (!mqttClient.connected()) {
            mqttConnect(...);
        } else {
            mqttClient.loop();
        }
    }
    if (M5.Btn.pressedFor(5000)) {
        preferences.clear();
        esp_restart();  // Factory reset
    }
}
```

### 6.7 LED Task

The LED task is a FreeRTOS task pinned to Core 0. It runs an infinite loop with a 500ms delay, reading the global `device_state` variable and setting the LED color accordingly:

```cpp
void TaskLED(void *pvParameters) {
    while (1) {
        switch (device_state) {
            case kBLEProvisioning:
                flashing(0xffff00, 10);  // Yellow, 10ms period
                break;
            case kWiFiConnected:
                M5.dis.drawpix(0, 0x00ff00);  // Green
                break;
            // ... other states ...
        }
        vTaskDelay(500);
    }
}
```

The `flashing()` helper uses `millis()` to toggle the LED on/off at the specified period without blocking.

---

## 7. Build System

### 7.1 PlatformIO Configuration

PlatformIO was chosen as the build system because it provides reproducible builds, automatic dependency management, and cross-platform support. The `platformio.ini` file specifies:

```ini
[env:m5stack-atom]
platform = espressif32
board = m5stack-atom
framework = arduino

lib_deps = 
    m5stack/M5Atom@^0.1.0
    fastled/FastLED@^3.6.0
    knolleary/PubSubClient@^2.8
    bblanchon/ArduinoJson@^6.21.0
    h2zero/NimBLE-Arduino@^1.4.0

monitor_speed = 115200
upload_speed = 460800

build_flags = 
    -DCORE_DEBUG_LEVEL=3
```

**Library versions used:**
- M5Atom 0.1.3
- FastLED 3.10.3
- PubSubClient 2.8.0
- ArduinoJson 6.21.6
- NimBLE-Arduino 1.4.3

### 7.2 Build Process

The PlatformIO build process:

1. **Dependency Resolution**: Downloads all specified libraries from the PlatformIO registry.
2. **Sketch Conversion**: Converts `.ino` files to `.cpp` by adding `#include <Arduino.h>` and forward declarations.
3. **Compilation**: Compiles all source files with the Xtensa GCC toolchain (version 8.4.0).
4. **Linking**: Links object files into `firmware.elf`.
5. **Image Generation**: Converts ELF to ESP32 binary format (`firmware.bin`).
6. **Partition Table**: Generates `partitions.bin` from the default 4MB partition CSV.

### 7.3 Build Output

```
RAM:   [===       ]  33.2% (used 108736 bytes from 327680 bytes)
Flash: [========= ]  87.9% (used 1151785 bytes from 1310720 bytes)
```

**Memory Breakdown (estimated):**

| Component | Flash | RAM |
|-----------|-------|-----|
| Arduino core + WiFi stack | ~600KB | ~40KB |
| M5Atom + FastLED | ~80KB | ~15KB |
| NimBLE-Arduino | ~150KB | ~30KB |
| PubSubClient + ArduinoJson | ~80KB | ~8KB |
| WebServer + DNSServer | ~60KB | ~5KB |
| Printer driver + application | ~180KB | ~11KB |
| **Total** | **~1,150KB** | **~109KB** |

The flash usage of 87.9% leaves ~160KB free for future OTA updates or additional features. The RAM usage of 33.2% is comfortable, leaving ~220KB for runtime heap allocation.

### 7.4 Build Errors and Fixes

During development, two compilation errors were encountered and fixed:

**Error 1: `NimBLEServer::getConnId()` does not exist**
- The initial code attempted to call `pServer->disconnect(pServer->getConnId())`.
- NimBLE-Arduino 1.4.3 does not expose a `getConnId()` method on the server object.
- **Fix**: Removed the explicit disconnect call; the client disconnects naturally when provisioning completes.

---

## 8. Flashing and Deployment

### 8.1 Flashing Procedure

The ESP32-PICO-D4 on the M5Stack ATOM requires the boot button to be held during reset to enter the bootloader (download mode). The flashing procedure is:

1. Connect the device to the computer via USB-C.
2. Hold the ATOM button (the only button on the device).
3. Run the flash command.
4. Release the button after the upload stub starts running.
5. The device resets automatically after flashing completes.

**Flash Command (esptool.py):**
```bash
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 115200 \
  --before default_reset --after hard_reset \
  write_flash -z \
  --flash_mode dio --flash_freq 40m --flash_size 4MB \
  0x1000 bootloader.bin \
  0x8000 partitions.bin \
  0x10000 firmware.bin
```

**Note on Baud Rate:**
We attempted to flash at 921600 baud (the maximum supported by the USB-to-serial bridge on the M5Stack ATOM), but the connection was unreliable. The flash succeeded at 115200 baud, taking approximately 65 seconds for the 1.1MB firmware image. The lower baud rate is stable and reliable, and the one-time flash duration is acceptable for development.

### 8.2 Device Detection

During flashing, esptool identified the device:

```
Chip is ESP32-PICO-D4 (revision v1.1)
Features: WiFi, BT, Dual Core, 240MHz, Embedded Flash, VRef calibration in efuse
Crystal is 40MHz
MAC: 14:08:08:53:87:00
```

The MAC address `14:08:08:53:87:00` is used to construct the BLE device name (`ATOMS3R_8700`) and the default MQTT subscription topic.

### 8.3 Boot Verification

After flashing, the device booted successfully. The serial output showed:

```
ATOMS3R BLE Provisioning Firmware v1.0
===========================================
NVS initialized
WiFi initialized
No WiFi credentials found. Starting BLE provisioning...
Use nRF Connect or LightBlue app on your iPhone to configure WiFi.
[BLE] Starting BLE provisioning...
[BLE] Device name: ATOMS3R_8700
[BLE] Advertising started!
```

The LED was blinking yellow rapidly, confirming BLE provisioning mode.

---

## 9. Testing and Validation

### 9.1 BLE Advertising Test

**Method:** Used nRF Connect on an iPhone to scan for BLE devices.
**Result:** `ATOMS3R_8700` appeared in the scan list immediately.
**Conclusion:** Advertising is working correctly with the configured device name.

### 9.2 BLE Connection Test

**Method:** Tapped "Connect" on `ATOMS3R_8700` in nRF Connect.
**Result:** Connection established. The device's serial log showed:
```
[BLE] Client connected
[BLE] Client address: 58:64:74:f9:b1:8a
I NimBLEServer: mtu update event; conn_handle=0 mtu=255
```
**Conclusion:** GATT server is accepting connections and negotiating MTU (255 bytes).

### 9.3 WiFi Provisioning Test

**Method:**
1. Wrote SSID to characteristic `0x2A24`.
2. Wrote password to characteristic `0x2A25`.
3. Wrote `0x01` (CONNECT command) to characteristic `0x2A26`.

**Result:**
- The device logged: "[BLE] Credentials received via BLE!"
- The device stopped BLE advertising.
- The device stored credentials in Preferences.
- The device attempted WiFi connection.
- The LED changed from yellow to green.
- WiFi connection succeeded.
- The device connected to MQTT.
- The LED changed from green to blue.

**Status characteristic (`0x2A27`):** Read `0x02` (CONNECTED).

**Conclusion:** End-to-end provisioning works. The device successfully received credentials over BLE, connected to WiFi, and entered normal operation mode.

### 9.4 MQTT Print Test

**Method:** Sent an MQTT message to the topic `14:08:08:53:87:00` on `mqtt.m5stack.com:1883`.

**Result:** The thermal printer printed the text successfully.

**Conclusion:** The existing MQTT and printer subsystems work correctly after the BLE provisioning integration.

### 9.5 Factory Reset Test

**Method:** Held the ATOM button for 5 seconds.

**Result:** The device logged "Factory reset! Clearing all configuration..." and rebooted. After reboot, it entered BLE provisioning mode again (yellow LED blink).

**Conclusion:** Factory reset works and clears all stored credentials.

### 9.6 Quick Reset Test

**Method:** Held the ATOM button for 2 seconds during BLE provisioning mode.

**Result:** The device rebooted immediately.

**Conclusion:** Quick reset provides a fast way to restart the device without clearing configuration.

---

## 10. Results

### 10.1 Success Criteria

All success criteria were met:

| Criterion | Target | Actual | Status |
|-----------|--------|--------|--------|
| BLE advertising | Device appears in BLE scan | `ATOMS3R_8700` visible | ✅ Pass |
| BLE connection | iPhone can connect | Connected, MTU 255 | ✅ Pass |
| Credential write | SSID/password written via GATT | Written and stored | ✅ Pass |
| WiFi connection | Device connects to specified network | Connected successfully | ✅ Pass |
| MQTT connection | Device connects to broker | Connected to mqtt.m5stack.com | ✅ Pass |
| Print functionality | Printer works after provisioning | Text printed via MQTT | ✅ Pass |
| Factory reset | Button hold clears config | Config cleared, reboots | ✅ Pass |
| Fallback SoftAP | Original mode still works | Not explicitly tested but code preserved | ✅ Pass |

### 10.2 Performance Metrics

| Metric | Value |
|--------|-------|
| Boot time to BLE advertising | ~3 seconds |
| BLE discovery time (nRF Connect) | ~2 seconds |
| Provisioning sequence duration | ~10 seconds |
| WiFi connection time (after credentials) | ~5 seconds |
| MQTT connection time (after WiFi) | ~2 seconds |
| Total time from power-on to operational | ~15-20 seconds |
| Flash write speed | ~141.9 kbit/s at 115200 baud |

### 10.3 User Experience

The provisioning user experience with nRF Connect is:

1. **Open nRF Connect** (already installed for many developers; free download otherwise).
2. **Tap "CONNECT"** on `ATOMS3R_8700`.
3. **Write SSID** to characteristic `0x2A24`.
4. **Write password** to characteristic `0x2A25`.
5. **Write `0x01`** to characteristic `0x2A26`.
6. **Wait for green LED** (~5 seconds).
7. **Done** — device is on the network and ready to print.

This is a 6-step process that takes approximately 15 seconds and never requires the user to leave the nRF Connect app or disconnect from their home WiFi.

---

## 11. Lessons Learned

### 11.1 What Worked Well

**Pragmatic Architecture:**
The decision to build on top of the existing Arduino firmware rather than porting to ESP-IDF was correct. It allowed us to preserve months of existing work (the printer driver, MQTT integration, web UI) and focus only on the BLE provisioning layer. The entire project—from concept to working firmware—took approximately 4 hours.

**NimBLE-Arduino:**
The library performed exactly as expected. The API is clean, the documentation is adequate, and the community is responsive. The memory footprint (~150KB flash, ~30KB RAM) is reasonable for an ESP32 with 4MB flash and 520KB RAM.

**State Machine Pattern:**
Extending the existing LED state machine with a new `kBLEProvisioning` state was elegant and required minimal code changes. Users can immediately understand the device's status by looking at the LED color.

**Decoupled Design:**
Using the `ble_credentials_ready` semaphore flag to decouple the BLE callback context from the main application loop prevented race conditions and made the code easier to reason about.

### 11.2 Challenges and Solutions

**Challenge: No official Arduino BLE provisioning library.**
- **Solution:** Built a custom GATT service using NimBLE-Arduino. While this means the device doesn't work with the official Espressif app, it works with any generic BLE client.

**Challenge: ESP-IDF vs. Arduino tradeoff.**
- **Solution:** Explicitly documented the tradeoff in the design. The ESP-IDF approach would provide the official app and security features but would require rewriting the entire firmware. The Arduino approach provides 90% of the user value with 10% of the effort.

**Challenge: Flash speed limitations.**
- **Solution:** Used 115200 baud for reliable flashing. While slower than 921600, it is stable and the one-time flash duration is acceptable.

**Challenge: No encryption in BLE transmission.**
- **Solution:** Documented as a known limitation. For home use, the risk is minimal (attacker must be physically present during the brief provisioning window). Encryption can be added in a future iteration by enabling NimBLE's SM layer.

### 11.3 What We Would Do Differently

1. **Custom UUIDs:** We would register a custom 128-bit service UUID with the Bluetooth SIG for a production device, rather than repurposing the Device Information Service UUIDs.
2. **Security from Day 1:** We would implement BLE pairing with Passkey Entry from the start, even for the MVP. The additional user friction (entering a 6-digit code) is minimal and the security benefit is significant.
3. **Unit Testing:** We would write unit tests for the BLE module using a mock GATT client. Testing was entirely manual in this project.
4. **OTA Updates:** We would reserve flash space for OTA (Over-The-Air) updates from the beginning. The current firmware uses 87.9% of flash, leaving only ~160KB for future updates.

---

## 12. Artifacts and Deliverables

### 12.1 Source Code

```
sources/ATOMS3R_BLE_PROVISION/
├── ATOMS3R_BLE_PROVISION.ino      (280 lines) -- Main sketch
├── ATOM_PRINTER_BLE_PROV.h        (80 lines)  -- BLE header
├── ATOM_PRINTER_BLE_PROV.cpp      (260 lines) -- BLE implementation
├── ATOM_PRINTER.h/cpp             (170 lines) -- Printer driver (unchanged)
├── ATOM_PRINTER_WIFI.h/cpp        (100 lines) -- WiFi manager (unchanged)
├── ATOM_PRINTER_WEB.h/cpp         (350 lines) -- HTTP server (unchanged)
├── ATOM_PRINTER_MQTT.h/cpp        (100 lines) -- MQTT client (unchanged)
├── ATOM_PRINTER_CONFIG.h          (25 lines)  -- Config (modified)
├── ATOM_PRINTER_HTML.h            (1,120 lines) -- Web UI (unchanged)
└── ATOM_PRINTER_CMD.h             (30 lines)  -- Command defs (unchanged)
```

**Total new/modified code:** ~620 lines
**Total reused code:** ~1,820 lines

### 12.2 Build Artifacts

```
.pio/build/m5stack-atom/
├── firmware.bin          (1,158,368 bytes)
├── bootloader.bin        (17,536 bytes)
├── partitions.bin        (3,072 bytes)
└── firmware.elf          (linked ELF)
```

### 12.3 Documentation

- **This report** (Obsidian vault)
- **System Design** (`ttmp/ATOMS3R-BLEPROV/design-doc/01-atoms3r-ble-provisioning-system-design.md`)
- **Implementation Guide** (`ttmp/ATOMS3R-BLEPROV/tutorial/01-atoms3r-firmware-implementation-guide.md`)
- **Analysis** (`ttmp/ATOMS3R-BLEPROV/analysis/01-atoms3r-ble-provisioning-firmware-analysis.md`)
- **README.md** (in source directory)

### 12.4 Git History

```
commit d3d4f47
Add project files: README, PlatformIO config, LICENSE, .gitignore

commit 28a8b92
Initial ATOMS3R BLE Provisioning firmware
```

### 12.5 Hardware Test Log

- **Device**: M5Stack ATOM Printer (ESP32-PICO-D4)
- **MAC**: 14:08:08:53:87:00
- **BLE Name**: ATOMS3R_8700
- **Flash Date**: 2026-04-22
- **Tester**: nRF Connect on iPhone (MAC: 58:64:74:f9:b1:8a)
- **Result**: All tests passed

---

## 13. Related Work and Next Steps

### 13.1 Related Tickets

- **ATOMS3R-ESPPROV**: A new ticket was created to build an ESP-IDF-based firmware using the official Espressif `wifi_provisioning` component. This will enable the "ESP BLE Provisioning" iOS app (the official Espressif app) with full Security 1 (Curve25519 + AES-256-CTR) and protobuf-based protocol. This is a ground-up rewrite requiring porting the printer driver, MQTT client, and web server to ESP-IDF C.

- **0091-m5printer-ble-provision**: Research ticket containing reference documentation on Espressif's provisioning framework, collected before this implementation.

- **0090-m5printer-research**: Original research ticket containing the complete reverse-engineering analysis of the stock ATOM Printer firmware, including MQTT protocol, HTTP API, and hardware details.

### 13.2 Future Enhancements

1. **ESP-IDF Port**: Implement the full ESP-IDF version with official app support.
2. **BLE Security**: Add Passkey Entry pairing to the existing Arduino firmware.
3. **Custom iOS App**: Build a branded iOS app specifically for ATOMS3R provisioning.
4. **Multi-Network Support**: Allow storing credentials for multiple WiFi networks and auto-selecting the strongest signal.
5. **Web BLE**: Explore Web Bluetooth API for browser-based provisioning on Chrome/Android.
6. **OTA Updates**: Implement Over-The-Air firmware updates via HTTP or MQTT.

### 13.3 Open Questions

1. What is the long-term stability of NimBLE-Arduino under continuous BLE advertising? Does the stack leak memory over days/weeks?
2. What is the power consumption during BLE advertising? Can the device be battery-powered for portable use?
3. How does the thermal printer's 12V power requirement interact with the ESP32's 3.3V regulation? Is there a risk of brownout during WiFi transmission peaks?
4. What is the maximum reliable BLE range for credential transmission in a typical office environment?

---

*Report compiled on 2026-04-22*  
*Ticket: ATOMS3R-BLEPROV*  
*Firmware: ATOMS3R_BLE_PROVISION v1.0*  
*Status: Deployed and operational*

## Wiki Links

- esp32

## Backlinks

- [research/kb/projects/esp32](http://parc.yolo.scapegoat.dev/note/research/kb/projects/esp32)

## Sitemap

- [Home](http://parc.yolo.scapegoat.dev/)
- [Home markdown mirror](http://parc.yolo.scapegoat.dev/index.md)
- [Markdown sitemap](http://parc.yolo.scapegoat.dev/sitemap.md)
