メインプログラム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