WebSocketで双方向通信する方法(Arduinoプログラミング)

ArduinoでWebSocketの使い方

Arduinoコマンドで、双方向にリアルタイム通信が行える「WebSocket」を使用して遠隔操作、監視(IoT)を行う方法を詳しく紹介します。

今回はM5Stack社製の「M5StickC Plus2」を「WebSocketサーバー」に設定して、パソコンやスマホのブラウザから遠隔操作、監視することを例に紹介していきます。

過去記事では「HTTP」通信を使用したラジコンや遠隔操作をいくつか紹介しましたが、遅延が発生することがよくありました。
今回実践した「WebSocket」は「HTTP」と比べて応答が早く、通信も途切れにくいように思います。通信環境によるとは思いますが、リアルタイム双方向通信はとても快適なのでぜひ試してみてください。

「HTTP」通信を使用した遠隔操作、データ監視については以下のリンクで詳しく紹介しています。

Wi-Fi ラジコンの作り方 スマホで簡単遠隔操作(Arduinoコマンド)
スマホで操作できるラジコンカーの作り方を詳しく紹介。市販部品の組み合わせでサンプルプログラムを書き込むだけで動作確認できます。ArduinoコマンドAtomS3使用
WiFi遠隔操作Arduinoコマンドでブラウザベースのスマホ、PCリモートコントローラの紹介
ブラウザベースで遠隔操作、リアルタイムデータ通信を行う方法をコピペ用サンプルプログラムを使って紹介します。 サーバー機能を利用して「JavaScript」の「fetch」を使うことでデータの送受信を行います。

スポンサーリンク

PCBGOGOバナー600_1
PCBGOGOバナー600_2
スポンサーリンク

1.今回やること

今回は「WebSocket」を使用したリアルタイム双方向通信を、Arduinoのコマンドを使用して実現する方法を確認していきます。

まずは「基礎編」としてWebSocketの基本的な使用方法をWebアプリ(html)を使用して確認し、「応用編」ではより実用的にスマホや複数のデバイスから同時アクセスして双方向データ通信をする方法を確認していきます。
「基礎編」と「応用編」の詳細は以下になります。

・基礎編:Webアプリからサーバーへ接続

まずは基本的な「WebSocket」の使用方法を「Arduinoコマンド」を使用したサンプルプログラムで確認していきます。

「M5StickC Plus2」を「WebSocketサーバー」に設定し、html(JavaScript)で作成した「Webアプリ」からアクセスして動作確認を行います。

「Webアプリ」についても「サンプルプログラム」を準備していますので、パソコンのブラウザで開いて基本的な動作とプログラムの動作確認を行うことができます。

・応用編:サーバーへ直接接続

「応用編」も動作については「基礎編」とほぼ同じですが「WebSocketサーバー」に設定したデバイス(M5StickC Plus2)に「Webアプリ」のhtmlファイルを埋め込んでいます。

これにより「Webアプリ」を使用しなくてもブラウザから「IPアドレス」でサーバーにアクセスするだけでWebSocket対応ページが表示されるため、パソコンだけでなくスマホからも同時接続で遠隔操作、監視を行うことができます。

スポンサーリンク

2.WebSocketについて

・WebSocketとは

「WebSocket」とは、双方向の通信を可能にするための通信プロトコルです。
従来の「HTTPリクエスト」とは異なり、1回の接続を確立した後、クライアントとサーバーはリアルタイムで双方向にデータの送受信をすることができます。

リアルタイムな情報の更新が可能なため、「チャット」や「Web会議」「オンラインゲーム」などのインタラクティブな機能を実現することができます。

・HTTPとの違い

従来からの通信プロトコルである「HTTP」と「WebSocket」の大きな違いは、「WebSocket」では双方向リアルタイム通信ができることです。

「HTTP」はクライアント(ブラウザ)側からの「リクエスト」に対して、サーバー側は受信した「リクエスト」に対応した「レスポンス」を返すことしかできません。
このため、サーバー側からクライアント側へ単独でデータを送信することはできません。

「WebSocket」は一度接続が確立されると、クライアントとサーバー間で接続を維持し続け、互いの情報を受け入れて処理することができるため「リアルタイムな双方向通信」を行うことができます。

・メリット/デメリット

「WebSocket」について「HTTP」と比較したメリットとデメリットは以下になります。

メリット

  • リアルタイム性:「WebSocket」は双方向通信を提供するため、データのリアルタイムな送受信が可能です。
  • ヘッダー情報の削減:「HTTP」では、毎回のリクエストとレスポンスによる送受信データに多くのヘッダー情報が含まれますが、WebSocketは一度接続されると接続状態を維持するため、ヘッダ情報のやり取りを削減することができます。
  • サーバーからデータ送信可能:「HTTP」では、サーバーはクライアントからの「リクエスト」に対する「レスポンス」データしか送信できませんが、「WebSocket」はサーバーからクライアントに対してデータを単独で送信することができます。

デメリット

  • 追加のセキュリティ対策が必要:WebSocketの接続は長時間維持されるため、追加のセキュリティ対策が必要です。用途に応じた適切な認証やデータの暗号化が必要となります。
  • サーバーの負荷:WebSocket接続は長時間オープン状態を維持するため、サーバーのリソースを多く消費します。大量の同時接続がある場合には、サーバーのパフォーマンスに影響を与える可能性があります。
  • 対応ブラウザの制限:最近のブラウザならほとんど対応していますが「Internet Explorer」等の古いバージョンでは対応していないものもあります。
スポンサーリンク

3.M5StickC Plus2とは

今回動作確認に使用した「M5StickC Plus2」について紹介します。

「M5StickC Plus2」とは「M5Stackテクノロジー社」のマイコンボードで1.14インチTFT液晶画面や入出力端子、ボタン、LED、赤外線送信、ブザー、3軸ジャイロ+3軸加速度センサ、マイク、RTC(リアルタイムクロック)、バッテリーが内蔵されています。

シリアル通信はもちろんWiFiやBluetooth通信にも対応しており、これらの機能をプログラムで自由に使用することができるため、電子工作やデータ表示器、IoT機器の製作等いろいろなアイデアを形にすることができます。

端子配列や主な機能は以下になります。

M5StickC Plus2端子配列
プログラムは「C言語」ベースの「Arduino IDE」「PlatformIO」や「ビジュアルプログラミング(UiFlow)」「Python(MicroPython)」で作成できます。

「M5StickC Plus2」については、以下のリンクで詳しく紹介しています。

M5StickC Plus2の使い方、初期設定、旧モデルとの違い等サンプルプログラムで詳しく紹介
M5StickCの最新版M5StickC Plus2について、旧モデルとの違いを確認しながら、初期設定や端子配列、機能、使い方をサンプルプログラムで詳しく紹介します。

4.動作紹介

サンプルプログラムの動作について「基礎編」と「応用編」で以下からそれぞれ紹介していきます。

基本的な動作はどちらでも同じですが、「基礎編」ではサーバーへの接続数を1つとして基本的な「WebSocket」の動作確認をしていきます。
応用編では、サーバーへ複数の接続が可能で、各接続情報をサーバー側で表示し、サーバーからの情報は全ての接続先に同時に送信されるようにしています。(少し修正すれば個別に送信することも可能です。)

・基礎編

「基礎編」の動作は以下になります。

ArduinoでWebSocketの動作確認

サンプルプログラムを書き込んで、本体が起動すると、まずは設定したWi-Fi環境への接続が開始されます。

ArduinoでWebSocketの動作確認

接続が完了すると画面に「IPアドレス(上画像は192.168.0.7 ※環境によって異なります)」が表示されます。

「基礎編」ではパソコンのブラウザで動作する「Webアプリ(htmlファイル)」のサンプルプログラムも準備しています。
これを「メモ帳」等の「テキストアプリ」に貼り付けて、拡張子を「.html」で保存してから開くと、ブラウザ上に以下のような画面が表示されます。

ArduinoでWebSocketの動作確認

ArduinoでWebSocketの動作確認

上画像のように画面のテキストボックスに、サーバー側の液晶画面に表示された「IPアドレス」を入力して「エンター」キーを押すと「WebSocket」接続が開始されます。

ArduinoでWebSocketの動作確認

接続が完了すると上画像のように「Opened!」が表示され、サーバーからクライアント(ブラウザ側)へ「connected!」が送信されて「Webアプリ」上にも表示されます。

ArduinoでWebSocketの動作確認

接続が完了したら[ON]ボタンを押すと、サーバーへテキストデータで「LED ON」が送信されます。

ArduinoでWebSocketの動作確認

サーバー側ではテキストデータ「LED ON」を受信すると本体LEDを点灯(側面のため写真ではわからないですが・・・)させます。
クライアントへ「LED ON!」を送り返し「Webアプリ」上にも表示され、サーバーのLEDが点灯したことが確認できます。

ArduinoでWebSocketの動作確認

[OFF]ボタンを押すとONの時と同様にサーバーへテキストデータ「LED OFF」が送信されます。

ArduinoでWebSocketの動作確認

サーバー側で「LED OFF」を受信すると本体LEDはOFFして、クライアントへ「LED OFF!」を送り返し、「Webアプリ」上にも表示され、サーバーのLEDが消灯したことが確認できます。

ArduinoでWebSocketの動作確認

サーバー側で「本体ボタンA」を押すと、クライアントへテキストデータ「BtnA ON!」が送信されます。

ArduinoでWebSocketの動作確認

「Webアプリ」上では受信した「BtnA ON!」が表示され、サーバーのボタンAが押されたことが確認できます。

ArduinoでWebSocketの動作確認

サーバー側で「本体ボタンA」を離すと、クライアントへテキストデータ「BtnA OFF!」が送信されます。

ArduinoでWebSocketの動作確認

「Webアプリ」上では受信した「BtnA OFF!」が表示され、サーバーのボタンAが離されたことが確認できます。

ArduinoでWebSocketの動作確認

クライアント側の「Webアプリ」からの接続がない状態で、サーバー側の「本体ボタンA」を押すと「No Client!」と表示され、クライアント側でデータ監視が行われていないことが確認できます。

M5StickC Plus2 本体LED赤

「M5StickC Plus2」の本体LEDは側面にあるため、正面から見ると点灯しているのがわかりません。
側面から見ると上画像のように点灯しているのが確認できます。


「Webアプリ」を表示しているブラウザでは送受信のログが確認できます。
これには使用しているブラウザの「開発ツール」で確認できるため、以下「Edge」と「Chrome」の場合でログの表示方法を紹介しておきます。

「Edge」の場合

Edgeコンソールログの確認

「Edge」ブラウザの場合は画面右上の[3点リーダー]→[その他ツール]→[開発ツール]の順にクリックすると、画面右に開発ツールが表示されます。

「Chrome」の場合

chromeコンソールログの確認

「Chrome」ブラウザの場合は画面右上の[3点リーダー]→[その他ツール]→[デベロッパーツール]の順にクリックすると、画面右にデベロッパーツールが表示されます。

「開発ツール」の表示はWindowsの場合は「Ctrl+Shift+I」、Macの場合は「option+command+I」でもOKです。
コンソールログの確認

表示された「開発ツール/デベロッパーツール」画面上部の「コンソール」タブをクリックすると上画像のように操作ログが確認できます。

サンプルプログラムを希望の動作になるように修正して、ブラウザの「コンソール」や、サーバー側デバイスの「シリアルモニタ」で動作確認しながら「WebSocket」の理解を深めましょう。

・応用編

「基礎編」ではパソコンのブラウザから「Webアプリ」を使用してサーバーへ接続しましたが、「応用編」ではサーバーとして動作させているデバイス(M5StickC Plus2)に、ブラウザで表示させる「htmlファイル」をテキストデータで埋め込んでいます。

このためパソコンに限らず、スマホのブラウザのアドレスバーに「IPアドレス」入力するだけでも「基礎編」と同じ動作確認を行うことができます。

ArduinoでWebSocketの動作確認

上画像では「Crome」ブラウザのアドレスバーに「IPアドレス」を入力している様子です。
この状態で「エンター」キーを押すと、ブラウザ上に操作画面が表示されます。


ArduinoでWebSocketの動作確認

基本的な動作は「基礎編」と同じで、サーバーのデバイスが起動するとサーバー側の液晶画面にサーバーの「IPアドレス」が表示されます。

ArduinoでWebSocketの動作確認

ブラウザのアドレスバーに「IPアドレス」を入力して「エンター」キーを押すと「WebSocket」接続が開始され、上画像のような画面が表示されます。

ArduinoでWebSocketの動作確認

接続が完了すると液晶画面上に「[0]IPアドレス」が表示されます[0]はクライアント番号で接続数が1つなら0、2つなら1となり現在接続されているクライアントの数が確認できます。
「IPアドレス」はクライアント側のIPアドレスです。

ArduinoでWebSocketの動作確認

接続が完了するとブラウザ画面では「connected!」と表示され、ボタン操作が可能となります。

ArduinoでWebSocketの動作確認

接続が完了したら[ON]ボタンを押すと、サーバーへテキストデータで「LED ON」が送信されます。

ArduinoでWebSocketの動作確認

サーバー側ではテキストデータ「LED ON」を受信すると本体LEDを点灯(側面のため写真ではわからないですが・・・)させます。
クライアントへ「LED ON!」を送り返し「ブラウザ」上にも表示され、サーバーのLEDが点灯したことが確認できます。

ArduinoでWebSocketの動作確認

[OFF]ボタンを押すと、サーバーへテキストデータで「LED OFF」が送信されます。

ArduinoでWebSocketの動作確認

サーバー側で「LED OFF」を受信すると本体LEDはOFFして、クライアントへ「LED OFF!」を送り返し、「ブラウザ」上にも表示され、サーバーのLEDが消灯したことが確認できます。

ArduinoでWebSocketの動作確認

サーバー側で「本体ボタンA」を押すと、クライアントへテキストデータ「BtnA ON!」が送信されます。

ArduinoでWebSocketの動作確認

ブラウザ上では受信した「BtnA ON!」が表示され、サーバーのボタンAが押されたことが確認できます。

ArduinoでWebSocketの動作確認

サーバー側で「本体ボタンA」を離すと、クライアントへテキストデータ「BtnA OFF!」が送信されます。

ArduinoでWebSocketの動作確認

ブラウザ上では受信した「BtnA OFF!」が表示され、サーバーのボタンAが離されたことが確認できます。

ArduinoでWebSocketの動作確認

クライアントからの接続が遮断されると、サーバー側には「Disconnected!」が表示されます。
クライアントからの接続が無い状態でサーバーの「本体ボタンA」を押すと「No Client!」と表示されます。

「応用編」では複数のクライアントからの接続に対応しているため、1つでも接続があればそこに対して情報を送信します。複数の接続があれば全てに情報を送信します。

パソコンのブラウザから接続したまま、スマホのブラウザからも「IPアドレス」を入力して接続すると以下のように同じ操作画面が表示されます。
クライアント番号[1]で新しい接続が開始され、スマホの「IPアドレス」が表示されます。

ArduinoでWebSocketの動作確認

[ON]ボタンを押すと以下のように、サーバー本体のLEDが点灯し、スマホブラウザに「LED ON!」が表示されます。

ArduinoでWebSocketの動作確認
同時に、先に接続したパソコン側のブラウザにも「LED ON!」が表示されているので確認してみましょう。

最後に、「応用編」でも「基礎編」と同様に通信ログが確認できるようにしてあるので、下画像のようにブラウザの「開発ツール」で表示(Ctrl+Shift+I)させて確認しながら動作確認してみましょう。

コンソールログの確認

5.サンプルプログラム(基礎編)

「WebSocket」の基本的な動作確認を行うサンプルプログラムをArduinoの「WebSocket」ライブラリを使用した「WebSocketサーバー」プログラムと、動作確認のための「Webアプリ(html/JavaScript)」プログラムに分けて以下に準備しました。

・WebSocketサーバー

「WebSocketサーバー」のサンプルプログラムは以下になります。
「ArduinoIDE」等の開発環境にコピペで貼り付けて書き込んでください。

以下サンプルプログラムの「18,19行目」のWi-Fi接続先「SSID」と「パスワード」を、お使いのネットワーク環境に合わせて設定してから書き込んでください。

※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

#include <M5StickCPlus2.h>
#include <WebSocketsServer.h> // WebSocket通信用
#define LED 19 // 本体LED

M5Canvas canvas(&M5.Lcd); // メモリ描画領域表示(スプライト)のインスタンスを作成
WebSocketsServer webSocket = WebSocketsServer(81); // WebSocketサーバーをポート81で準備

// 変数宣言
bool state = false;       // ボタン状態確認用
bool led_state = false;   // LED状態確認用
float battery;            // バッテリー残量表示用
String client_text = "";  // クライアントからのテキスト格納用
String send_message = ""; // クライアントへの送信情報表示用

// ローカルWi-Fi接続実行関数 *****************************************************
void localWifiConnect() {
  // ローカルWi-Fi接続先設定
  const char ssid[] = "自宅のWi-Fi接続先を設定";      //接続先SSID
  const char pass[] = "Wi-Fi接続先のパスワードを設定"; //接続先パスワード

  // Wi-Fi接続確認(IPアドレスの取得で確認)
  M5.Lcd.println("Wi-Fi Search!");
  while (WiFi.localIP()[0] == 0) { // IPアドレスが取得されるまで繰り返し
    WiFi.begin(ssid, pass);        //ローカル Wi-Fi接続実行
    M5.Lcd.print(". ");
    delay(3000); // 再接続待ち
  }
  Serial.println(WiFi.SSID()); // シリアル出力
  Serial.println(WiFi.localIP());
}
// クライアントへメッセージを送信する関数 ********************************************
void sendMessage(String message) {
  uint8_t clientNum = webSocket.connectedClients(); // 現在の接続クライアント数を取得
  if (clientNum) { // クライアントからの接続があれば
    Serial.printf("Send Message [%s] to %d Client\n", message, clientNum);
    webSocket.sendTXT(0, message);
    send_message = "Send: " + message; // 通信情報格納
  } else {         // クライアントからの接続がなければ
    Serial.println("No cliant!");
    send_message = "No Client!";       // 通信情報格納
  }
}
// Webscketのイベント設定関数 *****************************************************
void webSocketEvent(uint8_t client_num, WStype_t type, uint8_t * payload, size_t length) {
  switch (type) {
    case WStype_DISCONNECTED: // クライアントとの接続が遮断された時の処理
      Serial.printf("[%u] Disconnected!\n", client_num);
      break;

    case WStype_CONNECTED: {  // サーバー接続時処理
      IPAddress ip = webSocket.remoteIP(client_num); // クライアントのIPアドレスを取得
      Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", client_num, ip[0], ip[1], ip[2], ip[3], payload);
      client_text = (char*)payload; // 受信テキスト取得
      sendMessage("connected!");
      break;
    }
    case WStype_TEXT:         // テキストメッセージ受信時の処理
      Serial.printf("[%u] get Text: %s\n", client_num, payload); // 受信したメッセージ(payload)を表示
      client_text = (char*)payload; // 受信テキスト取得
      break;
    
    case WStype_BIN:          // バイナリデータ受信時の処理
      Serial.printf("[%u] get binary length: %u\n", client_num, length);
      // payload と length を使用してバイナリデータを扱う(今回未使用)
      break;
  }
}
// 初期設定 -----------------------------------------
void setup() {
  auto cfg = M5.config(); // 本体初期設定
  StickCP2.begin(cfg);
  Serial.begin(9600);     // シリアル出力初期化

  // 出力端子設定
  pinMode(LED, OUTPUT);   // 本体LED赤
  digitalWrite(LED, LOW); // 本体LED初期値OFF(LOW)

  // ベース画面の初期設定
  M5.Lcd.fillScreen(BLACK); // 背景色
  M5.Lcd.setRotation(1);    // 画面向き設定(USB位置基準 0:下/ 1:右/ 2:上/ 3:左)
  M5.Lcd.setTextSize(1);    // 文字サイズ(整数倍率)
  M5.Lcd.setFont(&fonts::Font4); // フォント
  canvas.setTextWrap(false); // 改行をしない(画面をはみ出す時自動改行する場合はtrue。書かないとtrue)
  canvas.createSprite(M5.Lcd.width(), M5.Lcd.height()); // canvasサイズ(メモリ描画領域)設定(画面サイズに設定)

  localWifiConnect(); // Wi-Fi接続実行

  // WebSocketサーバー設定
  webSocket.begin();                 // WebSoket通信の初期化
  webSocket.onEvent(webSocketEvent); // WebSocketのイベントのハンドラ設定
}
// メイン -----------------------------------------
void loop() {
  webSocket.loop(); // WebSocketによるイベント処理を行う
  M5.update();      // 本体ボタン状態更新

  // ボタンA処理
  if (M5.BtnA.wasPressed()) {  // ボタンAが押されたら
    state = true;  // ボタン状態true
    StickCP2.Speaker.tone(4000, 100); // ブザーON(周波数 Hz, 発音時間 ms)
    sendMessage("BtnA ON!"); // クライアント番号0へテキスト送信
  }
  if (M5.BtnA.wasReleased()) { // ボタンA離されたら
    state = false; // ボタン状態false
    sendMessage("BtnA OFF!");// クライアント番号0へテキスト送信
  }
  // クライアントからの受信情報処理
  if (client_text == "LED ON" && led_state == false) { // 受信テキストが「LED ON」でLED状態がfalseなら
    led_state = true;        // LED状態確true
    digitalWrite(LED, HIGH); // 本体LED赤点灯
    sendMessage("LED ON!");  // クライアント番号0へテキスト送信
  }
  if (client_text == "LED OFF" && led_state == true) { // 受信テキストが「LED OFF」でLED状態がtrueなら
    led_state = false;       // LED状態確false
    digitalWrite(LED, LOW);  // 本体LED赤消灯
    sendMessage("LED OFF!"); // クライアント番号0へテキスト送信
  }
  // バッテリー残量取得(パーセント)
  battery = StickCP2.Power.getBatteryLevel();  // 0〜100% で取得

  // LCD表示処理
  canvas.fillScreen(BLACK);              // 画面初期化
  canvas.setFont(&fonts::Font4);         // フォント
  canvas.setCursor(5, 0);                // 座標設定(x, y)
  canvas.setTextColor(WHITE, BLACK);     // (文字色, 背景)
  canvas.print("SSID : " + WiFi.SSID()); // SSID表示
  canvas.setCursor(5, 25);
  canvas.print("IP : " + WiFi.localIP().toString()); // IPアドレス表示
  canvas.setCursor(5, 50);
  canvas.setTextColor(ORANGE, BLACK);
  canvas.print("WebSocket Info :");
  canvas.setCursor(5, 75);
  canvas.print(client_text);  // 受信テキスト表示
  canvas.setCursor(5, 100);
  canvas.setTextColor(CYAN, BLACK);
  canvas.print(send_message); // 送信メッセージ表示
  canvas.setTextFont(2);      // フォント
  canvas.setCursor(202, 120);
  canvas.setTextColor(GREENYELLOW, BLACK);
  canvas.printf("%.0f%%", battery); // バッテリー残量パーセント表示

  // メモリ描画領域を座標を指定して一括表示(スプライト)
  canvas.pushSprite(&M5.Lcd, 0, 0); // (0, 0)座標に一括表示実行
  delay(200); // 遅延時間(ms)
}

スポンサーリンク

PCBGOGOバナー600_1
PCBGOGOバナー600_2

・Webアプリ

「WebSocketサーバー」の動作確認用「Webアプリ」のサンプルプログラムは以下になります。

パソコンの「メモ帳」等のアプリにコピペで貼り付けて、ファイル名は何でも良いので拡張子を「.html」として「名前をつけて保存」してから開いて使用してください。

※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

<!DOCTYPE html>
<html lang="jp">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Socket Test</title>
  <style>
    body { display: flex; justify-content: center; align-items: center; flex-direction: column; margin: 0; }
    #messageContainer {margin: 5px;}
  </style>
</head>
<body>
  <h1>Web Socket Test</h1>
  <form name="form" id="id_form" onsubmit="return startWebSocket(event)" action="">
    <input name="textBox" id="input_IP" type="text" value="" placeholder="サーバーIPアドレス" />
  </form>
  <div id="messageContainer">Please Input IP Address & Enter Key</div>
  <div>
    <button onclick='buttonOn()'>ON</button>
    <button onclick='buttonOff()'>OFF</button>
  </div>

  <script>
    let socket; // グローバル変数としてsocketを宣言
    let messageElement = document.getElementById('messageContainer'); // メッセージ表示部の要素を取得

    function startWebSocket(event) { // WebSocketの開始      
      let ip_addr = document.getElementById('input_IP').value;
      console.log(ip_addr);
      socket = new WebSocket(`ws://${ip_addr}:81`); // WebSocketインスタンスを作成し、指定されたホストとポートに接続する

      socket.addEventListener('open', (event) => { // 接続が開いた時の処理
        console.log('WebSocket opened');
        socket.send('Opened!');
      });
      socket.addEventListener('message', (event) => { // メッセージを受信した時の処理
        console.log(`Received response: ${event.data}`);
        messageElement.textContent = event.data;
      });
      socket.addEventListener('close', (event) => { // 接続が閉じられた時の処理
        console.log('Connection closed!');
        messageElement.textContent = 'Connection closed!';
      });
      return false; // 必要な場合、フォームの送信をキャンセルする
    }
    // メッセージ送信
    function sendMessage(message) {
      if (socket && socket.readyState === WebSocket.OPEN) {
        socket.send(message); // メッセージ送信
      } else {
        console.log('WebSocket is not connected!');
      }
    }
    // ボタンON時メッセージ送信
    function buttonOn() { sendMessage('LED ON'); }
    // ボタンOFF時メッセージ送信
    function buttonOff() { sendMessage('LED OFF'); }

  </script>
</body>
</html>

6.WebSocketプログラムの詳細(Arduino)

今回「WebSocket」を使用するためにArduinoのライブラリ「WebSockets」を使用しています。
「WebSockets」ライブラリの使用方法について以下から詳しく紹介していきます。

・ライブラリの準備

まずは「WebSockets」ライブラリをインストールします。

「ArduinoIDE」の場合はメニューの「ライブラリを管理」から「websockets」で検索して以下のライブラリをインストールしましょう。

Arduino WebSoketライブラリ

プログラム内では以下のように、サーバーに使用するデバイスのライブラリと一緒にインクルードします。(今回はM5StickCPlus2をインクルード)

次に以下「4行目」のように「WebSocketsServer」のインスタンスを「通信ポート81」で「webSocket」として作成します。

#include <M5StickCPlus2.h>    // 
#include <WebSocketsServer.h> // WebSocket通信用

WebSocketsServer webSocket = WebSocketsServer(81); // WebSocketサーバーをポート81で準備

・初期設定

初期設定では、以下の「3,4行目」のように書くことで「WebSocket」の初期化とイベントハンドラ(データを受信したときの処理)の設定を行うことができます。

void setup() {
  // WebSocketサーバー設定
  webSocket.begin();                 // WebSoket通信の初期化
  webSocket.onEvent(webSocketEvent); // WebSocketのイベントのハンドラ設定
}

・WebSocketイベントの設定、データ送信

イベントハンドラの設定

データを受信したときの処理は、以下のように初期設定で指定したイベントハンドラ「webSocketEvent関数」にまとめて設定しておきます。

関数内には「サーバーとの接続が遮断された時」「接続が開始された時」「テキストデータを受信した時」「バイナリデータを受信した時(今回未使用)」に実行したい処理を個別に設定しておきます。
// Webscketのイベント設定関数 *****************************************************
void webSocketEvent(uint8_t client_num, WStype_t type, uint8_t * payload, size_t length) {
  switch (type) {
    case WStype_DISCONNECTED: // クライアントとの接続が遮断された時の処理
      Serial.printf("[%u] Disconnected!\n", client_num);
      break;

    case WStype_CONNECTED: {  // サーバー接続時処理
      IPAddress ip = webSocket.remoteIP(client_num); // クライアントのIPアドレスを取得
      Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", client_num, ip[0], ip[1], ip[2], ip[3], payload);
      client_text = (char*)payload; // 受信テキスト取得
      sendMessage("connected!");
      break;
    }
    case WStype_TEXT:         // テキストメッセージ受信時の処理
      Serial.printf("[%u] get Text: %s\n", client_num, payload); // 受信したメッセージ(payload)を表示
      client_text = (char*)payload; // 受信テキスト取得
      break;
    
    case WStype_BIN:          // バイナリデータ受信時の処理
      Serial.printf("[%u] get binary length: %u\n", client_num, length);
      // payload と length を使用してバイナリデータを扱う(今回未使用)
      break;
  }
}

データ送信

データの送信は以下のように「sendMessage関数」内で行なっています。

// クライアントへメッセージを送信する関数 ********************************************
void sendMessage(String message) {
  uint8_t clientNum = webSocket.connectedClients(); // 現在の接続クライアント数を取得
  if (clientNum) { // クライアントからの接続があれば
    Serial.printf("Send Message [%s] to %d Client\n", message, clientNum);
    webSocket.sendTXT(0, message);
    send_message = "Send: " + message; // 通信情報格納
  } else {         // クライアントからの接続がなければ
    Serial.println("No cliant!");
    send_message = "No Client!";       // 通信情報格納
  }
}

データの送信は上コードの「6行目」の「webSocket.sendTXT()」で行なっており、詳細は以下のようになります。

webSocket.sendTXT( クライアント番号 文字列) ;

クライアント番号:サーバーに接続中のクライアントごとに割り振られる番号で、接続順に「0」から割り振られます。
文字列:送信する文字列を指定します。

サンプルプログラムの「基礎編」では1つの接続先にのみデータを送信するため「クライアント番号」は「0」に指定しています。
サンプルプログラムの「応用編」では複数の接続先にデータを送信するため「webSocket.connectedClients()」でクライアントの接続数を取得して、接続数分繰り返すことで全てのクライアントへデータを送信しています。

・メインプログラムでの処理

最後にメインプログラムの中で以下「2行目」のように書いておくことで「WebSocket」通信を行うことができます。

void loop() {
  webSocket.loop(); // WebSocketによるイベント処理を行う
}

・Webアプリ

「WebSocket」通信を行うための「Webアプリ」は「html/css/JacaScript」で作成しています。
私はあまり得意ではないので、全て「ChatGPT」にお任せしてます^^;

以下にWebアプリの動作の流れを「ChatGPT」の質問風に書いておきますので、このまま「ChatGPT」等の「生成AI」に聞けば、それなりに動くものを書いてくれると思います。

WebSocket通信を行うためのWebアプリをhtmlとcss、Javascriptを使って書いてください。
以下の要件を満たすようにしてください。
  1. タイトルを「Web Socket Test」として画面上に表示
  2. IPアドレスを入力するためのテキストボックスを配置。その横に「接続」ボタンを配置。
  「接続」ボタンを押すと、テキストボックスに入力されたIPアドレスを取得して、ポート81でwebSocketサーバへ接続
  3. テキストボックスの下にはサーバーから受信したメッセージを受信するごとに更新して表示。
  4. メッセージ表示部の下にはONボタンとOFFボタンを横並びで表示。
   ONボタンを押すとテキストデータで「LED ON」を送信
   OFFボタンを押すとテキストデータで「LED OFF」を送信
  5. 1〜4の要素は縦並びで上中央揃えで配置
  6. htmlとcss、javascriptは分けずにコピペで動作できるように1つのデータとして動作可能なように全部書いて

何度か試しましたが、ほぼ動くものを書いてくれます。
以下は生成例です。うまく伝わってない部分もありますが、どれも十分動作確認できるものでした。

WebSoketアプリChatGPT生成例
WebSoketアプリChatGPT生成例

7.サンプルプログラム(応用編)

「応用編」では「Webアプリ」なしで「WebSocket」を使用した遠隔操作、データ監視を行うことができます。(複数接続可能)
以下のサンプルプログラムを書き込んで起動すると「M5StickC Plus2」の液晶画面に「IPアドレス」が表示されるので、パソコンやスマホのブラウザのアドレスバーに入力して接続してください。

「IPアドレス」でサーバーに接続されると、「HTTP」通信でサーバーから「htmlデータ(19〜81行目)」が送信されてブラウザに「WebSocket」対応の操作画面が表示されます。
以下サンプルプログラムの「97,98行目」のWi-Fi接続先「SSID」と「パスワード」を、お使いのネットワーク環境に合わせて設定してから書き込んでください。

※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

#include <M5StickCPlus2.h>
#include <WebServer.h>        // サーバー設定用
#include <WebSocketsServer.h> // WebSocket通信用

#define LED 19 // 本体LED

M5Canvas canvas(&M5.Lcd); // メモリ描画領域表示(スプライト)のインスタンスを作成
WebServer server(80);                              // Webサーバーをポート80で準備
WebSocketsServer webSocket = WebSocketsServer(81); // WebSocketサーバーをポート81で準備

// 変数宣言
bool state = false;       // ボタン状態確認用
bool led_state = false;   // LED状態確認用
float battery;            // バッテリー残量表示用
String client_text = "";  // クライアントからのテキスト格納用
String client_info = "";  // クライアントからの受信情報表示用
String send_message = ""; // クライアントへの送信情報表示用

// htmlメイン画面データ ※R"この中の文字列は改行は無視され連続した文字列として扱われる"
char html[] = R"(
<!DOCTYPE html>
<html lang="jp">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Socket Demo</title>
  <style>
    body { display: flex; justify-content: center; align-items: center; flex-direction: column; margin: 0; }
    .button {width: 100px;height: 55px;border-radius: 5px;margin: 10px;cursor: pointer;font-size: 20px;font-weight: bold;color: white;text-align: center;line-height: 50px;}
    #on-button {background-color: green;}
    #off-button {background-color: red;}
  </style>
</head>
<body>
  <h1>Web Socket Demo</h1>
  <div id="messageContainer">Please wait...</div>
  <div>
    <button onclick='buttonOn()' id="on-button" class="button">ON</button>
    <button onclick='buttonOff()' id="off-button" class="button">OFF</button>
  </div>

  <script>
    let socket; // グローバル変数としてsocketを宣言
    let messageElement = document.getElementById('messageContainer');
    // サーバー側IPアドレスの取得
    async function getIPAddress() {
      const response = await fetch("/get/ip");
      const text = await response.text();
      console.log("Server IPAddress : " + text);
      return text;
    }
    // WebSocketの開始
    async function startWebSocket() {
      let host = await getIPAddress(); // WebSocketサーバーのIPアドレス取得
      let port = 81;                   // WebSocketサーバーのポート番号
      console.log('WebSocket Ready...');
      socket = new WebSocket(`ws://${host}:${port}`); // WebSocketを作成
      
      socket.addEventListener('open', (event) => {    // 接続が開いた時の処理
        console.log('WebSocket opened');
      });
      socket.addEventListener('message', (event) => { // メッセージを受信した時の処理
        console.log(`Received response: ${event.data}`);
        messageElement.textContent = event.data;
      });
      socket.addEventListener('close', (event) => {   // 接続が閉じられた時の処理
        console.log('Connection closed!');
        messageElement.textContent = 'Connection closed!';
      });
    }
    // ボタンON時メッセージ送信
    function buttonOn() { socket.send('LED ON'); }
    // ボタンOFF時メッセージ送信
    function buttonOff() { socket.send('LED OFF'); }
    
    startWebSocket(); // WebSocketの開始
  </script>
</body>
</html>
)";

// サーバーリクエスト時処理関数 **************************************************
void handleRoot() {     // ルートアクセス時の応答関数
  server.send(200, "text/html", html); //レスポンス200を返しhtml送信
}
void getIpAddress() {   // IPアドレスリクエスト時処理
  server.send(200, "text/plane", WiFi.localIP().toString()); //レスポンス200を返しIPアドレスを送信
}
void handleNotFound() { // エラー(Webページが見つからない)時の応答関数
  server.send(404, "text/plain", "404 Not Found!");  // レスポンス404を返し、エラーメッセージ送信
}

// ローカルWi-Fi接続実行関数 *****************************************************
void localWifiConnect() {
  // ローカルWi-Fi接続先設定
  const char ssid[] = "自宅のWi-Fi接続先を設定";  //接続先SSID
  const char pass[] = "Wi-Fi接続先のパスワードを設定"; //接続先パスワード

  // Wi-Fi接続確認(IPアドレスの取得で確認)
  M5.Lcd.println("Wi-Fi Search!");
  while (WiFi.localIP()[0] == 0) { // IPアドレスが取得されるまで繰り返し
    WiFi.begin(ssid, pass);        //ローカル Wi-Fi接続実行
    M5.Lcd.print(". ");
    delay(3000); // 再接続待ち
  }
  Serial.println(WiFi.SSID()); // シリアル出力
  Serial.println(WiFi.localIP());
}

// Webscketのイベント設定関数 *****************************************************
void webSocketEvent(uint8_t client_num, WStype_t type, uint8_t * payload, size_t length) {
  char buff[50]; // 通信情報格納バッファ
  switch (type) {
    case WStype_DISCONNECTED: // クライアントとの接続が遮断された時の処理
      Serial.printf("[%u] Disconnected!\n", client_num);
      sprintf(buff, "[%u]Disconnected!", client_num); // 受信情報整理
      client_info = buff;                             // 通信情報格納
      break;

    case WStype_CONNECTED: {  // サーバー接続時処理
      IPAddress ip = webSocket.remoteIP(client_num);  // クライアントのIPアドレスを取得
      Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", client_num, ip[0], ip[1], ip[2], ip[3], payload);
      webSocket.sendTXT(client_num, "connected!");    // クライアントにメッセージを送信
      sprintf(buff, "[%u]%d.%d.%d.%d", client_num, ip[0], ip[1], ip[2], ip[3]);    // 受信情報整理
      client_info = buff;                             // 通信情報格納
      break;
    }
    case WStype_TEXT:         // テキストメッセージ受信時の処理
      Serial.printf("[%u] get Text: %s\n", client_num, payload); // 受信したメッセージ(payload)を表示
      sprintf(buff, "[%u]%s", client_num, payload);   // 受信情報整理
      client_text = (char*)payload;                   // 受信テキスト取得
      client_info = buff;                             // 通信情報格納
      break;
    
    case WStype_BIN:          // バイナリデータ受信時の処理
      Serial.printf("[%u] get binary length: %u\n", client_num, length);
      // payload と length を使用してバイナリデータを扱う
      break;
  }
}
// クライアントへメッセージを送信する関数 ******************************************************
void sendMessage(String message) {
  uint8_t clientNum = webSocket.connectedClients(); // 現在の接続クライアント数を取得
  if (clientNum) { // クライアントからの接続があれば
    Serial.printf("Send Message [%s] to %d Client\n", message, clientNum);
    for (int i = 0; i < clientNum; i++) { // 接続クライアント数分繰り返し
      webSocket.sendTXT(i, message);      // クライアントにメッセージを送信(クライアント番号は0から始まる) 
    }
    send_message = "Send: " + message; // 通信情報格納
  } else {         // クライアントからの接続がなければ
    Serial.println("No cliant!");
    send_message = "No Client!";       // 通信情報格納
  }
}
// 初期設定 -----------------------------------------
void setup() {
  auto cfg = M5.config(); // 本体初期設定
  StickCP2.begin(cfg);
  Serial.begin(9600);     // シリアル出力初期化

  // 出力端子設定
  pinMode(LED, OUTPUT);   // 本体LED赤
  digitalWrite(LED, LOW); // 本体LED初期値OFF(LOW)

  // ベース画面の初期設定
  M5.Lcd.fillScreen(BLACK); // 背景色
  M5.Lcd.setRotation(1);    // 画面向き設定(USB位置基準 0:下/ 1:右/ 2:上/ 3:左)
  M5.Lcd.setTextSize(1);    // 文字サイズ(整数倍率)
  M5.Lcd.setFont(&fonts::Font4); // フォント

  canvas.setTextWrap(false); // 改行をしない(画面をはみ出す時自動改行する場合はtrue。書かないとtrue)
  canvas.createSprite(M5.Lcd.width(), M5.Lcd.height()); // canvasサイズ(メモリ描画領域)設定(画面サイズに設定)

  localWifiConnect(); // Wi-Fi接続実行

  // Webサーバー設定
  server.on("/", handleRoot);         // ルートアクセス時の応答関数を設定
  server.on("/get/ip", getIpAddress); // IPアドレスを返す
  server.onNotFound(handleNotFound);  // Webページが見つからない時の応答関数を設定
  server.begin();                     // Webサーバー開始

  // WebSocketサーバー設定
  webSocket.begin();                  // WebSoket通信の初期化
  webSocket.onEvent(webSocketEvent);  // WebSocketのイベントのハンドラ設定
}

// メイン -----------------------------------------
void loop() {
  server.handleClient(); //クライアントからのアクセス確認
  webSocket.loop();      // WebSocketによるイベント処理を行う
  M5.update();           //本体ボタン状態更新

  // ボタンA処理
  if (M5.BtnA.wasPressed()) { // ボタンAが押されたら
    state = true;                     // ボタン状態true
    StickCP2.Speaker.tone(4000, 100); // ブザーON(周波数 Hz, 発音時間 ms)
    sendMessage("BtnA ON!");          // WebSocketでテキスト送信
  }
  if (M5.BtnA.wasReleased()) {// ボタンA離されたら
    state = false;                    // ボタン状態false
    sendMessage("BtnA OFF!");         // WebSocketでテキスト送信
  }
  // クライアントからの受信情報処理
  if (client_text == "LED ON" && led_state == false) { // 受信テキストが「LED ON」でLED状態がfalseなら
    digitalWrite(LED, HIGH); // 本体LED赤点灯
    sendMessage("LED ON!");  // WebSocketでテキスト送信
    led_state = true;        // LED状態true
  }
  if (client_text == "LED OFF" && led_state == true) { // 受信テキストが「LED OFF」でLED状態がtrueなら
    digitalWrite(LED, LOW);  // 本体LED赤消灯
    sendMessage("LED OFF!"); // WebSocketでテキスト送信
    led_state = false;       // LED状態false
  }
  // バッテリー残量取得(パーセント)
  battery = StickCP2.Power.getBatteryLevel();  // 0〜100% で取得

  // LCD表示処理
  canvas.fillScreen(BLACK);              // 画面初期化
  canvas.setFont(&fonts::Font4);         // フォント
  canvas.setCursor(5, 0);                // 座標設定(x, y)
  canvas.setTextColor(WHITE, BLACK);     // (文字色, 背景)
  canvas.print("SSID : " + WiFi.SSID()); // SSID表示
  canvas.setCursor(5, 25);
  canvas.print("IP : " + WiFi.localIP().toString()); // IPアドレス表示
  canvas.setCursor(5, 50);
  canvas.setTextColor(ORANGE, BLACK);
  canvas.print("WebSocket Info :");
  canvas.setCursor(5, 75);
  canvas.print(client_info);  // 通信情報表示
  canvas.setCursor(5, 100);
  canvas.setTextColor(CYAN, BLACK);
  canvas.print(send_message); // 通信情報表示
  canvas.setTextFont(2); // フォント
  canvas.setCursor(202, 120);
  canvas.setTextColor(GREENYELLOW, BLACK);
  canvas.printf("%.0f%%", battery); // バッテリー残量パーセント表示

  // メモリ描画領域を座標を指定して一括表示(スプライト)
  canvas.pushSprite(&M5.Lcd, 0, 0); // (0, 0)座標に一括表示実行
  delay(200); // 遅延時間(ms)
}

8.まとめ

Arduinoコマンドで「WebSocket」でリアルタイム双方向通信する方法を詳しく紹介しました。

「WebSocket」とは、リアルタイムな双方向通信を可能にするための通信プロトコルで「HTTPリクエスト」とは異なり、1回の接続を確立した後、クライアントとサーバーはリアルタイムで双方向にデータの送受信をすることができるため、「チャット」や「Web会議」「オンラインゲーム」など多くの場面で使用されています。

今回はM5Stack社製の「M5StickC Plus2」を「WebSocketサーバー」に設定して、パソコンやスマホのブラウザから遠隔操作、監視することを例に紹介していきました。

小型なデバイスですが、サーバーとして動作させることができ、液晶表示や入出力端子も備えているため、遠隔操作、監視可能な「IoT」デバイスを簡単に作成することができます。

今回初めて「WebSocket」を使用してみましたが「HTTP」と比較すると応答が速く、双方向でのデータのやり取りも簡単で非常に快適でした♪

過去に作った「HTTP」通信のラジコンではイマイチ反応が悪く、操作不能で暴走したりということもありましたが「WebSocket」ならもっと快適に操作できそうです。

「WebSocket」を使用した「ラジコン」もまた紹介していきたいと思います。

「HTTP」通信を使用したラジコンは以下のリンクで詳しく紹介しています。

Wi-Fi ラジコンの作り方 スマホで簡単遠隔操作(Arduinoコマンド)
スマホで操作できるラジコンカーの作り方を詳しく紹介。市販部品の組み合わせでサンプルプログラムを書き込むだけで動作確認できます。ArduinoコマンドAtomS3使用

コメント

タイトルとURLをコピーしました