Skip to the content.

MycilaESPConnect

Latest Release PlatformIO Registry

GPLv3 license License: MIT

Contributor Covenant

Build GitHub latest commit

Simple & Easy Network Manager for ESP32 and ESP8266 with WiFi, Ethernet and Captive Portal support.

This library is based on the UI from https://github.com/ayushsharma82/ESPConnect (and this part falls under GPL v3). I highly recommend looking at all OSS projects (and products) from @ayushsharma82. He is making great Arduino libraries.

Features

Installation

PlatformIO

Add the library to your platformio.ini with its required dependencies:

ESP32 (WiFi only):

[env:esp32dev]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
board = esp32dev
framework = arduino
lib_compat_mode = strict
lib_ldf_mode = chain
lib_deps =
  mathieucarbou/MycilaESPConnect @ ^10.6.0
  ESP32Async/AsyncTCP @ ^3.4.10
  ESP32Async/ESPAsyncWebServer @ ^3.11.0
  bblanchon/ArduinoJson @ ^7.4.3

ESP32 (with Ethernet support):

[env:esp32-eth]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
board = esp32dev
framework = arduino
build_flags =
  -D ESPCONNECT_ETH_SUPPORT
lib_compat_mode = strict
lib_ldf_mode = chain
lib_deps =
  mathieucarbou/MycilaESPConnect @ ^10.6.0
  ESP32Async/AsyncTCP @ ^3.4.10
  ESP32Async/ESPAsyncWebServer @ ^3.11.0
  bblanchon/ArduinoJson @ ^7.4.3

ESP8266:

[env:esp8266]
platform = espressif8266
board = huzzah
framework = arduino
lib_compat_mode = strict
lib_ldf_mode = chain
lib_deps =
  mathieucarbou/MycilaESPConnect @ ^10.6.0
  ESP32Async/ESPAsyncTCP @ ^2.0.0
  ESP32Async/ESPAsyncWebServer @ ^3.11.0
  bblanchon/ArduinoJson @ ^7.4.3
  vshymanskyy/Preferences @ ^2.1.0

Note: ESPAsyncWebServer and ArduinoJson are mandatory. AsyncTCP (or ESPAsyncTCP on ESP8266) must be provided separately as a transport layer. Preferences is only needed on ESP8266 when using the built-in configuration persistence.

ESP32 platform: The official espressif32 platform dropped Arduino support. Use the pioarduino fork instead, as shown above.

Arduino IDE

Search for MycilaESPConnect in the Arduino Library Manager and install it along with its dependencies:

Compile Flags

Flag Description
-D ESPCONNECT_ETH_SUPPORT Enable Ethernet support (ESP32 only)
-D ESPCONNECT_ETH_RESET_ON_START Pull ETH_PHY_POWER LOW before powering the Ethernet PHY (useful for some boards)
-D ESPCONNECT_NO_CAPTIVE_PORTAL Disable Captive Portal and the ESPAsyncWebServer / ArduinoJson dependencies
-D ESPCONNECT_NO_MDNS Disable mDNS (~25 KB flash saving)
-D ESPCONNECT_NO_COMPAT_CP Disable multi-OS captive portal detection endpoints (~2 KB flash saving)
-D ESPCONNECT_NO_STD_STRING Use Arduino String instead of std::string
-D ESPCONNECT_NO_LOGGING Disable all serial logging
-D ESPCONNECT_CONNECTION_TIMEOUT=<sec> Override the default WiFi connection timeout (default: 20 seconds)
-D ESPCONNECT_PORTAL_TIMEOUT=<sec> Override the default captive portal timeout (default: 180 seconds)

mDNS

mDNS takes quite a lot of space in flash (about 25 KB). You can disable it with -D ESPCONNECT_NO_MDNS. When enabled, the hostname is registered automatically when NETWORK_CONNECTED is reached.

Usage

Blocking mode

begin() blocks until the network is ready (either AP_STARTED or NETWORK_CONNECTED). The captive portal is served inline while blocking. With setAutoRestart(true) (the default), the ESP restarts automatically after the captive portal completes or times out, so execution never reaches the code after begin() in those cases.

#include <MycilaESPConnect.h>

AsyncWebServer server(80);
Mycila::ESPConnect espConnect(server);

void setup() {
  Serial.begin(115200);

  espConnect.listen([](Mycila::ESPConnect::State previous, Mycila::ESPConnect::State state) {
    // react to state changes
  });

  espConnect.setAutoRestart(true);
  espConnect.setBlocking(true);
  espConnect.begin("arduino", "My Captive Portal");

  // reached only when NETWORK_CONNECTED or AP_STARTED
  Serial.println("Network is ready!");
  server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
    request->send(200, "text/plain", "Hello World!");
  });
  server.begin();
}

void loop() {
  espConnect.loop();
}

See also the BlockingCaptivePortal example.

Non-blocking mode

begin() returns immediately. All network transitions happen inside loop(). Use the state-change callback to start or stop your server in reaction to network events.

#include <MycilaESPConnect.h>

AsyncWebServer server(80);
Mycila::ESPConnect espConnect(server);

void setup() {
  Serial.begin(115200);

  espConnect.listen([](Mycila::ESPConnect::State previous, Mycila::ESPConnect::State state) {
    switch (state) {
      case Mycila::ESPConnect::State::NETWORK_CONNECTED:
      case Mycila::ESPConnect::State::AP_STARTED:
        server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
          request->send(200, "text/plain", "Hello World!");
        }).setFilter([](AsyncWebServerRequest*) {
          return espConnect.getState() != Mycila::ESPConnect::State::PORTAL_STARTED;
        });
        server.begin();
        break;
      case Mycila::ESPConnect::State::NETWORK_DISCONNECTED:
        server.end();
        break;
      default:
        break;
    }
  });

  espConnect.setAutoRestart(true);
  espConnect.setBlocking(false);
  espConnect.begin("arduino", "My Captive Portal");

  Serial.println("setup() done, network starting in background...");
}

void loop() {
  espConnect.loop();
}

See also the NonBlockingCaptivePortal example.

No Captive Portal mode

Compile with -D ESPCONNECT_NO_CAPTIVE_PORTAL to remove the web UI and its dependencies entirely. The constructor takes no arguments in this mode. On connection timeout, the state reaches NETWORK_TIMEOUT and you must handle it manually (e.g. switch to AP mode).

#include <MycilaESPConnect.h>

// Compile with: -D ESPCONNECT_NO_CAPTIVE_PORTAL

Mycila::ESPConnect espConnect;

void setup() {
  Serial.begin(115200);

  espConnect.listen([](Mycila::ESPConnect::State previous, Mycila::ESPConnect::State state) {
    if (state == Mycila::ESPConnect::State::NETWORK_TIMEOUT) {
      // Failed to connect: fall back to AP mode
      espConnect.getConfig().apMode = true;
    }
  });

  espConnect.setBlocking(true);
  espConnect.begin("arduino", "AP SSID");
}

void loop() {
  delay(100);
}

See also the NoCaptivePortal example.

External configuration system

Use the two-argument begin() overload to supply and manage your own Config struct. ESPConnect will not touch NVS — you load and save the configuration yourself.

#include <MycilaESPConnect.h>
#include <Preferences.h>

AsyncWebServer server(80);
Mycila::ESPConnect espConnect(server);

void setup() {
  Serial.begin(115200);

  espConnect.listen([](Mycila::ESPConnect::State previous, Mycila::ESPConnect::State state) {
    switch (state) {
      case Mycila::ESPConnect::State::NETWORK_CONNECTED:
      case Mycila::ESPConnect::State::AP_STARTED:
        server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
          request->send(200, "text/plain", "Hello World!");
        }).setFilter([](AsyncWebServerRequest*) {
          return espConnect.getState() != Mycila::ESPConnect::State::PORTAL_STARTED;
        });
        server.begin();
        break;
      case Mycila::ESPConnect::State::NETWORK_DISCONNECTED:
        server.end();
        break;
      case Mycila::ESPConnect::State::PORTAL_COMPLETE: {
        // save whatever the user chose in the captive portal
        Preferences prefs;
        prefs.begin("app", false);
        prefs.putBool("ap", espConnect.getConfig().apMode);
        prefs.putString("ssid", espConnect.getConfig().wifiSSID.c_str());
        prefs.putString("pass", espConnect.getConfig().wifiPassword.c_str());
        prefs.end();
        break;
      }
      default:
        break;
    }
  });

  espConnect.setAutoRestart(true);
  espConnect.setBlocking(false);

  // load config from your own storage
  Preferences prefs;
  prefs.begin("app", true);
  Mycila::ESPConnect::Config config = {
    .hostname    = "arduino",
    .wifiSSID    = prefs.getString("ssid", "").c_str(),
    .wifiPassword = prefs.getString("pass", "").c_str(),
    .apMode      = prefs.getBool("ap", false),
  };
  prefs.end();

  espConnect.begin("My Captive Portal", "", config);
}

void loop() {
  espConnect.loop();
}

See also the AdvancedCaptivePortal and LoadSaveConfig examples.

Static IP

Set static IP fields on the Config before (or after) calling begin() and call saveConfiguration() to persist them. Pass an empty IPConfig (all zeroes) to revert to DHCP.

// Set static IP
espConnect.getConfig().ipConfig.ip.fromString("192.168.1.99");
espConnect.getConfig().ipConfig.gateway.fromString("192.168.1.1");
espConnect.getConfig().ipConfig.subnet.fromString("255.255.255.0");
espConnect.getConfig().ipConfig.dns.fromString("192.168.1.1");
espConnect.saveConfiguration();

// Revert to DHCP
espConnect.getConfig().ipConfig = {};
espConnect.saveConfiguration();

The static IP is applied automatically on the next connection attempt. See also the WiFiStaticIP example.

API Reference

Constructor

// With Captive Portal (default)
Mycila::ESPConnect espConnect(server);   // AsyncWebServer& required

// Without Captive Portal (-D ESPCONNECT_NO_CAPTIVE_PORTAL)
Mycila::ESPConnect espConnect;

Lifecycle

// Auto-load/save variant: loads config from NVS, persists changes automatically.
// hostname  — mDNS name and AP hostname
// apSSID    — SSID of the captive portal / AP
// apPassword — optional password (must be >= 8 chars or it is ignored)
void begin(const char* hostname, const char* apSSID, const char* apPassword = "");

// Manual config variant: you provide and own the Config struct; nothing is read from / written to NVS.
void begin(const char* apSSID, const char* apPassword, Mycila::ESPConnect::Config config);

// Must be called from the Arduino loop() function.
void loop();

// Stops the network stack and resets the state machine to NETWORK_DISABLED.
void end();

Configuration

// Register a callback invoked on every state transition.
void listen(StateCallback callback);
// callback signature: void(Mycila::ESPConnect::State previous, Mycila::ESPConnect::State current)

// Blocking behaviour (default: true)
// When true, begin() loops internally until AP_STARTED or NETWORK_CONNECTED is reached.
void setBlocking(bool blocking);
bool isBlocking() const;

// Auto-restart (default: true)
// When true, the ESP restarts automatically after PORTAL_COMPLETE or PORTAL_TIMEOUT.
// When false, ESPConnect re-enters the state machine with the new configuration.
void setAutoRestart(bool autoRestart);
bool isAutoRestart() const;

// Delay in milliseconds between PORTAL_COMPLETE and the actual restart (default: 1000 ms).
void setRestartDelay(uint32_t delayMs);
uint32_t getRestartDelay() const;

// Maximum time in seconds to wait for a WiFi connection before giving up (default: 20 s).
void setConnectTimeout(uint32_t seconds);
uint32_t getConnectTimeout() const;

// Maximum time in seconds the captive portal stays open before timing out (default: 180 s).
// Only applies when a WiFi SSID is already configured; the portal stays open indefinitely
// when no SSID is known.
void setCaptivePortalTimeout(uint32_t seconds);
uint32_t getCaptivePortalTimeout() const;

// Access the current Config (mutable — changes take effect on the next connection attempt).
Mycila::ESPConnect::Config& getConfig();
const Mycila::ESPConnect::Config& getConfig() const;
void setConfig(Mycila::ESPConnect::Config config);

// NVS persistence (only relevant when using the auto-load/save begin() overload,
// or when managing persistence yourself via the manual-config overload).
void loadConfiguration();                        // load into internal Config
static void loadConfiguration(Config& config);   // load into a provided Config
void saveConfiguration();                        // save internal Config to NVS
static void saveConfiguration(const Config& config);
void clearConfiguration();                       // erase NVS entry and reset Config

// SSID and password used for the captive portal / AP.
const ESPCONNECT_STRING& getAccessPointSSID() const;
const ESPCONNECT_STRING& getAccessPointPassword() const;

State and Mode

// Current state
Mycila::ESPConnect::State getState() const;
const char* getStateName() const;
const char* getStateName(State state) const;

// Current network mode (AP / STA / ETH / NONE).
// ETH takes priority over STA when both are connected.
Mycila::ESPConnect::Mode getMode() const;

// True when any interface has a valid IP address.
bool isConnected() const;

Network Information

// MAC address of the active interface, or of a specific interface.
ESPCONNECT_STRING getMACAddress() const;
ESPCONNECT_STRING getMACAddress(Mode mode) const;   // Mode::AP, STA, or ETH

// IPv4 address of the active interface, or of a specific interface.
IPAddress getIPAddress() const;
IPAddress getIPAddress(Mode mode) const;

// IPv6 addresses (ESP32 only).
IPAddress getLinkLocalIPv6Address() const;
IPAddress getLinkLocalIPv6Address(Mode mode) const;
IPAddress getGlobalIPv6Address() const;
IPAddress getGlobalIPv6Address(Mode mode) const;

// WiFi-specific
ESPCONNECT_STRING getWiFiSSID() const;           // configured SSID or AP SSID
ESPCONNECT_STRING getWiFiBSSID() const;          // BSSID of connected AP, or "" if not connected
int8_t getWiFiRSSI() const;                      // signal strength in dBm, or -1
int8_t getWiFiSignalQuality() const;             // 0–100 %, or -1

JSON serialization

toJson() is available when the Captive Portal is enabled (i.e. ESPCONNECT_NO_CAPTIVE_PORTAL is not set):

JsonDocument doc;
espConnect.toJson(doc.to<JsonObject>());
serializeJsonPretty(doc, Serial);

The JSON object contains:

Key Description
ip_address Active IP address
ip_address_ap AP interface IP
ip_address_sta_v4 STA IPv4 address
ip_address_eth_v4 ETH IPv4 address
ip_address_sta_v6_local STA link-local IPv6
ip_address_sta_v6_global STA global IPv6
ip_address_eth_v6_local ETH link-local IPv6
ip_address_eth_v6_global ETH global IPv6
hostname Configured hostname
mac_address Active interface MAC
mac_address_ap AP MAC address
mac_address_sta STA MAC address
mac_address_eth ETH MAC address
mode "AP", "STA", "ETH", or "NONE"
state Current state name string
wifi_ssid Connected / configured SSID
wifi_bssid Connected AP BSSID
wifi_rssi RSSI in dBm
wifi_signal Signal quality 0–100 %

State machine

NETWORK_DISABLED
  └─ begin() ──────────────────────────────────────────► NETWORK_ENABLED
                                                              │
                           ┌──────────────────────────────────┤
                           │                                  │
                   apMode=true                        apMode=false
                           │                                  │
                           ▼                       wifiSSID configured?
                      AP_STARTING                 YES ◄──────┴──────► NO
                           │                      │                    │
                           ▼                      ▼              (captive portal
                       AP_STARTED           NETWORK_CONNECTING    or idle if
                      (final state)               │            NO_CAPTIVE_PORTAL)
                                          timeout?│connected?         │
                                             ▼    │    ▼              ▼
                                      NETWORK_TIMEOUT  NETWORK_CONNECTED   PORTAL_STARTING
                                             │         (final state)        │
                                             │                              ▼
                                     PORTAL_STARTING               PORTAL_STARTED
                                                                          │
                                                         user submits ────┤──── timeout
                                                              │                      │
                                                              ▼                      ▼
                                                       PORTAL_COMPLETE        PORTAL_TIMEOUT
                                                       (final state)          (final state)

NETWORK_CONNECTED ──── disconnected ──► NETWORK_DISCONNECTED ──► NETWORK_RECONNECTING ──► (reconnects)

Final states are states in which ESPConnect stays until the application takes action:

Config struct

struct Mycila::ESPConnect::Config {
  ESPCONNECT_STRING hostname;     // mDNS hostname and AP hostname
  ESPCONNECT_STRING wifiBSSID;    // preferred BSSID (useful in mesh networks)
  ESPCONNECT_STRING wifiSSID;     // WiFi SSID to connect to
  ESPCONNECT_STRING wifiPassword; // WiFi password
  bool apMode;                    // force AP mode (ignores wifiSSID/wifiPassword)
  IPConfig ipConfig;              // optional static IP (all-zero = DHCP)
};

struct Mycila::ESPConnect::IPConfig {
  IPAddress ip;      // static IP address (leave as 0.0.0.0 for DHCP)
  IPAddress subnet;  // subnet mask (e.g. 255.255.255.0)
  IPAddress gateway; // default gateway
  IPAddress dns;     // DNS server
};

ESP8266 Specifics

Ethernet Support

Set -D ESPCONNECT_ETH_SUPPORT to add Ethernet support (ESP32 only).

Behaviour:

SPI-based adapters (W5500, etc.) are detected automatically when all of ETH_PHY_SPI_SCK, ETH_PHY_SPI_MISO, ETH_PHY_SPI_MOSI, ETH_PHY_CS, ETH_PHY_IRQ, and ETH_PHY_RST are defined. In that case SPI.begin() and ETH.begin() are called with those pins.

Hints:

Known compatibilities:

Board Compile Tested
OLIMEX ESP32-PoE (esp32-poe)
OLIMEX ESP32-GATEWAY
Wireless-Tag WT32-ETH01 Ethernet Module (wt32-eth01)
T-ETH-Lite ESP32 S3 (esp32s3box)
USR-ES1 W5500
Waveshare ESP32-S3 ETH Board

Example of flags for wt32-eth01:

build_flags =
  -D ESPCONNECT_ETH_SUPPORT
  -D ETH_PHY_ADDR=1
  -D ETH_PHY_POWER=16

Example of flags for T-ETH-Lite ESP32 S3 (SPI W5500):

build_flags =
  -D ESPCONNECT_ETH_SUPPORT
  -D ETH_PHY_ADDR=1
  -D ETH_PHY_CS=9
  -D ETH_PHY_IRQ=13
  -D ETH_PHY_RST=14
  -D ETH_PHY_SPI_MISO=11
  -D ETH_PHY_SPI_MOSI=12
  -D ETH_PHY_SPI_SCK=10
  ; requires ESP-IDF >= 5
  ; -D ETH_PHY_TYPE=ETH_PHY_W5500

Example of flags for USR-ES1 W5500 with esp32dev (tested by @MicSG-dev):

build_flags =
  -D ESPCONNECT_ETH_SUPPORT
  -D ETH_PHY_ADDR=1
  -D ETH_PHY_CS=5
  -D ETH_PHY_IRQ=4
  -D ETH_PHY_RST=14
  -D ETH_PHY_SPI_MISO=19
  -D ETH_PHY_SPI_MOSI=23
  -D ETH_PHY_SPI_SCK=18
  ; requires ESP-IDF >= 5
  ; -D ETH_PHY_TYPE=ETH_PHY_W5500

Note: this project makes use of the ETHClass library from Lewis He.

You can display a custom logo in the captive portal by registering a /logo route before calling begin():

server.on("/logo", HTTP_GET, [](AsyncWebServerRequest* request) {
  AsyncWebServerResponse* response = request->beginResponse(
    200, "image/png", logo_png_gz_start, logo_png_gz_end - logo_png_gz_start);
  response->addHeader("Content-Encoding", "gzip");
  response->addHeader("Cache-Control", "public, max-age=900");
  request->send(response);
});

If no /logo handler is registered, the logo area in the portal is simply hidden.

Captive Portal Detection Endpoints

MycilaESPConnect serves OS-specific detection endpoints so that Android, iOS, Windows, and Linux all automatically open the captive portal UI when a device joins the ESP’s access point network.

Endpoint OS / Client Action
/connecttest.txt Windows Redirects to http://logout.net
/wpad.dat All Returns 404 (no proxy)
/generate_204 Android Redirects to AP IP
/redirect Generic Redirects to AP IP
/hotspot-detect.html Apple iOS / macOS Redirects to AP IP
/canonical.html Ubuntu / Linux Redirects to AP IP
/success.txt Microsoft Returns 200 OK
/ncsi.txt Microsoft NCSI Redirects to portal
/startpage Generic Redirects to portal

Disable all of these endpoints with -D ESPCONNECT_NO_COMPAT_CP (saves ~2 KB flash). This may reduce automatic portal detection reliability on some devices.