Documentation
Everything you need to build, flash, and use esp32-wol — from first-time setup to understanding the internals.
Prerequisites
Hardware
- Any ESP32 board — NodeMCU-32S, HiLetgo ESP-32S, Espressif DevKitC, or equivalent. No PSRAM required.
- USB cable — for the initial flash only. After that, just 5 V power.
- Target PC — must have an Ethernet NIC with Wake-on-LAN enabled in BIOS and OS. WiFi NICs generally do not support WoL.
Software
- ESP-IDF v5.5.3 — the setup script will check for it and print install instructions if missing.
- Tailscale — installed and logged in on the machine you'll use to send wake commands.
- A Tailscale account (free tier is fine).
- Python 3 — for the
wake-pcscript.
Quick Start
./scripts/setup.sh, then use python3 scripts/wake-pc <ip> to wake your PC.
1. Clone the repository
git clone https://github.com/medinajaime/esp32-wakeonlan.git
cd esp32-wakeonlan
2. Run the setup script
./scripts/setup.sh
The script will:
- Check that ESP-IDF is installed (and print install instructions if not)
- Prompt for WiFi SSID and password, Tailscale auth key, target PC MAC address, device name, and serial port
- Build the firmware and flash it to the connected ESP32
4. Note the Tailscale IP
After flashing, open the serial monitor:
. ~/esp/esp-idf/export.sh
idf.py monitor -p /dev/ttyUSB0
Wait for the banner — it shows the ESP32's Tailscale IP:
I wol: ╔══════════════════════════════════════════╗
I wol: ║ ESP32 Wake-on-LAN — READY ║
I wol: ║ Tailscale IP : 100.126.x.x ║
I wol: ║ Listen port : 9999 ║
I wol: ╚══════════════════════════════════════════╝
5. Wake your PC
python3 scripts/wake-pc 100.126.x.x
Enable WoL on Target PC
BIOS / UEFI
- Enter BIOS setup (usually Delete or F2 at boot)
- Find Wake on LAN — typically under Power Management, Advanced, or Network Boot
- Enable it
- Disable ErP / EuP Ready — this setting cuts standby power to the NIC and will break WoL
- Save and exit
Windows
- Open Device Manager → expand Network Adapters
- Right-click your Ethernet adapter → Properties
- Power Management tab → check Allow this device to wake the computer
- Advanced tab → find Wake on Magic Packet → set to Enabled
Linux
# Check WoL status (g = enabled, d = disabled)
sudo ethtool eth0 | grep Wake-on
# Enable temporarily (until reboot)
sudo ethtool -s eth0 wol g
# Persist with systemd-networkd: create /etc/systemd/network/10-wol.link
[Match]
MACAddress=10:FF:E0:08:CC:4C
[Link]
WakeOnLan=magic
Configuration Reference
All settings are configured once via idf.py menuconfig or the setup script, then compiled into the firmware. There are no runtime config files to manage.
| Setting | Default | Description |
|---|---|---|
WOL_WIFI_SSID |
empty | SSID of your home WiFi network |
WOL_WIFI_PASSWORD |
empty | WiFi password (leave blank for open networks) |
WOL_TAILSCALE_AUTH_KEY |
placeholder | Auth key from Tailscale admin console. Use a reusable ephemeral key. |
WOL_DEVICE_NAME |
esp32-wol |
Name shown in the Tailscale admin console |
WOL_TARGET_MAC |
AA:BB:CC:DD:EE:FF |
Ethernet MAC of the NIC to wake. Find it with ip link show (Linux) or ipconfig /all (Windows) |
WOL_LISTEN_PORT |
9999 |
UDP port to listen on (over Tailscale). Range 1024–65535. |
WOL_BROADCAST_IP |
255.255.255.255 |
Broadcast address for the magic packet. Use subnet broadcast (e.g. 192.168.1.255) if your router blocks global broadcast. |
Tailscale Auth Key
The auth key registers the ESP32 with your Tailscale network. To get one:
- Go to tailscale.com/admin/settings/keys
- Click Generate auth key
- Check Reusable and Ephemeral
- Copy the key — it looks like
tskey-auth-kXXXXXXX-XXXXXXXXXX
Setup Script
The setup script (scripts/setup.sh) is the recommended way to configure and flash the firmware for the first time.
./scripts/setup.sh
What it does:
- Checks for ESP-IDF (looks in
~/esp/esp-idf,~/esp-idf, and$IDF_PATH) - Sources
export.shto activate the IDF environment - Prompts interactively for all required settings (passwords are hidden)
- Validates the MAC address format
- Generates a temporary config file and builds with
SDKCONFIG_DEFAULTS - Flashes to the connected ESP32 and optionally opens the serial monitor
Manual Build
If you prefer to configure manually:
# 1. Activate ESP-IDF
. ~/esp/esp-idf/export.sh
# 2. Configure (opens interactive menuconfig)
idf.py menuconfig
# Navigate to: Wake-on-LAN Configuration
# 3. Build
idf.py build
# 4. Flash
idf.py flash -p /dev/ttyUSB0
Serial Monitor
idf.py monitor -p /dev/ttyUSB0
Expected steady-state output (after ~25 s boot):
I wol: *** Tailscale CONNECTED ***
I wol: ╔══════════════════════════════════════════╗
I wol: ║ ESP32 Wake-on-LAN — READY ║
I wol: ║ Tailscale IP : 100.126.x.x ║
I wol: ║ Listen port : 9999 ║
I wol: ╚══════════════════════════════════════════╝
I wol: UDP socket ready on Tailscale port 9999
wake-pc Script
The scripts/wake-pc script sends triggers every second until the ESP32 confirms receipt with an ACK, then exits immediately. The setup script installs it to ~/.local/bin so you can run it from anywhere.
First use — save your ESP32 IP
After flashing, find the ESP32's Tailscale IP in the serial monitor (or in the Tailscale admin console), then save it:
wake-pc --save 100.126.x.x
This stores the IP in ~/.config/wake-pc/default-ip. From now on, just run:
wake-pc
Options
# Explicit IP (overrides saved default)
wake-pc 100.126.x.x
# Custom port (default: 9999)
wake-pc --port 9999
# Custom timeout (default: 25 s — one full reconnect cycle)
wake-pc --timeout 30
# Save a default IP
wake-pc --save 100.126.x.x
Example output:
Waking PC via ESP32 (100.126.x.x:9999) ...
Done! ESP32 confirmed (attempt 3).
Install on other devices
Copy scripts/wake-pc to any Tailscale-connected machine (laptop, server, Raspberry Pi) and run wake-pc --save <ip>. It works from any device on your tailnet — not just the machine the ESP32 was flashed from. The only requirement is Python 3.
The script uses a single persistent UDP socket (same source port throughout), so when the ESP32 sends back ok, the response is received by the same socket that sent the trigger.
Manual Trigger
You can also trigger WoL manually with nc:
# Single attempt (may need to retry if ESP32 is reconnecting)
echo wake | nc -u 100.126.x.x 9999 -w1
# Retry loop (reliable — covers a full reconnect cycle)
for i in $(seq 1 25); do
echo wake | nc -u 100.126.x.x 9999 -w1
sleep 1
done
nc approach doesn't receive the ACK. Use scripts/wake-pc for reliable confirmation.
State Machine
The firmware progresses through these states on boot and after each coordination reconnection:
| State | What happens |
|---|---|
REGISTERING | TLS 1.2 HTTPS POST to controlplane.tailscale.com. Sends WireGuard public key + auth key. Receives Tailscale VPN IP (100.x.x.x). |
FETCHING_PEERS | HTTP/2 long-poll (/machine/map). Receives peer list as JSON. cJSON parses it — this is the peak memory moment (~16 KB minimum free heap). |
CONFIGURING_WG | Programs wireguard-lwip with each peer's public key and allowed IPs. |
CONNECTED | WireGuard is up. Transitions to MONITORING immediately. |
MONITORING | Steady state. Sends WireGuard keepalives. UDP socket accepts triggers. Transitions back to REGISTERING when the coordination server closes the connection (~10–12 s). |
The WireGuard netif is never torn down during a normal coordination reconnect — only if the device enters ERROR state. This means the UDP socket stays valid and can receive triggers even during the ~12-second reconnection window.
WireGuard & DERP
WireGuard
The firmware uses wireguard-lwip, which implements WireGuard as a lwIP network interface (wg0). The handshake uses Noise_IK_25519_ChaChaPoly_BLAKE2s:
- X25519 — Diffie-Hellman key agreement
- ChaCha20-Poly1305 — authenticated encryption of all data
- BLAKE2s — keyed hash / MAC
DERP relay
When direct UDP between peers is blocked by NAT (the common case for a home ESP32), Tailscale routes traffic through a DERP (Designated Encrypted Relay for Packets) server. The firmware connects to DERP over TLS 1.2 on port 443 — the same as HTTPS — so it passes through almost every firewall.
DERP is transparent to the application. WireGuard-encrypted packets are wrapped in a DERP frame and relayed. The relay server sees only opaque ciphertext; all traffic is still end-to-end encrypted.
Data flow
Your device (any Tailscale peer)
│ UDP payload ("wake")
│ encrypted by WireGuard (ChaCha20-Poly1305)
▼
DERP relay server
│ Opaque ciphertext — relay can't read it
▼
ESP32 — wireguard-lwip decrypts the frame
│ Plaintext UDP payload delivered to lwIP socket
▼
wol_task — UDP recv returns ESP_OK
│ Builds 102-byte magic packet
│ Broadcasts to 255.255.255.255:9 via WiFi STA interface
▼
Target PC NIC — recognises its own MAC × 16 — powers on
│
▼
ESP32 sends ACK ("ok") back to sender via WireGuard/DERP
Magic Packet
A WoL magic packet is exactly 102 bytes:
Bytes 0–5 : FF FF FF FF FF FF (synchronisation stream)
Bytes 6–101 : <target MAC> × 16 (MAC repeated 16 times)
The magic packet is sent as a plain UDP broadcast (not through WireGuard) — it uses a separate BSD socket bound to the WiFi STA interface. This is critical: without binding to the WiFi interface, lwIP might try to route the broadcast through the WireGuard tunnel, where it would be dropped (WireGuard has no concept of broadcasts).
Memory Layout
The NodeMCU-32S has 520 KB SRAM and no PSRAM. Running a full TLS + WireGuard + cJSON stack in this space requires careful tuning.
| Allocation | Size (approx) |
|---|---|
| WiFi driver (static RX/TX buffers) | ~48 KB |
| mbedTLS handshake + buffers (dynamic) | ~40 KB |
| Tailscale state + peer table | ~20 KB |
| WireGuard session state | ~16 KB |
| FreeRTOS task stacks | ~20 KB |
| lwIP buffers | ~16 KB |
| cJSON MapResponse parse (peak, freed after) | ~32 KB |
| Free at steady state | ~16 KB |
Key tunings in sdkconfig.defaults:
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=4096— reduced from default 16 KBCONFIG_MBEDTLS_DYNAMIC_BUFFER=y— TLS buffers freed after handshakeCONFIG_TS_COORD_BUFFER_SIZE_KB=16— limits peak JSON parse allocationCONFIG_TS_MAX_PEERS=4— caps per-peer heapCONFIG_COMPILER_OPTIMIZATION_SIZE=y—-Osreduces binary size
Reconnection Behaviour
The Tailscale coordination server closes the HTTP/2 long-poll connection every 10–12 seconds. This is normal. When it happens:
- The firmware transitions
MONITORING → REGISTERING - Re-registers and re-fetches the peer list (~12 s total)
- Returns to
MONITORING
During this window:
- The UDP socket is kept alive — the WireGuard netif is not torn down
- Triggers received during reconnection are still processed
- When coordination comes back up, the socket is refreshed (new WireGuard handshakes + CallMeMaybe to all peers)
The wake-pc script's 25-second retry window is designed to cover one full reconnect cycle, guaranteeing delivery.
Troubleshooting
ESP32 crashes on boot (heap overflow)
Reduce the coordination buffer. In sdkconfig.defaults:
CONFIG_TS_COORD_BUFFER_SIZE_KB=12
Then delete sdkconfig and rebuild.
Device doesn't appear in Tailscale admin console
- Check that the auth key is valid and not expired
- Verify WiFi credentials — the serial monitor will show
WiFi retry N/20if connection fails - Make sure the auth key is for the correct Tailscale account
wake-pc times out (no ACK received)
- Check that both your PC and the ESP32 are on the same Tailscale network (
tailscale status) - Verify the ESP32 Tailscale IP — it's shown in the serial monitor banner
- Try
tailscale ping 100.126.x.xto confirm reachability - Check the serial monitor for
Trigger fromandMagic packet sentmessages
PC doesn't wake despite "Magic packet sent"
- Verify WoL is enabled in BIOS (see Enable WoL)
- Check that ErP/EuP Ready is disabled in BIOS
- Verify the target MAC address in config matches the Ethernet NIC (not WiFi)
- On Linux, confirm
ethtool eth0 | grep Wake-onshowsg - If your router uses VLANs, change
WOL_BROADCAST_IPto the subnet broadcast (e.g.192.168.1.255)
DISCO socket errors in serial output
E ml_disco: disco_send_udp4: socket not initialized!
This is non-fatal. DISCO is used for direct UDP path optimisation (bypassing the DERP relay). If it fails, the firmware falls back to DERP, which works reliably for WoL purposes. You can ignore these messages.
Known Limitations
| Limitation | Details |
|---|---|
| WoL requires Ethernet | The target PC must be connected via wired Ethernet. WiFi NICs do not support WoL magic packets. |
| Single target MAC | The firmware wakes one pre-configured PC. Supporting multiple targets would require the trigger payload to encode the MAC — a small firmware change. |
| No trigger authentication | Any device on your Tailscale network can trigger a wake. Use Tailscale ACL rules to restrict access if needed. |
| Coordination reconnects every ~12 s | The Tailscale coordination server closes the long-poll HTTP/2 connection frequently. This is a server-side behaviour; the device handles it gracefully. |
| No PSRAM support | All heap usage must fit in 520 KB SRAM. The firmware runs close to the edge — avoid adding memory-heavy features. |