アクセスポイントに接続したブラウザの画面を最初に掲載する。画面上部から順に表示内容を説明する。

- WebSocket接続状態:”Ready, Connect, Closing, Closed”のいずれかが示される。
- インターバル選択ボタン:100, 300, 500, 1000msecから選択。タイムアウト動作確認用の5000msecを用意。
- データサイズ選択ボタン:1から15360byteまでの5種類から選択。
- クライアントの接続状況:センサデバイスは緑、ブラウザは黄色で表示。
- ID – WebSocketがクライアントに0から昇順で付与する番号
- IP – IPアドレスの末尾数値(ホスト部)
- Client – センサデバイス”Device”、ブラウザ”Browser”の識別(識別が確定するまでは”Unknown”)
- Status – 接続”Connected”、切断”Disconnected”の識別
- ElpsTime – 接続開始からの連続接続経過時間[時:分:秒](センサデバイスのバッテリー駆動試験で活用)
- Data – センサデバイスから受信したセンサ情報(プロトタイプとして、0から99の循環カウンター値に代替)
ブラウザからの接続に対して、関連するファイル(HTML, Javascriptなど)を送信するプログラムを以下に掲載する。
// MIMEタイプ取得
String getContentType(String filename){
if(webServer.hasArg("download")) return "application/octet-stream";
else if(filename.endsWith(".htm")) return "text/html";
else if(filename.endsWith(".html")) return "text/html";
else if(filename.endsWith(".css")) return "text/css";
else if(filename.endsWith(".js")) return "application/javascript";
else if(filename.endsWith(".png")) return "image/png";
else if(filename.endsWith(".gif")) return "image/gif";
else if(filename.endsWith(".jpg")) return "image/jpeg";
else if(filename.endsWith(".ico")) return "image/x-icon";
else if(filename.endsWith(".xml")) return "text/xml";
else if(filename.endsWith(".pdf")) return "application/x-pdf";
else if(filename.endsWith(".zip")) return "application/x-zip";
else if(filename.endsWith(".gz")) return "application/x-gzip";
return "text/plain";
}
// ファイルシステム内のファイルをクライアントへ送信
void handleFileRead(String path) {
// ファイルのMIMEタイプを取得
String contentType = getContentType(path);
// ファイル存在の確認
if (SPIFFS.exists(path)){
// ファイルオープン
File file = SPIFFS.open(path, "r");
// クライアントへの送信
webServer.streamFile(file, contentType);
Serial.print("File Stream: "); Serial.println(path);
// ファイルクローズ
file.close();
}
}
// ルート接続時のindex.html読み込み
void handleRoot() {
WiFiClient client = webServer.client();
Serial.printf("[%u] Handle from Web browser\n", client);
Serial.println("Cliant connection request");
handleFileRead("/index.html");
}
// 未登録ファイル要求に対する処理
void handleNotFound() {
String message = "File Not Found\n\n";
message += "URI: ";
message += webServer.uri();
message += "\nMethod: ";
message += (webServer.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += webServer.args();
message += "\n";
for (uint8_t i = 0; i < webServer.args(); i++) {
message += " " + webServer.argName(i) + ": " + webServer.arg(i) + "\n";
}
webServer.send(404, "text/plain", message);
}
// ファイル要求処置
void handleConfirmFile(void) {
// URI取得
String path = webServer.uri();
// ファイルシステム内のファイル読み込み
if (SPIFFS.exists(path)) handleFileRead(path);
else handleNotFound();
}
ウェブページを生成するindex.html, server.js, server.cssを順番に掲載する。プログラム上の要点はJavascriptにおける以下の2点。
- WebSocketの状態を定期的(ここでは3秒間隔)に確認し、切断していれば新規ソケットを生成して再接続を試みる。
- アクセスポイントからは、JSONフォーマットのクライアント状態が送信される。その配列からデータを取り出し、整形してHTML要素を書き換える。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv='Content-Type' content='text/javascript; text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Server</title>
<script src="js/jquery.min.js"></script>
<script src="js/server.js"></script>
<link rel="stylesheet" href="css/server.css" type="text/css">
<link rel="shortcut icon" href="icon/AP.ico" type="image/vnd.microsoft.icon">
<link rel="icon" href="icon/AP.ico" type="image/vnd.microsoft.icon">
</head>
<body onload="startMonitor()">
<!-- WebSocket状態 -->
<font size="+1">Socket Status</font>
<font size="+1"><span id="STATUS">--</span></font>
<br>
<!-- インターバル更新 -->
<font size="+1">Interval [x100ms]</font>
<input type="radio" name="INTERVAL" onchange="radioInterval(100)">1
<input type="radio" name="INTERVAL" onchange="radioInterval(300)">3
<input type="radio" name="INTERVAL" onchange="radioInterval(500)">5
<input type="radio" name="INTERVAL" onchange="radioInterval(1000)" checked>10
<input type="radio" name="INTERVAL" onchange="radioInterval(5000)">50 <font size="-2">(Timeout Test)</font>
<br>
<!-- データサイズ更新(1024byteの倍数, 例外0:1byte) -->
<font size="+1">Data Size [byte]</font>
<input type="radio" name="SIZE" onchange="radioSize(0)" checked>1
<input type="radio" name="SIZE" onchange="radioSize(1)">1024
<input type="radio" name="SIZE" onchange="radioSize(4)">4084
<input type="radio" name="SIZE" onchange="radioSize(8)">8096
<input type="radio" name="SIZE" onchange="radioSize(15)">15360
<!-- クライアント状況 -->
<pre style="font-size: 24px;" id="CLIENTS">Clients Status</pre>
<br>
</body>
</html>
// WebSocket状態定期確認(3秒間隔)
var statusTime = 3000;
// センシング要求インターバル(初期値1000msec)
var reqInterval = 1000;
// センシング要求データサイズ(1024の倍数, 例外0:1byte)
var reqSize = 0;
// クライアント最大数(main.cppで定義の"CLIENT_MAX"と整合)
var clientMAX = 14;
// ソケットクローズフラグ
var socketClose = true;
// WebSocket変数
var ws = new WebSocket('ws://' + window.location.hostname + ':81/');
// 接続状態でのアスタリスク点滅動作変数
var flush = true;
// 起動時の実行関数
function startMonitor() {
document.getElementById("STATUS").style.color = "white";
document.getElementById("STATUS").innerHTML = "Start";
WebSocketEventInit(); // WebSocketイベント初期化
setTimeout(getWSStatus, 0); // タイマー開始
}
// WebSocket状態確認
function getWSStatus() {
// 次回タイマー設定
setTimeout(getWSStatus, statusTime);
if (socketClose) {
// ws.readyState
// 0 CONNECTING ソケットは作成されているが、まだコネクションが開いていない状態
// 1 OPEN コネクションが開き、通信の準備ができている状態
// 2 CLOSING コネクションが閉じる過程にある状態
// 3 CLOSED コネクションが閉じられたか、もしくは開けていなかった状態
if (ws.readyState == 0) {
document.getElementById("STATUS").style.color = "white";
document.getElementById("STATUS").innerHTML = "Ready";
}
else if (ws.readyState == 1) {
document.getElementById("STATUS").style.color = "aqua";
document.getElementById("STATUS").innerHTML = "Connect";
// 新規生成ソケットに対するWebSocketイベント初期化
WebSocketEventInit();
// ブラウザ接続の告知(センサデバイスとの識別)
ws.send("Web"); // Web Browser
socketClose = false;
}
else if (ws.readyState == 2) {
document.getElementById("STATUS").style.color = "orange";
document.getElementById("STATUS").innerHTML = "Closing";
}
else if (ws.readyState == 3) {
document.getElementById("STATUS").style.color = "orange";
document.getElementById("STATUS").innerHTML = "Closed";
// 1つの新規ソケット生成を試行
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
}
}
}
// インターバル更新リクエスト
function radioInterval(time) {
if (time != reqInterval) {
reqInterval = time;
ws.send("I" + reqInterval); // コマンド "I"nterval
}
}
// データサイズ更新リクエスト
function radioSize(size) {
if (size != reqSize) {
reqSize = size;
ws.send("S" + reqSize); // コマンド "S"ize
}
}
// WebSocketイベント初期化
function WebSocketEventInit() {
ws.binaryType = "arraybuffer";
// 接続開始
ws.onopen = function() {
document.getElementById("STATUS").style.color = "white";
document.getElementById("STATUS").innerHTML = "Open";
};
// メッセージ取得
ws.onmessage = function(evt) {
var type = (evt.data).constructor; // String, ArrayBuffer, Blod
// ソケット接続状態(アスタリスク点滅)
document.getElementById("STATUS").style.color = "aqua";
if (flush) document.getElementById("STATUS").innerHTML = "Connect *";
else document.getElementById("STATUS").innerHTML = "Connect";
flush = !flush;
console.log("M");
// テキスト受信
if (type == String) {
// 表形式ヘッダー情報
var clients = "ID IP Client Status ElpsTime Data\n";
// クライアント状況
var status = JSON.parse(evt.data)["DAT"];
var item = 5; // 5項目(IP, Client, Status, Time, Data)
for (var n = 0; n < clientMAX; n++) {
var ip = status[n * item];
if (ip != 0) {
// 内部ID(想定100未満)
var clt = Number(n);
if (n < 10) clt = " " + clt;
// IPアドレス末尾(想定100未満)
if (ip < 10) clt += " " + Number(ip);
else clt += " " + Number(ip);
// クライアント識別
if (status[n * item + 1] == 0) clt += " " + "Unknown";
else if (status[n * item + 1] == 1) clt += " " + "Browser";
else clt += " " + "Device ";
// 接続状態
if (status[n * item + 2] == 0) clt += " " + "Disconnected";
else clt += " " + "Connected ";
// 連続接続時間(最大表示99:59:59)
var elapsed = status[n * item + 3];
if (elapsed > 360000) elapsed = 360000 - 1;
s = elapsed % 60;
m = ((elapsed - s) / 60) % 60;
h = (elapsed - s - m * 60) / 3600;
clt += " " + ('00' + h).slice(-2) + ":" + ('00' + m).slice(-2) + ":" + ('00' + s).slice(-2);
// デバイスからのデータ受信開始確認
if (status[n * item + 1] == 2) {
if (status[n * item + 2] != 2) clt += " " + "--";
else {
var data = Number(status[n * item + 4]);
if (data == 255) clt += " Error"; // エラー発生(データ255受信)
else {
if (data < 10) data = "0" + data;
clt += " " + data;
}
}
}
// ブラウザとデバイスのフォント色設定
if (status[n * item + 1] == 1) clt = "" + clt + "";
else if (status[n * item + 1] == 2) clt = "" + clt + "";
clients += clt + "\n";
}
}
document.getElementById("CLIENTS").innerHTML = clients;
}
// バイナリ受信
else if (type == ArrayBuffer) {
var size = (evt.data).byteLength; // 受信サイズ
var command = new Uint8Array((evt.data), 0, 1); // コマンド(先頭1byte)
// インターバル変更
if (size == 2 && command == 0x49) { // "I"
var interval = new Uint8Array((evt.data), 1, 1);
var elements = document.getElementsByName("INTERVAL");
for (var n = 0; n < 5; n++) {
elements[n].checked = false;
if (interval == 1) { // 100msec
elements[0].checked = true; reqInterval = 100;
}
else if (interval == 3) { // 300msec
elements[1].checked = true; reqInterval = 300;
}
else if (interval == 5) { // 500msec
elements[2].checked = true; reqInterval = 500;
}
else if (interval == 10) { // 1000msec
elements[3].checked = true; reqInterval = 1000;
}
else if (interval == 50) { // 5000msec
elements[4].checked = true; reqInterval = 5000;
}
}
}
// データサイズ変更
else if (size == 2 && command == 0x53) { // "S"
var datasize = new Uint8Array((evt.data), 1, 1);
var elements = document.getElementsByName("SIZE");
for (var n = 0; n < 5; n++) {
elements[n].checked = false;
if (datasize == 0) { // 1byte
elements[0].checked = true; reqSize = 0;
}
else if (datasize == 1) { // 1024byte
elements[1].checked = true; reqSize = 1;
}
else if (datasize == 4) { // 4096byte
elements[2].checked = true; reqSize = 4;
}
else if (datasize == 8) { // 8096byte
elements[3].checked = true; reqSize = 8;
}
else if (datasize == 15) { // 15360byte
elements[4].checked = true; reqSize = 15;
}
}
}
}
};
// 切断
ws.onclose = function(evt) {
ws.close();
document.getElementById("STATUS").style.color = "orange";
document.getElementById("STATUS").innerHTML = "Closed";
socketClose = true;
console.log("Event: onclose");
};
// エラー発生
ws.onerror = function(evt) {
document.getElementById("STATUS").style.color = "red";
document.getElementById("STATUS").innerHTML = "Error";
};
}
html { background-color: #555555; }
body {
color: #FFFFFF;
margin: auto;
margin-top: 20px;
margin-left: 20px;
}
.header {
line-height: 1.4;
}