Arduino UNO R4 WiFiをサーバにする・その2

Arduino UNO R4

クライアントから送信されてきたデータを受け取る

前回の極限機能限定サーバ『HelloWorldサーバ』では、Arduino UNO R4 WiFiをサーバとして機能させるための必要最小限のプログラムを作成しました。これはクライアントからの接続リクエストがあったら問答無用で『Hello,World!』という文字列をクライアントに送信し、すぐ通信を終了という非常に単純なのものでした。

今回は応用範囲が広がるように、クライアントから送信されたデータをサーバ側が受け取る方法を実験してみます。

オウム返しサーバ

今回作成するサーバの仕様は、

クライアントから送信された1行の文字列を、そのままクライアントに返す

です。言われた言葉をそのまま返すことから『オウム返しサーバ』と名付けます(名付けるほどの価値はありませんが…)。これだけだと通信を切断する条件が判らないので、

『クライアントから “exit” という文字列を送信されたら、通信を切断する』

という仕様も追加しておきましょう。

スケッチ

では、さっそくスケッチを見てみましょう。

C++
#include "WiFiS3.h"

// 無線LAN接続情報は別ファイルに移した
#include "arduino_secrets.h"
char ssid[] = _SSID;
char pass[] = _PASS;

// リスンポートを23番としてサーバのインスタンスを作成
WiFiServer server(23);

void setup(){
  // シリアルインターフェイスの初期化
  Serial.begin(9600);
  delay(1000);

  // WiFiモジュールの存在確認
  if(WiFi.status() == WL_NO_MODULE){
    Serial.println("WiFiモジュールがありません");
    while(true);
  }

  // WiFiファームウェアバージョンの確認
  String fv = WiFi.firmwareVersion();
  if(fv < WIFI_FIRMWARE_LATEST_VERSION){
    Serial.println("ファームウェアが最新のものではありません");
  }

  // 無線LANへの接続
  Serial.print("接続中...");
  while(true){
    if(WiFi.begin(ssid, pass) == WL_CONNECTED)break;
    Serial.print(".");
    delay(1000);
  }
  Serial.println("完了");

  // Arduinoに割り当てられたIPアドレスの確認
  Serial.print("IPアドレス:");
  Serial.println(WiFi.localIP().toString());

  // サーバの起動
  server.begin();
}

void loop(){
  WiFiClient client = server.available();
  if(client){
    // クライアントと接続した時の処理
    while(true){

      // 1行入力
      String currentLine = "";
      while(true){
        if(!client.connected()){
          currentLine = "exit";
          break;
        }
        if(client.available()){
          // 受信した文字がある場合
          char c = client.read();
          if(c == '\n'){
            break;
          }
          if(c != '\r'){
            currentLine += c;
          }
        }
      }
      // もし入力文字列が"exit"だったら終了
      if(currentLine=="exit"){
        client.stop();
        break;
      }else{
        // クライアントに『オウム返し』      
        client.println(currentLine);
        Serial.println(currentLine);
      }
    }
  }
}

別ファイル arduino_secrets.h を以下の内容で作成し、メインの .ino ファイルと同じフォルダに保存しておきます。Arduino IDE で別ファイルを作る方法はこちら

もちろん、”your SSID”と”your PASSWORD”の部分は、ご利用中の無線LANのSSIDとPASSWORDに書き換えてください。

C++
#define _SSID "your SSID"
#define _PASS "your PASSWORD"

プログラムの解説

ヘッダ部及びsetup()の中身は前回の helloworldサーバとほとんど同じですので、変更した部分のみ解説します。

無線LANの接続情報
C++
// 無線LAN接続情報は別ファイルに移した
#include "arduino_secrets.h"
char ssid[] = _SSID;
char pass[] = _PASS;

今回は無線LANの接続情報(SSIDとパスワード)を別ファイル arduino_secrets.h に移しました。こうしておけば間違えて自分が使っている無線LANの接続情報を他人に漏らしてしまう危険が減ります。

プログラムをブログに掲載したりgithubで公開したりコピーして他人に渡したり、という機会が絶対にないという人は従来通りでも何も問題ないと思います。

ファームウェアバージョンの確認
C++
  // WiFiファームウェアバージョンの確認
  String fv = WiFi.firmwareVersion();
  if(fv < WIFI_FIRMWARE_LATEST_VERSION){
    Serial.println("ファームウェアが最新のものではありません");
  }

ファームウェアが最新ではない場合にアップデートを促すメッセージを表示するという処理です。

WiFiモジュールのファームウェアのバージョンを確認・最新でなければその旨メッセージをシリアル出力する処理を追加しました。まぁサーバの動作に必須の処理ではないので省略しても構わないのですが、Arduino公式が公開しているWiFi関係サンプルプログラムのほとんどすべてに記述されているので。

…というわけで、setup()関数の中身は前回とほとんど変わりません。

クライアントと接続した時の処理
C++
void loop(){
  WiFiClient client = server.available();
  if(client){
    // クライアントと接続した時の処理
    while(true){

      // 1行入力
      String currentLine = "";
      while(true){
        if(!client.connected()){
          currentLine = "exit";
          break;
        }
        if(client.available()){
          // 受信した文字がある場合
          char c = client.read();
          if(c == '\n'){
            break;
          }
          if(c != '\r'){
            currentLine += c;
          }
        }
      }
      // もし入力文字列が"exit"だったら終了
      if(currentLine=="exit"){
        client.stop();
        break;
      }else{
        // クライアントに『オウム返し』      
        client.println(currentLine);
        Serial.println(currentLine);
      }
    }
  }
}

まず、新たに接続したクライアントを server.available()関数で確認・取得して、if(client) でクライアントがあった場合の処理を行う点は前回の helloworldサーバと同じです。

また、helloworldサーバとは異なり、今回は一方的にメッセージを送信してすぐ終了ではなく、クライアントとサーバの間で繰り返しやりとりをする想定なので、while(true) でループを作っています。

一行入力
C++
      // 1行入力
      String currentLine = "";
      while(true){
        if(!client.connected()){
          currentLine = "exit";
          break;
        }
        if(client.available()){
          // 受信した文字がある場合
          char c = client.read();
          if(c == '\n'){
            break;
          }
          if(c != '\r'){
            currentLine += c;
          }
        }
      }

『オウム返し』するためにはクライアントから送信されてきた文字列を1行単位でまとめて取得する必要があります。しかしクライアントから送られてくるデータが読もうとした時点で1行分以上到着しているとは限りません。そこで『1文字ずつ取得して文字列として連結する』という作業を繰り返し『改行記号が来たら終了』という処理を行っています。

まず、

C++
      // 1行入力
      String currentLine = "";

あたらしい行の入力を開始するために、受信文字列を表す変数 currentLine をクリアします。

C++
      while(true){
        if(!client.connected()){
          currentLine = "exit";
          break;
        }
        if(client.available()){
          // 受信した文字がある場合
          char c = client.read();
          if(c == '\n'){
            break;
          }
          if(c != '\r'){
            currentLine += c;
          }
        }
      }

『1文字ずつ受信して文字列として連結する』の基本的な部分はこの通りです。

抜き出して書くと以下のようになります。

C++
while(true){
  if(client.available()){
    char c = client.read();
    currentLine += c;
  }
}

まず、クライアントから送信されてきたデータがあるかどうかをチェックするのが WiFiClient.available()関数です。

書式
C++
int WiFiClient.available()
返却値

ストリームで利用可能なバイト数(クライアントから送信されてきたデータのうち、未読のもののバイト数)を整数値で返します。

C/C++の場合、条件式の代わりに結果が数値となる式を使用すると『0ならfalse、0以外ならtrue』と読み替えますので、今回のプログラム例にある『 if(client.available()) 』という部分は『まだ読んでいない受信データがあれば』という判断をしていることになります。

受信データが残っている場合、残っているうちの最初の1バイトを読み込みます。
読み込みには WiFiClient.read()関数を使用します。

書式
C++
int WiFiClient.read()
返却値

クライアントから送信されてきたデータを1バイト取得して返します(0~255)。取得に失敗した場合は負の値を返します。

今回のプログラム例では、コードをできるだけ簡単にするためclient.read()関数が負値を返した場合の処理は記述していません。

そして、『currentLine += c』で、文字列変数 currentLine の末尾に文字 c を追加しています。

これを無限に繰り返します。

改行コードの扱い

ここまでだと一行入力どころか永久に入力待ちを続けてしまいますので、『改行コード』を受信したらそこまでの入力内容を『一行』として処理を終了しなければなりません。そのための処理を考えてみましょう。

さて、一口に『改行コード』といっても、実はコンピュータデータで『改行』を表すコードは3種類あります。深く突っ込み始めるとそれだけで長文の記事になりそうな話なので要点だけまとめると下の表のとおりです。

名称16進表記エスケープ文字使われる環境
CR(改行)0x0D\rver.9以前のMac OSなど
LF(復帰)0x0A\nUNIX、Linux、X以降のmacOSなど
CR(改行) LF(復帰)0x0D 0x0A\r\nMS-DOS、Windowsなど

コードをできるだけ簡単にするため、この3種のうち今後使用頻度が減りそうな『CR(改行)だけ』というコードは対応を諦めることにしました。そうすると、

  • 『LF(復帰)』=『\n』が来たら1行終了
  • 『CR(改行)』=『\r』は読み飛ばす

という単純な処理で下2種の改行コードに対応可能です。該当するコードは以下の部分です。

C++
      while(true){
        if(!client.connected()){
          currentLine = "exit";
          break;
        }
        if(client.available()){
          // 受信した文字がある場合
          char c = client.read();
          if(c == '\n'){
            break;
          }
          if(c != '\r'){
            currentLine += c;
          }
        }
      }

読み込んだ1文字 c が ‘\n’(LF)だった場合、breakで一行入力のループを脱出します。その時点での文字列変数currentLineの内容が、入力された『一行』になります。

読み込んだ1文字 c が ‘\r’(CR)だった場合、単に読み飛ばします。’\r’以外だった場合のみ、文字列変数currentLine に文字 c を連結します。

意図しない切断時の処理
C++
      while(true){
        if(!client.connected()){
          currentLine = "exit";
          break;
        }

最後にこの部分ですが、これはクライアントとの接続が維持されているかをチェックしています。チェックには WiFiClient.connected()関数を使用しています。

書式
C++
uint8_t WiFiClient.connected()
返却値
意味
0クライアントとの接続が切断された
0以外クライアントとの接続が保たれている

1行入力は無限ループになっているので、もしその間にクライアントとの接続が切断してしまった場合にはループを脱出します。このとき、”exit”が入力されたものとすることによりサーバ側でもクライアントとの切断処理を行うようにしてあります。

C/C++だとJavaのような例外のスローができないので、エラー処理をスマートに書くのが難しいです…。

ネット上でみかけるコード例には、WiFiClient.connected()の判定をするタイミングが不適切で、通信中にtelnetクライアントを閉じてしまうとサーバが無限ループから脱出できなくなるバグのあるものが多く見られます。

1行入力が終わった後の処理
C++
      // もし入力文字列が"exit"だったら終了
      if(currentLine=="exit"){
        client.stop();
        break;
      }else{
        // クライアントに『オウム返し』      
        client.println(currentLine);
        Serial.println(currentLine);
      }

一行入力のwhile(true)ループを抜けた後の処理です。この時点で文字列変数currentLineには読み込んだ一行(またはクライアントと切断してしまった場合に”exit”)が入っています。なお『改行コードまでが一行』と定義しましたがcurrentLine変数の末尾には改行コードを含んでいません。

もしcurrentLineが “exit” の場合は、クライアントとの切断処理をします。クライアントとの切断処理には WiFiClient.stop()関数を使用します。

書式
C++
void WiFiClient.stop()

この関数には引数も返却値もありません。

クライアントとの切断後はbreakでループを脱出します。

currentLineが “exit” 以外の場合は、クライアントにその文字列を WiFiClient.println()関数で送信します(ここが『オウム返し』)。また動作確認用に同じ文字列をSerial.println()関数でシリアル出力します。出力後はループを継続し、1行入力待ちに戻ります。

動作確認

Windowsのコマンドプロンプトのtelnetコマンドで接続する

telnetコマンドはデフォルトではエコーバックしない(入力した文字が画面に表示されない)ので、一手間掛ける必要があります。

1.Arduino側でサーバを起動する

Arduino側でサーバを起動します。シリアルモニタにArduinoが取得したIPアドレスが表示されるので確認しておきます(この例では 192.168.0.15)

2.Windowsのコマンドプロンプトでtelnetコマンドを実行する

Windowsのコマンドプロンプトで、『telnet <ArduinoのIPアドレス>』を実行します。<ArduinoのIPアドレス>は先ほど確認した値です。

3.telnetのコマンドモードに入る

telnetコマンドでArduinoに接続すると一度画面がクリアされるので、キーボードのctrlキーと]キーを同時に押します。すると上の画面写真のようなメッセージが表示され、コマンドを入力できるようになります。

4.ローカルエコーを設定する

『set localecho』と入力し、Enterキーを押します。上の写真のように『ローカルエコー:オン』と表示されれば設定完了です。

この設定は保存されないので、面倒ですがtelnetコマンドを使用するたびにやりなおさなければなりません。

5.オウム返し動作開始

もう一度Enterキーを押すと画面がクリアされます。そのあとは好きな言葉を打ち込んでみてください。 Enterキーを押すたびに、それまで入力した言葉が表示されます。

“exit”と入力すると通信が切断され、コマンドプロンプトに戻ります。

動画
TeraTermで接続する

TeraTermで接続する場合も少々設定が必要です。

1.『新しい接続』のウィンドウをいったん閉じる

TeraTermを起動すると自動的に『新しい接続』ダイアログが開くので、『キャンセル』をクリックしていったんダイアログを閉じます。

2.『端末の設定』ダイアログ

メニューの『設定』→『端末』を選択します。

『端末の設定』ダイアログが開くので、

①改行コードを『受信』『送信』ともに『LF』にします。
②『ローカルエコー』にチェックを入れます。
③『OK』ボタンをクリックします。

3.『TCP/IP設定』ダイアログ

メニューの『設定』→『TCP/IP』を選択します。

『TCP/IP設定』ダイアログが開くので、

①『自動的にウィンドウを閉じる』のチェックを外します。
②『OK』ボタンをクリックします。

4.Arduinoに接続

メニューの『ファイル』→『新しい接続』を選択します。

『新しい接続』ダイアログが開くので、

①TCP/IPを選択します。
②Arduinoに割り当てられたIPアドレスを入力します。
③『サービス』で『Telnet』を選択します。
④『OK』ボタンをクリックします。

5.オウム返し動作開始

そのあとは好きな言葉を打ち込んでみてください。 Enterキーを押すたびに、それまで入力した言葉が表示されます。

“exit”と入力すると通信が切断されます。

動画

まとめ

  • クライアントから送られてきたデータがあるか調べるにはWiFiClient.available()関数を使う
  • クライアントから送られてきたデータはWiFiClient.read()関数で1バイトずつ取得する

コメント

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