ESP-WROOM-02で鉄道模型を遠隔制御（第４回）


#概要
　鉄道模型をマイコン制御してやろうと思われている皆さん、こんにちは。綾瀬です。ESP-WROOM-02を使って鉄道模型を制御してきたことをまとめていくシリーズの第４回の今回は、無線LANで接続したデバイス（タブレット端末やスマートフォンなど）から鉄道模型車両を遠隔制御するために、前回ESP-WROOM-02に構築したWebサーバにWebAPIを実装してみたいと思います。
　今回実装するWebAPIは、無線LAN内蔵SDカードである東芝FlashAirのWebAPIと互換性を持たせます。これは、私がこれまでFlashAirを使って製作した鉄道模型制御装置と、クライアントアプリを共用できるようにするためです。

　本シリーズの過去記事は以下を参照してください。
　[第１回](http://qiita.com/ayasehiro/items/8cab78a938299d5e6594)
　[第２回](http://qiita.com/ayasehiro/items/50394987324e63f665cd)
　[第３回](http://qiita.com/ayasehiro/items/9a685c2e1a2c895183b3)

#構成

###ハードウェア構成
　今回は[第２回](http://qiita.com/ayasehiro/items/50394987324e63f665cd)で製作した構成をそのまま使います。

###ソフトウェア構成
　今回構築するソフトウェアの構成を簡単なイラストで示します。
![image1.png](https://qiita-image-store.s3.amazonaws.com/0/89137/60f6c3f6-a5e9-f597-ef8c-88f7a62d55de.png)

　第３回の記事で作成したのは、このイラストのWebAPIを作成するための土台であるWebサーバを構築しました。今回は、ここにWebAPIを実装します。
　WebAPIでは制御コマンドを受け取って、そのままPWM出力しても良いのですが、FlashAirのWebAPI互換とするため、今回は共有メモリ領域を確保し、ここにステータス情報を定義します。ステータス情報には、速度値と進行方向を設けます。

![image2.png](https://qiita-image-store.s3.amazonaws.com/0/89137/40f7753d-8720-76d8-84ee-5edac35ceb1e.png)

　このステータス情報を、WebAPIから変更できるようにします。PWM出力処理は、周期的にステータス情報を参照して、変化に応じてPWM出力処理を行うようにします。
　こうすることで、WebAPIで制御コマンドを受け取る処理とPWM出力処理を非同期に処理することができます。

###WebAPIの仕様
　今回作成するWebAPIは、共有メモリ領域のステータス情報を変更するものとします。ついでにステータス情報を参照できるようにもします。WebAPIの仕様は、FlashAirのWebAPI互換とします。
　FlashAirのAPIリファレンスは、[FlashAir Developers](https://flashair-developers.com/ja/documents/api/)で公開されています。今回、互換性を持たせるAPIは、command.cgi内の以下のAPIです。

- [共有メモリからのデータの取得 (op=130)](https://flashair-developers.com/ja/documents/api/commandcgi/#130)
- [共有メモリへのデータの書き込み (op=131)](https://flashair-developers.com/ja/documents/api/commandcgi/#131)


#準備
　前回同様、ESP-WROOM-02をArduino化して使用します。そのため、[Arduino core for ESP8266](https://github.com/esp8266/Arduino)をあらかじめArduino IDEに組み込んでおきます。


#ソースコード
　前回実装したWebサーバのソースコードに以下の実装をします。

- 共有メモリ領域shaerdMemを定義
- setup関数内で共有メモリ領域shaerdMemを初期化
- 共有メモリの書き込み／読み出しのWebAPIであるhandleCommand関数の処理を実装
- [第２回](http://qiita.com/ayasehiro/items/50394987324e63f665cd)で実装したPWM出力制御処理を追加
- 共有メモリ上のステータス情報を参照し、変化があれば処理を行うshaerdMemCheck関数を実装
- loop関数にWebサーバの接続要求待ちを定義
- loop関数にshaerdMemCheck関数の呼び出しを定義

```webserver_parts.ino
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
/*---------------------------------------------------------------------------
//定数定義
---------------------------------------------------------------------------*/
ESP8266WebServer server(80); //Webサーバの待ち受けポートを標準的な80番として定義します

String shaerdMem;            //共有メモリを定義
const int shareMenSize = 512;//共有メモリのサイズを定義

const int pwmPinR = 4;       //PWM出力ピンをIO4で定義（Cerevoのブレイクアウトボードでは10番ピン）
const int pwmPinL = 5;       //PWM出力ピンをIO5で定義（Cerevoのブレイクアウトボードでは14番ピン）
      int status_speed = 0;  //速度の情報
      int status_run_lr = 0; //進行方向の情報
/*---------------------------------------------------------------------------
//サーバリクエスト受信時の正常処理 ルート
---------------------------------------------------------------------------*/
void handleRoot() {
　//ルートにアクセスされた時の処理を書く。
  //ここではESP8266で応答していることと、ESP8266が接続しているアクセスポイントから取得したIPアドレスを返す。
  //DHCPでIPを取得している場合に便利。
  IPAddress myAddr = WiFi.localIP();  
  String mes = "hello from esp8266! IP address:" + String(myAddr[0]) + "." + String(myAddr[1]) +"." +  String(myAddr[2]) +"." +  String(myAddr[3]) + "\n";
  server.send(200, "text/plain", mes);
}
/*---------------------------------------------------------------------------
//サーバリクエスト受信時の正常処理 command.cgi
//
//共有メモリへのデータ書き込み要求
//GET /command.cgi?op=131&ADDR=0&LEN=8&DATA=01234567 HTTP/1.1
//op: 131がデータ書き込みを示す
//ADDR:書き込み開始位置、LEN:書き込みデータの長さ、DATA:書き込むデータ
//共有メモリからデータ読み出し要求
//GET /command.cgi?op=130&ADDR=0&LEN=8 HTTP/1.1
//op: 130がデータ読み出しを示す
//ADDR:読み出し開始位置、LEN:読み出しデータの長さ
---------------------------------------------------------------------------*/
void handleCommand() {
  if(server.method() != HTTP_GET) return;
  String rtnStr = "ERROR";
  String command_cmd;
  String command_addr;
  String command_len;
  String command_data;
  
  for (uint8_t i=0; i<server.args(); i++){
    String strCMD = server.argName(i);
    if(strCMD == "op"){
      command_cmd = server.arg(i);
    }
    else if(strCMD == "ADDR"){
      command_addr = server.arg(i);
    }
    else if(strCMD == "LEN"){
      command_len = server.arg(i);
    }
    else if(strCMD == "DATA"){
      command_data = server.arg(i);
    }
  }

  if(command_cmd == "130"){
      char pbuf[4];
      command_addr.toCharArray(pbuf, sizeof(pbuf));
      int iPos = atoi(pbuf);
      command_len.toCharArray(pbuf, sizeof(pbuf));
      int iLen = atoi(pbuf);

      if(iPos >= shareMenSize){
        server.send(200, "text/plain", rtnStr);
        return;
      }
      if(iLen > shareMenSize - iPos) iLen = shareMenSize - iPos;

      rtnStr = shaerdMem.substring(iPos, iPos + iLen);
  }
  else if(command_cmd == "131"){
      String leftShaerdMem;
      String rightShaerdMem;
            
      char pbuf[4];
      command_addr.toCharArray(pbuf, sizeof(pbuf));
      int iPos = atoi(pbuf);
      command_len.toCharArray(pbuf, sizeof(pbuf));
      int iLen = atoi(pbuf);
      int iLenData = command_data.length();
      
      if(iPos >= shareMenSize){
        server.send(200, "text/plain", rtnStr);
        return;
      }
      if(iLen > shareMenSize - iPos) iLen = shareMenSize - iPos;
      if(iLenData < iLen){
        iLen = iLenData;
      }
      for(int idx = iPos; idx < iPos + iLen; ++idx){
        shaerdMem.setCharAt(idx, command_data[idx - iPos]);
      }
      rtnStr = "SUCCESS";
  }
  server.send(200, "text/plain", rtnStr);
  return;
}
/*---------------------------------------------------------------------------
//サーバリクエスト受信時の異常処理
---------------------------------------------------------------------------*/
void handleNotFound(){
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET)?"GET":"POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i=0; i<server.args(); i++){
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}
/*---------------------------------------------------------------------------
//初期化処理
---------------------------------------------------------------------------*/
void setup() {
  Serial.begin(115200);
  delay(10);

  pinMode(pwmPinR, OUTPUT);   //PWM出力ピン１を出力に定義
  pinMode(pwmPinL, OUTPUT);   //PWM出力ピン２を出力に定義
  analogWrite(pwmPinR, 0);    //PWM出力ピン１の出力値を0に定義
  analogWrite(pwmPinL, 0);    //PWM出力ピン２の出力値を0に定義
 
  status_speed = 0;           //速度情報を0に初期化
  status_run_lr = 0;          //進行方向情報を0に初期化

  for(int idx = 0; idx < shareMenSize; idx++){
    shaerdMem[idx] = '0';     //共有メモリを初期化
  }

  //自分のSSIDとパスコードを設定する
　//mySSIDとmyPassは任意のものに書き換えること
  Serial.println("WiFi module setting... ");
  WiFi.softAP("mySSID", "myPass");
  Serial.print("IP address: ");
  Serial.println(WiFi.softAPIP());

  //アクセスポイント（myAPSSID）に接続する（20回接続要求して失敗したらエラー表示）
　//myAPSSIDとmyAPPassは自分の環境のものに書き換えること
  Serial.print("Connecting to ");
  WiFi.begin("myAPSSID", "myAPPass");
  int retry_sum = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    retry_sum++;
    if(retry_sum > 20) break;
  }
  if(WiFi.status() == WL_CONNECTED){
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
  }
  else{
    Serial.println("WiFi not connected");
  }

  //Webサーバの設定を行い、サーバを起動する
  server.on("/", handleRoot);               //ルートに接続要求があった時の処理を指定
  server.on("/command.cgi", handleCommand); //command.cgiに接続要求があった時の処理を指定
  server.onNotFound(handleNotFound);        //設定外に接続要求があった時の処理を指定
  server.begin();                           //Webサーバを起動
  Serial.println("HTTP server started");
}
/*---------------------------------------------------------------------------
//PWM出力制御
//speed_num: 速度（0〜200）
//LR: 1:右方向　0:左方向
---------------------------------------------------------------------------*/
void funcspeed_run(int speed_num, int LR){
  if(status_speed == speed_num && status_run_lr == LR) return;
  if(speed_num < 0 || speed_num > 200) return;
  
  int speed_setnum = speed_num;
  if(speed_setnum > 0) speed_setnum = speed_setnum + 50;
  
  if(LR == 1){
    status_run_lr = 1;
    analogWrite(pwmPinR, speed_setnum*4);
    analogWrite(pwmPinL, 0);
  }
  else{
    status_run_lr = 0;
    analogWrite(pwmPinR, 0);
    analogWrite(pwmPinL, speed_setnum*4);
  }
  status_speed = speed_num;
}
/*---------------------------------------------------------------------------
//共有メモリをチェックする
//shaerdMem [ 0000 0000 ]
//先頭から3文字が速度情報(000〜200）、4文字目が進行方向(1:右方向、0：左方向）を示す。
---------------------------------------------------------------------------*/
void shaerdMemCheck(){
  String tmpStr;
  char pbuf[4];
  tmpStr = shaerdMem.substring(0, 3);
  tmpStr.toCharArray(pbuf, sizeof(pbuf));
  int target_speed = atoi(pbuf);

  tmpStr = shaerdMem.substring(3, 4);
  if(tmpStr == "1"){
    funcspeed_run(target_speed, 1);
  }
  else{
    funcspeed_run(target_speed, 0);
  }
}
/*---------------------------------------------------------------------------
//本体処理
---------------------------------------------------------------------------*/
void loop() {
  server.handleClient(); //Webサーバの接続要求待ち
  shaerdMemCheck();      //共有メモリのステータス情報を参照し変化があれば処理を行う
}
```


#まとめ
　以上の通り、ESP-WROOM-02をArduino化して、WebAPIを実装しました。
　WebAPIですので、ESP-WROOM-02に無線LANで接続したデバイスのブラウザからアクセスすることで、鉄道模型を制御することができます。
　例えば、以下のURIでアクセスします。

```c:鉄道模型車両を右方向（1）に速度50で走らせる
http://192.168.4.1/command.cgi?op=131&ADDR=0&LEN=8&DATA=05010000
```

```c:鉄道模型車両を左方向（0）に速度80で走らせる
http://192.168.4.1/command.cgi?op=131&ADDR=0&LEN=8&DATA=08000000
```

```c:鉄道模型車両を停止させる（どちらでもよい）
http://192.168.4.1/command.cgi?op=131&ADDR=0&LEN=8&DATA=00010000
http://192.168.4.1/command.cgi?op=131&ADDR=0&LEN=8&DATA=00000000
```

```c:現在の速度と進行方向を参照する
http://192.168.4.1/command.cgi?op=130&ADDR=0&LEN=8
```

#次回予告
　次回は、今回作成したWebAPIを使って鉄道模型を制御するクライアントアプリを作成します。


#参考資料

- [Arduino core for ESP8266](https://github.com/esp8266/Arduino)
- [FlashAir Developers APIガイド](https://flashair-developers.com/ja/documents/api/)

#最後に宣伝
　新刊原稿は無事入稿されました！事故がなければ、新刊出ますよ〜。

　コミックマーケット89において、ESP-WROOM-02とFlashAirの電子工作における比較記事を掲載した電子工作（と酒）の同人誌を頒布します。ESP-WROOM-02とFlashAirで比較製作したGゲージ鉄道模型車両の遠隔制御装置の回路図も掲載しています。冬コミにお越しの際は、ぜひお寄りください。また、冬コミ後には通販でも入手できると思います。
**12/31(木) ３日目 東メ－08a「空と月」**
![サ－クルカット.png](https://qiita-image-store.s3.amazonaws.com/0/89137/15a2c023-0a31-4688-69ec-69c9be008981.png)