1.コード上のHTMLの記述
Webアプリケーションでは、クライアントのWebブラウザーに表示したり入力させるための仕組みをHTMLで記述します。前章で述べたように、表示スタイルを指示するCSSやプログラム言語JavaScriptも使用します。凝った画面や複雑な処理をする場合は、CSSとJavaScriptをそれぞれ独立したファイルに記述しますが、これからの実験はいずれも比較的シンプルなので、HTMLの中に直接書くことにします。
今回の計測情報表示画面をどのようなデザインにするか、まずそのあたりから着手してみましょう。簡単な方法として、ドキュメントルート配下に、テキストエディターなどでラフな画面デザインを書いたHTMLファイルを作成します。これをWebブラウザーからアクセスして表示させ、ボタンや一覧表示などのパーツの配置を調整しながら画面を仕上げます。
HTMLファイルの保管場所は開発環境によって異なりますが、OSがLinuxでApacheを使用している場合は通常「/var/www/html」が、Windowsでxampp環境なら「/xampp/htdocs」がドキュメントルートになっています。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html { font-family: Helvetica; display: inline-block; margin: 0px auto;text-align: center;}
h1 {font-size:28px;}
body {text-align: center;}
table { border-collapse: collapse; margin-left:auto; margin-right:auto; }
th { padding: 12px; background-color: #0000cd; color: white; border: solid 2px #c0c0c0; }
tr { border: solid 2px #c0c0c0; padding: 12px; }
td { border: solid 2px #c0c0c0; padding: 12px; }
.value { color:blue; font-weight: bold; padding: 1px;}
</style>
</head>
<body>
<h1>Measured Value</h1>
<p style="color:brown; font-weight: bold">計測値の更新は[再読み込み]をクリック!</p>
<table><tr><th>ELEMENT</th><th>VALUE</th></tr>
<tr><td>Temperature</td><td><span class='value'>99.99 ℃</span></td></tr>
<tr><td>Humidity</td><td><span class='value'>88.88 %</span></td></tr>
</table>
</body>
</html>
Webブラウザーで上記のHTMLファイル(ファイル名がtest1.htmlとして)を立ち上げるには、アドレスバーから「http://local/test1.html」を入力すると次のような画面が表示されます。事前にこのようにしてデザインを確認・修正することで、表示に必要なHTMLを手早く完成させることができます。

ここで問題になるのは、ArduinoのC++風のスケッチとは異質な(文体・様式が異なる)HTMLやCSSなどのコードを、どこにどのように記述するかということです。
表示機能に限定すれば、接続が確立されたクライアントに対してHTMLのコードを次々に送出すればいいのですが、これをだらだらと記述するとたいへん複雑になってしまいます。HTMLは改行なしに「べた書き」しても動作しますが、そうするときわめて見にくくなりメンテナンス性が損なわれます。
C++には、エスケープ文字('\r', '\n', '\t'など)などを使わずに文字定数そのままを定義できる、「生の文字リテラル」という定義方法があります。「R"delimiter(」で始まり、「)delimiter";」で終える構文で、カッコ内にありのままの文字列を記述することができます。なお、ここでdelimiterには16文字以内の任意の文字列を指定します。
具体的な形式は次のとおりです。
const String データ名 = R"rawliteral( ~~ HTML文 ~~ )rawliteral"; |
次に、HTMLのページ・ヘッダーの定義を例示します。
const String strHtmlHeader = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html { font-family: Helvetica; display: inline-block; margin: 0px auto;text-align: center;}
h1 {font-size:28px;}
body {text-align: center;}
table { border-collapse: collapse; margin-left:auto; margin-right:auto; }
th { padding: 12px; background-color: #0000cd; color: white; border: solid 2px #c0c0c0; }
tr { border: solid 2px #c0c0c0; padding: 12px; }
td { border: solid 2px #c0c0c0; padding: 12px; }
.value { color:blue; font-weight: bold; padding: 1px;}
</style>
</head>
)rawliteral";
HTMLのページタイトルや測定値、表示オブジェクトなど、実行時に表示内容を書き換えたい部分は、両側を%マークで挟んだプレースホルダーとして設定しておきます。このデータ全体をテンプレートに見立て、実行時には別の作業領域に全体を複写したのち、プレースホルダーを実際の名前や数値でリプレースします。
次にHTMLのボディ部を例示します。
const String strHtmlBody = R"rawliteral(
<body>
<h1>%PAGE_TITLE%</h1>
<p style="color:brown; font-weight: bold">計測値の更新は[再読み込み]をクリック!</p>
<table><tr><th>ELEMENT</th><th>VALUE</th></tr>
<tr><td>Temperature</td><td><span class="value">%TEMPERATURE%</span></td></tr>
<tr><td>Humidity</td><td><span class="value">%HUMIDITY%</span></td></tr>
</table>
</body></html>
)rawliteral";
2.HTTPリスン処理の定型化
Webサーバーは、Wi-Fiルーターに接続した後はひたすらクライアントからのリクエストを待ちます。このHTTPポートをリスンしている状態でクライアントから着信があると、一連の電文を受信して必要な処理を行ってクライアントとのコネクションを解放します。
これらの処理は少々込み入っていますが、Arduino開発環境のWiFiライブラリーのexamplesフォルダー内にあるSimpleWiFiServer.inoというサンプルスケッチを参考にして、HTTPリスン処理を専用メソッドにまとめてみました。
メソッド名はhttpListen()です。実際のコードは後で見ることにして、まずフローチャートで動作を把握しておきましょう。

このhttpListenメソッドは、反復処理void loop()内で呼ばれます。クライアントからの接続を取得して、接続が発生するまで待ちます。メソッド先頭で確保している受信領域strBufferには、最終的に受信した文のすべてが収納されます。
接続が発生すると1行文の受信文を格納するラインバッファcurrentLineを確保して、クライアントとの接続を確認します。接続が失われていればclient.stop()で接続を閉じます。
データがあれば1バイト読み込んで受信領域につなぎ、受信文を形成します。読んだ文字が'\n'でラインバッファが空なら、クライアントからのリクエストが終了したので、strBufferの受信データをもとに「HTTPリクエスト処理:httpRequestProccess(String*)」で受信データの解析と処理を行います。続いて、「HTTPレスポンス処理:httpSendResponse(WiFiClient*)」を実行してクライアントとのコネクションを閉じます。
すべての処理をvoid loop()内に記述すると大変複雑になり、可読性が悪化しメンテナンスがしにくくなります。これに対して「HTTPリクエスト処理」や「HTTPレスポンス処理」を独立させると、コードは格段に見やすくなりますし、流用できる部分は関数ごとコピーすることもできます。
ついでながら、今回の処理ではクライアントからのデータを受信して処理することはないので、「HTTPリクエスト処理」は存在しません。
図の破線内は、受信シーケンスに沿ってラインバッファへの格納とリクエスト終了を検出するための処理です。'\r'以外の文字はラインバッファに繋いでいます。'\n'を受信するたびにラインバッファをクリアしているので、クライアントから空白行である"\r\n"が届くことでリクエストの終端を検出しています。
3.HTTPレスポンス処理
受信が完了すると、クライアントにHTTPレスポンスを送信する必要があります。この処理は、「HTTPレスポンス処理」httpSendResponse(WiFiClient* client)で行います。
まず、文字列定数として定義したメッセージヘッダー「HTTP/1.1 200 OK\r\nContent-Type:text/html\r\n」を送信し、続いてメッセージボディーを送信します。
次にページヘッダーを送信し、ボディー部のプレースホルダーの内容をリプレース編集して送信。そして最後に終端の空行を送信します。
4.コードの解説
①ヘッダーファイルとデータ等の定義
・14行目 Wi-Fiライブラリー用。
・29~30行 環境に合った情報を設定してください!
・34行目 WiFiServerオブジェクトを、HTTPポートアドレスを指定してインスタンス化します。
・44~45行 HTTPレスポンスヘッダーとページヘッダーを文字リテラルで定義しています。
・46行目 47行目以降に、先に掲げたHTMLのページヘッダーとボディー部を記述します。
#include <Arduino.h>
#include <WiFi.h>
#include <DHT.h> // DHTセンサー用
/* Function Prototype */
void doInitialize();
void doMeasurement();
void httpListen();
void httpSendRespons(WiFiClient*);
void connectToWifi();
/* 基本属性定義 */
#define SPI_SPEED 115200 // SPI通信速度
#define DHTTYPE DHT11 // DHTセンサーの型式
// ルーター接続情報
#define WIFI_SSID "your_ssid"
#define WIFI_PASSWORD "your_password"
// Webサーバーオブジェクト
#define HTTP_PORT 80
WiFiServer server(HTTP_PORT);
String strTemperature; // 計測結果
String strHumidity;
/* DHTセンサー*/
const int DHTPin = 14; // DHTセンサーの接続ピン
DHT dht(DHTPin, DHTTYPE); // DHTクラスの生成
/* HTMLレスポンスヘッダーとページヘッダー */
const String strResponseHeader = "HTTP/1.1 200 OK\r\nContent-Type:text/html\r\n"
"Connection:close\r\n\r\n";
/* HTMLページ */
const String strHtmlHeader = R"rawliteral(
②定型のsetup()とloop()
・setup()では初期化処理とルーター接続の関数を実行します。
・loop()では「HTTPリスン処理」を実行し続けます。
void setup() {
doInitialize(); // 初期化処理をして
connectToWifi(); // Wi-Fiルーターに接続する
}
void loop() {
httpListen(); // HTTPをリスンする
}
③初期化処理
・シリアルポートを設定し、DHTセンサーを起動します。
void doInitialize() {
Serial.begin(SPI_SPEED);
dht.begin(); // DHTセンサーを起動
}
④計測処理
・DHTセンサーから温度と湿度を取得して表示文字列を作成し、シリアルモニターに表示します。
void doMeasurement() {
// 温度と湿度を読み取る
float t = dht.readTemperature();
float h = dht.readHumidity();
// 読み取りに失敗したかどうかを確認し、失敗なら再試行させる
if (isnan(h) || isnan(t)) {
Serial.println("Failed to read DHT!");
return;
}
//温度と湿度の測定値を文字列に変換して
char temperatureTemp[7];
dtostrf(t, 6, 2, temperatureTemp);
char humidityTemp[7];
dtostrf(h, 6, 2, humidityTemp);
// 表示文字列を形成する
strTemperature = String(temperatureTemp) + " ℃";
strHumidity = String(humidityTemp) + " %";
Serial.print("Temperature: "); Serial.print(strTemperature);
Serial.print(", Humidity: "); Serial.println(strHumidity);
}
⑤HTTPリスン処理
・先にフローチャートで示した「HTTPリスン処理の専用メソッド」のコードです。
・134行目 クライアントからの受信処理は不要なので、すぐにレスポンス処理を行います。
void httpListen() {
String strBuffer = "";
WiFiClient client = server.available();
if (client) { // クライアントから着信があれば
Serial.println("New Client.");
String currentLine = "";
while (client.connected()) { // 接続中に以下を繰り返す
if (client.available()) { // 着信データがあれば
char c = client.read(); // 1バイト読み込んで
strBuffer += c; // 受信文を形成する
if (c == '\n') {
// 改行文字で受信領域が空なら、レスポンス
// を送信してループを脱出する
if (currentLine.length() == 0) {
httpSendResponse(&client);
break;
} else { // それ以外で改行なら受信領域をクリア
currentLine = "";
}
} else if (c != '\r') { // 改行以外なら受信領域に結合する
currentLine += c;
}
}
}
// コネクションを閉じる
client.stop();
Serial.println("Client disconnected.\n");
}
}
⑥HTTPレスポンス処理
・HTTPレスポンスヘッダー、ページヘッダー、ボディー部を順番にクライアントへ送信します。
・162行目 編集のために、生の文字リテラルで定義したボディー部全体を作業エリアに取り込みます。
・163~165行 Stringのreplace関数を使ってプレースホルダー部を置換編集します。
void httpSendResponse(WiFiClient* client) {
// HTTPレスポンスヘッダーを送信
client->println(strResponseHeader);
// 計測を実行して
doMeasurement();
// ページヘッダーを送信する
client->println(strHtmlHeader);
// ページボディー部を編集して送信する
String buf = strHtmlBody;
buf.replace("%PAGE_TITLE%", "Measured Value");
buf.replace("%TEMPERATURE%", strTemperature);
buf.replace("%HUMIDITY%", strHumidity);
client->println(buf);
client->println(); // 最後に、HTTP終端の空行を送信
}
⑦コネクション確立用
・174行目 所定のSSIDとパスワードでWi-Fiのアクセスポイントに接続します。
・175行目 接続完了のWL_CONNECTEDが返却されるまで待ちます。
・183行目 割り当てられたIPアドレスをシリアルモニターに表示します。
/* Wi-Fiルーターに接続する */
void connectToWifi() {
Serial.print("Connecting to Wi-Fi ");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
// モニターにローカル IPアドレスを表示する
Serial.println("WiFi connected.");
Serial.print(" *IP address: ");
Serial.println(WiFi.localIP());
server.begin();
}
5.動作検証
シリアルモニターを立ち上げて実行を開始します。Wi-Fi接続が完了すると「割り当てられたIPアドレス」が表示されます(ここでは192.168.0.32)。
パソコンまたはスマートフォンのWebブラウザーを起動して、アドレスバーに「割り当てられたIPアドレス」を入力すると下図(スマートフォンの場合は本章表題部の写真)のように計測値が表示されます。シリアルモニターには新たなクライアントとの接続と計測値の送信内容が表示され、直ちにクライアントとの接続が切断されます。
このようにブラウザからの接続によって、一回だけ計測値を表示して動作を完結します。表示を更新したい場合は、ブラウザーの[更新]ボタンをクリックするかスワイプすることで再度リクエストを送信します。


スケッチ全体は、ページ先頭の[Download]ボタンでダウンロードしてください。
これでシンプルな計測値表示が可能になりました。しかし[更新]ボタンを押すことなく、一定間隔で計測した結果をその都度ブラウザー画面に反映させるようにするには、まったく別のアプローチが必要になります。
次回ではそのテーマに取り組みます。お楽しみに!