ラズパイサーバーで始めるIoT：WebブラウザからLEDを制御してみた

# 0. はじめに
今回はラズパイを使った簡単なIoTプロジェクトとして、LEDをWebブラウザから制御するシステムを作ってみましたので備忘録もかねて記事にしました。  

最近、ラズパイでforgejoサーバーを立ち上げました。[1](https://zenn.dev/hiro345/articles/20240215_rpi5)  
forgejoの運用だけではもったいないと思い、ラズパイでWebサーバーも立ち上げることにしました。  
IoTにも興味があったため、ラズパイでホストしたWebアプリから別のラズパイへ信号を送るシステムを作ってみました。  

Webアプリからラズパイへ信号を送る方法としてはクラウドサービスを利用する方法が有力だと思いますが、お金がかかったりクラウドについての学習も必要なため、ハードルが高いように感じます。
今回はラズパイのみで構成されているため、ハードルは低いと思います。(ラズパイを2台使用しているため、お金は少しかかっていますが、、、)  

ラズパイを使って電子工作をちょっとやったことはあるが、Webにはあまり詳しくない方が本記事の対象になります。

# 1. やりたいこと
WebアプリからLEDを操作したい。

https://x.com/hamadaira0412/status/1844648987313451328


# 2. 実装概要
## 構成図

``` mermaid
graph LR
  Webブラウザ -->|リクエスト| Webサイトサーバー
  Webサイトサーバー -->|Webページ| Webブラウザ
  Webブラウザ -->|httpリクエスト| Webアプリケーションサーバー
  Webアプリケーションサーバー -->|GPIO操作| LED

  subgraph PC
  Webブラウザ
  end
  subgraph Raspberry Pi 5
  Webサイトサーバー
  end
  subgraph Raspberry Pi 4 
  Webアプリケーションサーバー
  end
  subgraph 電子回路 
  LED
  end
```
この図は今回作成したものの構成図を示しています。  
WebサイトサーバーにRaspberry Pi **5**を、WebアプリケーションサーバーにRaspberry Pi **4**を使用していますが、家にあったものを使っているだけなので特に意味はありません。  
現状はPCのWebブラウザからはアクセスできるのですが、スマホのWebブラウザからWebアプリケーションサーバーにリクエストを送信することはできません。    
原因がすぐにわかっていないため対処できていないですが、今後スマホのWebブラウザからでもリクエストを送信できるようにしたいです。

## シーケンス図

``` mermaid
sequenceDiagram
  ユーザー ->> +Webブラウザ :Webページリクエスト
  Webブラウザ ->> +Webサイトサーバー :Webページリクエスト
  Webサイトサーバー ->> -Webブラウザ :html
  Webブラウザ ->> -ユーザー :Webサイト
  ユーザー ->> Webブラウザ :ボタンを押す
  Webブラウザ　->> Webアプリケーションサーバー :httpリクエスト
  Webアプリケーションサーバー ->> LED :LED点灯/消灯操作
```
この図はブラウザからラズパイにリクエストが送信され、LEDを制御する全体の流れを示しています。
1. ユーザーがPCのWebブラウザからWebサーバーにアクセスし、Webページを閲覧する。  
2. Webページのボタンを押すと、WebブラウザからWebアプリケーションサーバーであるRaspberry Pi 4にリクエストが送られ、Raspberry Pi 4につながっているLEDが点灯/消灯する。  

# 3. Webサーバー実装
Webサーバーのミドルウェアとしてはapacheとnginxが有名なようです。[2](https://qiita.com/kamihork/items/49e2a363da7d840a4149)  
今回は私がWebに明るくないということもあり、比較的簡単に設定ができると噂のapacheを採用しました。

本項目の作業はWebサイトサーバーを実装するラズパイで実行してください。

## apache2の設定
確か参考サイトのように設定したと思います、、、
[3](https://www.fabshop.jp/Webserver-apache/#:~:text=%E3%83%A9%E3%82%BA%E3%83%99%E3%83%AA%E3%83%BC%E3%83%91%E3%82%A4%E3%81%A7%E3%82%A4%E3%83%B3)

apache2のインストール
``` bash
sudo apt update
sudo apt upgrade
sudo apt install apache2
```

ラズパイと同じプライベートネットワーク内の別のPCからWebページにアクセスしてみましょう。  
まずラズパイで```ifconfig```コマンドを実行し、IPアドレスを調べます。  
次に別のPCのWebブラウザからそのIPアドレスにアクセスしてページが表示されたらOK！  

## httpsの設定
Webサイトをhttps設定しないと、Webブラウザ、Webサイトサーバー、Webアプリケーションサーバーが同一のプライベートネットワークに所属していてもWebブラウザからWebアプリケーションサーバーにhttpリクエストを送ることができません。[4](https://developer.chrome.com/blog/cors-rfc1918-feedback?hl=ja)  
[参考サイト](https://itc.tokyo/apache/apache-https/)に基づいて設定しましたが、一部操作(下記6,7)が異なったので、備忘録もかねて記載します。  

1. SSLモジュールの導入  
``` bash
sudo a2enmod ssl
```

2. 秘密鍵の生成  
``` bash 
openssl genpkey -algorithm RSA -out server.key
```
いろいろ聞かれますが、とりあえず全部Enterを押せばOKです。

3. サーバ証明書の生成 
``` bash
openssl req -new -key server.key -out server.csr
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
```

4. 秘密鍵と証明書を適切な場所に配置  
``` bash
sudo mv server.key /etc/apache2/ssl/
sudo mv server.crt /etc/apache2/ssl/
```

5. 証明書と秘密鍵のパーミッション設定  
``` bash
sudo chmod 600 /etc/apache2/ssl/server.key
sudo chmod 644 /etc/apache2/ssl/server.crt
```  

6. SSLモジュールの適切な設定  
``` bash
sudo nano /etc/apache2/sites-available/default-ssl.conf
```  

変更前
``` default-ssl.conf
SSLCertificateFile      /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile   /etc/ssl/private/ssl-cert-snakeoil.key
```
変更後
``` default-ssl.conf
SSLCertificateFile      /etc/apache2/ssl/server.crt
SSLCertificateKeyFile   /etc/apache2/ssl/server.key
```

7. 設定へのパス付け  
``` bash
sudo apache2ctl configtest
sudo apachectl configtest
sudo service apache2 restart
```

## Webサイト実装
下記コマンドでapache2でホストされているhtmlを表示します。(上3行で元のindex.htmlのバックアップを取っています)  
``` bash
cd /var/www/html/
sudo mkdir backup
sudo mv index.html backup/
sudo nano /var/www/html/index.html
```

その後、index.htmlを下記のように編集します。
``` index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
  <link rel="stylesheet" href="styles.css">

  <script language="javascript" type="text/javascript">
    function OnButtonClick() {
      var request = new XMLHttpRequest();

      request.open("GET", "http://127.0.0.1:5000/on");

      request.send();
    }

    function OffButtonClick() {
      var request = new XMLHttpRequest();

      request.open("GET", "http://127.0.0.1:5000/off");

      request.send();
    }
    </script>
</head>

<body>
    <h1>LED</h1>
    <button class="led_turn_on"
    type="button" onclick="OnButtonClick();">turn on</button>
    <button class="led_turn_off"
    type="button" onclick="OffButtonClick();">turn off</button>
</body>
</html>
```

cssも実装して、少し見た目をよくします。
``` bash
sudo nano /var/www/html/styles.css
```

``` css styles.css
h1 {
    font-size: 400%;
    width: 25%;
    position: relative;
    padding: 0.25em 1em;
    border-top: solid 2px black;
    border-bottom: solid 2px black;
  }
  h1:before, h1:after {
    content: '';
    position: absolute;
    top: -7px;
    width: 2px;
    height: -Webkit-calc(100% + 14px);
    height: calc(100% + 14px);
    background-color: black;
  }
  h1:before {
    left: 7px;
  }
  h1:after {
    right: 7px;
  }

.led_turn_on{
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    width: 300px;
    height: 300px;
    background: #3eb370;
    color: #FFF;
    text-decoration: none;
    text-align: center;
    margin: 10px 0;
    font-size: 400%;
}


.led_turn_off{
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    width: 300px;
    height: 300px;
    background: #7d7d7d;
    color: #FFF;
    text-decoration: none;
    text-align: center;
    margin: 10px 0;
    font-size: 400%;
}
```

## 動作確認
その後、WebブラウザからWebサーバーにアクセスし、ページが表示されていればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3300264/dbebc9eb-428a-35ec-5d69-7812c5deae75.png)


# 4. Webアプリケーションサーバー実装
Webアプリケーションフレームワークとしては様々なものがありますが、今回は下記の2点からflask[5](https://flask.palletsprojects.com/en/3.0.x/)を採用しました。  

- ラズパイのGPIOをpythonで操作するため、pythonを使用したい。
- Webアプリケーションに詳しくないので、軽量なフレームワークを使用したい。

本項目の作業はWebアプリケーションサーバーを実装するラズパイで実行してください。

## 前準備
下記コマンドからモジュールをインストールしてください。
``` bash
pip3 install Flask
pip3 install -U flask-cors
```

## フォルダ構成
参考サイトを基に作成したフォルダ構成です。[6](https://qiita.com/usaitoen/items/0184973e9de0ea9011ed#:~:text=heroku%E4%B8%8A%E3%81%ABWe)
``` bash
.
├── app
│   ├── __init__.py
│   ├── app.py
│   └── light_emitting_diode.py
└── run.py
```

## 実装
### run.py
Webサーバを立ち上げる際に実行するファイルです。  

``` python run.py
from app.app import app

if __name__ == "__main__":
    app.run()
```

### app.py
アプリロジックを書くファイルです。リクエストされたURLに応じてどのような処理を実施するかを指定します。  
<ラズパイのIPアドレス>はWebサイトを実装するラズパイのIPアドレスに書き換えてください。

``` python app.py
from flask import Flask, jsonify
from flask_cors import CORS

from app.light_emitting_diode import LED

# ポイント⓵
app = Flask(__name__)
CORS(app, resources={
    r"/on": {"origins": "<ラズパイのIPアドレス>"},
    r"/off": {"origins": "<ラズパイのIPアドレス>"}
})

# インスタンス生成
led = LED()

@app.route("/on")
def led_turn_on():
    """http://127.0.0.1:5000/onにリクエストされると実行される関数"""
    led.turn_on()
    return jsonify({"status": "success", "message": "Device turned on"}), 200 # ポイント⓶

@app.route("/off")
def led_turn_off():
    """http://127.0.0.1:5000/offにリクエストされると実行される関数"""
    led.turn_off()
    return jsonify({"status": "success", "message": "Device turned off"}), 200

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=False)
```
解説
ポイント⓵
CORSを設定しないと、ブラウザの開発者ツールを確認するとエラーコードが出力されます。  
CORSについては[こちらの動画](https://www.youtube.com/watch?v=yBcnonX8Eak)が参考になりました。  

ポイント⓶
returnを返さないとhttpリクエストを受信したときに正常に処理は実施されるのですが、flask側でワーニングが出力されます。  
そのため、本実装では使用しないですがreturnを返しています。

### light_emitting_diode.py
GPIOからLEDを操作する処理を記載しています。  

``` python light_emitting_diode.py
import RPi.GPIO as GPIO

class LED:
    def __init__(self):
        """初期化をする"""
        self.ledGpio = 17
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.ledGpio, GPIO.OUT)

    def turn_on(self):
        """LEDを点灯させる"""
        GPIO.output(self.ledGpio, True)

    def turn_off(self):
        """LEDを消灯させる"""
        GPIO.output(self.ledGpio, False)
```

## 動作確認
コードを実装したフォルダに移動し、下記コマンドから実装したflaskコードを実行します。  
``` bash
python3 run.py
```

```* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)```のように表示されたら、別のターミナルを立ち上げ、下記コマンドを実行しましょう。
```bash 
curl http://127.0.0.1:5000/on
```
```{"message":"Device turned on","status":"success"}```とターミナルに表示されれば成功です！  
(すでにラズパイに電子回路をつないでいれば、LEDが点灯します。)  
同様にoffのリクエストも実施してみましょう。
```bash 
curl http://127.0.0.1:5000/off
```

# 5. 電子回路実装
今回は簡単な回路を作成しました。

``` mermaid
graph LR
  17番ポート --> LED
  LED --> 抵抗
  抵抗 --> GND
  subgraph ラズパイ 
  17番ポート
  GND
  end
  subgraph ブレッドボード 
  LED
  抵抗
  end
```
抵抗は390Ωのものを使用しました。

# 6. テスト
PCのWebブラウザからWebサーバーにアクセスし、Webサイトのボタンを押しましょう。  
"1.やりたいこと"の動画のようにLEDが点灯すると成功です！！

# 7. さいごに
クラウドサービスを利用せずになんとかPCとラズパイ2台をつなげることができました。  
普段は組み込みをメインで行っているので、モノとモノをネットワークでつなげることができてとてもうれしかったです。  

実装中はWebブラウザからWebアプリケーションサーバーにリクエストをなかなか送ることができず苦労しましたが、その分いい勉強になりました。(CORSやプライベートネットワークの概念、リバースプロキシなど)  

今回は電子回路を単純なLED点灯だけにしましたが、今後ももう少し機能を増やしたり、外部のネットワークからもWebサイトにアクセスできるようにしていきたいです。

# 99. https設定をしなくてもWebアプリケーションサーバーにリクエストを送信する方法
私の環境ではWebブラウザがのデフォルトの設定がプライベート ネットワーク リソースへのリクエストは、HTTPS ウェブページからのみ許可されるように設定されていたため、httpのホームページからWebアプリケーションサーバーにリクエストを送信することができなかったようです。  
設定を変えるためにgoogle chromeの検索窓に下記のように入力してください。
```chrome://flags/#block-insecure-private-network-requests```  
Block insecure private network requests.の項目をDisableに設定し、ブラウザを再起動してください。  
すると、httpのホームページからWebアプリケーションサーバーにリクエストを送信することができるようになります。  
しかし、セキュリティ上よくないと思いますので、あまりこのような設定はしない方がよいのではないかと思っています。