Community project

ESP32 Wi-Fi Repeater Router

ESP32
Photo of ESP32 Wi-Fi Repeater Router
Generated with AI

Gem Galangue

Published October 17, 2026

Turn an ESP32 into a Wi-Fi repeater router that extends your existing network coverage. The device connects to your primary Wi-Fi network and broadcasts its own access point, allowing other devices to join and route traffic through it. This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to get the repeater up and running.

The firmware handles Wi-Fi station mode (connecting to your main network), access point mode (broadcasting a new network), and Network Address Translation (NAT) to route traffic between the two. Configuration happens through a web interface accessible from any connected device, with settings stored in the ESP32's non-volatile memory so they persist across power cycles.

Wiring diagram

Gather all the parts

QtyComponent
1

USB data and power cable

The USB cable that powers the ESP32 board and carries the firmware from the computer.

Assemble it in 3 steps

1. Place the ESP32 where it can receive the existing Wi-Fi

Use the ESP32-WROOM-32 development board by itself; this project uses its built-in Wi-Fi radio and needs no signal wires or extra electronic parts.

  • Place it roughly between the existing router and the room where you need the new Wi-Fi signal. Keep the metal antenna area clear of metal objects and cables.
  • Do not power the board from more than one source at once.

2. Connect the board by USB

Plug a data-capable USB cable from your computer into the ESP32 board. The USB cable provides safe power and lets Schematik install the router firmware.

  • The default ESP32 Wi-Fi name is ESP32-Router and its default password is ConfigureMe123.

3. Set up the router from a phone or computer

After the firmware is deployed, join the ESP32-Router Wi-Fi network, then open http://192.168.4.1 in a web browser. Enter the name and password of the existing 2.4 GHz Wi-Fi router, save the page, and reconnect to the ESP32 Wi-Fi if you changed its name or password.

  • The web page works even when the upstream router is unavailable, so it can always be used to correct the saved settings.
  • Use an upstream 2.4 GHz Wi-Fi network; this ESP32-WROOM-32 board cannot join a 5 GHz-only network.

Review all connections

1. Connections between "usb_cable_1" and "ESP32"

Functionusb_cable_1ESP32
powerVBUSVIN
groundGNDGND

Deploy the firmware

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <Preferences.h>

// 1. Configuration. Requires Arduino-ESP32 3.x with WiFi.AP.enableNAPT().
const char *DEFAULT_AP_SSID = "ESP32-Router";
const char *DEFAULT_AP_PASSWORD = "ConfigureMe123";
const uint8_t DEFAULT_AP_CHANNEL = 9;
const IPAddress AP_IP(192, 168, 4, 1), AP_GATEWAY(192, 168, 4, 1), AP_SUBNET(255, 255, 255, 0);
const IPAddress FALLBACK_DNS(1, 1, 1, 1);
const unsigned long RECONNECT_INTERVAL_MS = 10000, STATUS_INTERVAL_MS = 30000, AP_CHECK_INTERVAL_MS = 15000;

struct RouterConfig { String staSsid, staPassword, apSsid, apPassword; uint8_t apChannel; } config;
Preferences preferences;
WebServer server(80);
unsigned long lastReconnectAttempt = 0, lastStatusReport = 0, lastApCheck = 0;
bool naptEnabled = false;
bool apNeedsRestart = false;
IPAddress advertisedDns = FALLBACK_DNS;

String htmlEscape(const String &value) {
  String out;
  for (size_t i = 0; i < value.length(); ++i) {
    switch (value[i]) {
      case '&': out += "&amp;"; break; case '<': out += "&lt;"; break;
      case '>': out += "&gt;"; break; case '"': out += "&quot;"; break;
      case '\'': out += "&#39;"; break; default: out += value[i]; break;
    }
  }
  return out;
}

// 6. NVS/Preferences storage
void loadConfig() {
  preferences.begin("router", true);
  config.staSsid = preferences.getString("staSsid", "");
  config.staPassword = preferences.getString("staPass", "");
  config.apSsid = preferences.getString("apSsid", DEFAULT_AP_SSID);
  config.apPassword = preferences.getString("apPass", DEFAULT_AP_PASSWORD);
  config.apChannel = preferences.getUChar("apChan", DEFAULT_AP_CHANNEL);
  preferences.end();
  if (config.apSsid.isEmpty()) config.apSsid = DEFAULT_AP_SSID;
  if (config.apPassword.length() < 8) config.apPassword = DEFAULT_AP_PASSWORD;
  if (config.apChannel < 1 || config.apChannel > 13) config.apChannel = DEFAULT_AP_CHANNEL;
}

void saveConfig() {
  preferences.begin("router", false);
  preferences.putString("staSsid", config.staSsid);
  preferences.putString("staPass", config.staPassword);
  preferences.putString("apSsid", config.apSsid);
  preferences.putString("apPass", config.apPassword);
  preferences.putUChar("apChan", config.apChannel);
  preferences.end();
}

void printStatus() {
  Serial.println("\n--- ESP32 Router status ---");
  Serial.printf("Upstream SSID: %s\n", config.staSsid.length() ? config.staSsid.c_str() : "not configured");
  Serial.printf("STA status: %s\n", WiFi.status() == WL_CONNECTED ? "connected" : "disconnected");
  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("STA IP: %s\n", WiFi.localIP().toString().c_str());
    Serial.printf("RSSI: %d dBm\n", WiFi.RSSI());
    Serial.printf("Active Wi-Fi channel: %d (2.4 GHz only)\n", WiFi.channel());
  }
  Serial.println("5 GHz: unavailable on ESP32-WROOM-32 hardware.");
  Serial.printf("SoftAP SSID: %s\n", config.apSsid.c_str());
  Serial.printf("SoftAP IP: %s\n", WiFi.softAPIP().toString().c_str());
  Serial.printf("Connected clients: %d\n", WiFi.softAPgetStationNum());
  Serial.printf("Hotspot DNS: %s\n", advertisedDns.toString().c_str());
  Serial.printf("NAT routing: %s\n", naptEnabled ? "enabled" : "waiting for upstream");
}

// 2. Wi-Fi STA initialization
void beginUpstreamConnection() {
  if (config.staSsid.isEmpty()) {
    Serial.println("No upstream Wi-Fi is configured. Open http://192.168.4.1 after joining the ESP32 Wi-Fi.");
    return;
  }
  Serial.printf("Connecting to upstream Wi-Fi: %s\n", config.staSsid.c_str());
  WiFi.begin(config.staSsid.c_str(), config.staPassword.c_str());
}

// 3. SoftAP initialization
void startAccessPoint() {
  WiFi.mode(WIFI_AP_STA);
  // DHCP gives hotspot clients a usable DNS server. It is replaced with the
  // upstream router's DNS address once the STA connection receives an IP.
  WiFi.softAPConfig(AP_IP, AP_GATEWAY, AP_SUBNET, IPAddress(0, 0, 0, 0), advertisedDns);
  bool started = WiFi.softAP(config.apSsid.c_str(), config.apPassword.c_str(), config.apChannel);
  apNeedsRestart = !started;
  Serial.printf("SoftAP %s: %s at %s\n", started ? "started" : "failed", config.apSsid.c_str(), WiFi.softAPIP().toString().c_str());
}

void ensureAccessPoint() {
  wifi_mode_t mode = WiFi.getMode();
  bool apModeEnabled = (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA);
  if (apNeedsRestart || !apModeEnabled) {
    Serial.println("Restoring the ESP32 Wi-Fi access point.");
    startAccessPoint();
  }
}

// 4. NAT/routing initialization
void publishUpstreamDnsToClients() {
  IPAddress dns = WiFi.dnsIP();
  if (dns == IPAddress(0, 0, 0, 0)) dns = FALLBACK_DNS;
  if (dns == advertisedDns) return;

  // Update DHCP's DNS option without changing the hotspot address or subnet.
  // Rejoining the ESP32 Wi-Fi makes an already-connected phone request the
  // new DHCP lease immediately.
  if (WiFi.softAPConfig(AP_IP, AP_GATEWAY, AP_SUBNET, IPAddress(0, 0, 0, 0), dns)) {
    advertisedDns = dns;
    Serial.printf("Hotspot clients will use DNS: %s\n", advertisedDns.toString().c_str());
  } else {
    Serial.println("Could not update hotspot DHCP DNS setting.");
  }
}

void setNapt(bool enable) {
  if (naptEnabled == enable) return;
  if (WiFi.AP.enableNAPT(enable)) {
    naptEnabled = enable;
    Serial.printf("NAT routing %s\n", enable ? "enabled" : "disabled");
  } else {
    Serial.println("Could not change NAT routing state.");
  }
}

void onWiFiEvent(arduino_event_id_t event, arduino_event_info_t info) {
  (void)info;
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_CONNECTED: Serial.println("Upstream Wi-Fi link connected."); break;
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.printf("Upstream IP received: %s\n", WiFi.localIP().toString().c_str());
      publishUpstreamDnsToClients();
      setNapt(true);
      break;
    case ARDUINO_EVENT_WIFI_STA_LOST_IP: Serial.println("Upstream IP lost."); setNapt(false); break;
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: Serial.println("Upstream Wi-Fi disconnected; reconnection will be retried."); setNapt(false); break;
    case ARDUINO_EVENT_WIFI_AP_STACONNECTED: Serial.println("A device joined the ESP32 Wi-Fi."); break;
    case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: Serial.println("A device left the ESP32 Wi-Fi."); break;
    case ARDUINO_EVENT_WIFI_AP_STOP:
      Serial.println("ESP32 Wi-Fi stopped; it will be restored.");
      apNeedsRestart = true;
      break;
    default: break;
  }
}

// 5. Web configuration server
String pageHeader(const String &title) {
  return "<!doctype html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'><title>" + title + "</title>"
         "<style>body{font-family:Arial;max-width:650px;margin:24px auto;padding:0 14px}input,select,button{box-sizing:border-box;width:100%;padding:10px;margin:5px 0 14px}button{background:#1261a0;color:white;border:0;border-radius:4px}.card{border:1px solid #ddd;border-radius:7px;padding:16px;margin:12px 0}.ok{color:#087f23}.bad{color:#aa1d1d}</style></head><body><h1>ESP32 Wi-Fi Router</h1>";
}

void handleRoot() {
  String state = WiFi.status() == WL_CONNECTED ? "<span class='ok'>Connected</span>" : "<span class='bad'>Not connected</span>";
  String page = pageHeader("ESP32 Router setup");
  String channelText = WiFi.status() == WL_CONNECTED ? String(WiFi.channel()) : "not connected";
  page += "<div class='card'><b>Router status</b><br>Upstream: " + state + "<br>STA IP: " + WiFi.localIP().toString() + "<br>Active channel: " + channelText + " (2.4 GHz)<br>5 GHz: unavailable on this ESP32-WROOM-32 board<br>ESP32 Wi-Fi: " + htmlEscape(config.apSsid) + "<br>ESP32 Wi-Fi address: " + WiFi.softAPIP().toString() + "<br>DNS given to connected devices: " + advertisedDns.toString() + "<br>Connected devices: " + String(WiFi.softAPgetStationNum()) + "<br>NAT routing: " + String(naptEnabled ? "enabled" : "waiting for upstream") + "</div>";
  page += "<form method='post' action='/save'><div class='card'><h2>Upstream Wi-Fi</h2><label>Router name (SSID)</label><input name='staSsid' maxlength='32' value='" + htmlEscape(config.staSsid) + "'><label>Router password (leave blank to keep current password)</label><input name='staPass' type='password' maxlength='63'></div><div class='card'><h2>ESP32 Wi-Fi</h2><label>Wi-Fi name</label><input name='apSsid' maxlength='32' value='" + htmlEscape(config.apSsid) + "'><label>Wi-Fi password (8–63 characters; leave blank to keep current password)</label><input name='apPass' type='password' maxlength='63'><label>Wi-Fi channel</label><select name='apChan'>";
  for (int ch = 1; ch <= 13; ++ch) page += "<option value='" + String(ch) + "'" + (ch == config.apChannel ? " selected" : "") + ">" + String(ch) + "</option>";
  page += "</select><button type='submit'>Save and restart router</button></div></form><form method='post' action='/reset' onsubmit=\"return confirm('Erase saved Wi-Fi settings?')\"><button type='submit'>Erase saved settings</button></form><p>After changing settings, reconnect to the ESP32 Wi-Fi using its new name if needed.</p></body></html>";
  server.send(200, "text/html", page);
}

void handleSave() {
  String apSsid = server.arg("apSsid"), staSsid = server.arg("staSsid"), apPass = server.arg("apPass");
  uint8_t channel = (uint8_t)server.arg("apChan").toInt();
  if (apSsid.isEmpty() || apSsid.length() > 32 || (apPass.length() > 0 && apPass.length() < 8) || channel < 1 || channel > 13) {
    server.send(400, "text/plain", "Use an ESP32 Wi-Fi name of 1-32 characters, a new password of at least 8 characters, and channel 1-13."); return;
  }
  config.staSsid = staSsid;
  if (server.arg("staPass").length() > 0) config.staPassword = server.arg("staPass");
  config.apSsid = apSsid;
  if (apPass.length() > 0) config.apPassword = apPass;
  config.apChannel = channel;
  saveConfig();
  server.send(200, "text/html", pageHeader("Saved") + "<p>Settings saved. The ESP32 is restarting now.</p></body></html>");
  delay(800); ESP.restart();
}

void handleReset() {
  preferences.begin("router", false); preferences.clear(); preferences.end();
  server.send(200, "text/html", pageHeader("Reset") + "<p>Saved settings erased. The ESP32 is restarting with its default setup Wi-Fi.</p></body></html>");
  delay(800); ESP.restart();
}

void startWebServer() {
  server.on("/", HTTP_GET, handleRoot); server.on("/save", HTTP_POST, handleSave); server.on("/reset", HTTP_POST, handleReset);
  server.onNotFound([]() { server.send(404, "text/plain", "Not found"); });
  server.begin(); Serial.println("Configuration page: http://192.168.4.1/");
}

void setup() {
  Serial.begin(115200); delay(300); Serial.println("\nESP32 Wi-Fi NAT Router starting");
  loadConfig(); WiFi.onEvent(onWiFiEvent); WiFi.persistent(false); WiFi.setSleep(false);
  WiFi.mode(WIFI_AP_STA);
  startAccessPoint();
  delay(300);
  WiFi.setAutoReconnect(true);
  startWebServer(); beginUpstreamConnection(); printStatus();
}

// 7. Reconnection handling, 8. Client monitoring, 9. Main loop
void loop() {
  server.handleClient();
  unsigned long now = millis();
  if (WiFi.status() != WL_CONNECTED && !config.staSsid.isEmpty() && now - lastReconnectAttempt >= RECONNECT_INTERVAL_MS) {
    lastReconnectAttempt = now; Serial.println("Retrying upstream Wi-Fi connection."); WiFi.reconnect();
  }
  if (now - lastApCheck >= AP_CHECK_INTERVAL_MS) {
    lastApCheck = now;
    ensureAccessPoint();
  }
  if (now - lastStatusReport >= STATUS_INTERVAL_MS) { lastStatusReport = now; printStatus(); }
  delay(2);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik