wiki
Miscellaneous

ESP32 Wake-on-LAN watchdog for a home server

A tiny ESP32 that checks a server every 2 minutes and sends Wake-on-LAN magic packets until it comes back. Useful after power cuts, with PlatformIO build, flashing and gotchas.

A home server that loses power often stays off when the power returns, so everything it hosts stays down until someone presses the button. An ESP32 on the same LAN fixes that. It checks a TCP port on the server and, while the server doesn't answer, sends Wake-on-LAN magic packets until it does.

Hardware: any ESP32 dev board (here an esp32doit-devkit-v1), powered from USB Server: any machine whose NIC supports WoL (here a Proxmox host, checked on port 8006) Toolchain: PlatformIO, Arduino framework

The one-sentence mental model

The ESP32 boots in seconds after a power cut. The server doesn't. So the ESP32 keeps knocking (magic packets, with growing gaps) until the server's port answers again.

How it behaves

SituationWhat the ESP32 does
Boot / Wi-Fi connectedChecks the server immediately
Server answersBlinks once, checks again in 2 min
Server doesn't answerBlinks 3 times and sends a burst of 3 magic packets to the subnet broadcast
Still downRetries after 30 s, 60 s, 120 s… doubling up to 10 min, and never gives up
Server backResets the backoff and returns to 2-minute checks

"Up" means a TCP connect to the port succeeds, so it tests that the server's service is actually running, not just that the NIC is powered.

Don't stop retrying

An earlier version gave up after 5 retries (about 2.5 minutes). After a power cut the switch, router and Wi-Fi can take longer than that to come back, so the ESP32 used up all its attempts before the packets could reach the server. Retrying with backoff fixes it.

1. Prepare the server

Check that the NIC supports WoL and has it enabled (g = magic packet):

ethtool nic0 | grep -i wake
#   Supports Wake-on: pumbg
#   Wake-on: g

If it shows Wake-on: d, enable it and make it persistent. On Proxmox/Debian with ifupdown2, add it to the bridge port's stanza in /etc/network/interfaces:

iface nic0 inet manual
	post-up ethtool -s nic0 wol g

Also, in the BIOS/UEFI:

  • Enable Wake on LAN / "Power on by PCI-E".
  • Set Restore on AC power loss to Power On (or "Last state"). That alone powers the server back on after most power cuts. WoL is the safety net when the board doesn't, or when the server was off.
  • If there's an ErP / EuP power-saving option, disable it, because it cuts standby power to the NIC.

Note the NIC's MAC: ip -br link show nic0.

2. Configure the firmware

The project has three files that matter:

wake-on-lan/
  platformio.ini        # [env:esp32doit-devkit-v1], framework = arduino
  src/config.h          # server IP/port, MAC, broadcast, timing
  src/secrets.h         # Wi-Fi SSID + password (gitignored)
  src/main.cpp

src/config.h:

// WiFi — real credentials live in src/secrets.h (gitignored)
#if __has_include("secrets.h")
#include "secrets.h"
#else
const char* WIFI_SSID     = "";
const char* WIFI_PASSWORD  = "";
#endif

const char* SERVER_IP      = "192.168.1.250";
const uint16_t CHECK_PORT  = 8006;                                    // Proxmox web UI
const uint8_t WOL_MAC[6]   = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55 };  // server NIC
const char* WOL_BROADCAST  = "192.168.1.255";  // subnet broadcast, more reliable than 255.255.255.255

const unsigned long CHECK_INTERVAL_MS         = 2UL * 60 * 1000;   // 2 min between checks
const unsigned long WOL_RETRY_INTERVAL_MS     = 30UL * 1000;       // first retry after 30 s
const unsigned long WOL_MAX_RETRY_INTERVAL_MS = 10UL * 60 * 1000;  // backoff cap, never gives up
const uint8_t       WOL_PACKETS_PER_BURST     = 3;

src/secrets.h (copy secrets.h.example; never commit it):

#pragma once
const char* WIFI_SSID     = "your-2.4GHz-ssid";
const char* WIFI_PASSWORD  = "your-password";

2.4 GHz only

The ESP32 can't see 5 GHz networks. If your router uses one SSID for both bands, that's fine. If the bands are separate, use the 2.4 GHz one.

3. The core logic

The magic packet is 6 × 0xFF followed by the MAC 16 times, sent over UDP to port 9:

void sendWolBurst() {
  uint8_t packet[102];
  memset(packet, 0xFF, 6);
  for (int i = 0; i < 16; i++)
    memcpy(packet + 6 + i * 6, WOL_MAC, 6);

  for (uint8_t n = 0; n < WOL_PACKETS_PER_BURST; n++) {
    udp.beginPacket(WOL_BROADCAST, 9);
    udp.write(packet, sizeof(packet));
    udp.endPacket();
    delay(50);
  }
}

The retry loop backs off instead of giving up:

if (serverState == STATE_DOWN_WAKING && now - lastWol >= wolInterval) {
  if (isServerUp()) { /* back to STATE_UP, reset backoff */ return; }

  wolRetries++;
  wolInterval = min(wolInterval * 2, WOL_MAX_RETRY_INTERVAL_MS);
  Serial.printf("WoL: retry %u, next in %lu s\n", (unsigned) wolRetries, wolInterval / 1000);
  sendWolBurst();
  lastWol = now;
}

To check immediately on boot, use a checkedOnce flag. Don't rely on lastCheck = 0: with now - lastCheck >= CHECK_INTERVAL_MS, the first check would only happen 2 minutes after boot.

4. Build and flash

Install PlatformIO

uv tool install platformio      # or: pipx install platformio

Get access to the serial port

The board shows up as /dev/ttyUSB0 (CP210x/CH340), owned by the dialout group:

sudo setfacl -m u:$USER:rw /dev/ttyUSB0     # until unplugged
sudo usermod -aG dialout $USER              # permanent, after re-login

Build, flash, watch

pio run                                   # build
pio run -t upload --upload-port /dev/ttyUSB0
pio device monitor -b 115200              # Ctrl+C to quit

Expected output:

── Wake-on-LAN monitor ──
...
WiFi connected  IP: 192.168.1.2
Check 192.168.1.250:8006 … UP

Test the "down" path safely

You don't need to power the server off. Temporarily set CHECK_PORT to a closed port such as 1, then flash and watch. A magic packet does nothing to a machine that's already on.

Check 192.168.1.250:1 … DOWN
WoL: sent 3 packet(s) to 192.168.1.255:9
WoL: retry 1, next in 60 s
WoL: sent 3 packet(s) to 192.168.1.255:9
WoL: retry 2, next in 120 s

Set it back to the real port and flash again.

To test a real wake-up, shut the server down cleanly (poweroff) and wait. Within about 2 minutes the ESP32 sees it down and wakes it.

Gotchas

  • WoL after a full power loss. Many boards only wake from WoL after they've had standby power for a while, or not at all after a cold AC loss unless ErP is off. That's why "Restore on AC power loss" is the main fix and WoL is the backup.
  • Power the ESP32 from the same circuit, or better, a UPS. If it's on a different power source than the switch and router, it still has nothing to send packets through until they're back. Retrying forever covers this.
  • Broadcast address. Use the subnet broadcast (x.x.x.255). Some routers and APs drop 255.255.255.255 from Wi-Fi clients. The ESP32 and the server must be on the same L2 network, since magic packets don't cross routers.
  • DHCP address. The ESP32 uses DHCP. If you have static IPs in the same range, add a DHCP reservation so it doesn't take an address you've assigned elsewhere.
  • Keep secrets out of git. secrets.h is in .gitignore. Check with git check-ignore -v src/secrets.h before committing.

On this page