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 を使用してマジックパケットを送信するサンプルです。
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になることが確認できました。
ちなみにポート 9 を指定した場合でも電源がONになりました。
Raspberry Pi からマジックパケットを送信する
続いてラズパイからマジックパケットを送信してデスクトップPCの電源をONにする方法を考えました。
以前ラズパイでCGIを動かす環境を構築したので、CGIでマジックパケットを送信する処理を実装することにしました。ユーザーインターフェースはHTMLでWebページを作成して、そこからCGIにリクエストを送信します。
WoLのマジックパケットを送信するCGIはBashで以下の内容で作成しました。
#!/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を以下の内容で作成しました。
#!/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で作成します。
<!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 を使用しました。
Elegant styles for all native HTML elements without .classes and dark mode automatically enabled. 7.9 kB minified and gzipped!
作成したツールの資材をラズパイに配置してブラウザでアクセスして動作確認を行いました。
成功の alert が表示されデスクトップPCが起動することが確認できました。
まとめ
Wake On LANを使用してコンピュータを起動する方法(マジックパケットの送信)について調べて実際にマジックパケットを送信してデスクトップPCの電源がONになることを確認しました。
その後ラズパイにマジックパケットを送信するツールを作成してツールからデスクトップPCの電源をONにできるようになりました。
CGIはレガシーな技術であまり使われていないイメージですが、気軽に作成できることとプログラミング言語の縛りもないので個人的にはラズパイで動かす簡易的なツールのインターフェースの作成にちょうどいいなと思いました。