ESP-WROOM-02でLinuxサーバーを操作する

FTP + pyinotify CUI編


ESPからLinuxサーバー(RaspberryPiやOrangePi)を操作できたら面白いと以前から考えていました。
Linuxサーバーをリモートで操作するにはrsh/rexec/sshなどを使うのが一般的ですが、ESP側にこれらを実装するのは敷居が高い です。
いろいろ調べたら、pyinotifyでLinux側のファイルやディレクトリを監視できることが分かりました。
pyinotifyについては、こちらこちらで 紹介されています。
そこで、FTP+pyinotifyを使って、ESPからLinuxサーバーを操作する方法を紹介します。
手順は以下の様になります。
RaspberryPi/OrangePi ESP8266
FTPサーバを立てる
pyinotifyで特定のディレクトリを監視する

FTPクライアントを使ってファイルを書き込む
ファイル名に応じたコマンドを実行する

FTPサーバーを立てる

Linuxサーバ側にFTPサーバーを立てます。私はvsftpdを使いました。
インストール方法はこ ちらこちらに 紹介されています。

python+pyinotifyで特定のディレクトリを監視する

pyinotifyのインストールは以下の通りです。
$ sudo apt-get install python-pip
$ sudo pip install pyinotify

次に、監視対象のフォルダーを作ります。
私は「/home/pi/inotify」としました。

pythonのプログラム(ftp_exec.py)は以下の通りです。
こちらを 参考に作りました。
「/home/pi/inotify」ディレクトリを監視し、ファイル更新があれば、ファイル名に応じたコマンドを実行します。
「start_led」に変化があったとき:「start_led.sh」を実行
「stop_led」に変化があったとき:「stop_led.sh」を実行
「hoge」に変化があったとき:「hoge.sh」を実行
「hogehoge」に変化があったとき:「hogehoge.sh」を実行
import pyinotify
import os.path

wm = pyinotify.WatchManager()  # Watch Manager

#mask = pyinotify.IN_MODIFY  # watched events
mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY
#mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE


class EventHandler(pyinotify.ProcessEvent):

    def process_IN_CREATE(self, event):
        print event
        print "Create:", event.pathname


    def process_IN_DELETE(self, event):
        print event
        print "Delete:", event.pathname

    def process_IN_MODIFY(self, event):
        print event
        print "Modify:", event.pathname
        basename = os.path.basename(event.pathname)
        print "basename:", basename
        command = "/home/pi/inotify/" + basename + ".sh"
        print "command:", command
        if os.path.exists(command):
            os.system(command)


handler = EventHandler()
notifier = pyinotify.Notifier(wm, handler)
wdd = wm.add_watch('/home/pi/inotify', mask, rec=True)
notifier.loop()

サンプルとして以下の2つのスクリプトを作ります。

  • /home/pi/inotify/start_led.sh
  • オンボードLEDをフラッシュを開始するスクリプト
    #!/bin/bash
    sudo sh -c "echo none >/sys/class/leds/led0/trigger"
    sudo sh -c "echo heartbeat >/sys/class/leds/led0/trigger"

  • /home/pi/inotify/stop_led.sh
  • オンボードLEDのフラッシュを停止するスクリプト
    #!/bin/bash
    sudo sh -c "echo none >/sys/class/leds/led0/trigger"
    sudo sh -c "echo 0 >/sys/class/leds/led0/brightness"

    FTPクライアントを使ってファイルを書き込む

    ESP側のFTPクライアントはこ ちらを参考に作りました。(というかほぼそのままです)
    エラー処理の一部に「ん??」というコードが有りますが、面倒なのでオリジナルのままです。
    FTPサーバーのユーザ名、パスワード、IPアドレス(192, 168, 10, 15)は、自分の環境に合わせて変更してください。
    // File: WiWi_FTP_Client.ino for ESP8266 NodeMCU
    /*
       Original Arduino: FTP passive client
       http://playground.arduino.cc/Code/FTP
       Modified 6 June 2015 by SurferTim

       You can pass flash-memory based strings to Serial.print() by wrapping them with F().

       2015-12-09 Rudolf Reuter, adapted to ESP8266 NodeMCU, with the help of Markus.
    */
    #include <ESP8266WiFi.h>
    #include <FS.h>

    // Set these to your desired softAP credentials. They are not configurable at runtime.
    const char* ssid = "アクセスポイントのSSID";
    const char* password = "アクセスポイントのパスワード";
    const char* hostuser = "FTPサーバーのユーザ名";
    const char* hostpasswd = "FTPサーバーのパスワード";

    //boolean debug = false;  // true = more messages
    boolean debug = true;

    // LED is needed for failure signalling
    //const short int BUILTIN_LED2 = 16;  //GPIO16 on NodeMCU (ESP-12)
    const short int BUILTIN_LED2 = 0;

    // provide text for the WiFi status
    const char *str_status[]= {
      "WL_IDLE_STATUS",
      "WL_NO_SSID_AVAIL",
      "WL_SCAN_COMPLETED",
      "WL_CONNECTED",
      "WL_CONNECT_FAILED",
      "WL_CONNECTION_LOST",
      "WL_DISCONNECTED"
    };

    // provide text for the WiFi mode
    const char *str_mode[]= { "WIFI_OFF", "WIFI_STA", "WIFI_AP", "WIFI_AP_STA" };

    // change to your server
    IPAddress server( 192, 168, 10, 145 );

    WiFiClient client;
    WiFiClient dclient;

    char outBuf[128];
    char outCount;

    const char* ESP8266File = "/esp8266.txt";

    // SPIFFS file handle
    File fh;

    void signalError() {  // loop endless with LED blinking in case of error
      while(1) {
        if (BUILTIN_LED2 > 0) {
          digitalWrite(BUILTIN_LED2, LOW);
          delay(300); // ms
          digitalWrite(BUILTIN_LED2, HIGH);
          delay(300); // ms
        } // end if
      } // end while
    }

    //----------------------- WiFi handling
    void connectWifi() {
      Serial.print("Connecting as wifi client to SSID: ");
      Serial.println(ssid);

      // use in case of mode problem
      WiFi.disconnect();
      // switch to Station mode
      if (WiFi.getMode() != WIFI_STA) {
        WiFi.mode(WIFI_STA);
      }

      WiFi.begin ( ssid, password );

      if (debug) WiFi.printDiag(Serial);

      // ... Give ESP 10 seconds to connect to station.
      unsigned long startTime = millis();
      while (WiFi.status() != WL_CONNECTED && millis() - startTime < 10000) {
        delay(500);
        Serial.print(".");
      }
      Serial.println("");
      // Check connection
      if (WiFi.status() == WL_CONNECTED) {
        Serial.print("IP address: ");
        Serial.println(WiFi.localIP());
      } else {
        Serial.print("WiFi connect failed to ssid: ");
        Serial.println(ssid);
        Serial.print("WiFi password <");
        Serial.print(password);
        Serial.println(">");
        Serial.println("Check for wrong typing!");
      }
    }  // connectWiFi()

    //----------------- FTP fail
    void efail() {
      byte thisByte = 0;

      client.println(F("QUIT"));

      while (!client.available()) delay(1);

      while (client.available()) {
        thisByte = client.read();
        Serial.write(thisByte);
      }

      client.stop();
      Serial.println(F("Command disconnected"));
      fh.close();
      Serial.println(F("SD closed"));
    }  // efail

    //-------------- FTP receive
    byte eRcv() {
      byte respCode;
      byte thisByte;

      while (!client.available()) delay(1);

      respCode = client.peek();

      outCount = 0;

      while (client.available()) {
        thisByte = client.read();
        Serial.write(thisByte);

        if (outCount < 127) {
          outBuf[outCount] = thisByte;
          outCount++;
          outBuf[outCount] = 0;
        }
      }

      if (respCode >= '4') {
        efail();
        return 0;
      }
      return 1;
    }  // eRcv()

    //--------------- FTP handling
    byte doFTP(char* HostFile) {
      char cmd[64];
     
      fh = SPIFFS.open(ESP8266File, "r");
      if (!fh) {
        Serial.println(F("SPIFFS open fail"));
        return 0;
      }

      if (!fh.seek((uint32_t)0, SeekSet)) {
        Serial.println(F("Rewind fail"));
        fh.close();
        return 0;
      }

      if (debug) Serial.println(F("SPIFFS opened"));

      if (client.connect(server, 21)) {  // 21 = FTP server
        Serial.println(F("Command connected"));
      } else {
        fh.close();
        Serial.println(F("Command connection failed"));
        return 0;
      }

      if (!eRcv()) return 0;
      if (debug) Serial.println("Send USER");
      sprintf(cmd, "USER %s",hostuser);
      client.println(cmd);

      if (!eRcv()) return 0;
      if (debug) Serial.println("Send PASSWORD");
      sprintf(cmd, "PASS %s",hostpasswd);
      client.println(cmd);

      if (!eRcv()) return 0;
      if (debug) Serial.println("Send SYST");
      client.println(F("SYST"));

      if (!eRcv()) return 0;
      if (debug) Serial.println("Send Type I");
      client.println(F("Type I"));

      if (!eRcv()) return 0;
      if (debug) Serial.println("Send PASV");
      client.println(F("PASV"));

      if (!eRcv()) return 0;

      char *tStr = strtok(outBuf, "(,");
      int array_pasv[6];
      for ( int i = 0; i < 6; i++) {
        tStr = strtok(NULL, "(,");
        array_pasv[i] = atoi(tStr);
        if (tStr == NULL) {
          Serial.println(F("Bad PASV Answer"));
        }
      }
      unsigned int hiPort, loPort;
      hiPort = array_pasv[4] << 8;
      loPort = array_pasv[5] & 255;

      if (debug) Serial.print(F("Data port: "));
      hiPort = hiPort | loPort;
      if (debug) Serial.println(hiPort);

      if (dclient.connect(server, hiPort)) {
        Serial.println(F("Data connected"));
      }
      else {
        Serial.println(F("Data connection failed"));
        client.stop();
        fh.close();
        return 0;
      }

      if (debug) Serial.println("Send STOR filename");
      client.print(F("STOR "));
      client.println(HostFile);

      if (!eRcv()) {
        dclient.stop();
        return 0;
      }

      if (debug) Serial.println(F("Writing"));
      // for faster upload increase buffer size to 1460
    //#define bufSizeFTP 64
    #define bufSizeFTP 1460
      uint8_t clientBuf[bufSizeFTP];
      //unsigned int clientCount = 0;
      size_t clientCount = 0;
     
      while (fh.available()) {
          clientBuf[clientCount] = fh.read();
          clientCount++;
          if (clientCount > (bufSizeFTP - 1)) {
            dclient.write((const uint8_t *) &clientBuf[0], bufSizeFTP);
            clientCount = 0;
            delay(1);
          }
      }
      if (clientCount > 0) dclient.write((const uint8_t *) &clientBuf[0], clientCount);

      dclient.stop();
      Serial.println(F("Data disconnected"));

      if (!eRcv()) return 0;

      client.println(F("QUIT"));
      if (!eRcv()) return 0;

      client.stop();
      Serial.println(F("Command disconnected"));

      fh.close();
      if (debug) Serial.println(F("SPIFS closed"));
      return 1;
    }  // doFTP()


    void display_Running_Sketch (void){
      String the_path = __FILE__;
      int slash_loc = the_path.lastIndexOf('/');
      String the_cpp_name = the_path.substring(slash_loc+1);
      int dot_loc = the_cpp_name.lastIndexOf('.');
      String the_sketchname = the_cpp_name.substring(0, dot_loc);

      Serial.print("\nArduino is running Sketch: ");
      Serial.println(the_sketchname);
      Serial.print("Compiled on: ");
      Serial.print(__DATE__);
      Serial.print(" at ");
      Serial.print(__TIME__);
      Serial.print("\n");
    }

    void setup() {
      delay(1000);
      Serial.begin(9600);
      display_Running_Sketch();
      if (BUILTIN_LED2 > 0) {
        // signal start
        pinMode(BUILTIN_LED2, OUTPUT);
        digitalWrite(BUILTIN_LED2, LOW);
        delay(100); // ms
        digitalWrite(BUILTIN_LED2, HIGH);
        delay(300); // ms
      }

      Serial.println ( "Connect to Router requested" );
      connectWifi();
      if (WiFi.status() == WL_CONNECTED) {
        Serial.print("WiFi mode: ");
        Serial.println(str_mode[WiFi.getMode()]);
        Serial.print ("WiFi Status: " );
        Serial.println (str_status[WiFi.status()]);
        if (BUILTIN_LED2 > 0) {
          // signal WiFi connect
          digitalWrite(BUILTIN_LED2, LOW);
          delay(300); // ms
          digitalWrite(BUILTIN_LED2, HIGH);     
        }
      } else {
        Serial.println("");
        Serial.println("WiFi connect failed, push RESET button.");
        signalError();
      }

      if (!SPIFFS.begin()) {
         Serial.println("SPIFFS failed, needs formatting");
         signalError();
      }

      if (!SPIFFS.exists(ESP8266File)){
        Serial.println(F("SPIFFS File Create"));
        fh = SPIFFS.open(ESP8266File, "w");
        fh.close();
      }
     
    }  // setup()


    void loop() {
      byte inChar;
      char HostFile[64];
     
      if (Serial.available() > 0) {
        inChar = Serial.read();

        if (inChar == '0') {
          strcpy(HostFile, "inotify/start_led");
          Serial.print("HostFile=");
          Serial.print(HostFile);
          if (doFTP(HostFile)) Serial.println(F("FTP OK"));
          else Serial.println(F("FTP FAIL"));
        } 

        if (inChar == '1') {
          strcpy(HostFile, "inotify/stop_led");
          Serial.print("HostFile=");
          Serial.print(HostFile);
          if (doFTP(HostFile)) Serial.println(F("FTP OK"));
          else Serial.println(F("FTP FAIL"));
        }
      } 

      delay(10);  // give time to the WiFi handling in the background
    }

    Arduino-IDEのシリアルコンソールで[0]を入力するとRaspberryのオンボードLEDがフラッシュし、
    [1]を入力するとLEDのフラッシュが止まります。

    同様のライブラリにwatchdogが有ります。
    FTP+watchdogについてはこちらで紹介しています。

    HSES-LCD-24を使うと、簡単にタッチパネルが使えるようになります。
    そこで、次回はタッチ操作のGUIを追加してみます。

    続く....