下記にサーバー用のソースプログラムを記載する。常時接続のメインプログラムウェブページ生成と比較して欲しい。

連続駆動試験のトライアル中に、サーバーが原因不明でリブートする事象が発生した。この事象は非常に稀で、2日間の連続駆動で1回発生する程度である。サーバーをPCに接続した状態で確認したところ、シリアルモニターに”CORRUPT HEAP: Bad head at …”というメッセージが出力された。これはヒープ領域へのアクセスによるものだが、根本的な対応策は見つからなかった。

この事象が本番の駆動試験で発生すると、メモリに保管したセンサデバイスの駆動時間が失われてしまい、試験を最初からやり直さなければならない。そこで、1分間隔で、駆動時間をEEPROMに書き込み、仮にリブートしたとしても前回書き込まれた時間からカウントアップを始めることにした。1回のリブートに対して、駆動時間が最大1分少なくなるので、全体への影響は極めて小さいという判断である。

#include <Arduino.h>
#include <SPIFFS.h>
#include <WiFi.h>
#include <WiFiGeneric.h>
#include <WiFiServer.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <SSD1306.h>
#include <EEPROM.h>
// OLED関連定義
#define OLED_Address  0x3C
#define OLED_SDAPin   21
#define OLED_SCLPin   22
#define OLED_Font24   ArialMT_Plain_24
#define OLED_Font16   ArialMT_Plain_16
SSD1306 display(OLED_Address, OLED_SDAPin, OLED_SCLPin);
// EEPROM領域初期化ピン
#define EEPROMPin     19
// 接続最大クライアント数
#define CLIENT_MAX 14
// ブラウザのJSON形式データ
const char SERVER_JSON[] PROGMEM = R"=====({"DAT":[%s]})=====";
// テキスト・ペイロードサイズ(最大クライアント数と整合)
#define PayloadTXTSize 512
// アクセスポイント定義(パスワードは8文字以上)
const char *ssid = "AP-ESP32";
const char *password = "12345678";
const IPAddress ip(192, 168, 5, 1);
const IPAddress subnet(255, 255, 255, 0);
// ポート番号設定
int webPort = 80;
int socketPort = 81;
WebServer webServer(webPort);
WebSocketsServer webSocket = WebSocketsServer(socketPort);
WebSocketsServerCore webSocketCore;
// グローバル変数
int unitTime = 100;         // 100msec基本割込み
bool oneSecFlag = false;    // 1秒経過フラグ
bool oneMinFlag = false;    // 1分経過フラグ(EEPROM書き込みタイミング)
uint8_t sleepTime = 3;      // スリープ時間(秒単位)
uint8_t datasizeBase = 1;   // 基本データサイズ 1024byte x N(例外0:2byte)
bool sleepFlag = false;     // スリープ時間更新告知フラグ
bool datasizeFlag = false;  // データサイズ更新告知フラグ
uint32_t setupCounter = 0;  // ESP32起動カウンター(毎秒更新, 最大4294967295秒 = 1193時間)
uint32_t rebootCounter = 0; // リブートカウンター(エラー発生状況確認用)
// クライアント状態変数構造体
typedef struct {
  uint8_t  Address[CLIENT_MAX]; // IPアドレス末尾
  uint8_t  Connect[CLIENT_MAX]; // 切断:0, 接続:1
  uint32_t Elapsed[CLIENT_MAX]; // 連続接続時間(秒単位)
} browser;
browser browseSTAT; // ブラウザ状態
typedef struct {
  uint8_t  Address[CLIENT_MAX]; // IPアドレス末尾
  uint32_t BootSet[CLIENT_MAX]; // 初回ブート時セットアップカウンター
  uint16_t BootDat[CLIENT_MAX]; // ブート回数取得データ
  uint32_t Elapsed[CLIENT_MAX]; // 連続接続時間(秒単位)
} device;
device deviceSTAT; // センサデバイス状態
// ヒープ領域アクセスによるリブート発生 "CORRUPT HEAP: Bad head at ..."
// 対処方法不明のため、連続試験に必要な情報を1分間隔でEEPROMに保管
// EEPROM収納フォーマット
//  1st Block. リブートカウンター:4byte
//  2nd Block. セットアップカウンター:4byte
//  3rd Block. スリープ時間:4byte
//  4th Block. 基本データサイズ:4byte
//  5th Block. センサデバイス情報 接続最大デバイス数:14台
//    A. IPアドレス末尾:1byte ⇒ 拡張4byte
//    B. 初回ブート時セットアップカウンター:4byte
#define EEPROM_SIZE 16 + 8 * CLIENT_MAX // EEPROM使用サイズ(128byte = 32 x 4byte)
// 通信速度測定用バッファ
// WEBSOCKETS_MAX_DATA_SIZE (15 * 1024) ⇒ 15,360 (WebSockets.hで定義)
uint8_t buffer[WEBSOCKETS_MAX_DATA_SIZE];
// タイマー管理用の構造体ポインター
hw_timer_t *timer = NULL;
// 基本割込みサービスルーチン(100msec間隔)
void IRAM_ATTR baseInterrupt() {
  static int sec = 0, min = 0;
  // 1秒経過フラグ設定
  sec++;
  if (sec == 10) {
    oneSecFlag = true;
    sec = 0;
  }
  // 1分経過フラグ設定(EEPROM書き込みタイミング)
  min++;
  if (min == 600) {
    oneMinFlag = true;
    min = 0;
  }
}
/****************
 ディスプレイ関連
 ****************/
// OLED初期化
void displayInit() {
  display.init();                 // SSD1306初期化
  display.clear();                // 表示エリアクリア
  display.flipScreenVertically(); // 180度回転表示
  display.display();              // 表示更新
}
// 経過時間の文字列変換(最大表示 999時間59分59秒 = 3,599,999秒)
void timeConvert(uint32_t counter, char time[10]) {
  uint32_t limit = 3600000, n;
  uint16_t h;
  uint8_t m, s;
  if (counter >= limit) n = limit - 1;
  else n = counter;
  s = n % 60;
  m = ((n - s) / 60) % 60;
  h = (n - s - m * 60) / 3600;
  time[0] = h / 100 + 0x30;
  time[1] = (h / 10) % 10 + 0x30;
  time[2] = h % 10 + 0x30;
  time[3] = ':';
  time[4] = m / 10 + 0x30;
  time[5] = m % 10 + 0x30;
  time[6] = ':';
  time[7] = s / 10 + 0x30;
  time[8] = s % 10 + 0x30;
  time[9] = 0x00; // 文字列終端
}
// スリープ時間・データサイズ文字列変換
void dataConvert(uint8_t time, uint8_t size, char data[5]) {
  uint8_t n;
  if (time > 100) n = 99;
  else n = time;
  data[0] = n / 10 + 0x30;
  data[1] = n % 10 + 0x30;
  if (size > 100) n = 99;
  else n = size;
  data[2] = n / 10 + 0x30;
  data[3] = n % 10 + 0x30;
  data[4] = 0x00; // 文字列終端
}
// リブートカウンター文字列変換
void rebootConvert(uint32_t counter, char data[5]) {
  uint32_t n;
  if (counter > 9999) n = 9999;
  else n = counter;
  data[0] = n / 1000 + 0x30;
  data[1] = (n /100) % 10 + 0x30;
  data[2] = (n / 10) % 10 + 0x30;
  data[3] = n % 10 + 0x30;
  data[4] = 0x00; // 文字列終端
}
// クライアント接続状態の文字列変換
void clientConvert(char stat[6]) {
  uint8_t n = 0, w = 0;
  for (uint8_t num = 0; num < CLIENT_MAX; num++) {
    if (browseSTAT.Connect[num] == 1) {
      n++; // 接続クライアント数
      w++; // 接続ブラウザ数
    }
    if (deviceSTAT.Address[num] != 0) n++; // 接続済みセンサデバイス数
  }
  stat[0] = w / 10 + 0x30;
  stat[1] = w % 10 + 0x30;
  stat[2] = '/';
  stat[3] = n / 10 + 0x30;
  stat[4] = n % 10 + 0x30;
  stat[5] = 0x00; // 文字列終端
}
// APモード表示更新
void displayAPMode() {
  static uint8_t loop = 0;
  char time[9], data[5], stat[6];
  display.clear();                            // 表示エリアクリア
  display.setFont(OLED_Font24);               // フォント24使用
  display.drawString(  0,  0, ssid);          // SSID表示設定
  timeConvert(setupCounter, time);            // 経過時間の文字列変換
  display.drawString(  0, 20, time);          // 経過時間表示設定
  // リブートカウンター文字列変換(10回毎に表示)
  if (loop == 0) rebootConvert(rebootCounter, data);
  // スリープ時間・データサイズ文字列変換
  else dataConvert(sleepTime, datasizeBase, data);
  display.drawString( 76, 40, data);          // リブート or スリープ・サイズ表示設定
  clientConvert(stat);                        // クライアント接続状態の文字列変換
  display.drawString(  0, 40, stat);          // クライアント表示設定
  display.display();                          // 表示更新
  loop++;
  if (loop == 10) loop = 0;
}
/*************
 WebSocket関連
 *************/
// WebSocketイベント発火
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
  IPAddress ip = webSocket.remoteIP(num);
  switch(type) {
    case WStype_DISCONNECTED: {
      webSocket.disconnect(num);
      // Serial.printf("[%u] Disconnected\n", num);
    }
      break;
    case WStype_CONNECTED: {
      // Serial.printf("[%u] Connected from %d.%d.%d.%d\n", num, ip[0], ip[1], ip[2], ip[3]);
    }
      break;
    case WStype_TEXT: {
      if (payload[0] == 'W') { // "Web" ブラウザ確認
        browseSTAT.Address[num] = ip[3]; // IPアドレス末尾
        browseSTAT.Connect[num] = 1;     // 接続設定
        browseSTAT.Elapsed[num] = 0;     // 連続接続時間初期化
      }
      else if (payload[0] == 'T') { // スリープ時間変更要求
        uint32_t sleep = (uint32_t)strtol((const char *) &payload[1], NULL, 10); // 10進変換
        if (sleepTime != (uint8_t)sleep) {
          sleepTime = (uint8_t)sleep;
          sleepFlag = true; // スリープ時間変更告知要求
        }
      }
      else if (payload[0] == 'S') { // データサイズ変更要求
        uint32_t datasize = (uint32_t)strtol((const char *) &payload[1], NULL, 10); // 10進変換
        if (datasizeBase != (uint8_t)datasize) {
          datasizeBase = (uint8_t)datasize;
          datasizeFlag = true; // データサイズ変更告知要求
        }
      }
    }
      break;
    case WStype_BIN: {
      // スリープ時間間隔でのセンサデバイスからのバイナリデータ(ブート回数)受信
      if (length <= WEBSOCKETS_MAX_DATA_SIZE) {
        // 通信速度測定バッファへ保存
        for (uint16_t i = 0; i < length; i++) buffer[i] = payload[i];
        for (uint8_t n = 0; n < CLIENT_MAX; n++) {
          // 新規接続センサデバイス
          if (deviceSTAT.Address[n] == 0) {
            deviceSTAT.Address[n] = ip[3]; // IPアドレス末尾設定
            deviceSTAT.BootSet[n] = setupCounter; // セットアップカウンター保管
            deviceSTAT.BootDat[n] = buffer[1] * 256 + buffer[0]; // ブート回数
            deviceSTAT.Elapsed[n] = 0; // 連続接続時間初期化
            break;
          }
          // 接続済みセンサデバイス
          else if (deviceSTAT.Address[n] == ip[3]) { 
            // ブート回数更新
            deviceSTAT.BootDat[n] = buffer[1] * 256 + buffer[0];
            if (deviceSTAT.BootDat[n] == 0) { // ブート回数ゼロ(センサデバイス再起動)
              // 再接続センサデバイス
              deviceSTAT.BootSet[n] = setupCounter; // セットアップカウンター保管
              deviceSTAT.Elapsed[n] = 0; // 連続接続時間初期化
            }
            // 連続接続時間更新(セットアップカウンター値との差分)
            else {
              deviceSTAT.Elapsed[n] = setupCounter - deviceSTAT.BootSet[n];
            }
            break;
          }
        }
        // Serial.printf("[%u] Boot: %d\n", ip[3], buffer[1] * 256 + buffer[0]);
        // ブート回数受信報告
        buffer[0] = 'R'; // "R"eceive
        webSocket.sendBIN(num, buffer, 1);
        webSocket.disconnect(num); // センサデバイス強制切断
      }
    }
      break;
    case WStype_ERROR: Serial.printf("[%u] Socket error!\n", num);
      break;
    case WStype_FRAGMENT: Serial.printf("[%u] Fragment\n", num);      
      break;
    case WStype_FRAGMENT_FIN: Serial.printf("[%u] Fragment Fin\n", num);         
      break;    
    case WStype_FRAGMENT_TEXT_START: Serial.printf("[%u] Fragment Text Start\n", num);   
      break;
    case WStype_FRAGMENT_BIN_START: Serial.printf("[%u] Fragment Bin Start\n", num);   
      break;
    case WStype_PING: Serial.printf("[%u] Ping\n", num);
      break;
    case WStype_PONG: {
      // Serial.printf("[%u] Pong from IP %d\n", num, ip[3]); // 接続後にイベント発生
      // 最新スリープ時間・データサイズ送信
      uint8_t payloadBIN[2];
      payloadBIN[0] = 'T'; // "T"ime
      payloadBIN[1] = sleepTime;
      webSocket.sendBIN(num, payloadBIN, 2);
      payloadBIN[0] = 'S'; // "S"ize
      payloadBIN[1] = datasizeBase;
      webSocket.sendBIN(num, payloadBIN, 2);
    }
      break;
  }
}
/******************
 ウェブブラウザ関連
 ******************/
// 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();
}
/************
 APモード関連
 ************/
// APモード起動シリアルメッセージ
void serialAPMode() {
  // Serial.println("");
  Serial.print(F("SSID: ")); Serial.println(ssid);
  Serial.print(F("Password: ")); Serial.println(password);
  Serial.print(F("Address: ")); Serial.println(WiFi.softAPIP().toString());
  Serial.print(F("MAC Address: ")); Serial.println(WiFi.softAPmacAddress());
  Serial.println("HTTP server started");
  Serial.println("");
}
// APモード起動(または再稼働)
void setupAPMode() {
  int channel = 1;     // channel 1 - 13
  int ssid_hidden = 0; // 0 = boroadcast SSID, 1 = hide SSID
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(ip, ip, subnet);
  WiFi.softAP(ssid, password, channel, ssid_hidden, CLIENT_MAX);
  webServer.on("/", handleRoot); // ルート接続要求設定
  webServer.on("/index.html", handleRoot);
  webServer.onNotFound(handleConfirmFile); // ルート以外の接続要求設定
  webServer.begin(); // Webサーバー起動
  webSocket.begin(); // WebSocketサーバー起動
  webSocket.onEvent(webSocketEvent); // サーバーイベント設定
  serialAPMode(); // シリアルメッセージ
}
// スリープ時間更新告知
void updateSleepTime() {
  uint8_t payloadBIN[2];
  if (sleepFlag) {
    sleepFlag = false;
    payloadBIN[0] = 'T'; // "T"ime
    payloadBIN[1] = (uint8_t)sleepTime;
    // ブラウザへスリープ時間告知
    for (uint8_t num = 0; num < CLIENT_MAX; num++) {
      if (browseSTAT.Connect[num] != 0) webSocket.sendBIN(num, payloadBIN, 2);
    }
    // Serial.printf("Sleep Update %d min.\n", payloadBIN[1]);
  }  
}
// データサイズ変更告知
void updateDataSize() {
  uint8_t payloadBIN[2];
  if (datasizeFlag) {
    datasizeFlag = false;
    payloadBIN[0] = 'S'; // "S"ize
    payloadBIN[1] = datasizeBase;
    // ブラウザへデータサイズ告知
    for (uint8_t num = 0; num < CLIENT_MAX; num++) {
      if (browseSTAT.Connect[num] != 0) webSocket.sendBIN(num, payloadBIN, 2);
    }
    // Serial.printf("DataSize Update %d x 1024byte\n", payloadBIN[1]);
  }
}
// APモードループ
void loopAPMode() {
  uint8_t num, payloadBIN[2];
  updateSleepTime(); // スリープ時間更新告知
  updateDataSize();  // データサイズ変更告知
}
// APモード動作確認
void confirmAPMode() {
  if (!WiFi.enableAP(true)) setupAPMode(); // 再稼働
}
// ウェブページ配信
void webAPMode() {
  char payloadTXT[PayloadTXTSize], buf[PayloadTXTSize];
  uint8_t num;
  int size, pnt;
  for (pnt = 0; pnt < PayloadTXTSize; pnt++) buf[pnt] = 0x00;
  pnt = 0;
  // ブラウザ状態抽出
  for (num = 0; num < CLIENT_MAX; num++) {
    if (browseSTAT.Address[num] != 0) {
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", browseSTAT.Address[num]); // IPアドレス末尾
      pnt += size;
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", 1); // ブラウザ:1
      pnt += size;
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", browseSTAT.Connect[num]); // 切断:0, 接続:1 
      pnt += size;
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", browseSTAT.Elapsed[num]); // 連続接続時間
      pnt += size;
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", 0); // ダミーデータ
      pnt += size;
    }
  }
  // センサデバイス状態抽出
  for (num = 0; num < CLIENT_MAX; num++) {
    if (deviceSTAT.Address[num] != 0) {
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", deviceSTAT.Address[num]); // IPアドレス末尾
      pnt += size;
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", 2); // デバイス:2
      pnt += size;
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", 1); // 接続:1
      pnt += size;
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", deviceSTAT.Elapsed[num]); // 連続接続時間
      pnt += size;
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", deviceSTAT.BootDat[num]); // ブート回数
      pnt += size;
    }
  }
  if (pnt > 0) buf[pnt - 1] = 0x00; // 終端処理
  // PROGMEM文字列使用フォーマッティング
  snprintf_P(payloadTXT, sizeof(payloadTXT), SERVER_JSON, buf);
  // 各ブラウザへ配信
  for (num = 0; num < CLIENT_MAX; num++) {
    if (browseSTAT.Connect[num] == 1) {
      // ブラウザ切断時には送信結果確認に10秒程度要する
      // この期間はタイマー割込み発生せず(サーバーはロック状態)
      bool send = webSocket.sendTXT(num, payloadTXT, strlen(payloadTXT));
      if (!send) {
        browseSTAT.Address[num] = 0; // ブラウザへの配信エラー
        browseSTAT.Connect[num] = 0;
        webSocket.disconnect(num);   // ブラウザ強制切断
        Serial.printf("Browser Send Error %d\n", num);
      }
    }
  }
}
/********
 初期設定
 ********/
// クライアント状態変数初期化
void initClientStatus() {
  for (uint8_t num = 0; num < CLIENT_MAX; num++) {
    browseSTAT.Address[num] = 0; // ブラウザIPアドレス末尾
    browseSTAT.Connect[num] = 0; // 切断
    browseSTAT.Elapsed[num] = 0; // 連続接続時間
    deviceSTAT.Address[num] = 0; // センサデバイスIPアドレス末尾
    deviceSTAT.BootSet[num] = 0; // セットアップカウンター
    deviceSTAT.BootDat[num] = 0; // ブート回数
    deviceSTAT.Elapsed[num] = 0; // 連続接続時間
  }
}
// EEPROM領域初期化
void eepromInit() {
  for (uint16_t n = 0; n < EEPROM_SIZE; n += 4) EEPROM.put(n, 0);
  EEPROM.commit();
}
// EEPROM読み込み
void eepromRead() {
  uint8_t num = 0;
  uint32_t data;
  for (uint16_t n = 0; n < EEPROM_SIZE; n += 4) {
    EEPROM.get(n, data);
    if (n == 0) rebootCounter = data; // リブートカウンター読み込み
    else if (n == 4) setupCounter = data; // セットアップカウンター読み込み
    else if (n == 8) sleepTime = (uint8_t)data; // スリープ時間
    else if (n == 12) datasizeBase = (uint8_t)data; // 基本データサイズ
    else {
      if (((n / 4 ) % 2) == 0) { // 16, 24, 32, ...
        if (data == 0) break;
        // センサデバイス・IPアドレス末尾読み込み
        deviceSTAT.Address[num] = (uint8_t)data;
      }
      else { // 20, 28, 36, ...
        // センサデバイス・初回ブート時カウンター読み込み
        deviceSTAT.BootSet[num] = data;
        num++;
      }
    }
  }
  Serial.printf("Reboot %d, Setup %d, Sleep %d, Size %d, Devices %d\n",
    rebootCounter, setupCounter, sleepTime, datasizeBase, num);
}
// EEPROM書き込み
void eepromWrite() {
  uint16_t n = 0;
  EEPROM.put(n, rebootCounter); // リブートカウンター書き込み
  n += 4;
  EEPROM.put(n, setupCounter); // セットアップカウンター書き込み
  n += 4;
  EEPROM.put(n, (uint32_t)sleepTime); // スリープ時間書き込み
  n += 4;
  EEPROM.put(n, (uint32_t)datasizeBase); // 基本データサイズ
  for (uint8_t num = 0; num < CLIENT_MAX; num++) {
    if (deviceSTAT.Address[num] != 0) {
      n += 4;
      // センサデバイス・IPアドレス末尾書き込み
      EEPROM.put(n, (uint32_t)deviceSTAT.Address[num]);
      n += 4;
      // センサデバイス・初回ブート時カウンター書き込み
      EEPROM.put(n, (uint32_t)deviceSTAT.BootSet[num]);
    }
  }
  EEPROM.commit();
}
// セットアップ
void setup() {
  Serial.begin(115200);
  while(!Serial) {}   // シリアルポート準備待ち  
  Serial.println("");
  SPIFFS.begin();     // SPIFFS初期設定
  initClientStatus(); // クライアント状態変数初期化
  pinMode(EEPROMPin, INPUT_PULLUP); // EEPROM領域初期化ピン設定
  EEPROM.begin(EEPROM_SIZE);        // EEPROM使用サイズ設定
  if (digitalRead(EEPROMPin) == 0) {
    eepromInit();                   // EEPROM領域初期化
    Serial.printf("Init EEPROM\n");
  }
  else {
    eepromRead();    // EEPROM領域読み込み
    rebootCounter++; // リブートカウンター更新
  }
  setupAPMode();              // APモード起動
  displayInit();              // OLED初期化
  displayAPMode();            // OLED表示開始
  // タイマー割込み設定(ソケット通信に対して割込み優先を確認済み)
  timer = timerBegin(0, 80, true); // timer = 1usec
  timerAttachInterrupt(timer, &baseInterrupt, true);
  timerAlarmWrite(timer, unitTime * 1000, true);
  timerAlarmEnable(timer);
}
/***********
 メインループ
 ***********/
void loop() {
  webSocket.loop();         // WebSocketクライアント接続待ち
  webServer.handleClient(); // WebSocket新規クライアント対応
  loopAPMode();             // イベント発生確認
  if (oneSecFlag) {         // 1秒間隔処理
    oneSecFlag = false;
    setupCounter++;         // ESP32起動カウンター更新
    // ブラウザ連続接続時間更新
    for (uint8_t num = 0; num < CLIENT_MAX; num++) { 
      if (browseSTAT.Connect[num] == 1) ++browseSTAT.Elapsed[num];
    }
    confirmAPMode(); // APモード動作確認
    displayAPMode(); // OLED表示更新
    webAPMode();     // ウェブページ配信
  }
  if (oneMinFlag) { // 1分間隔処理
    oneMinFlag = false;
    // EEPROM書き込み(セットアップカウンター・センサデバイス接続状態更新)
    eepromWrite();
  }
}
// End of File
<!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">Sleep Time [sec]</font>&nbsp;
    <input type="radio" name="SLEEP" onchange="radioSleep(3)" checked>3
    <input type="radio" name="SLEEP" onchange="radioSleep(5)">5
    <input type="radio" name="SLEEP" onchange="radioSleep(10)">10
    <input type="radio" name="SLEEP" onchange="radioSleep(30)">30
    <input type="radio" name="SLEEP" onchange="radioSleep(60)">60
    <br>
    <!-- データサイズ更新(1024byteの倍数, 例外0 ⇒ 2byte) -->
    <font size="+1">Data Size [byte]</font>&nbsp;&nbsp;
    <input type="radio" name="SIZE" onchange="radioSize(0)">2
    <input type="radio" name="SIZE" onchange="radioSize(1)" checked>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;
// センシング要求スリープ時間(秒単位)
var reqSleep = 3;
// センシング要求データサイズ(1024の倍数, 例外0:2byte)
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.close(); // すでにソケット切断状態のため不要
            ws = new WebSocket('ws://' + window.location.hostname + ':81/');
        }
    }
}
// スリープ時間更新リクエスト
function radioSleep(time) {
    if (time != reqSleep) {
        reqSleep = time;
        ws.send("T" + reqSleep); // コマンド "T"ime
    }
}
// データサイズ更新リクエスト
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;
        // テキスト受信
        if (type == String) {
            // var size = (evt.data).length; // 受信サイズ(使用せず)
            // 表形式ヘッダー情報
            var clients = "ID   IP   Client    Status         ElapsTime   Boots\n";
            // クライアント状況
            var status = JSON.parse(evt.data)["DAT"];
            var item = 5; // 5項目(IP, Client, Status, ElapsedTime, Boots)
            var clientNUM = status.length / item;
            for (var n = 0; n < clientNUM; n++) {
                var ip = status[n * item];
                // 昇順番号
                var clt = Number(n);
                if (n < 10) clt = " " + clt;
                // IPアドレス末尾
                if (ip < 10) clt += "    " + Number(ip);
                else clt += "   " + Number(ip);
                // クライアント識別
                if (status[n * item + 1] == 1) clt += "   " + "Browser";
                else if (status[n * item + 1] == 2) clt += "   " + "Device ";
                else clt += "   " + "Unknown";
                // 接続状態
                if (status[n * item + 2] == 1) clt += "   " + "Connected   ";
                else clt += "   " + "Disconnected";
                // 連続接続時間(最大表示999:59:59)
                var elapsed = status[n * item + 3];
                if (elapsed > 3600000) elapsed = 3600000 - 1;
                s = elapsed % 60;
                m = ((elapsed - s) / 60) % 60;
                h = (elapsed - s - m * 60) / 3600;
                clt += "   " + ('000' + h).slice(-3) + ":" + ('00' + m).slice(-2) + ":" + ('00' + s).slice(-2); 
                // デバイスからの受信データ
                if (status[n * item + 1] == 2) {
                    // ブート回数(想定100000未満)
                    var boot = Number(status[n * item + 4]);
                    clt += "   " + ('00000' +boot).slice(-5);
                }
                // ブラウザとデバイスのフォント色設定
                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 == 0x54) { // "T"
                var sleep = new Uint8Array((evt.data), 1, 1);
                var elements = document.getElementsByName("SLEEP");
                for (var n = 0; n < 5; n++) {
                    elements[n].checked = false; 
                    if (sleep ==  3) { // 3sec
                        elements[0].checked = true; reqSleep = 3; }
                    else if (sleep ==  5) { // 5sec
                        elements[1].checked = true; reqSleep = 5; }
                    else if (sleep ==  10) { // 10sec
                        elements[2].checked = true; reqSleep = 10; }
                    else if (sleep == 30) { // 30sec
                        elements[3].checked = true; reqSleep = 30; }
                    else if (sleep == 60) { // 60sec
                        elements[4].checked = true; reqSleep = 60; }
                }
            }
            // データサイズ変更
            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) { // 例外2byte
                        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";
    };
}
// End of File
html { background-color: #555555; }

body {
    color: #FFFFFF;
    margin: auto;
    margin-top: 20px;
    margin-left: 20px;
}

.header {
    line-height: 1.4;
}

動作例を以下に示す。サーバーにブラウザが接続。IPアドレス21のセンサデバイスが接続し、ブート回数を送信。サーバーからの受信確認を得て3秒スリープに突入。これを繰り返す。次に、IPアドレス22のセンサデバイスが接続。途中で21のデバイスを手動でリセット。ブート回数0から再接続する。

コメントを残す

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