📟️

ESP32 と MQTT を使って家の PC の Wake On LAN を実現する

に公開

KeyVisual

はじめに

遅ればせながら最近 IoT にハマっています。

直近では MQTT を使ってメッセージを送り、LED を光らせる、なんていうものを作っていました。(ちょっとやりたいことがあるので)

https://x.com/edo_m18/status/1972871361824780538

今回はこの MQTT と ESP32 を使って、スリープ中の家の PC を WOL(Wake On LAN)する方法について書こうと思います。

MQTT とは

MQTT は「Message Queuing Telemetry Transport」の略とされていますが、Queuing の機能はなく名称だけが残っているようです。HTTP よりも軽量なため消費電力が少なく、非同期な双方向通信が可能です。そのため IoT に最適なメッセージングプロトコルです。Pub/Sub パターンを採用しており、非同期に 1 対多の通信が可能です。特に大事なのは組み込みデバイスやセンサーなど、メモリやネットワーク環境に制限があるような状況を想定して作られている点です。そのため IoT を用いた開発では重要になってくる技術です。

MQTT 自体の詳細な解説はここでは割愛します。どんなものかを知りたい方は以下の記事が参考になります。

https://dev.classmethod.jp/articles/mqtt_illustrations/

用語

とはいえ、まったく説明がないと理解しづらいと思うので軽く用語を整理しておきます。

  • ブローカー
  • パブリッシュ/サブスクライブ
  • メッセージ
  • トピック
  • QoS (Quality of Service) レベル

ブローカーとクライアント

ブローカーは、簡単に言えばサーバのことです。ここへ、クライアントである IoT デバイスが接続し、特定のトピックを購読します。
またクライアントはパブリッシュする場合もあります。こちらはメッセージを送る側のクライアントですね。

メッセージとトピック

MQTT の基本単位はメッセージです。その中にトピックという、いわば仕分けようのデータがあり、それを元にブローカーはどこに通知を行うかを決めるようになっています。

トピックは hoge/fuga/foo/bar のように / で区切られた文字列のことを言い、基本的には / で始まらない文字列を指定します。
これはディレクトリをイメージしてもらえれば分かりやすいと思いますが、それぞれのカテゴリごとに下層があり、それを識別できるようにしています。

またワイルドカードの指定などもできるようになっており、例えば hoge/+/foo などのように指定することもできます。

が、今回はひとつの操作のみをするため、ここはあまりこだわらなくても大丈夫です。

QoS (Quality of Service)

その名の通り、メッセージ通知の信頼性です。QoS0 ~ QoS2 までのレベルがあり、QoS の違いは以下の通りです。

  • QoS0: 最大 1 回のメッセージ送信。到達は保証しない
  • QoS1: 最低 1 回のメッセージ到達保証。ただし重複の可能性あり
  • QoS2: 正確に 1 度メッセージを届ける。一番確実だがその分処理負荷も高い

Wake On LAN とは

PC はスリープなど起動していない状態でも、ネットワークから特定のパケット(マジックパケット)を受信すると復帰する機能を有しているものがあります。また、シャットダウン状態にもいくつか段階があるようで、どの段階まで対応しているかは OS によります。

PC 側の設定

WOL を有効にするためにはいくつかの設定を変更しないとなりません。中には BIOS の設定をいじる部分もあります。

詳細については以下の記事が参考になるので、こちらを見ながらご自身の環境に合わせて設定を行ってください。

https://eizone.info/window-wake-on-lan/

主な必要な設定は以下です。

  • IP アドレスの固定
  • ネットワークアダプタの設定
  • BIOS / UEFI の設定
  • マジックパケットでの起動を有効にする

また、実装にあたって必要な情報は以下です。

  • 物理アドレス(MAC アドレス)
  • IPv4 アドレス

ESP32 とは

ESP32 は、Espressif Systems 社が開発した低消費電力の Wi-Fi と Bluetooth を搭載したマイコンチップです。Arduino と比較して高性能でありながら価格が安く、IoT プロジェクトで広く使われています。

今回使用する XIAO ESP32C3 は、Seeed Studio が販売する小型の ESP32 開発ボードで、以下のような特徴があります。

  • Wi-Fi と Bluetooth 5.0 (LE) を搭載
  • 小型サイズ(親指サイズ)でブレッドボードに直接挿せる
  • USB Type-C コネクタを搭載
  • Arduino IDE や PlatformIO で開発可能
  • 低消費電力設計

ESP32 は様々なセンサーやアクチュエータと組み合わせて使うことができ、Wi-Fi 経由で MQTT などのプロトコルを使った通信が簡単に実装できます。今回は ESP32 を MQTT クライアントとして動作させ、受信したメッセージをトリガーにして PC へマジックパケットを送信します。

今回使っているのはこんなちっちゃなボードです。大体 1,000 円くらいで買えます。

Seeed Studio XIAO ESP32C3

環境

今回の実装は Arduino IDE で行っています。
その他、利用しているサービスやハードは以下です。

ボードマネージャのインストール

デフォルトでは XIAO ESP32C3 向けのボードマネージャがないので、以下の URL を Arduino IDE の Additional boards manager URLs に追加します。

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json

設定は File > Preferences から開いた画面で行います。

Additional boards manager URLs

ライブラリのインストール

今回利用するライブラリは以下です。これをライブラリマネージャからインストールします。

  • WakeOnLan 1.1.7
  • PubSubClient 2.8

ライブラリのインストールは IDE 左側にある本のアイコンをクリックするとマネージャが開くので、そこから検索してインストールします。

Library Manager

実装

環境が整ったので実装していきましょう。
そんなに長くないのでひとまず全文コードを載せます。いくつかの箇所はご自身の環境に合わせて変更してください。
MQTT ブローカーの設定については HiveMQ を使うので後述します。

新規のスケッチを作成して以下のコードを貼り付けてください。

実装全文
#include <WiFi.h>
#include <WiFiUdp.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <WakeOnLan.h>

WiFiUDP UDP;
WakeOnLan WOL(UDP);

WiFiClientSecure secureClient;
PubSubClient mqtt(secureClient);

// Wi-Fi 設定
const char* ssid = "YOUR WI-FI SSID";
const char* password = "YOUR WI-FI PASSWORD";

// MQTT
const char* MQTT_HOST = "YOUR HOST URL";
const int MQTT_PORT = 8883;
const char* MQTT_CLIENT_ID = "XIAO_WOL";
const char* MQTT_TOPIC = "wakeup/pc/home";
const char* MQTT_USERNAME = "YOUR USER NAME";
const char* MQTT_PASSWORD = "YOUR PASSWORD";

// PC 情報の設定
const char* pcMacAddress = "YOUR MAC ADDRESS"; // 例: AA:BB:CC:DD:EE:FF
const IPAddress pcIP(192, 168, 1, 123); // ここも、自身で固定した IP を指定

void onMqtt(char* topic, byte* payload, unsigned int len) {
  String msg;
  msg.reserve(len + 1);
  for (unsigned int i = 0; i < len; ++i) {
    msg += (char)payload[i];
  }

  Serial.printf("[MQTT] %s <- %s\n", topic, msg.c_str());

  if (!strcmp(topic, MQTT_TOPIC)) {
    // マジックパケット送信
    WOL.sendMagicPacket(pcMacAddress); // MAC アドレスを使用してマジックパケットを送信
    Serial.println("Wake on LAN パケット送信");
  }
}

void ensureWifi() {
  if (WiFi.status() == WL_CONNECTED) return;

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.printf("WiFi connecting to %s", ssid);
  int tries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    if (++tries > 60) {
      ESP.restart();
    }
  }

  // サーバー証明書の検証をスキップ(自己署名テスト用)
  secureClient.setInsecure();

  Serial.println("\nConnected to Wi-Fi");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
}

void ensureMqtt() {
  if (mqtt.connected()) return;

  Serial.print("[MQTT] connect ...");

  if (mqtt.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
    Serial.println("OK");
    mqtt.subscribe(MQTT_TOPIC);
  }
  else {
    Serial.printf("fail rc=%d\n", mqtt.state());
    delay(1500);
  }
}

void setup() {
  Serial.begin(115200);

  // Wi-Fi に接続
  ensureWifi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMqtt);
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    ensureWifi();
  }
  if (!mqtt.connected()) {
    ensureMqtt();
  }
  mqtt.loop();
}

では順番に実装を見ていきましょう。

setup / loop

Arduino 開発ではふたつの重要な関数があります。それが setuploop です。

Unity などのゲームエンジンを触ったことがある人はピンとくると思いますが、 setup は起動時に初回一度だけ呼ばれ、 loop は起動後常に呼ばれる関数となっています。ある意味名前の通りですね。

今回で言えば Wi-Fi への接続やシリアル通信の初期化などを setup 関数内で行っています。

setup 関数
void setup() {
  Serial.begin(115200);

  // Wi-Fi に接続
  ensureWifi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMqtt);
}

冒頭の Serial.begin(115200); はシリアル通信を開始するための処理です。引数に渡しているのは転送レートです。(基本的には 115200 でいいはず?)

そして続く処理で Wi-Fi への接続と、MQTT ブローカーの設定を行っています。

Wi-Fi への接続処理は以下です。

ensureWifi 関数
void ensureWifi() {
  if (WiFi.status() == WL_CONNECTED) return;

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.printf("WiFi connecting to %s", ssid);
  int tries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    if (++tries > 60) {
      ESP.restart();
    }
  }

  // サーバー証明書の検証をスキップ(自己署名テスト用)
  secureClient.setInsecure();

  Serial.println("\nConnected to Wi-Fi");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
}

ここで行っているのは、Wi-Fi を、アクセスポイントではなく通常のアクセスとして設定しつつ、指定された SSID / Password で接続を試みます。

接続まで少し時間がかかるのでその間状態をチェックし、connected になったら処理を進めています。もし規定回数を超えても繋がらない場合は再起動しています。

また、今回はごく限られた状況かつ自分でしか利用しないため自己署名にしてサーバ証明書の検証をスキップしていますが、実際の運用など自分以外が利用する場合はしっかりと実装したほうがいいでしょう。

無事に Wi-Fi に接続できたら MQTT ブローカーの設定とコールバックの設定をして setup は終了です。

ループ処理は以下です。

loop 関数
void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    ensureWifi();
  }
  if (!mqtt.connected()) {
    ensureMqtt();
  }
  mqtt.loop();
}

ループの最初では Wi-Fi の接続確認を行っています。もし接続が切れていたら再接続する処理ということですね。

Wi-Fi に接続している場合は続いて MQTT ブローカーへの接続チェックを行います。もし接続されていない場合は ensureMqtt() 関数によって接続が試みられます。

ensureMqtt 関数
void ensureMqtt() {
  if (mqtt.connected()) return;

  Serial.print("[MQTT] connect ...");

  if (mqtt.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
    Serial.println("OK");
    mqtt.subscribe(MQTT_TOPIC);
  }
  else {
    Serial.printf("fail rc=%d\n", mqtt.state());
    delay(1500);
  }
}

行っていることはシンプルです。事前に設定されたクライアント ID と、ユーザ名、パスワードを用いて接続を試みます。
サーバのアドレスやポートはセットアップ時点で設定されているのでここでは指定していません。

そして無事に接続できたら、対象トピックについて購読( subscribe )して終了です。

マジックパケットの送信(スリープの解除)

さて、これでインターネットを介した MQTT ブローカーとの接続が完了しました。あとはサーバからメッセージが飛んできたら適切に処理することで WOL を達成できます。

その処理を行っているのがコールバックに指定している onMqtt() 関数です。

MQTT ブローカーからのコールバック
void onMqtt(char* topic, byte* payload, unsigned int len) {
  String msg;
  msg.reserve(len + 1);
  for (unsigned int i = 0; i < len; ++i) {
    msg += (char)payload[i];
  }

  Serial.printf("[MQTT] %s <- %s\n", topic, msg.c_str());

  if (!strcmp(topic, MQTT_TOPIC)) {
    // マジックパケット送信
    WOL.sendMagicPacket(pcMacAddress); // MAC アドレスを使用してマジックパケットを送信
    Serial.println("Wake on LAN パケット送信");
  }
}

コールバックに渡されるのは topicpayload(とその長さ)です。

冒頭の処理は payload から指定された長さ分だけ文字列を取り出しています。今回はペイロードの情報は使わないので意味がないのですが、汎用的にするために処理を残してあります。

大事な点は最後の !strcmp(topic, MQTT_TOPIC) です。なぜ ! なのかというと、strcmp 関数は文字列同士を比較して同じであれば 0 を返します。そのため if 文としては false に該当する処理が「同じ」を意味するため ! で反転しているというわけです。

そして目的のトピックだった場合に、PC に向けてマジックパケットを送信してスリープを解除しています。

スケッチのアップロード

プログラムが完成したら、スケッチをボードに書き込みます。(アップロードと呼びます)
ESP32 ボードを PC に接続し、Arduino IDE の左上のボタンを押すとアップロードできます。が、いくつかアップロードするための準備があるので以下の手順を実行してください。

ボードの選択

まずはスケッチをアップロードするボードを選択します。環境のところでボードをインストールしていると以下の項目が選択できるようになります。

Tools > Board: "..." > esp32 > XIAO ESP32C3 を選択します。(Board: のところには現在選択されているボード名が入ります)

ボードの選択

ポートの選択

次に、接続しているポートを選択します。ここは環境によって異なるため、ご自身の環境に合わせて適切なポートを選択してください。

ポートの選択

ログの確認

無事にアップロードが終わると自動的に起動され、setuploop 関数が呼ばれるようになります。

シリアル通信のログを表示するウィンドウを表示すると、シリアル通信のポートに流れてきたログを確認することができます。

ログウィンドウは右上のボタンを押すと表示されます。また、ウィンドウの右側にある転送レートを、プログラムで設定した 115200 に変更します。

ログ表示

正常に接続されていれば以下のようにログが表示されるはずです。

接続時のログ
20:27:43.191 -> WiFi connecting to Buffalo-2G-F5B0........
20:27:43.191 -> Connected to Wi-Fi
20:27:43.191 -> IP Address: 192.168.11.28
20:27:43.191 -> [MQTT] connect ...OK

HiveMQ Cloud で MQTT ブローカーを立ち上げる

以上で WOL 自体の実装は完成です。しかし、実際に外出先からメッセージを飛ばすにはインターネット上に存在する MQTT ブローカーを利用しないとなりません。(あるいは家の Wi-Fi ルータのポートを開けて自前サーバという手もありますが色々めんどくさい)

今回は冒頭でも紹介した HiveMQ Cloud というサービスを利用します。

https://www.hivemq.com/products/mqtt-cloud-broker/

こちらはフリーでも利用できる MQTT ブローカーを提供してくれているサービスです。今回の用途レベルでは有料にしなくても問題なく使えます。

まずはこちらのサービスでアカウントを作成してください。フリープランであればクレカの登録も不要です。

クラスターの作成

初回ではクラスターが作成されていないので CLUSTERS OVERVIEW という項目の横の「+」ボタンから新しくクラスターを作成します。

Create Cluster

プランが聞かれると思いますが、フリーの Create Serverless Cluster を選択してください。

Create Cluster

追加すると以下のように作成されるので「Manage Cluster」ボタンを押して設定画面に入ります。

Create Cluster

設定画面に入ったら Access Management タブを開き、右上の「Edit」ボタンを押して Add Credentials からユーザ名とパスワードを追加してください。

Add Credentials1
Add Credentials2
Add Credentials3

ここで設定したユーザ名とパスワードを、実装のところで出てきた MQTT_USERNAMEMQTT_PASSWORD に設定してください。

さぁこれで準備が整いました。

Web クライアントでチェック

HiveMQ の画面には Web Client というタブがあります。これを開くとこの画面から接続テストが行えます。デバイスを接続したのちに、ここから設定したトピックを送信して受信されるかをチェックするといいでしょう。

Web Client


サーバ証明書の設定

紹介したコードは開発時のもので、サーバ証明書の設定をしていません。しかし、本番運用の場合はそれを見越してしっかりと設定しておくのが望ましいです。

サーバ証明書の取得については Q&A で言及されています。

Where can I download the Root CA server certificate for Serverless and Starter?
The root ca can be downloaded from https://letsencrypt.org/certs/isrgrootx1.pem. This will create a file isrgrootx1.pem which should be used as a “server certificate”.

https://community.hivemq.com/t/frequently-asked-questions-hivemq-cloud/514

上記説明内の PEM ファイルのリンクからファイルをダウンロードします。
中身はテキストなのでそれをコピーして利用します。

プログラムでの指定

以下は、実際のコード例です。取得した PEM ファイルの中身を文字列として保持し、それを設定してアクセスします。

サーバ証明書の文字列指定
const char* root_ca = "-----BEGIN CERTIFICATE-----\n"
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
// 中略
"-----END CERTIFICATE-----";

長いので中略しています。実際のものと置き換えて使ってください。

開発用では setInsecure() としていた箇所を以下のように書き換えます。

サーバ証明書を設定
// secureClient.setInsecure();
//   ↓ こっちに書き換える
secureClient.setCACert(root_ca);

これでサーバ証明書を利用した形でアクセスすることができます。

スマホアプリのインストール

Web クライアントで確認できても、外出先から実際にオンにするにはなにかしら別の方法が必要です。今回は MQTT 関連をまとめて管理できるアプリがあるのでそれを利用します。

以下のアプリを、ご自身の使用スマホに合わせてインストールしてください。

▼ iOS 版
https://apps.apple.com/jp/app/iot-mqtt-panel/id6466780124

▼ Android 版
https://play.google.com/store/apps/details?id=snr.lab.iotmqttpanel.prod&hl=ja&pli=1

自分は iPhone ユーザなので iOS アプリを使います。

WOL 用ボタンを作成

以下の説明に沿って WOL 用のボタンを作成していきます。

基本的にこのアプリは Dashboard という単位がひとつの MQTT ブローカーとの接続単位となります。この Dashboard の中にボタンやスイッチなどを配置していく構成になっています。

そのためまずは Dashboard を作成します。

Create Dashboard

右下の「+」ボタンを押すと「Add Connection」画面に移ります。ここに、接続先ブローカーの情報を入力します。

ユーザ名とパスワードは Additional options の中にあるので、これを開いて先ほど自身で設定したものを入力してください。

Add connection

Dashboard が作成されたら、次はパネルを作成します。パネルは、ボタンやスイッチなどの単位です。

Create panel

今回はボタンを押したらスリープを解除したいのでボタンを選択します。

Create button

ボタンを選択するとボタンの名前や見た目を設定する画面になります。
ここはお好みで設定してください。

Button setting

無事にボタンが作成されると Dashboard にボタンが表示されます。(先ほどの再掲)
自分は「HOME PC WOL」という名前のボタンにしました。

Created panel


動作確認

以上ですべての準備が整いました。

ESP32 を起動し、Arduino IDE のシリアル通信のログ画面を表示した状態で MQTT ブローカーへの接続完了を確認したのち、先ほど作成したボタンを押して見てください。正常に接続できていればログが出力されるはずです。

受信ログ
20:45:03.170 -> [MQTT] wakeup/pc/home <- none
20:45:03.211 -> Wake on LAN パケット送信

最後に

最近は IoT デバイスをあれこれするのがとても楽しいです。特に最近では AI の進化が凄まじく、こうした IoT デバイスと組み合わせることで可能性が無限大に広がります。

今回この WOL を試したのも、AI を絡めた IoT デバイスを開発する過程で思いついた方法でした。これからも色々作ってブログ記事にしたいと思います。

Discussion