← The X99 Build

Wake on LAN Retrofit

Wake on LAN, kinda: an Orange Pi Zero listens for a magic packet over Tailscale and presses the X99's power button with a relay.

X99 build series: The build · mistral.rs vs llama.cpp · Four LLMs compared · Wake on LAN · ReBar BIOS mod · CMP 40HX unlock · V100 + VFIO

Overview

The 200 Bucks Inference Machine idles low, but it still has to be switched on before it can serve anything. Regular Wake on LAN is out for two reasons. First, the Huananzhi X99 BD4's BIOS has no Wake on LAN option at all. Second, even if it did, the apartment's shared Wi-Fi appears to isolate clients, so devices on it can't reach each other directly. A WoL broadcast can't cross Tailscale either.

So this retrofit fakes it. You tap a button in any WoL app on your phone, and the app sends a unicast magic packet over Tailscale to an Orange Pi Zero sitting next to the X99. A small listener on the Pi checks the packet, and if everything passes, it pulses a GPIO pin. That closes a relay wired across the motherboard's power switch header, and the board sees it as someone pressing the power button.

Phone sends a magic packet over Tailscale to the Orange Pi listener, which runs four checks, then pcpower drives GPIO pin 7 to close a relay across the X99's power switch header Phone any WoL app magic packet UDP :9 via Tailscale Orange Pi Zero wol_listener.py, bound to the Tailscale IP only 1 allowlist 2 payload 3 cooldown 4 X99 off? any check fails: log the reason, go back to recvfrom all pass check 4: one ping over Tailscale, 1 s timeout pcpower gpioset, line 6 pin 7 300 ms Relay H trigger, 5V coil COM/NO short X99 PWR_SW
One tap, four checks, one 300 ms button press.

Hardware

The Pi drives a single-channel 5V relay module (Songle SRD-05VDC-SL-C on a CW-025 board) from physical pin 7, which is line 6 on gpiochip0 (PA6). The pin only outputs 3.3V at a few mA, which isn't enough to move a relay coil directly. The module's input stage, set to trigger on high with the H jumper, takes that small signal and switches the coil's 70 to 90 mA from the Pi's 5V pin.

The relay's COM and NO contacts are wired across the X99's PWR_SW front panel header. One header pin is held at standby voltage through a resistor, and the other is ground. Closing the relay pulls the first pin to ground for 300 ms, drawing well under 1 mA, which is all a power button does anyway.

Relay module wired to the front panel power switch header on the Huananzhi X99 BD4, next to a 3D-printed enclosure Relay module wired to the front panel power switch header on the Huananzhi X99 BD4, next to a 3D-printed enclosure Orange Pi Zero GPIO header wired to the relay module, with the relay's output leads running to the motherboard Orange Pi Zero GPIO header wired to the relay module, with the relay's output leads running to the motherboard

The magic packet

A magic packet is 102 bytes: six 0xFF bytes followed by a MAC address repeated 16 times. Normally a network card watches for its own MAC in that pattern. Here, nothing about it is special to the network. It's an ordinary UDP payload that the Pi reads like any other data. The format is used only because every WoL app already knows how to send it.

The MAC in the packet, 02:00:00:00:99:01, is made up. The leading 02 marks it as locally administered, so it belongs to no real hardware. That keeps it from colliding with the X99's own WoL, and anyone who doesn't know it can't trigger the relay.

The listener builds the exact payload it expects once at startup:

def build_expected_payload(mac):
    mac_bytes = bytes.fromhex(mac.replace(":", ""))
    return b"\xff" * 6 + mac_bytes * 16

The listener

wol_listener.py opens a UDP socket bound to the Pi's Tailscale IP on port 9, not 0.0.0.0. Anything sent to the Pi's LAN address on port 9 goes nowhere, because nothing is listening there.

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((LISTEN_ADDR, LISTEN_PORT))

The main loop blocks on recvfrom until a packet arrives, then runs it through four checks in order. Failing any of them logs a reason and goes back to waiting.

  1. Allowlist. The sender's Tailscale IP has to be in ALLOWED_IPS, which holds the phone and the laptop. This runs first because it's the cheapest check. Over Tailscale, a source IP can't be faked, because WireGuard verifies each device cryptographically.
  2. Payload match. The first 102 bytes must equal the expected payload. Only the first 102 are compared because some WoL apps append an optional password field.
  3. Cooldown. WoL apps often send several packets per tap. If the last trigger was under 60 seconds ago, the packet is ignored, so one tap produces one press. The timer uses time.monotonic(), so an NTP sync or clock change can't make it jump.
  4. Already on? One ping to the X99's Tailscale IP with a 1 second timeout. A reply means the machine is running, so nothing happens. This check prevents the worst failure: pressing the button on a running machine starts a shutdown.
while True:
    data, addr = sock.recvfrom(1024)
    source_ip = addr[0]

    if source_ip not in ALLOWED_IPS:
        log(f"ignored packet from {source_ip} (not in allowlist)")
        continue

    if len(data) < len(expected) or data[:len(expected)] != expected:
        log(f"ignored packet from {source_ip} (MAC did not match)")
        continue

    now = time.monotonic()
    if now - last_trigger < COOLDOWN_SECONDS:
        log(f"ignored packet from {source_ip} (cooldown)")
        continue

    if is_x99_up():
        log(f"ignored packet from {source_ip} (X99 already up)")
        last_trigger = now
        continue

    log(f"magic packet from {source_ip}, pulsing relay")
    if pulse_relay():
        last_trigger = now

pulse_relay() runs pcpower through subprocess.run and checks its exit code. The cooldown only starts on success, so if the press fails, the next tap tries again right away instead of being ignored for a minute.

pcpower

Pressing the button is one gpioset call wrapped in a small shell script:

if [ "$1" = "hold" ]; then
    DURATION="5s"
else
    DURATION="300ms"
fi

gpioset -c gpiochip0 -t "${DURATION},0" 6=1

6=1 drives line 6 high, and -t "300ms,0" flips it back low after 300 ms. The trailing 0 tells gpioset to exit once done. pcpower hold holds for 5 seconds instead, which forces the PC off the same way holding the physical button would.

Keeping it running

A systemd service starts the listener at boot, after tailscaled:

[Unit]
After=network-online.target tailscaled.service
Wants=network-online.target

[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/wol_listener.py
Restart=always
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target

After= only orders startup. It doesn't guarantee Tailscale has assigned its IP yet. If it hasn't, bind fails, the script exits, and Restart=always retries every 5 seconds until it works. The same setting restarts the listener if it ever crashes. It runs as root because port 9 is a privileged port and GPIO access needs it.

Every check logs its own reason, so when a tap doesn't work, the journal shows which check rejected it:

journalctl -u wol-listener -f

Design patterns, kinda

This build lined up with CMU's 17-695 Design Patterns. The course keeps coming back to one line: the pattern is the decision, not the class diagram. Each pattern adds indirection to make one change cheaper, and if that change never comes, it's all cost. So here are the patterns this project uses, and two it deliberately doesn't.

Adapter: the whole retrofit

The course defines an Adapter as converting a shape you don't own into the one you want. Neither end of this system can be changed. WoL apps speak magic packets, and the X99 only understands two header pins being shorted. The Pi, the listener, and the relay together convert one into the other. Using the magic packet format instead of something custom, like an HTTP endpoint, is what makes this an Adapter and not a new protocol: any off-the-shelf WoL app works as a client with no changes.

The relay module is the same idea at the electrical level. It converts a 3.3V, few-mA GPIO signal into a dry contact closure the motherboard can't tell apart from a real button.

Facade: pcpower

"Adapter converts a shape you do not own. Facade shrinks a surface you do." gpioset has chips, line offsets, and timing flags. pcpower shrinks all of that to the two things a caller needs: press, or hold. The listener only knows RELAY_CMD = ["/usr/local/bin/pcpower"]. That same command works from an SSH session, and if the relay moves to another pin, only pcpower changes.

Chain of Responsibility: skipped

The four checks look like a Chain of Responsibility, where each handler either rejects the packet or passes it on. The course's "skip it when" table covers this case directly: a few rules, fixed order, none coming, so write the if/else with early returns. That's what the loop is. Each continue is an early return, and reading the loop top to bottom shows the order. A list of handler objects would only pay off if checks were being added, reordered, or configured, and none of those changes is planned.

State: skipped, and not stored at all

The power button is a toggle, so what a press does depends on whether the X99 is on. That sounds like a State pattern, but the listener doesn't track the machine's state at all. It asks, by pinging the X99 right before acting. Any stored copy would go stale as soon as someone pressed the physical button, the machine crashed, or the Pi rebooted. Querying the real system each time removes the sync problem entirely.

Brownfield: find the seam

The refactoring and brownfield week asks where a system's seams are and whether you can probe them. The X99's BIOS has no Wake on LAN option, so the board's own wake path is closed. The front panel PWR_SW header is the seam: a documented, two-pin interface that already exists for exactly this purpose. The retrofit changes nothing on the board. It just plugs in where a case button would.