再接続処理の必要性
Wifi が切断される頻度に比べ、WebSocket の切断の頻度は高いです。ESP32 のプログラムが提供されているライブラリを使用しているのもあって切断原因の追究は難しいです。ただ原因がどうであれ、通信切断は必ず起こりえるので再接続処理を検討しました
通信が切断されてしまうとサーバ側から ESP32 への再接続のアクションは恐らく無理と考え、ESP32 側で通信切断を判断して再接続するように考えました
PC側での切断検出と再接続処理
PC 側での切断判断は『onClose』イベントで捕捉でき、タイムアウト処理で『initWebSocket』関数をコールし再接続を行います
window.addEventListener( 'load', onLoad );
/*************************/
/* ロード完了イベント */
/*************************/
function onLoad()
{
initWebSocket(); // WebSocket初期化
}
/*************************/
/* WebSocket初期化 */
/*************************/
function initWebSocket()
{
gWebsocket = new WebSocket( 'ws:' + gGateway + ':8080' );
gWebsocket.onopen = onOpen;
gWebsocket.onclose = onClose;
gWebsocket.onerror = onError;
gWebsocket.onmessage = onMessage;
}
/*************************/
/* WebSocketオープン */
/*************************/
function onOpen( event )
{
}
/*************************/
/* WebSocketクローズ */
/*************************/
function onClose( event )
{
setTimeout( initWebSocket, 2000 ); // 2秒間隔で再接続を試行
}
/*************************/
/* WebSocketエラー */
/*************************/
function onError( event )
{
}
/*************************/
/* WebSocket受信 */
/*************************/
function onMessage( event )
{
rcvMessage( event.data );
}
ESP32側での切断検出処理
ESP32 側も同様に、WebSocket のコールバック関数を『setup』関数内でセットしていますので、コールバック関数の切断イベント『ConnectionClosed』で捕捉できます。ただ切断イベントがかなり遅かったりしたので、今回はこのイベントではなく『Ping』『Pong』での切断検出を行ってみました
/*************************/
/* Websockets接続 */
/*************************/
gClient.onMessage( onMessagesCallback ); // メッセージ受信イベントセット
gClient.onEvent( onEventsCallback ); // 受信イベントセット
if( gClient.connect( gServerHost, gServerPort, "/" )) // 接続チェック
{
Serial.println( "Connected!" );
}
else
{
Serial.println( "Not Connected!" ); // 未接続
}
void onEventsCallback( WebsocketsEvent Event, String Data )
{
switch( Event )
{
case WebsocketsEvent::ConnectionOpened:
Serial.println( "Event is opened" );
break;
case WebsocketsEvent::ConnectionClosed:
Serial.println( "Event is closed" );
break;
case WebsocketsEvent::GotPing:
break;
case WebsocketsEvent::GotPong:
break;
}
}
ESP32側でのPing・Pongによる切断検出処理
ESP32 は通信が起動したら一定間隔で『Ping』を送信します。この時 Ping 送信回数のカウンタ『Count++』をカウントアップします
PC 側のサーバでは『Ping』を受信したら『Pong』を返信します。ESP32 が『Pong』を受信したら Ping 送信回数のカウンタ『Count–』をカウントダウンします
今回の設計では『Ping』送信間隔を2秒とし、通信が遅れ(or 切断され)ると『Count』が増えるので、今回の設定では4になったら切断と判断しています。カウントダウン処理はコールバック関数内で行います(デフォルメしてます)
『Ping』『Pong』は、WebSocket に備わっている機能は使わず、通常通信の中で行っています。なので『疑似Ping』『疑似Pong』といった名称を使っています
WebSocket に備わっている機能を使っていないのは、ESP32 のプログラムが提供されているライブラリを使用していて、Ping を打てても Pong の受信ができなかったりしたことや(使い方の問題?)、経験的に オープンソースライブラリにバグが潜んでいることも時々あったりしたので、自分の手の内でプログラミングしたかったことがあります
コールバック関数内の処理です。ここで『Pong』受信でカウントダウンします
/*************************/
/* メッセージ受信イベント */
/*************************/
// Message:受信イベント
void onMessagesCallback( WebsocketsMessage Message )
{
if( Message が Pong ? )
{
if( gPingCount > 0 )
{
gPingCount--;
}
}
}
loop関数内の切断検出と再接続処理です
Mes_PingPongはサーバへのメッセージなので任意の値です。WebSocket の再接続は 『gClient.connect( gServerHost, gServerPort, “/” )』を実行しているだけです。Wifi の再接続は、『WiFi.disconnect()』『WiFi.reconnect()』を実行しているだけです
経験的な話ですが、Wifi 切断はほぼありません。WebSocket はメモリの関係なのかタスクの関係なのか不明ですが、カメラを接続した時などに、そこそこ発生しました
切断・接続処理の前後に delay を入れてますが、例えば切断してもすぐに切断されていないような感覚なものがあって入れてます。プログラムの詳細はプログラム内のコメントとフローチャートを参照してください
#define C_TIM_PING_INTERVAL 2000 // 疑似ピン打ち間隔[ms]
#define C_CNT_PONG_DISCONNECTED 4 // 疑似ピン打ちして疑似Pong戻りが無い場合の切断判定回数 (C_CNT_PONG_DISCONNECTED-1)*C_TIM_PING_INTERVAL 時間
const char* gServerHost = C_STR_HOST; // server adress
const uint16_t gServerPort = C_DAT_PORT; // server port
WebsocketsClient gClient;
unsigned long gPingCount = 0; // 疑似Ping送信回数
unsigned long gPingOldTime = 0; // 疑似Pings送信時間計測
bool gWifiConnect = false; // Wifi再接続中(true)
unsigned long gWifiOldTime = 0; // Wifi再接続時間計測
/*************************/
/* メインループ */
/*************************/
void loop()
{
/*************************/
/* 通信チェックと再接続 */
/*************************/
reconnect();
/*************************/
/* Websocket受信 */
/*************************/
if( gClient.available() )
{
gClient.poll();
}
}
/*************************/
/* 通信チェックと再接続 */
/*************************/
// 初期状態では『Wifi接続中』から開始される
// Wifi接続状態
// ・疑似PingでのWS切断判定
// ・WebSocket切断なら接続開始
// ・WebSocket接続中ならPing送信
// ・疑似PingでWebSocket接続中であっても切断状態になったら
// WebSocket切断とし接続開始へ遷移
// Wifi切断状態
// ・Wifi再接続開始し、Wifi再接続チェックへ遷移
// Wifi再接続チェック
// ・接続状態になったら、WebSocet接続開始
// ・切断状態のタイムアウトでWifi再接続開始へ遷移(リトライ)
void reconnect()
{
if( !gWifiConnect )
{
/*************************/
/* Wifi接続中 */
/*************************/
if( WiFi.status() == WL_CONNECTED )
{
/*************************/
/*疑似PingでのWS切断判定 */
/*************************/
// Ping送信回数 ー Pong受信回数 >= 4 で切断判定
if( gPingCount >= C_CNT_PONG_DISCONNECTED ) // 疑似Ping打ち回数 - Pong受信回数が指定値を超えたらWebsocket切断と判定する
{ // 時間で言えば (C_CNT_PONG_DISCONNECTED-1)*C_TIM_PING_INTERVAL [ms]
if( gClient.available() )
{
gClient.close();
delay( 1000 ); // すぐには切断されないようす(感覚的)なのでウエイトをいれた
}
Serial.println( "Start reconnecting to Websocket !!!" );
gClient.connect( gServerHost, gServerPort, "/" );// 接続
delay( 1000 ); // クローズに入れたのと同じような意味合いでウエイトを入れた
gPingCount = 0; // 疑似Ping打ち指定回数でチェックが開始される
gPingOldTime = millis();
}
/*************************/
/* 接続中の疑似Ping送信 */
/*************************/
// Ping送信間隔は、2秒(C_TIM_PING_INTERVAL)
else
{
if( gClient.available() ) // 接続中なら疑似Ping送信
{
if( millis() - gPingOldTime > C_TIM_PING_INTERVAL )
{
send( (uint8_t)Mes_PingPong ); // 疑似Ping送信 Mes_PingPong は任意の値
gPingCount++; // 疑似Ping送信回数カウントアップ
gPingOldTime = millis(); // 送信時の時間をセット
}
}
// 『疑似PingでのWS切断判定』で切断判定される前に切断された場合
else
{ // 接続処理しても接続できていない場合の処理。これをしないと gPingCount のカウントをし直すので
gPingCount = C_CNT_PONG_DISCONNECTED; // 再接続まで (C_CNT_PONG_DISCONNECTED-1)*C_TIM_PING_INTERVAL の2倍の時間がかかる
delay( 100 ); // 連続の接続処理に、念のためインターバルを入れた
}
}
}
/*************************/
/* Wifi再接続開始 */
/*************************/
else
{
WiFi.disconnect(); // 念のため切断処理から接続処理開始
delay( 1000 ); // WebSocket での再接続のウエイトと同じ意味合いでウエイト
WiFi.reconnect(); // 再接続
delay( 1000 ); // WebSocket での再接続のウエイトと同じ意味合いでウエイト
gWifiConnect = true; // 『Wifi再接続チェック』へ遷移
gWifiOldTime = millis(); // 再接続タイムアウト開始時間の取得
Serial.println( "Start reconnecting to Wifi !!!" );
}
}
else
{
/*************************/
/* Wifi再接続チェック */
/*************************/
if( WiFi.status() == WL_CONNECTED )
{
gClient.connect( gServerHost, gServerPort, "/" ); // Websocket接続
delay( 1000 );
gWifiConnect = false; // Websocket接続成功失敗に関わらず抜ける
}
else if( millis() - gWifiOldTime > 10000 ) // タイムアウトチェック
{
gWifiConnect = false; // 接続ができない場合は再び『Wifi再接続開始』に遷移(リトライ
}
}
}