メインプログラムmain.cppに関して。これまでは機能別に分割して説明してきた。ここでは、一連の処理の流れを理解しやすいように、ソースプログラム全体(500行余り)を記載する。

/********************************************
 センサネットワーク・サーバー(アクセスポイント)
 ********************************************/
#include <Arduino.h>
#include <SPIFFS.h>
#include <WiFi.h>
#include <WiFiGeneric.h>
#include <WiFiServer.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <SSD1306.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);
// 動作状態出力ピン
#define STATPin       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);

// グローバル変数
int unitTime = 100;             // 100msec基本割込み
bool oneSecFlag = false;        // 1秒経過フラグ
int intervalBase = 10;          // 基本インターバル 100msec x 10 = 1000msec
uint8_t datasizeBase = 0;       // 基本データサイズ 1024byte x N(例外0:1byte)
bool webFlag = false;           // ウェブブラウザ定期配信フラグ
bool intervalFlag = false;      // インターバル更新告知フラグ
bool datasizeFlag = false;      // データサイズ更新告知フラグ
int timeoutMAX = 3;             // デバイス非応答タイムアウト3秒
unsigned long setupCounter = 0; // ESP32起動カウンター(毎秒更新)

// クライアント状態変数構造体
typedef struct {
  uint8_t  Address[CLIENT_MAX]; // IPアドレス末尾
  uint8_t  Station[CLIENT_MAX]; // 未確認:0, ブラウザ:1, デバイス:2 識別
  uint8_t  Connect[CLIENT_MAX]; // 切断:0, 接続:1, データ受信開始:2 識別
  uint32_t Elapsed[CLIENT_MAX]; // 連続接続時間(秒単位)
  uint8_t  GetData[CLIENT_MAX]; // 受信先頭1byteデータ
  uint8_t  TimeOut[CLIENT_MAX]; // 非応答タイムアウトカウンター
} client;
client clientSTAT;

// 通信速度測定用バッファ
// 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 web = 0, sec = 0;
  // ウェブブラウザ定期配信フラグ設定(インターバル or 起動経過時間1秒間隔更新)
  web++;
  if (web >= intervalBase || web == 10) {
    webFlag = true;
    web = 0;
  }
  // 1秒経過フラグ設定
  sec++;
  if (sec == 10) {
    oneSecFlag = true;
    sec = 0;
  }
}

// タイムアウト・センサデバイス切断
bool timeoutDisconnect() {
  bool timeout = false;
  for (uint8_t num = 0; num < CLIENT_MAX; num++) {
    // 接続センサデバイス確認(ウェブブラウザ除く)
    if (clientSTAT.Connect[num] != 0 && clientSTAT.Station[num] == 2) {
      clientSTAT.TimeOut[num]++;
      // 接続センサデバイスの応答確認
      if (clientSTAT.TimeOut[num] > timeoutMAX) {
        clientSTAT.Connect[num] = 0;
        IPAddress ip = webSocket.remoteIP(num);
        Serial.printf("[%u] Timeout Disconnected %d.%d.%d.%d\n", num, ip[0], ip[1], ip[2], ip[3]);    
        webSocket.disconnect(num); // 強制切断
        timeout = true;
      }
    }
  }
  return timeout;
}

/****************
 ディスプレイ関連
 ****************/
// OLED初期化
void displayInit() {
  display.init();                 // SSD1306初期化
  display.clear();                // 表示エリアクリア
  display.flipScreenVertically(); // 180度回転表示
  display.display();              // 表示更新
}

// 経過時間の文字列変換(最大表示 99時間59分59秒 = 359,999秒)
void timeConvert(unsigned long counter, char time[9]) {
  unsigned int limit = 360000, 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 / 10 + 0x30;
  time[1] = h % 10 + 0x30;
  time[2] = ':';
  time[3] = m / 10 + 0x30;
  time[4] = m % 10 + 0x30;
  time[5] = ':';
  time[6] = s / 10 + 0x30;
  time[7] = s % 10 + 0x30;
  time[8] = 0x00; // 経過時間文字列終端
}

// インターバル文字列変換(最大表示99)
void intervalConvert(uint16_t interval, char base[3]) {
  uint8_t n;
  if (interval > 100) n = 99;
  else n = interval;
  base[0] = n / 10 + 0x30;
  base[1] = n % 10 + 0x30;
  base[2] = 0x00; // インターバル文字列終端

}

// データサイズ文字列変換(1024の倍数, 例外0:1byte)
void datasizeConvert(uint8_t data, char size[3]) {
  uint8_t n;
  if (data > 100) n = 99;
  else n = data;
  size[0] = n / 10 + 0x30;
  size[1] = n % 10 + 0x30; 
  size[2] = 0x00; // データサイズ文字列終端
}

// クライアント接続状態の文字列変換
void clientConvert(client c, char stat[6]) {
  uint8_t n = 0, w = 0;
  for (uint8_t num = 0; num < CLIENT_MAX; num++) {
    if (c.Connect[num]) {
      n++; // 接続クライアント数
      if (c.Station[num] == 1) w++; // 接続ウェブブラウザ数
    }
  }
  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() {
  char time[9], base[3], size[3], stat[6];
  display.clear();                     // 表示エリアクリア
  display.setFont(OLED_Font24);        // フォント24使用
  display.drawString(  0,  0, ssid);   // SSID表示設定
  timeConvert(setupCounter, time);     // 経過時間の文字列変換
  display.drawString(  0, 20, time);   // 経過時間表示設定
  intervalConvert(intervalBase, base); // インターバル文字列変換
  display.drawString(101, 20, base);   // インターバル表示設定
  datasizeConvert(datasizeBase, size); // データサイズ文字列変換
  display.drawString(101, 40, size);   // データサイズ表示設定
  clientConvert(clientSTAT, stat);     // クライアント接続状態の文字列変換
  display.drawString(  0, 40, stat);   // クライアント表示設定
  display.display();                   // 表示更新
}

/*************
 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: {
      clientSTAT.Connect[num] = 0;
      Serial.printf("[%u] Disconnected\n", num);
    }
      break;
    case WStype_CONNECTED: {
      clientSTAT.Address[num] = ip[3]; // IPアドレス末尾
      clientSTAT.Station[num] = 0;     // ブラウザ or デバイス未確認
      clientSTAT.Connect[num] = 1;     // 接続
      clientSTAT.Elapsed[num] = 0;     // 連続接続時間クリア
      clientSTAT.TimeOut[num] = 0;     // タイムアウトカウンター初期化
      Serial.printf("[%u] Connected from %d.%d.%d.%d\n", num, ip[0], ip[1], ip[2], ip[3]);
    }
      break;
    case WStype_TEXT: {
      Serial.printf("[%u] get Text: %s\n", num, payload);
      if (payload[0] == 'W') clientSTAT.Station[num] = 1; // "Web" ウェブブラウザ設定
      else if (payload[0] == 'D') clientSTAT.Station[num] = 2; // "Dev" デバイス設定
      else if (payload[0] == 'I') { // インターバル変更要求(msec単位 ⇒ 100msec倍数に変換)
        uint32_t interval = (uint32_t)strtol((const char *) &payload[1], NULL, 10); // 10進変換
        interval = interval / 100;
        // インターバル100msec以上の変更
        if (interval >= 1 && intervalBase != (int)interval) {
          intervalBase = (int)interval;
          intervalFlag = 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) {
        // 通信速度測定バッファへ保存
        digitalWrite(STATPin, HIGH);
        for (uint16_t i = 0; i < length; i++) buffer[i] = payload[i];
        digitalWrite(STATPin, LOW);
        clientSTAT.Station[num] = 2; // デバイス接続
        clientSTAT.Connect[num] = 2; // データ受信
        if (length != 1 && buffer[length - 1] != 0xFF)  buffer[0] = 255; // エラー発生(データ255送信)
        clientSTAT.GetData[num] = buffer[0]; // 先頭1byte保管
        clientSTAT.TimeOut[num] = 0; // タイムアウトカウンター初期化
      }
    }
      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\n", num); // 接続後にイベント発生
      intervalFlag = true; // 最新インターバル告知要求
      datasizeFlag = true; // 最新データサイズ告知要求
    }
      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(); // シリアルメッセージ
}

// APモードループ
void loopAPMode() {
  uint8_t payloadBIN[2];
  // インターバル更新告知
  if (intervalFlag) {
    intervalFlag = false;
    payloadBIN[0] = 'I'; // "I"nterval
    payloadBIN[1] = (uint8_t)intervalBase;
    // 全接続クライアントへインターバル告知
    for (uint8_t num = 0; num < CLIENT_MAX; num++) {
      if (clientSTAT.Connect[num] != 0) webSocket.sendBIN(num, payloadBIN, 2);
    }
    Serial.printf("Interval Broadcast %d x 100msec\n", payloadBIN[1]);
  }
  // データサイズ更新告知
  if (datasizeFlag) {
    datasizeFlag = false;
    payloadBIN[0] = 'S'; // "S"ize
    payloadBIN[1] = datasizeBase;
    // 全接続クライアントへデータサイズ告知
    for (uint8_t num = 0; num < CLIENT_MAX; num++) {
      if (clientSTAT.Connect[num] != 0) webSocket.sendBIN(num, payloadBIN, 2);
    }
    if (datasizeBase == 0) Serial.printf("DataSize Broadcast 1byte\n");
    else Serial.printf("DataSize Broadcast %d x 1024byte\n", payloadBIN[1]);
  }
}

// APモード動作確認
void confirmAPMode() {
  if (!WiFi.enableAP(true)) setupAPMode(); // 再稼働
}

// ウェブブラウザ配信
void webAPMode() {
  char payloadTXT[PayloadTXTSize], buf[PayloadTXTSize];
  uint8_t num;
  int size, pnt = 0;
  // 各クライアントの状態抽出
  for (num = 0; num < CLIENT_MAX; num++) {
    size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", clientSTAT.Address[num]); // IPアドレス末尾
    pnt += size;
    size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", clientSTAT.Station[num]); // 未確認:0, ブラウザ:1, デバイス:2 
    pnt += size;
    size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", clientSTAT.Connect[num]); // 切断:0, 接続:1, データ受信:2
    pnt += size;
    size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", clientSTAT.Elapsed[num]); // 連続接続時間
    pnt += size;
    if (num < CLIENT_MAX - 1) {
      size = snprintf_P(&buf[pnt], sizeof(buf), "%d,", clientSTAT.GetData[num]); // 受信先頭1byteデータ
      pnt += size;
    }
    else {
      snprintf_P(&buf[pnt], sizeof(buf), "%d", clientSTAT.GetData[num]); // 最終データ
    }
  }
  // PROGMEM文字列使用フォーマッティング
  snprintf_P(payloadTXT, sizeof(payloadTXT), SERVER_JSON, buf);
  // 各ウェブブラウザへ配信
  for (num = 0; num < CLIENT_MAX; num++) {
    if (clientSTAT.Station[num] == 1 && clientSTAT.Connect[num] == 1) {
      bool send = webSocket.sendTXT(num, payloadTXT, strlen(payloadTXT));
      if (!send) {
        clientSTAT.Connect[num] = 0; // ウェブブラウザへの配信エラー(強制切断)
        Serial.printf("Web Send Error %d\n", num);
      }
    }
  }
}

/********
 初期設定
 ********/
// クライアント状態変数初期化
void initClientStatus() {
  for (uint8_t num = 0; num < CLIENT_MAX; num++) {
    clientSTAT.Address[num] = 0; // IPアドレス末尾
    clientSTAT.Station[num] = 0; // 未確認, ブラウザ, デバイス 識別
    clientSTAT.Connect[num] = 0; // 切断, 接続, データ受信 識別
    clientSTAT.Elapsed[num] = 0; // 連続接続時間
    clientSTAT.GetData[num] = 0; // 受信1byteデータ
    clientSTAT.TimeOut[num] = 0; // 非応答タイムアウトカウンター
  }
}

// セットアップ
void setup() {
  Serial.begin(115200);
  while(!Serial) {}           // シリアルポート準備待ち  
  Serial.println("");
  pinMode (STATPin, OUTPUT);  // 動作状態出力ピン設定
  digitalWrite(STATPin, LOW);
  SPIFFS.begin();             // SPIFFS初期設定
  initClientStatus();         // クライアント状態変数初期化
  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() {
  bool timeout = false;
  webSocket.loop();                // WebSocketクライアント接続待ち
  webServer.handleClient();        // WebSocket新規クライアント対応
  loopAPMode();                    // イベント発生確認
  if (webFlag) {                   // ウェブブラウザ定期配信処理
    webFlag = false;
    webAPMode();                   // ウェブブラウザ配信
  }
  if (oneSecFlag) {                // 1秒間隔処理
    oneSecFlag = false;
    setupCounter++;                // ESP32起動カウンター更新
    for (uint8_t num = 0; num < CLIENT_MAX; num++) { 
      // クライアント連続接続時間更新
      if (clientSTAT.Connect[num] != 0) ++clientSTAT.Elapsed[num];
    }
    confirmAPMode();               // APモード動作確認
    timeout = timeoutDisconnect(); // タイムアウト切断処理
    if (timeout) webAPMode();      // タイムアウト即時配信
    displayAPMode();               // 表示更新
  }
}

// End of File

コメントを残す

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