LAN内のRaspberryPiを遠隔ON/OFFする方法【電子工作】

## はじめに
PCやスマホでデータを共有したいなと思い、自宅にRaspberryPiのファイルサーバーを作りました。ただ組込みSEの気性なのか、アクセスしていない時でもRaspberryPiが起動していることが気になります。本記事では、LAN内に設置してあるRaspberryPiを遠隔から起動/停止するための方法を記録します。
LAN内にマイコンを設置してWebページから制御するのですが、Webページなどはネットで探すと記事があるため、本記事では回路をメインにします。
なお、ブレッドボード上で実装もできますが、基板実装する際はハンダゴテを使用しますので、実践される方は安全に十分注意して行ってください。

## 技術選定
遠隔起動/停止について色々調べましたが、方法は限られているっぽいです。
RaspberryPi自体にこのような機能はなく、外部機器を用いての方法が一般的だと思います。

* PoE給電
PoE(Power over Ethernet)による給電です。給電を切ることもできるため、電源の制御が可能です。ネットワーク屋さんならよく使うのでしょうか？
PoE対応のHubを用意しなくてはならないため今回は外しましたが、既に機器を持っているのであれば、この方法が一番楽かと思います。
* SwitchBot給電
SwitchBotによる給電です。こちらも給電を切ることができるため、電源の制御が可能です。ただし、こちらも高いです。。。(金欠にはキツい。。。泣)
* GPIO3をGNDに接続する
GPIO3をGNDレベルまで落とすと起動/停止するそうです。
かなり時間をかけて海外の記事を見つけたのですが、よくよく探したら既にQiitaにも記事がありました。
案外そんなものです。

https://qiita.com/marumen/items/24d258786d4a5e05693a

https://qiita.com/tomoto335/items/8d4158b1625f6006e572

私が見つけた他の記事では、スイッチを接続し物理的(指など)に押下しています。ただし今回は、別室から起動/停止させたいので、ネットワーク経由でGPIO3をGNDに接続したいと思います。

## 回路図
本回路では、スイッチの代わりにフォトカプラを用います。ざっくりな説明ですが、フォトカプラは電気によってON/OFFするスイッチです。言い換えれば、電気の制御ができればスイッチの制御もできることになるため、マイコンを用いてフォトカプラを制御し、フォトカプラにつながったGPIO3をGNDに接続させます。
回路図を以下に示します。色々抵抗やLEDが接続されていますが、大事なのはマイコンのIO16に接続されている部品です。電流制御用の抵抗とプルダウン用の抵抗およびフォトカプラ(UPC817CG)です。この電子部品を用いてGPIO13をGNDに接続します。

![PXL_20250226_134202366~2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295695/cd9849bc-e886-4e92-b378-85373735a322.jpeg)


## プログラム
本記事では電子回路をメインにしているため、プログラムはさっと流します。
遠隔で操作するために、WebブラウザでフォトカプラをON/OFF(1.5秒ON)できるようにしています。
なお、プログラムはChatGPT o3-miniで作成しています。
実際の動作は次のセクションをご覧ください。

<details><summary>ソースコード</summary>

```c++
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include "secrets.h"  // WIFI_SSID, WIFI_PASSWORD等を定義

// ピン定義
const uint8_t PHOTOCOPLER_PIN = 14;  // IO14: UPC817CG(1番ピン)に接続
const uint8_t LED_PIN         = 16;  // IO16: LED(アノード)に接続

// タイミング設定
const unsigned long TRIGGER_DURATION = 1500;    // 1.5秒ON
const unsigned long BLINK_INTERVAL   = 500;     // 500ms周期で点滅

ESP8266WebServer server(80);

// 状態管理用
bool triggerActive = false;
unsigned long triggerStartTime = 0;
unsigned long lastBlinkTime = 0;
bool blinkState = false;

void setupWiFi() {
  // WiFi接続（SSID/PASSはsecrets.hから取得）
  WiFi.mode(WIFI_STA);
  WiFi.hostname("raspipow");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  
  Serial.print("WiFi接続中");
  // 接続完了までループ。ただし、接続失敗時は1分後に再試行
  unsigned long startAttemptTime = millis();
  while (WiFi.status() != WL_CONNECTED) {
    // LED点滅処理（非同期に行うため、ここでは短いdelay）
    if (millis() - lastBlinkTime >= BLINK_INTERVAL) {
      blinkState = !blinkState;
      digitalWrite(LED_PIN, blinkState ? HIGH : LOW);
      lastBlinkTime = millis();
    }
    // 10秒毎にシリアル出力
    if (millis() - startAttemptTime > 10000) {
      Serial.print(".");
      startAttemptTime = millis();
    }
    delay(10);
  }
  // 接続完了：LED消灯
  digitalWrite(LED_PIN, LOW);
  Serial.println();
  Serial.print("WiFi接続完了: ");
  Serial.println(WiFi.localIP());
}

void handleRoot() {
  // 初期表示ページ：操作ボタン有効
  String page = "<!DOCTYPE html><html lang='ja'><head><meta charset='UTF-8'>";
  page += "<title>RaspberryPi電源操作</title>";
  // CSS（淡い緑を基調）
  page += "<style>";
  page += "body { background-color: #e8f5e9; font-family: Arial, sans-serif; text-align: center; margin-top: 100px; }";
  page += ".btn { background-color: #a5d6a7; border: none; padding: 20px 40px; font-size: 24px; cursor: pointer; border-radius: 8px; }";
  page += ".btn:active { background-color: #81c784; }";
  page += "</style></head><body>";
  page += "<h1>RaspberryPi電源操作</h1>";
  page += "<form action='/toggle' method='POST'>";
  page += "<button class='btn' type='submit'>電源ON/OFF</button>";
  page += "</form>";
  page += "</body></html>";
  server.send(200, "text/html", page);
}

void handleToggle() {
  // ボタン連打防止のため、すでに動作中の場合は何もせず初期ページを再表示
  if (triggerActive) {
    server.sendHeader("Refresh", "0; url=/");
    server.send(200, "text/html", "既に動作中です。ページを再読み込みしてください。");
    return;
  }
  
  // フォトカプラをONにする（UPC817CGを1.5秒間ON）と同時にLEDもON
  triggerActive = true;
  triggerStartTime = millis();
  digitalWrite(PHOTOCOPLER_PIN, LOW);  // LOWでGNDに落とす
  digitalWrite(LED_PIN, HIGH);         // LED ON
  
  // ボタン操作済みページ
  String page = "<!DOCTYPE html><html lang='ja'><head><meta charset='UTF-8'>";
  page += "<title>RaspberryPi電源操作</title>";
  // CSS（淡い緑を基調）
  page += "<style>";
  page += "body { background-color: #e8f5e9; font-family: Arial, sans-serif; text-align: center; margin-top: 100px; }";
  page += ".btn { background-color: #c8e6c9; border: none; padding: 20px 40px; font-size: 24px; border-radius: 8px; color: #777; }";
  page += "</style></head><body>";
  page += "<h1>RaspberryPi電源操作</h1>";
  page += "<button class='btn' type='button' disabled>電源ON/OFF</button>";
  page += "<p>操作済み。再度操作したい場合は、Webページを再読み込みしてください。</p>";
  page += "</body></html>";
  server.send(200, "text/html", page);
}

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

  // ピン初期設定
  pinMode(PHOTOCOPLER_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(PHOTOCOPLER_PIN, HIGH);  // 通常は非アクティブ状態（HIGH）
  digitalWrite(LED_PIN, LOW);           // LED OFF（接続完了後は消灯）

  // WiFi接続（接続失敗時は1分ごとに再試行するため、ここはsetupで一度試行）
  setupWiFi();

  // MDNS初期化
  if (MDNS.begin("raspipow")) {
    Serial.println("MDNS responder started");
  }

  // Webサーバー設定
  server.on("/", HTTP_GET, handleRoot);
  server.on("/toggle", HTTP_POST, handleToggle);
  server.begin();
  Serial.println("HTTPサーバー開始");
}

void loop() {
  // 再接続処理（WiFi切断時は1分ごとに再接続）
  if (WiFi.status() != WL_CONNECTED) {
    static unsigned long lastReconnectAttempt = 0;
    if (millis() - lastReconnectAttempt >= 60000) {  // 1分間隔
      Serial.println("WiFi切断中。再接続を試みます...");
      WiFi.disconnect();
      WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
      lastReconnectAttempt = millis();
    }
    // 接続中はLEDで点滅（photocoupler動作中は優先）
    if (!triggerActive && millis() - lastBlinkTime >= BLINK_INTERVAL) {
      blinkState = !blinkState;
      digitalWrite(LED_PIN, blinkState ? HIGH : LOW);
      lastBlinkTime = millis();
    }
  } else {
    // WiFi接続済みの場合、かつ通常時はLEDはOFF（photocoupler動作中はLED ON）
    if (!triggerActive) {
      digitalWrite(LED_PIN, LOW);
    }
  }

  // photcoupler動作中のタイムアウト処理
  if (triggerActive && (millis() - triggerStartTime >= TRIGGER_DURATION)) {
    // 0.75秒経過後、元の状態に戻す
    digitalWrite(PHOTOCOPLER_PIN, HIGH);  // 非アクティブ状態に戻す
    // WiFi接続済みならLED OFF、未接続なら点滅制御に任せる
    if (WiFi.status() == WL_CONNECTED) {
      digitalWrite(LED_PIN, LOW);
    }
    triggerActive = false;
  }

  server.handleClient();
  MDNS.update();
}
```

</details>

<details><summary>プロンプト</summary>

```text
別室に置いてあるRaspberryPiを遠隔で起動するためのデバイス(ESP-WROOM-02)を作成したいので、プログラムを作成してください。
電子回路はこちらで作成するため、デバイスのソースコードのみ欲しいです。

# 事前情報
Raspberry PiのGPIO3をGNDに落とすと起動/シャットダウンができます。
GPIO3をGNDに落とす方法として、UPC817CG(UNISONICのPC817系フォトカプラ)を用います。

# デバイス概要
RaspberryPiを遠隔で起動するためのデバイス。
ユーザは所有しているスマートフォンなどのネットワーク機器からネットワーク経由でデバイスに接続し、ブラウザでRaspberryPiのON/OFFを操作する。
デバイスはGPIO3をGNDに落とすのみで、RaspberryPiの起動状態は取得しない。
(ユーザがRaspberryPiにSSHなどして、起動しているかを確認する)

# デバイス仕様
開発はArduino IDEで行う。
本プログラムで必要なライブラリは一覧で挙げてください。
(そのリストをもとに手動でインストールします)

## ネットワーク仕様
ローカルネットワークからの接続のみ対応する。
設置済みのAPに接続する。
ユーザは「http://raspipow:80」に接続し、ブラウザからRaspberryPiの電源操作を行う。
IPアドレス：動的
ホスト名：raspipow
ポート番号：80番
APのパスワードなどのセキュリティ上公開してはいけない情報は、別のヘッダーファイルに外出しする。
ローカルネットワークからのアクセスしか想定していないため、httpで問題なし。

## Webページ仕様
タイトル：RaspberryPi電源操作
「電源ON/OFF」というボタンが真ん中に表示される。
1度押されたら2回目は押せない。
1度押されたら「操作済み。再度操作したい場合は、Webページを再読み込みしてください。」と表示する。
CSSを使用して、デザイン性のあるページにしてください(淡い緑を基調としてください)

## 回路仕様
IO14：出力設定、UPC817CG(1番ピン)に接続
IO16：出力設定、LED(アノード)に接続

## 処理仕様
APに接続する。
接続中はLEDを点滅させる。
接続完了したらLEDを消灯させる。
※接続に失敗した場合は、1分ごとに再接続を試みる。

Webページを公開する。

Webページでボタンを押された場合に、UPC817CGを0.75秒ONする。
UPC817CGをONしている最中はLEDもONする。
```

</details>

## 動作確認
アプリ画面
![raspiPowCtl_webpage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295695/d549e63a-dd01-4ce9-9b6f-58c052ebda40.png)

![raspiPowCtl_webpage_done.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295695/f11fabe0-1c71-4c05-8464-e8b55ac44618.png)

回路(この写真から何かが分かるとは思えないが、、、)
アプリ上でボタンを押すと、RasbperryPiが起動します。
![PXL_20250228_141818891.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295695/bfc22184-d305-450f-b3b0-325cfeaded1b.jpeg)


## まとめ
RaspberryPiのGPIO3をGNDに落とすことで起動/停止する方法を紹介しました。
物理的なボタンを接続している記事は探すとあるのですが、ICを用いて電気的に実施している記事はパッと見た感じなかったため、備忘録として投稿します。