Raspberry Pi Zero WからWake On LANでデスクトップPCの電源を入れる

Raspberry Pi Zero Wからマジックパケットを送信して同じLAN内にあるデスクトップPCの電源をWake On LANで入れてみました。

はじめに

私の自宅はVPNで外出先から自宅のネットワークに接続できる環境はあるのですが自宅にあるデスクトップPCの電源が入っていないとリモートデスクトップで接続することができません。

常にデスクトップPCの電源をONにしておいてもよいのですが使用しないときはできるだけ電源を切っておきたいです。

そこで Wake On LAN でデスクトップPCの電源をONにするツールを作成しようと思いました。

Wake On LAN とは

Wake On LAN (WoL) とは、マジックパケットと呼ばれるパケットをネットワーク経由で送信することによって離れた場所にあるコンピュータの電源をネットワーク経由でONにする技術です。

WoLで電源をONにできるコンピュータは主に以下の要件が必要です。

  • そのコンピュータが WoL に対応している
  • LANケーブルでネットワークに接続されている

コンピュータの無線LANのネットワークボードが WoL に対応していれば Wi-Fi でネットワークに接続している場合でもスリープからの解除はできるみたいです。

対象のコンピュータは基本的にマジックパケットの送信元と同じネットワークに接続している必要があります。

マジックパケット

マジックパケットとは

マジックパケットは「FF:FF:FF:FF:FF:FF」で開始したあと対象のコンピュータのイーサーネットアダプタのMACアドレスを 16 回繰り返した 102 バイトのパケットです。

実際の送信するパケットはMACアドレスの「:」や「-」で区切られた各バイトを送信します。MACアドレスは 6 バイトで構成されますので送信するパケットは「6 * (1 + 16) = 102」バイトです。

マジックパケットは UDP で ブロードキャストアドレス「255.255.255.255」に向けて送信します。宛先のポートは 7 または 9 を指定します。

マジックパケットの動作確認

まず動作確認のため、WindowsのノートPCからマジックパケットを送信してデスクトップPCの電源がONになるかどうかを確認することにしました。

以下は Powershell を使用してマジックパケットを送信するサンプルです。

SendMagicPacket.ps1
Param(
  [Parameter(Mandatory)]
  [string]$MACAddress,
  [Parameter(Mandatory)]
  [int]$Port
)

$message = "FF:FF:FF:FF:FF:FF" + (":${MACAddress}" * 16)
$haxPackets = [regex]::Split($message, ":|-")
$binPackets = $haxPackets | ForEach-Object { [Convert]::ToByte($_, 16) }
$udpClient = New-Object System.Net.Sockets.UdpClient

try {
  $udpClient.Send($binPackets, $binPackets.Length, "255.255.255.255", $Port) | Out-Null
}
finally {
  $udpClient.Close()
}

デスクトップPCのイーサーネットアダプタのMACアドレスとポート 7 を指定して送信を行い、デスクトップPCの電源がONになることが確認できました。

PowershellでWoL

ちなみにポート 9 を指定した場合でも電源がONになりました。

Raspberry Pi からマジックパケットを送信する

続いてラズパイからマジックパケットを送信してデスクトップPCの電源をONにする方法を考えました。

以前ラズパイでCGIを動かす環境を構築したので、CGIでマジックパケットを送信する処理を実装することにしました。ユーザーインターフェースはHTMLでWebページを作成して、そこからCGIにリクエストを送信します。

WoLのマジックパケットを送信するCGIはBashで以下の内容で作成しました。

/usr/lib/cgi-bin/wakeup/wakeup.cgi
#!/bin/bash

set -euo pipefail

__wol() {
  local mac=${1}
  local message='FF:FF:FF:FF:FF:FF'
  for _ in {1..16}; do
    message="${message}:${mac}"
  done
  echo -en "\\x${message//:/\\x}" | nc -b -w1 -u '255.255.255.255' 9
}

__cgi() {
  local mac=${QUERY_STRING##*mac=}
  mac=${mac%%&*}
  mac=${mac//-/:}

  echo -e 'Content-Type: application/json; charset=utf-8\n'
  if __wol "${mac}"; then
    echo '{"ok": true}'
  else
    echo '{"ok": false}'
  fi
}

__cgi

URLクエリでMACアドレスを指定できるようにしました。CGIへのリクエストはjavascriptで行おうと思っていますので扱いやすいようにレスポンスはJSONで返すようにしました。

あと起動しているかどうかを確認する機能が欲しいのでIPアドレスを指定してPINGコマンドで疎通の確認を行うCGIを以下の内容で作成しました。

/usr/lib/cgi-bin/wakeup/ping.cgi
#!/bin/bash

__cgi() {
  local ipaddr=${QUERY_STRING##*ip=}
  ipaddr=${ipaddr%%&*}

  echo -e 'Content-Type: application/json; charset=utf-8\n'
  if ping -c 1 -W 0.1 "${ipaddr}" >/dev/null 2>&1; then
    echo '{"ok": true}'
  else
    echo '{"ok": false}'
  fi
}

__cgi

ツール全体の構成は以下のようになりました。

    • /usr/lib/cgi-bin/wakeup/
      • wakeup.cgiMACアドレスを指定してWoLのマジックパケットの送信を行うCGIです
      • ping.cgiIPアドレスを指定して疎通確認を行うCGIです
    • /var/www/html/wakeup/
      • index.htmlWoLツールのトップページのHTMLです

あとはツールのユーザーインターフェースとなるWebページをHTMLで作成します。

/var/www/html/wakeup/index.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf8" />
    <title>WoLツール</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css"
    />
  </head>
  <body>
    <main class="container">
      <h1>WoLツール</h1>
      <article>
        <select id="wol-target-select">
          <!-- MACアドレスとIPアドレスをカンマ区切りで指定する -->
          <option value="00-00-00-00-00-00,192.168.0.1">デスクトップPC</option>
        </select>
        <button id="wol-trigger-button">Wake On LAN</button>
      </article>
    </main>
    <script>
      ;(() => {
        /** @type {HTMLSelectElement} */
        const select = document.getElementById('wol-target-select')
        /** @type {HTMLButtonElement} */
        const button = document.getElementById('wol-trigger-button')

        button.addEventListener('click', async function (e) {
          e.preventDefault()
          select.setAttribute('disabled', 'disabled')
          button.setAttribute('aria-busy', 'true')
          await new Promise((resolve) => requestAnimationFrame(resolve))

          try {
            const [mac, ip] = select.value.split(',')
            await wol(mac)
            await ping(ip, 50, 300)
            alert('成功')
          } catch (e) {
            alert(`失敗:${e.message}`)
          } finally {
            select.removeAttribute('disabled')
            button.removeAttribute('aria-busy')
          }
        })

        /**
         * マジックパケットの送信を行うCGIにリクエストを送信
         *
         * @param {string} mac
         */
        async function wol(mac) {
          const res = await fetch(`/cgi-bin/wakeup/wol.cgi?mac=${mac}`)
          const ret = await res.json()
          if (!ret.ok) {
            throw new Error(ret.message)
          }
        }

        /**
         * 疎通確認を行うCGIにリクエストを送信
         *
         * @param {string} ip
         * @param {number} count
         * @param {number} interval
         */
        async function ping(ip, count, interval) {
          for (let i = 0; i < attempts; i++) {
            const res = await fetch(`/cgi-bin/wakeup/ping.cgi?ip=${ip}`)
            const ret = await res.json()
            if (ret.ok) {
              return
            }
            await new Promise((resolve) => setTimeout(resolve, interval))
          }
          throw new Error(`${ip}の疎通確認が取れませんでした`)
        }
      })()
    </script>
  </body>
</html>

ボタンをクリックするとマジックパケットの送信を行うCGIにリクエストを送信して成功した場合は疎通確認を行うCGIにリクエストを送信して起動しているか確認を行います。

成功や失敗の通知は簡易的な alert で行うようにしました。対象のコンピュータを増やしたいこともあるかもと思ったのでMACアドレスとIPアドレスはドロップダウンで指定するようにしました。(記載したHTMLにはダミーで 00-00-00-00-00-00 と 192.168.0.1 を指定しています)

デザインにはこだわりはないのですがそのままだと少し寂しいのでクラスレスのCSSフレームワークの Pico.css を使用しました。

作成したツールの資材をラズパイに配置してブラウザでアクセスして動作確認を行いました。

Wolツール

成功の alert が表示されデスクトップPCが起動することが確認できました。

まとめ

Wake On LANを使用してコンピュータを起動する方法(マジックパケットの送信)について調べて実際にマジックパケットを送信してデスクトップPCの電源がONになることを確認しました。

その後ラズパイにマジックパケットを送信するツールを作成してツールからデスクトップPCの電源をONにできるようになりました。

CGIはレガシーな技術であまり使われていないイメージですが、気軽に作成できることとプログラミング言語の縛りもないので個人的にはラズパイで動かす簡易的なツールのインターフェースの作成にちょうどいいなと思いました。