本稿では、スマートフォンのブラウザ画面にRaspberry Pi につながる温湿度センサーからのデータを表示すると共に、ブラウザ画面からLEDの制御を行う方法を紹介します。

概要
温湿度センサーはSHT31DISを用い、Raspberry Pi からはI2Cにより本センサーと通信を行います。
LEDは、Raspberry Pi のGPIOポートを使って、点灯・消灯を行います。
Raspberry Pi がサーバーになり、クライアントであるブラウザからのリクエストを受けて、サービスを実行します。サーバーにはApacheを用います。 ブラウザとRaspberry Pi のコミュニケーションは、Ajaxを用いてCGIを起動し、データや命令を記述したJSONデータを送受することで実現します。

動作の仕様
温度と湿度は、1秒周期で更新します。
LEDは、ボタンを押すと該当するLEDをRaspberry Pi が点灯させ、ブラウザに応答が返った後、ボタンの色を変更させます。
プログラムのディレクトリ
Raspberry Pi の/home/”ユーザー名”の中に以下のフォルダやファイル(プログラム)を作成します。
cgi-bin
┗ drive_led_gpio.py Ajax通信で起動されるCGIで、LEDの制御を行います。
┗ get_weather-data_i2c.py Ajax通信で起動されるCGIで、温湿度センサーと通信を行います。
CSS
┗ style_00.css index.htmlで示されるブラウザのデザインを修飾します。
SCRIPT
┗ test.js index.htmlのボタン操作に対する処理やAjax通信制御を記述しています。
Index.html ブラウザからのアクセスに対して最初に返されるページです。
ソースコード一式はこちらです。
開発の手順
センサーやLED/抵抗とRaspberry Pi との配線を行って、プログラムを開発していきます。
開発を行うにあたり、手順が決まっているわけではありませんが、最初にCGIとして動作させるLEDの点灯制御プログラムと、温湿度センサーと通信するプログラムを作成するのが良いでしょう。ここではこれらのプログラムはPythonで記述しました。こらのプログラム単体でLEDの点灯制御や温湿度センサーからのデータ受信のテストを行ったのち、クライアントからコマンドを受け取って応答する仕組みを実装します。
続いて、HTTPサーバーとしてApacheの設定を行います。(インストールがまだの場合はインストールを行います。)
次に、HTML/CSSにる操作画面プログラムを作成し、そこにJavaScriptによって、画面操作時の動きや通信制御の機能を付与していきます。システムテストは先ずは同一WiFi内で行って、その後、インターネット経由でのテストを行って完成です。以下の手順で見ていきましょう。
0.配線( Raspberry Pi と温湿度センサー、Raspberry Pi とLEDの配線接続)
1.CGIプログラムの作成(Python)
2.HTTPサーバーの準備 (Apacheの設定)
3.操作画面プログラムの作成(HTML/CSS、及びJavaScript)
4.システムテスト① 同一WiFi内からのアクセス
5.システムテスト② インターネット経由でのアクセス
0.配線接続
Raspberry のIOピンの配列は以下のとおりとなっています。

今回の例では、Raspberry Pi と温湿度センサー、Raspberry Pi とLEDの配線接続は以下のとおりとしました。

1.CGIの作成
1.1 CGIの実行ファイル
CGIの実行ファイルはPythonで作成しています。
cgi-bin
┗ drive_led_gpio.py Ajax通信で起動されるCGIで、LEDの制御を行います。
┗ get_weather-data_i2c.py Ajax通信で起動されるCGIで、温湿度センサーと通信を行います。
いずれも、標準入力sys.stdin.read()と、json.loadsにより、ブラウザからのAjax通信によるJSONデータを受け取って、内容分析してLED制御または温湿度センサーからのデータ受信を行い、結果をJSONデータで送り返す構造としました。
#!/usr/bin/env python
# デバッグ用
import cgitb
cgitb.enable()
import cgi
import json
import sys
import time
import RPi.GPIO as GPIO
print("Content-Type: text/plain")
print()
# Setting GPIO condition
GPIO.setmode(GPIO.BCM)
blueLED = 22 # call GPIO22 blueLED
yellowLED = 23 # call GPIO23 blueLED
redLED = 24 # call GPIO24 blueLED
GPIO.setup(blueLED, GPIO.OUT)
GPIO.setup(yellowLED, GPIO.OUT)
GPIO.setup(redLED, GPIO.OUT)
LED_TABLE = {
"LED":{"LED1":blueLED, "LED2":yellowLED, "LED3":redLED},
"ONOFF":{"ON":True, "OFF":False}
}
data = sys.stdin.read()
cmd_json = json.loads(data)
# jsonデータからkeyを取り出して指定するLEDを点灯または消灯させる
for key in cmd_json:
GPIO.output(LED_TABLE.get("LED").get(key), LED_TABLE.get("ONOFF").get(cmd_json[key]))
# 受け取ったJSONデータをループバックする。ブラウザ側ではこちらのjsonデータが届いたことの確認の印としてJSONデータを受け取る。
print(json.dumps(cmd_json))
#!/usr/bin/env python
# デバッグ用
import cgitb
cgitb.enable()
import cgi
import json
import sys
import time
import smbus
print("Content-Type: text/plain")
print()
def getSH31data():
i2c = smbus.SMBus(1)
i2c_addr = 0x45
i2c.write_byte_data(i2c_addr, 0x21, 0x30)
time.sleep(0.5)
i2c.write_byte_data(i2c_addr, 0xE0, 0x00)
dataFromSH31 = i2c.read_i2c_block_data(i2c_addr, 0x00, 6)
T = ( dataFromSH31[0] << 8 ) | dataFromSH31[1]
fTemp = float(T)* 175 / 65535.0 -45.0
H = ( dataFromSH31[3] << 8 ) | dataFromSH31[4]
fHumi = float(H) / 65535.0 * 100.0
return fTemp, fHumi
receiveData = sys.stdin.read()
cmd_json = json.loads(receiveData)
for key in cmd_json:
if(key =="GET"):
cmd_json["TEMP"], cmd_json["HUMI"] = getSH31data()
print(json.dumps(cmd_json))
1.3 CGIファイルの実行権限
作成したCIGを実行できるように、実行権限を与えます。
sudo chmod 755 /home/”ユーザー名”/cgi-bin/drive_led_gpio.py
sudo chmod 755 /home/”ユーザー名”/cgi-bin/get_weather-data_i2c.py
1.4 I2CやGPIOの有効化
Raspberry Pi のコンフィグレーション設定メニューを開いて、I2CとGPIOを有効化します。
www-dataユーザーへI2CとGPIOアクセス権設定
www-dataユーザーがI2CとGPIOへアクセスできるよう、アクセス権を設定します。
sudo gpasswd -a www-data i2c
sudo gpasswd -a www-data gpio
2.Apacheの設定
Apacheのインストールがまだであればインストールを行います。
その後、作成したファイルがHTTPのサービスとして機能するよう必要な設定をApacheやRaspberry Pi に行っていきます。
2.1 DocumentRoot
Apachサーバーが返すページがあるドキュメントルートは、デフォルト設定では、/var/www/htmlディレクトリの中にあるIndex.htmlファイルです。任意の場所にある自分で作成したindex.htmlを返したい場合は、ドキュメントルートを変更する必要があります。エディタで000-default.confを開き、DocumentRoot を自分で決めたディレクトリに変更します。(たとえば、/home/”ユーザー名”)
sudo nano /etc/apache2/sites-available/000-default.conf
2.2 ディレクトリへのアクセス権限
デフォルトでは、apache.confにて、/var/www/ディレクトリにアクセス権が与えられているので、自分で決めたディレクトリにアクセス権を付与するよう編集します。
sudo nano /etc/apache2/apache2.conf
以下は編集箇所の抜粋。デフォルトの記載を抜粋し、自分で決めたディレクトリを記載している。
#<Directory /var/www/>
<Directory /home/bigdog/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
2.3 CGIの有効化
CGIを有効にします。
sudo ln -s /etc/apache2/mods-available/cgi.load/etc/apache2/mods-enabled/cgi.load
CGI設定ファイル”mime.conf”を開き、AddHandler cgi-script .cgiのコメントアウトを外して有効化します。
sudo nano /etc/apache2/mods-available/mime.conf
Apache2設定ファイル”000-default.conf”を開き、Include conf-available/serve-cgi-bin.confのコメントアウトを外して有効化います。
sudo nano /etc/apache2/sites-available/000-default.conf
Apacheをリスタートします。
(設定変更した場合はリスタートします。Raspberri pi 起動時にはApacheの設定がデフォルトのままならばApacheは自動的にスタートします。)
sudo service apache2 restart
2.4 ブラウザ経由でのCGI実行権限の設定
ブラウザからサーバにアクセスした場合、ユーザ名はwww-dataとなります。www-dataがCGIを起動するのにパスワード入力が不用となるよう設定します。”sudoers”ファイルを開き編集します。
sudo nano /etc/sudoers
一番最後に以下を記載する。
www-data ALL=(ALL) NOPASSWD:ALL
3.操作画面プログラムの作成
3.1 HTML/CSS
Index.htmlに、画面上部に温湿度センサーからの情報を表示し、画面下部にLEDを制御するボタンを配置するようレイアウトしています。ブラウザでindex.htmlを読み込んだ最後にに次項で示すtest.jsをロードします。
文字の大きさや色など画面デザインの装飾はCCSで指定します。
3.2 JavaScript
test.jsでは、Ajaxを用いた二つの非同期通信を実行するよう記載しています。
1.index.htmlが開かれているときに、”drive_led_gpio.py”を起動してPOST送信を行う処理を1秒毎に繰り返します。
2.LED制御ボタンが押されたときに、”get_weather-data_i2c.py”を起動してPOST送信を行います。
いずれもPOSTメソッドによって、JSONデータをCGIに送り、CGIから送られたJSONデータの内容に応じてIndex.htmlの見た目を更新します。
var url_post_port = "/cgi-bin/drive_led_gpio.py";
var url_post_sensor = "/cgi-bin/get_weather-data_i2c.py";
var flgTime = false;
var timerHundle = null;
colorTable = {
"LED1":"background-color:#00BCF9",
"LED2":"background-color:#FFFF33",
"LED3":"background-color:#FF3300",
"OFF":"background-color:#FFFFFF"
}
// サーバーにPOST送信するJSONデータを生成するクラス
class makeingPortStatJsonData{
constructor() {
this.portStat = {
"LED1":"OFF", "LED2":"OFF", "LED3":"OFF"
};
console.log(this.portStat);
this.sensorStat = {
"GET":"DUMMY", "TEMP":0.00, "HUMI":0.00
};
console.log(this.sensorStat);
}
// 指定したLEDをONまたはOFFしてポートマップを作り、JSONデータを生成する。
getJSON_port(portname){
var before = Object.assign({}, this.portStat);
if ( this.portStat[portname] === "OFF" ) {
this.portStat[portname] = "ON";
} else {
this.portStat[portname] = "OFF";
}
console.log(before);
console.log(this.portStat);
var json_data = JSON.stringify(this.portStat);
return json_data;
}
// GET命令と温度と湿度データを受け取る箱からなるマップを作り、JSONデータを生成する。
getJSON_sensor(){
var json_data = JSON.stringify(this.sensorStat);
return json_data;
}
}
PortStatJsonData = new makeingPortStatJsonData();
// clickイベント処理
document.body.addEventListener("click", function(event){
var eventClass = event.target.className;
if ( eventClass === "PORT" ) {
// LED制御テスト画面からの、LED番号指定によるJSONデータの生成と、POST送信
var json_data = PortStatJsonData.getJSON_port(event.target.id);
xhrPost_port(url_post_port, json_data);
}
} ,false);
var json_data = PortStatJsonData.getJSON_sensor();
if(flgTime == false) {
timerHundle = setInterval(xhrPost_sensor, 1000, url_post_sensor, json_data);
flgTime = true;
}
window.onbeforeunload = function(e) {
if(flgTime) {
clearInterval(timerHundle);
}
}
function xhrPost_port(url, jsondata) {
// XHRの宣言
var XHR = new XMLHttpRequest();
// openメソッドにPOSTを指定して送信先のURLを指定する
XHR.open("POST", url, true);
// sendメソッドにデータを渡して送信を実行する
XHR.setRequestHeader('Context-Type', 'application/json');
XHR.send( jsondata );
// サーバの応答をonreadystatechangeイベントで検出して正常終了したらデータを取得する
XHR.onreadystatechange = function(){
if(XHR.readyState == 4 && XHR.status == 200){
// POST送信した結果を表示する
// サーバーが返したjsonデータを用いてボタンの色(状態)を変える
let responseJson = JSON.parse(XHR.response);
for( var key in responseJson ) {
if ( responseJson[key] === "ON" ) {
document.getElementById(key).style= colorTable[key];
} else {
document.getElementById(key).style= colorTable["OFF"];
}
}
} else {
}
};
}
function xhrPost_sensor(url, jsondata) {
// XHRの宣言
var XHR = new XMLHttpRequest();
// openメソッドにPOSTを指定して送信先のURLを指定する
XHR.open("POST", url, true);
// sendメソッドにデータを渡して送信を実行する
XHR.setRequestHeader('Context-Type', 'application/json');
XHR.send( jsondata );
// サーバの応答をonreadystatechangeイベントで検出して正常終了したらデータを取得する
XHR.onreadystatechange = function(){
if(XHR.readyState == 4 && XHR.status == 200){
// POST送信した結果を表示する
// サーバーが返したjsonデータを用いて温度や湿度を表示する。
let responseJson = JSON.parse(XHR.response);
for( var key in responseJson ) {
if ( (key === "TEMP") || (key ==="HUMI" ) ) {
document.getElementById(key).innerHTML = (responseJson[key]).toFixed(2);
}
}
} else {
}
};
}
4.システムテスト① 同一WiFi内からのアクセス
4.1 Raspberry Pi のIPアドレスを固定
無線LANでルータに接続しているのであれば、wlan0の設定で、Raspberry Pi へ割り当てられるIPアドレスを固定にします。(ルーターが192.168.1.1の場合、Raspberry Pi は、192.168.1.**のIPアドレスが自動で割り当てられますが毎回変わると困るので固定にします。 (自動で割り振られる値をメモし、その値を固定値として使えば良いでしょう。)
4.2 Raspberry Pi で解放するポートを設定
ファイアーウォールの導入がまだであれば、インストールします。
sudo apt install ufw
そして、ファイアーウォールを有効にします。
sudo ufw enable
続いて、HTTP Serverにポート80でリッスンさせたいので、ポート80へのアクセスを許可します。
sudo ufw allow 80
この時点で、同一WiFi内から、Raspberry Pi に割り当てたIPアドレスにブラウザでアクセスできるはずです。
事項では、同一WiFi外から、すなわちインターネットからRaspberry Pi へアクセスするための環境を整えます。
5.システムテスト② インターネット経由でのアクセス
5.1 無料のDNS MyDNSを活用
MyDNSは、無料のDNSです。
こちら(https://www.mydns.jp/)でユーザー登録すれば、無償でドメイン名がもらえます。
このDNSにより、あなたのドメイン名とグローバルIPアドレスを紐付けてくれます。
(ただし、毎週1回は、使用しているルーターのグローバルIPアドレスをMyDNS側に通知する必要があります。詳しくはMyDNSのページの説明を参照してください。)
5.2 ルーターのポートフォワーディング
スマホなどの機器からドメイン名にアクセスするとDNSによりルーターのグローバルIPアドレスに紐付けてくれます。ルーターは、Raspberry Pi へできるようポート変換のルールを設定する必要があります。
以下は、ルーターのポート変換の設定例です。

ルーターにポート80のサービス(HTTP)へのリクエストが来たら、Raspberry Pi (本稿の例では、 Raspberry Pi の(プライベート)IPアドレスは、192.168.11.25に固定設定しています。)のIPアドレスのポート80へポート変換するよう設定しています。
これにより、Raspberry Pi が接続するWiFiの外から、スマホなどのブラウザを使ってRaspberry Pi にアクセスできます。
