Auto-Provisioning Not Broadcasting WiFi AP – Edgent, ESP32, FastLED

Device: ESP32-S3
Blynk Library: 1.2.x
Board Package: ESP32 (latest via Arduino IDE)
Project: Headless device using Blynk Edgent, with FastLED and custom shutdown logic
Behavior: Auto-provisioning (WiFi AP) not broadcasting when expected

Summary of Problem

I’m using the Blynk Edgent framework for a WiFi device that should automatically erase credentials and enter provisioning (AP setup) mode if it can’t connect to WiFi/Blynk for a set time, or if an invalid token is detected (such as when the device is deleted in Blynk Cloud).

I patched my configmode.h per this forum workaround (https://community.blynk.cc/t/how-to-wipe-a-device-for-a-new-user-and-new-wifi-provisioning/59509/7), replacing MODE_WAIT_CONFIG with MODE_RESET_CONFIG on invalid token. I also have logic in my sketch to trigger provisioning after 1 minute of no connection.

The problem:
When provisioning should trigger, the device restarts but does not broadcast a WiFi AP.
I do not see the “Blynk Quickstart-XXXX” (or similar) network on my phone or laptop.

Troubleshooting Steps Taken

  • Serial monitor shows the device rebooting and going through Edgent states.
    I added a state debug print and can see transitions to MODE_RESET_CONFIG and MODE_WAIT_CONFIG.
  • FastLED is used for status effects. I tried commenting out FastLED code, but still no AP appears.
  • I verified I’m using a 2.4GHz-capable phone/laptop.
  • Stock Edgent example does work and enters AP/provisioning mode as expected.
  • Board and library versions are up to date.
  • I reverted all changes except the provisioning patch; issue persists.

Questions

  1. What else should I check to ensure the AP is started in provisioning mode?
  2. Could FastLED or my shutdown pin logic interfere with WiFi AP mode, even if commented out?
  3. Are there known limitations or requirements for Edgent AP mode on ESP32 in recent Blynk/Arduino releases?

Main Sketch Excerpt

void loop() {
  if (!Blynk.connected()) {
    if (wifiLostSince == 0) wifiLostSince = millis();
    else if (millis() - wifiLostSince > WIFI_TIMEOUT) {
      resetToProvisioning();
    }
  } else {
    wifiLostSince = 0;
  }

  // LED feedback, shutdown, and timer logic omitted for brevity...

  BlynkEdgent.run();
}
void resetToProvisioning() {
    Serial.println("WiFi not connected for 1 minute. Entering provisioning...");
    BlynkState::set(MODE_RESET_CONFIG);
    delay(100);
    ESP.restart();
}

Any advice on what I might be missing, or why AP mode isn’t starting as expected after forced reset/provisioning would be appreciated. Thanks.

@Randy Please edit your post, using the pencil icon at the bottom, and add triple backticks at the beginning and end of your code so that it displays correctly.
Triple backticks look like this:
```

Copy and paste these if you can’t find the correct symbol on your keyboard.

Pete.

All good now?

I was able to fix the dynamic provisioning. Here is my full code for anyone that may be interested. After 2 minutes if the ESP32 cannot find a network it will erase the wifi credentials and enter back into provisioning mode.

#define BLYNK_TEMPLATE_ID "TMPL2IJw3-ytG"
#define BLYNK_TEMPLATE_NAME "Quickstart Template"

// Always include WiFi.h first!
#include <WiFi.h>
#include <Arduino.h>
#include <FastLED.h>
#include "BlynkEdgent.h"

// ---- User LED Setup ----
#define DATA_PIN    2
#define NUM_LEDS    8
#define KILL_PIN    5
#define INT_PIN     4
CRGB leds[NUM_LEDS];

// ---- Blynk Virtual Pins ----
#define VPIN_RED     V2
#define VPIN_GREEN   V3
#define VPIN_BLUE    V4
#define VPIN_BRIGHT  V6
#define VPIN_JOY_X   V7
#define VPIN_JOY_Y   V8
#define VPIN_MODE    V9
#define VPIN_WHITE   V10

uint8_t colorR = 0, colorG = 0, colorB = 255;
uint8_t brightnessSlider = 255;
int joyX = 0, joyY = 0;
int mode = 1;
bool whiteOverride = false;

// ---- BlynkTimer ----
BlynkTimer timer;

// ---- Network Connection Monitoring ----
const unsigned long WIFI_CONNECT_TIMEOUT = 120UL * 1000UL; // 120 seconds
bool provisioningTriggered = false;
unsigned long wifiConnectStart = 0;

// ---- Shutdown Logic ----
unsigned long startMillis = 0;
const unsigned long SHUTDOWN_TIMEOUT = 45UL * 60UL * 1000UL; // 45 min
bool shutdownInitiated = false;

// ---- LED State Effects ----
enum LedMode { LED_NORMAL, LED_FLASH_YELLOW, LED_SOLID_BLUE };
LedMode currentLedMode = LED_NORMAL;
unsigned long lastLedEffectTime = 0;
bool blinkState = false;

// ---- Edgent-Proper Provisioning Erase ----
void forceProvisioningProper() {
  Serial.println("Requesting WiFi config erase and provisioning...");
  BlynkState::set(MODE_RESET_CONFIG);
  unsigned long start = millis();
  while (BlynkState::get() != MODE_WAIT_CONFIG && millis() - start < 5000) {
    BlynkEdgent.run();
    delay(10);
  }
  Serial.println("Erase complete! Rebooting into provisioning mode.");
  ESP.restart();
}

// ---- LED Effects ----
void flashYellowEffect() {
  unsigned long now = millis();
  if (now - lastLedEffectTime > 300) {
    blinkState = !blinkState;
    fill_solid(leds, NUM_LEDS, blinkState ? CRGB::Yellow : CRGB::Black);
    FastLED.show();
    lastLedEffectTime = now;
  }
}
void solidBlueEffect() {
  fill_solid(leds, NUM_LEDS, CRGB::Blue);
  FastLED.show();
}
void setLedMode(LedMode mode) {
  if (currentLedMode != mode) {
    currentLedMode = mode;
    lastLedEffectTime = millis();
    blinkState = false;
  }
}

// ---- User Effects ----
void effectJoystickControl() {
  if (currentLedMode != LED_NORMAL) return;
  const int deadZone = 20;
  fill_solid(leds, NUM_LEDS, CRGB::Black);

  if (abs(joyX) < deadZone && abs(joyY) < deadZone) {
    FastLED.show();
    return;
  }

  float angle = atan2(-joyY, joyX) * 180 / PI;
  if (angle < 0) angle += 360;
  angle = fmod((angle + 135), 360.0);
  float anglePerLED = 360.0 / NUM_LEDS;

  for (int i = 0; i < NUM_LEDS; i++) {
    float ledAngle = i * anglePerLED;
    float diff = abs(angle - ledAngle);
    if (diff > 180) diff = 360 - diff;
    float fade = constrain(1.0 - (diff / anglePerLED), 0, 1);
    uint8_t level = (brightnessSlider * fade);
    leds[i] = CRGB((colorR * level) / 255, (colorG * level) / 255, (colorB * level) / 255);
  }
  FastLED.show();
}
void effectRainbowComet() {
  if (currentLedMode != LED_NORMAL) return;
  static uint8_t cometPos = 0;
  static uint8_t hue = 0;
  fadeToBlackBy(leds, NUM_LEDS, 20);
  leds[cometPos] = CHSV(hue++, 255, brightnessSlider);
  cometPos = (cometPos + 1) % NUM_LEDS;
  FastLED.show();
}
void effectElectricChase() {
  if (currentLedMode != LED_NORMAL) return;
  static uint8_t pos = 0;
  static uint8_t hueBase = 0;
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  for (int i = 0; i < 3; i++) {
    leds[(pos + i) % NUM_LEDS] = CHSV(hueBase + i * 20, 255, brightnessSlider);
  }
  pos = (pos + 1) % NUM_LEDS;
  hueBase += 8;
  FastLED.show();
}
void effectBlazingSparkle() {
  if (currentLedMode != LED_NORMAL) return;
  fadeToBlackBy(leds, NUM_LEDS, 40);
  for (int i = 0; i < 3; i++) {
    int idx = random8(NUM_LEDS);
    leds[idx] = CHSV(random8(), 255, brightnessSlider);
  }
  FastLED.show();
}
void effectHandler() {
  if (currentLedMode != LED_NORMAL) return;
  FastLED.setBrightness(brightnessSlider);
  if (whiteOverride) {
    fill_solid(leds, NUM_LEDS, CRGB::White);
    FastLED.show();
    return;
  }
  switch (mode) {
    case 1: effectJoystickControl(); break;
    case 2: effectRainbowComet(); break;
    case 3: effectElectricChase(); break;
    case 4: effectBlazingSparkle(); break;
    default: fill_solid(leds, NUM_LEDS, CRGB::Black); FastLED.show(); break;
  }
}

// ---- Blynk Handlers ----
BLYNK_CONNECTED() {
  Blynk.syncVirtual(VPIN_RED, VPIN_GREEN, VPIN_BLUE, VPIN_BRIGHT, VPIN_JOY_X, VPIN_JOY_Y, VPIN_MODE, VPIN_WHITE);
}
BLYNK_WRITE(VPIN_RED)     { colorR = param.asInt(); }
BLYNK_WRITE(VPIN_GREEN)   { colorG = param.asInt(); }
BLYNK_WRITE(VPIN_BLUE)    { colorB = param.asInt(); }
BLYNK_WRITE(VPIN_BRIGHT)  { brightnessSlider = param.asInt(); }
BLYNK_WRITE(VPIN_JOY_X)   { joyX = param.asInt(); }
BLYNK_WRITE(VPIN_JOY_Y)   { joyY = param.asInt(); }
BLYNK_WRITE(VPIN_MODE)    { mode = param.asInt(); }
BLYNK_WRITE(VPIN_WHITE)   { whiteOverride = param.asInt(); }

// ---- Setup ----
void setup() {
  pinMode(KILL_PIN, OUTPUT);
  digitalWrite(KILL_PIN, HIGH);
  pinMode(INT_PIN, INPUT);
  Serial.begin(115200);

  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
  FastLED.clear();
  FastLED.show();

  timer.setInterval(100L, effectHandler);
  BlynkEdgent.begin();

  startMillis = millis();    // 45-min shutdown timer

  // For auto-provision: start timer at boot if not connected
  wifiConnectStart = millis();
}

// ---- Main Loop ----
void loop() {
  // Power button shutdown (INT_PIN low = kill)
  if (digitalRead(INT_PIN) == LOW) {
    Serial.println("Shutdown requested");
    delay(50);
    digitalWrite(KILL_PIN, LOW);
    while (true) delay(1000);
  }

  // ---- Auto-provision if not connected ----
  if (!provisioningTriggered && 
      (BlynkState::get() == MODE_CONNECTING_NET || BlynkState::get() == MODE_CONNECTING_CLOUD)) {

    if (wifiConnectStart == 0) wifiConnectStart = millis();
    if (millis() - wifiConnectStart > WIFI_CONNECT_TIMEOUT) {
      provisioningTriggered = true;   // only run once
      forceProvisioningProper();
    }
  }
  // If running, reset the timer
  if (BlynkState::get() == MODE_RUNNING) {
    wifiConnectStart = 0;
    provisioningTriggered = false;
  }

  // ---- LED Feedback for Edgent state ----
  if (BlynkState::get() == MODE_CONNECTING_NET || BlynkState::get() == MODE_CONNECTING_CLOUD) {
    setLedMode(LED_FLASH_YELLOW);
    flashYellowEffect();
  } else if (BlynkState::get() == MODE_WAIT_CONFIG) {
    setLedMode(LED_SOLID_BLUE);
    solidBlueEffect();
  } else {
    setLedMode(LED_NORMAL);
    timer.run();
  }

  // ---- Auto Shutdown after 45 minutes ----
  if (!shutdownInitiated && (millis() - startMillis > SHUTDOWN_TIMEOUT)) {
    Serial.println("45 minutes elapsed. Automatic shutdown now.");
    digitalWrite(KILL_PIN, LOW);
    shutdownInitiated = true;
    while (true) delay(1000);
  }

  BlynkEdgent.run();
}