ESP32のサーバーサイドTCP/IPでのSocket通信

概要

ESP32でSocket通信ができることは知っていましたが、使ったことがありませんでした。今回ちょっと気になることがあったので実験してみました。サーバーサイトで待ち受けする側と、接続する側のクライアントがありますが、サーバーサイドになります。送信はMQTTとかHTTPで通信すればいいですよね。

Socket通信とは?

一般的にはTCP/IPを使った通信になります。サーバー側が特定のポート番号で待ち受けて、クライアント側からサーバーのIPと待ち受けているポート番号に対して接続を行います。接続したあとは通信を来ない、最後に切断をするまでが一般的な流れです。

UDPを使った通信もソケット通信と呼びます。こちらはサーバーやクライアントという役割が特別あるわけではなく、各デバイスが特定のポート番号で待ち受けていますのでそこに直接通信を投げ込みます。接続や切断がなくて、通信のみのプロトコルです。

基本的にはUDPの方が接続、切断がないので軽いプロトコルです。TCPは若干重いのですが、途中の経路でパケットが行方不明になっても自動再送してくれる仕組みなどがあります。

今回試すのはTCPを使ったソケット通信になります。

Wi-Fi接続

#include <WiFi.h>

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println();

  WiFi.begin("SSID", "KEY");
  Serial.print("WiFi.begin()");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.println("WiFi Connected.");
  Serial.printf("IP Address  : ");
  Serial.println(WiFi.localIP());
}

void loop() {
}

私は通常のスケッチの中にWi-Fiアクセスポイントの設定は下記ませんので、まずは上記のようなスケッチを実行してWi-Fiに一度接続させます。次回以降はESP32内部に保存されている最後に接続したWi-Fiアクセスポイントに接続するという設定を利用します。

ソケット受信実験

#include <WiFi.h>
#include <WiFiClient.h>

WiFiServer server(5000);

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println();

  WiFi.begin();
  Serial.print("WiFi.begin()");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.println("WiFi Connected.");
  Serial.printf("IP Address  : ");
  Serial.println(WiFi.localIP());

  server.begin();
  Serial.println("server.begin()");
}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    Serial.println("New Client.");
    while (client.connected()) {
      int size = client.available();
      if (size) {
        Serial.println();
        Serial.print("size:");
        Serial.println(size);
        for (int i = 0; i < size; i++) {
          uint8_t c = client.read();
          Serial.print(c);
        }
      }

      delay(1);
    }
    client.stop();
    Serial.println();
    Serial.println("client.stop()");
  }

  delay(1);
}

まずは動きを確認するスケッチです。

待ち受け設定

WiFiServer server(5000);

上記が待ち受け設定で、ポート5000番のサーバーサイドで通信をするための設定になります。ポート番号は65535番まであって、1024より小さい番号は使わないほうがよいと思います。通常のパソコンやサーバーですと他にもいろいろ動いているので、ポート番号の管理は重要ですがESP32は自分で起動したものしかないので、適当な番号で大丈夫です。

サーバーの起動

server.begin();

Wi-Fiの接続部分は定形なので飛ばして、上記がサーバーの起動です。beginするだけですね。

通信

  WiFiClient client = server.available();

  if (client) {
    Serial.println("New Client.");
    while (client.connected()) {
      int size = client.available();
      if (size) {
        Serial.println();
        Serial.print("size:");
        Serial.println(size);
        for (int i = 0; i < size; i++) {
          uint8_t c = client.read();
          Serial.print(c);
        }
      }

      delay(1);
    }
    client.stop();
    Serial.println();
    Serial.println("client.stop()");
  }

server.available()が取得できる場合には、接続してきたクライアントがいるってことになります。client.connected()で接続状況を確認しつつ、client.available()がある場合は受信したデータがあります。

client.read()で実際のデータを受信しているデータですね。結構シンプルに使えそうです。

クライアント側

上記のアプリを使わせてもらいました。

上記みたいな画面で、ESP32のIPアドレスの5000番ポートに対して通信を行います。最初に接続をしてからTEXT送信をすることで結果が表示されます。

同じようなツールはいろいろあるので、何を使ってもかまわないと思います。

文字列のパース

#include <WiFi.h>
#include <WiFiClient.h>

WiFiServer server(5000);

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println();

  WiFi.begin();
  Serial.print("WiFi.begin()");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
  Serial.println("WiFi Connected.");
  Serial.printf("IP Address  : ");
  Serial.println(WiFi.localIP());

  server.begin();
  Serial.println("server.begin()");
}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    Serial.println("New Client.");
    while (client.connected()) {
      int size = client.available();
      if (size) {
        Serial.println();
        Serial.print("size:");
        Serial.println(size);

        char input[256];
        memset(input, 0, sizeof(input));
        client.read((uint8_t*)input, sizeof(input) - 1);
        Serial.printf("get: [%s]\n", input);

        char* command0 = strtok(input, ":");
        String command = command0;
        command0 = strtok(NULL, ":");
        String command2 = command0;
        command0 = strtok(NULL, ":");
        String command3 = command0;
        command0 = strtok(NULL, ":");
        String command4 = command0;

        command.trim();
        command2.trim();
        command3.trim();
        command4.trim();
        Serial.printf("command: [%s]\n", command);
        Serial.printf("command2: [%s]\n", command2);
        Serial.printf("command3: [%s]\n", command3);
        Serial.printf("command4: [%s]\n", command4);

        String ret = "";
        if (command == "*IDN?") {
          uint8_t mac[6];
          esp_read_mac(mac, ESP_MAC_WIFI_STA);
          char mac_str[18];
          snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

          ret = String("ESP32,VISA Test,") + mac_str + "," + __DATE__"\n";
          Serial.printf("\n");
        } else if (command == "*RST") {

        }

        if (ret != "") {
          client.write(ret.c_str());
        }
      }

      delay(1);
    }
    client.stop();
    Serial.println();
    Serial.println("client.stop()");
  }

  delay(1);
}

ソケット通信で文字列のコマンドを投げて、それに対応する場合のスケッチ例です。

受信

        char input[256];
        memset(input, 0, sizeof(input));
        client.read((uint8_t*)input, sizeof(input) - 1);
        Serial.printf("get: [%s]\n", input);

先ほどとあまり変わりませんが、256文字分のバッファを用意して一括して受信しています。これって256文字にNULL文字含んでいるのかな。とりあえず-1しておきます。

スプリット

        char* command0 = strtok(input, ":");
        String command = command0;
        command0 = strtok(NULL, ":");
        String command2 = command0;
        command0 = strtok(NULL, ":");
        String command3 = command0;
        command0 = strtok(NULL, ":");
        String command4 = command0;

今回は:の文字で分割しています。C言語だとかんたんに文字列を分割する方法がないので、上記みたいな方法を使っています。aaa:bbb:ccc:dddみたいな文字列がcommandにはaaa、command2にはbbbと分割してくれます。通常はスペース区切りとかが多いと思います。

コマンド処理

        String ret = "";
        if (command == "*IDN?") {
          uint8_t mac[6];
          esp_read_mac(mac, ESP_MAC_WIFI_STA);
          char mac_str[18];
          snprintf(mac_str, sizeof(mac_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);

          ret = String("ESP32,VISA Test,") + mac_str + "," + __DATE__"\n";
          Serial.printf("\n");
        } else if (command == "*RST") {

        }

とりあえず*IDN?という文字列の場合には、MACアドレスとビルドした日を返す処理だけを入れてみました。

送信

        if (ret != "") {
          client.write(ret.c_str());
        }

コマンドの結果がある場合には送信するだけのコードです。

まとめ

結構かんたんにソケット通信することができました。基本的にはSerialの処理と同じですね。このへんはラッパーされている関数群があるのでわかりやすいですね。

コメント