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

  • 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点。

  1. WebSocketの状態を定期的(ここでは3秒間隔)に確認し、切断していれば新規ソケットを生成して再接続を試みる。
  2. アクセスポイントからは、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>&nbsp;
    <font size="+1"><span id="STATUS">--</span></font>
    <br>
    <!-- インターバル更新 -->
    <font size="+1">Interval [x100ms]</font>&nbsp;
    <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>&nbsp;&nbsp;&nbsp;&nbsp;
    <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
    <!-- クライアント状況 -->
    &nbsp;
    <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;
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です