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 port8006) 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
| Situation | What the ESP32 does |
|---|---|
| Boot / Wi-Fi connected | Checks the server immediately |
| Server answers | Blinks once, checks again in 2 min |
| Server doesn't answer | Blinks 3 times and sends a burst of 3 magic packets to the subnet broadcast |
| Still down | Retries after 30 s, 60 s, 120 s… doubling up to 10 min, and never gives up |
| Server back | Resets 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: gIf 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 gAlso, 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.cppsrc/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 platformioGet 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-loginBuild, flash, watch
pio run # build
pio run -t upload --upload-port /dev/ttyUSB0
pio device monitor -b 115200 # Ctrl+C to quitExpected output:
── Wake-on-LAN monitor ──
...
WiFi connected IP: 192.168.1.2
Check 192.168.1.250:8006 … UPTest 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 sSet 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 drop255.255.255.255from 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.his in.gitignore. Check withgit check-ignore -v src/secrets.hbefore committing.