2018年7月1日日曜日

Alexa と Raspberry Piでルンバに掃除してもらう:【最終回】Node-REDからFlashAir/Arduino経由でルンバを動かす

前回、Arduinoからルンバに赤外線コードを送れるようにしました。
今回は、Node-REDからFlashAirを呼び出して、Arduinoの赤外線送信をキックします。

  1. 材料を揃える。
  2. 母艦のRaspberry PiにNode-REDをセットアップしてAmazon Echo Dotに認識させる
  3. Raspberry Pi Zero WをヘッドレスでWiFi対応セットアップする
  4. Raspberry Piで赤外線リモコンのコードをコピーする
  5. Node-REDから赤外線機能を呼び出してテレビを操作する
  6. FlashAirのGPIO機能でArduinoをHTTP API経由で制御する
  7. (おまけ)ArduinoからROI経由でルンバを動かす
  8. Arduinoから赤外線経由でルンバを動かす
  9. Node-REDからFlashAir/Arduino経由でルンバを動かす【今回】

FlashAirのGPIO

FlashAirではSDカードの各ピンをGPIOとして使用できます。
詳しくはFlashAir Developersで説明されています。

準備として、FlashAirのSD_WLANフォルダにある「CONFIG」ファイルに「IFMODE=1」と書き足します。
CONFIGは隠しフォルダになっているのですが、MacやLinuxの場合はTerminalで普通にアクセスできます。
Windowsでも隠しフォルダにアクセスできるツールを使えばOKです。

今回、FlashAir Developersで紹介されているものと同様に秋月電子通商の「SDカードスロットDIP化モジュール」を使いました。
意外に盲点なのですが、このDIP化モジュールはFlashAirの全てのピンが引き出されている訳ではありません。
そのため、FlashAirのGPIOピンを全て使えるわけではありません。

具体的には、DIP化モジュールの取り扱い説明書の回路図を参照すると分かります。
下図の通り、DAT1とDAT2はプルアップ抵抗で3.3Vに繋がっているだけなので、ピンが引き出されていません。
つまり、FlashAirの0x04と0x08を使えません。(使うにはジャンパーを半田付けする必要があります。)

半田付けなしで使用できるピンの対応付けは次のようになります。
「Arduino」列は今回接続したArduino側のピン、「用途」列は今回の用途です。
  1. ビット
    割り当て
    FlashAirDIP化
    モジュール
    Arduino用途
    0x01
    CMD
    SDI
    D5
    ルンバON
    0x02
    DAT0
    SDO
    D4
    スリープ enable/disable
    0x10
    DAT3
    CS
    D2
    ソフトウェアリセット
    -
    3.3V
    VCC
    3.3V
    FlashAirへの給電
    -
    GND
    GND
    GND
    GND

ArduinoとFlashAirの接続

上の表の配線を使って、Arduinoには次のような動作をさせます。
  1. D2ピンのHIGH/LOWが変化するとソフトウェアリセットでスリープから復帰する。
  2. D5ピンがHIGHになっていたらルンバに赤外線送信する。
  3. D4ピンがHIGHになっていたらスリープする。
ルンバの起動にしか使わないのであればD5ピン無しでも可能です。
とは言え、DAT2, DAT3 を外部に引き出すと、WiFi経由で合計3ビット分のコマンドを表現できるので、
8種類(2の3乗)の赤外線コードを出し分けられます。
そのような将来的な拡張を考慮してD5ピンにFlashAirのCMDを割り当てました。

Node-REDからFlashAir

Node-REDからFlashAirへは次のようにHTTPリクエストを出すことでArduinoを動作させます。
  1. FlashAirの全てのピンをLOWにする。つまりFlashAirに0x00を送信する。(http://〜/command.cgi?op=190&CTRL=0x1f&DATA=0x00)
  2. 一秒待つ(1.がFlashAir側で確実に受け取り完了させるため)
  3. FlashAirの0x10, 0x01, 0x02 をHIGHにする。つまり、FlashAirに0x13を送信する。(http://〜/command.cgi?op=190&CTRL=0x1f&DATA=0x13)
このリクエストによって、DAT3(0x10)がLOWからHIGHになります。
つまり、ArduinoのリセットピンをLOWからHIGHに変えることで、Arduinoをスリープから復帰させます。

スリープ復帰時にArduinoがCMD(0x01)を参照してルンバに赤外線コードを送信してくれれば、ルンバが起動します。

さらにその後、ArduinoがDAT0(0x02)を参照し、HIGHの時にスリープさせることで省電力化します。
本当は、LOWでスリープさせた方が省電力になるのかもしれません。
ところが、FlashAirは最初に電源が入った時点で全てのGPIOピンをHIGHにしてしまいます。
つまり、DAT0=LOWでスリープに入るようにしてしまうと、電源投入時にスリープしなくなってしまいます。
DAT0=HIGHをスリープに割り当てておけば電源投入直後にいきなりスリープしてくれます。

Arduinoのコーディング

以上の動作をArduinoに組み込むと次のようになります。
赤外線の記憶と送信に関しては前回と同じなので省略します。
#include <EEPROM.h>
#include <avr/sleep.h>
#include <avr/interrupt.h> 

// Arduino - board - SD
// 2 pin   - CS    - DAT3 - 1pin - 0x10 (RESET)
// 4 pin   - SDO   - DAT0 - 7pin - 0x02 (SLEEP)
// 5 pin   - SDI   - CMD  - 2pin - 0x01 (CLEAN)

#define PIN_IR 10
#define PIN_IR_SENSOR 11

// FlashAir SDD pin (0x02)
#define PIN_SD_SLEEP 4

// FlashAir SDI pin (0x01)
#define PIN_SD_CLEAN 5

#define SCAN_INITIAL_TIMEOUT 10000000
#define SCAN_TIMEOUT 10000000
#define IR_BUF_LEN 64

volatile int IR_BUF[IR_BUF_LEN];
volatile bool SETUP_FLAG = false;

void setup() {
  Serial.begin(9600);
  SETUP_FLAG = true;
  pinMode(PIN_IR, OUTPUT);
  pinMode(PIN_IR_SENSOR, INPUT);
  pinMode(PIN_SD_CLEAN, INPUT);
  pinMode(PIN_SD_SLEEP, INPUT);
  pinMode(2, INPUT_PULLUP);

  Serial.println("Start!");
}

// リセットからの復帰時はloopさえ動けば良いので特に処理無し
void wakeup(){
  Serial.println("Wakeup");
}

// Arduino起動直後は全てHIGHになっているがSETUP_FLAGを使って無視させる
// 一旦LOWに落ちてからHIGHになると清掃開始
void loop() {
  //Serial.println(".");
  Serial.println("Run");
  if ( ! SETUP_FLAG ) {
    if ( digitalRead(PIN_SD_CLEAN) == HIGH ){
      Serial.println("Run Roomba in clean mode");
      loadBuffer(0);
      executeIR();
      executeIR();
      executeIR();
    }
    // FlashAirの他のピンを引き出してコマンド拡張する場合はここに書く
  }
  SETUP_FLAG = false;
  delay(500);
  check_sleep(digitalRead(PIN_SD_SLEEP));
  delay(500);
}

// sleep pinがHIGHだったらスリープ
void check_sleep(bool flag) {
  if ( ! flag ) { return; }
  Serial.println("Sleep");
  delay(1000);
  // リセットピンの状態が変化したら復帰する
  attachInterrupt(0, wakeup, CHANGE);
  set_sleep_mode(SLEEP_MODE_STANDBY);
  sleep_enable();
  sleep_mode();
  sleep_disable();
}

完成

以上で完成です。
テレビ、プロジェクター、ルンバを音声操作できました。
紹介しませんでしたが、Node-REDを工夫すると1つの音声コマンドで複数機器を同時に動作させることも可能です。
この動画の例ではプロジェクターを起動する際にアンプの音声出力をテレビからプロジェクターに切り替えています。

2018年6月17日日曜日

Alexa と Raspberry Piでルンバに掃除してもらう:(6)Arduinoから赤外線でルンバを動かす

前回、ROIを使ってルンバをシリアル経由で操作する方法を紹介しました。
ROIを使うとかなり細かいコントロールができる一方、シリアル接続なのでルンバにArduinoを載せる必要があります。
頑張ってルンバから電源を取りつつArduinoをルンバに内蔵することも可能ではありますが。。。
今回やりたいことは単なる電源ON/OFFだけなので、ルンバとは有線接続せずに赤外線で済ませたいところです。

lircでやろうとしたところ、リモコンコードを読み取れずエラーになってしまいました。
テレビと同様に38Hzのようですが、信号が長すぎるのかもしれません。

そこで、Arduinoで赤外線のリモコンコードを読み取って赤外線LEDでルンバに送ります。
やっていることはRaspberry Pi+licdと同じですが、Arduinoなのでもっと泥臭い感じになります。
  1. 材料を揃える。
  2. 母艦のRaspberry PiにNode-REDをセットアップしてAmazon Echo Dotに認識させる
  3. Raspberry Pi Zero WをヘッドレスでWiFi対応セットアップする
  4. Raspberry Piで赤外線リモコンのコードをコピーする
  5. Node-REDから赤外線機能を呼び出してテレビを操作する
  6. FlashAirのGPIO機能でArduinoをHTTP API経由で制御する
  7. (おまけ)ArduinoからROI経由でルンバを動かす
  8. Arduinoから赤外線経由でルンバを動かす【今回】
  9. Node-REDからFlashAir/Arduino経由でルンバを動かす

回路を組む

Arduinoに赤外線受信モジュールと赤外線送信モジュールを繋ぎます。
受信モジュールは赤外線リモコンを覚えさせる際に使用するだけなので、覚えた後は外しても構いません。


リモコンコードをArduinoのEEPROMに書き込む

赤外線受信モジュールからの入力信号がHIGHとLOWの変化する時間間隔を計測します。
計測結果を配列に詰めてEEPROMに書き込めばOK。
このコードでは2種類のリモコンコードを覚えられるようになっています。
#include <EEPROM.h>

#define PIN_IR_SENSOR 11

#define SCAN_INITIAL_TIMEOUT 10000000
#define SCAN_TIMEOUT 10000000
#define IR_BUF_LEN 64

volatile int IR_BUF[IR_BUF_LEN];
volatile bool SETUP_FLAG = false;

void setup() {
  Serial.begin(9600);
  SETUP_FLAG = true;
  pinMode(PIN_IR_SENSOR, INPUT);
  updateIR(0);
  updateIR(1);
}

int updateIR(int pos) {
  Serial.println("SCAN START");
  memset(IR_BUF,0,IR_BUF_LEN);
  for( int i=0; i<3; i++ ){
    Serial.print("PUSH BUTTON ");
    Serial.println(pos);
    int result = scanIR(i);
    for(int j=0; j<result; j++){
      Serial.print(IR_BUF[j]);
      Serial.print(" ");
    }
    Serial.print("\n");
    Serial.print(result);
    Serial.println(" bytes were read");
    delay(3000);
  }
  saveBuffer(pos * IR_BUF_LEN*2);
  Serial.println("SCAN END");
  return 1;
}


int scanIR(int repeat) {
  int idx=0;
  int ir_status = HIGH;  
  unsigned long lastStatusChanged = 0;
  unsigned long timeout = SCAN_INITIAL_TIMEOUT;
  while(1){
    if ( ir_status == LOW ){
      while( digitalRead(PIN_IR_SENSOR) == LOW){
        // wait
        ;
      }
    } else {
      if( wait_high_signal( micros() + timeout ) < 0 ){
        break;
      }
    }
  
    unsigned long now = micros();
    if( lastStatusChanged > 0 ){
      int val = (int)((now - lastStatusChanged) / 10);
      if( repeat > 0 ){
        if( IR_BUF[idx] > 1 ){ break; }
        if( abs((int)(IR_BUF[idx] - (int)val)) >  IR_BUF[idx]*0.3 ){ idx = 0; break; }
        IR_BUF[idx] = (int)( IR_BUF[idx] * repeat + val ) / (repeat + 1);
      } else {
        IR_BUF[idx] = val;
      }
      idx++;
      if( idx == IR_BUF_LEN -1 ){ break; }
    }
    lastStatusChanged = now;
    if (ir_status == HIGH) {
      ir_status = LOW;
    } else {
      ir_status = HIGH;
    }
    timeout = SCAN_TIMEOUT;
  } // while 1
  
  if( idx > 0 ){ IR_BUF[idx] = -1; }
  return idx;
}

int wait_high_signal(unsigned long timeout) {
  while( digitalRead(PIN_IR_SENSOR) == HIGH ){
    if( micros() > timeout ) { return -1; }
  }
  return 1;
}

void saveBuffer(int pos) {
  Serial.println("SAVE START");
  byte buf;
  for( int i=0; i < IR_BUF_LEN; i++ ){
    buf = lowByte(IR_BUF[i]);
    EEPROM.write( pos+i*2, buf );
    delay(5);
    buf = highByte(IR_BUF[i]);
    EEPROM.write( pos+i*2+1, buf );
    delay(5);
    if( buf < 0 ){ break; }
  }
  Serial.println("SAVE END");
}


EEPROMから読み込んだコードで赤外線送信する

今度はEEPROMから配列を読み込んで、覚えていた時間間隔でLEDを点滅させます。
我が家のRoombaが具合悪いだけかもしれませんが、
1回送信しただけでは正常に反応しないことが多いので、3回連続で送信しています。
また、loop関数で繰り返し送信されてしまうので送信し終わったらスリープさせます。
#include <EEPROM.h>

#define PIN_IR 10

#define IR_BUF_LEN 64

volatile int IR_BUF[IR_BUF_LEN];

void setup() {
  Serial.begin(9600);
  pinMode(PIN_IR, OUTPUT);
  Serial.println("Start!");
}

void loop() {
  Serial.println("Run");
  loadBuffer(0);
  executeIR();
  executeIR();
  executeIR();
  check_sleep(true);
}

void wakeup(){
  Serial.println("Wakeup");
}
void check_sleep(bool flag) {
  if ( ! flag ) { return; }
  Serial.println("Sleep");
  delay(1000);
  attachInterrupt(0, wakeup, CHANGE);
  set_sleep_mode(SLEEP_MODE_STANDBY);
  sleep_enable();
  sleep_mode();
  sleep_disable();
}

void loadBuffer(int pos) {
  Serial.println("LOAD START");
  int h, l;
  for( int i=0; i<IR_BUF_LEN; i++ ){
    l = EEPROM.read(pos+i*2);
    h = EEPROM.read(pos+i*2+1);
    IR_BUF[i] = (h << 8) + l;
    
    Serial.print(IR_BUF[i]);
    Serial.print(" ");
    if( IR_BUF[i] < 0 ){ break; }
  }
  Serial.println("\nLOAD END");
}

void executeIR() {
  for(int i=0; IR_BUF[i] > 0; i++){
    unsigned long len = (unsigned long)IR_BUF[i] * 10;
    unsigned long now = micros();
    
    do {
      digitalWrite(PIN_IR, 1-i&1);
      delayMicroseconds(8);
      digitalWrite(PIN_IR, 0);
      delayMicroseconds(7);
    } while( now + len > micros() );
  }
}


ここまででArduinoからRoombaに掃除をスタートするコマンドを送れるようになりました。
ところが、このままではArduinoの電源を入れた直後にしか信号が飛びません。
次回、Node-REDからのリクエストをFlashAirで受けて、Arduinoをスリープから復帰させるように改造します。

2018年4月29日日曜日

Alexa と Raspberry Piでルンバに掃除してもらう:(おまけ)ArduinoからROI経由でルンバを動かす

前回まででAlexaからNode-RED経由でArduinoを呼び出せるようになりました。
で、Arduinoからルンバを動かすにはいくつかの方法があります。

1つが今回紹介するROI(Roomba Open Interface)です。
この方法のメリットはルンバ側にArduinoを載せるので、ルンバが移動しても通信し続けることができることでしょうか。
一方で、当然ながらルンバに出っ張りが出来てしまうので、掃除中に引っかかるリスクがあります。

もう一つの方法として、ルンバから離してArduinoを設置して赤外線経由で操作することもできます。
この方法の場合はルンバに何も付ける必要はありません。
その代わり、ドックにいる時にしかコマンドを送れません。

通常、ルンバは掃除が終われば自動的にドックに戻ってくるので、掃除中にコマンドを送りたいケースは少ないように思います。
そのため、普通に掃除させたいだけなら赤外線経由の方が本命だと思います。

そのような理由から、ROI経由での制御は「おまけ」として紹介します。
  1. 材料を揃える。
  2. 母艦のRaspberry PiにNode-REDをセットアップしてAmazon Echo Dotに認識させる
  3. Raspberry Pi Zero WをヘッドレスでWiFi対応セットアップする
  4. Raspberry Piで赤外線リモコンのコードをコピーする
  5. Node-REDから赤外線機能を呼び出してテレビを操作する
  6. FlashAirのGPIO機能でArduinoをHTTP API経由で制御する
  7. (おまけ)ArduinoからROI経由でルンバを動かす【今回】
  8. Arduinoから赤外線経由でルンバを動かす
  9. Node-REDからFlashAir/Arduino経由でルンバを動かす

ROIとは?

ROIは外部機器からルンバを制御するためのシリアルインターフェイスです。
ROIの詳細についてはiRobotのサイトに仕様書があります。

コネクタが付いている場所は機種によって微妙に異なるのですが、我が家にあるルンバ760では上部ハンドルの裏に付いています。

他の機種だと天板パネルを剥がした中に隠されている場合もあるようです。

Arduinoとルンバを接続する

ROIは7ピンのミニDINコネクタです。
秋葉原などに行くとプラグが売っているようですが、ジャンパピンを挿して接続しても何とかなります。

ピンアサインはROIの仕様書に載っています。


今回の場合、3番(RXD), 4番(TXD), 6番(GND) だけ接続すれば通信できます。
Arduino側のRX,TXは0番,1番ですが、それを使ってしまうとPCとのシリアル通信ができなくてデバッグしにくいです。
そのため、Arduinoの0番,1番を避けて、他のピンに繋ぎ、ソフトウェアシリアルで通信させます。

また、ルンバの電源でArduinoを駆動する場合はルンバの1番ピンから引き出してレギュレータを通してからArduinoのVinピンに入れれば良いと思います。

Arduinoからコマンドを送る

前回、FlashAirと接続するようにしたArduinoのコードにルンバとのシリアル通信を追加します。
下記の例ではArduinoの12番,13番ピンにルンバのTXDとRXDを繋いでいます。
ちなみに、ルンバのデフォルトのボーレートは115200らしいです。
#include <softwareserial.h>

#define PIN_SD_CLEAN 11

SoftwareSerial device(12, 13);

void setup() {
  Serial.begin(9600);
  pinMode(PIN_SD_CLEAN, INPUT);
}

// Arduino起動直後は全てHIGHになっているが無視させる
// 一旦LOWに落ちてからHIGHになると清掃開始
volatile bool clean_flag = HIGH;
void loop() {
  if ( clean_flag == LOW && digitalRead(PIN_SD_CLEAN) == HIGH ){
    Serial.println("Run Roomba with clean mode");
    device.begin(115200);
    byte buffer[] = {
      byte(128), // Start
      byte(135)  // Clean
    };
    device.write(buffer, 2);
  }
  clean_flag = digitalRead(PIN_SD_CLEAN);
  delay(1000);
}
はい、これだけ。
ROIは1コマンドが1バイトになっていて、コマンド順に並べたバイト列を送り込めばその順に実行されます。
ルンバはいくつかのモードを持っていて、低レベルコマンドを送るにはモードを切り替える必要があります。
128が起動なのですが、起動後のデフォルトはPassiveモードになっています。
Passiveモードではルンバのセンサ読み込みと掃除スタートができます。
今回は掃除したいだけなので、いきなりCleanコマンド(135)を送っています。

モードとコマンドについてはROIの仕様書に書かれています。
駆使すると、ルンバをラジコン化したり音を鳴らしたりインジケータを表示したり色々できます。

完成

出来上がると、こんな感じになります。

次回はROIではなく赤外線で起動します。

2018年4月21日土曜日

Alexa と Raspberry Piでルンバに掃除してもらう:(5)FlashAirのGPIO機能でArduinoのHTTP API

前回までで、Alexaからテレビを操作できるようになりました。
同じ方法でルンバも操作しようとしたところ、lircでルンバのリモコンコードを読み取る際にエラーになってしまいました。
理由がまるで分からず。。。

そこで、Raspberry PiではなくArduinoからルンバを操作することにしました。
ただ単に、Raspberry PiとArduinoのどちらでも赤外線が使える、というだけの話ですが。

Arduinoを使う場合、まずは母艦のNode-REDからの通信を受け取らないとどうにもなりません。
とは言え、数ビットの無線信号を受け取れれば良いだけなので、ESP-WROOM-02やBluetoothなど手段はいくらでもあります。
今回は、余っていた古いFlashAir(W-02)を使いました。

  1. 材料を揃える。
  2. 母艦のRaspberry PiにNode-REDをセットアップしてAmazon Echo Dotに認識させる
  3. Raspberry Pi Zero WをヘッドレスでWiFi対応セットアップする
  4. Raspberry Piで赤外線リモコンのコードをコピーする
  5. Node-REDから赤外線機能を呼び出してテレビを操作する
  6. FlashAirのGPIO機能でArduinoをHTTP API経由で制御する【今回】
  7. (おまけ)ArduinoからROI経由でルンバを動かす
  8. Arduinoから赤外線経由でルンバを動かす
  9. Node-REDからFlashAir/Arduino経由でルンバを動かす

FlashAirのGPIO機能

FlashAirのGPIO機能を使うと、SDカードの端子のうちの5つをHTTP経由でGPIOピンとして使用できます。
FlashAir Developersに詳しく説明されています。

つまり、FlashAirの端子とArduinoのピンを接続すると、ArduinoのピンのHigh/LowをHTTPから切り替えられます。
そのかわり、FlashAirのストレージとしての機能は使えなくなります。

FlashAirの動作を変更するには /SD_WLAN/CONFIG ファイルを編集します。
通常は不可視フォルダになっていますが、LinuxやMacでマウントするとShell経由で普通に編集できます。
Windowsの場合は不可視フォルダを表示すれば良いと思います。

このファイルを開いて末尾にIFMODE=1を追加すればGPIO機能が有効になります。

FlashAirをWiFi子機にする

通常、FlashAirはWiFiの親機として動作します。
そのため、自宅のLANの中にあるNode-REDからのHTTPリクエストを受け取れません。
そこで、FlashAirを無線子機としてWiFiルータに接続させます。

この設定についてもFlashAir Developersに詳しく書かれています。
ステーションモードの利用

そんなこんなで、出来上がったCONFIGファイルは次のようになります。
APPMODE=5
APPNAME={FlashAirの名前}
APPSSID={WiFiのSSID}
APPNETWORKKEY={WiFiのパスワード}
CIPATH=/DCIM/100__TSB/FA000001.JPG
VERSION=F19BAW3AW2.00.00
CID=02544d535730384708c00b7d7800d201
PRODUCT=FlashAir
VENDOR=TOSHIBA
MASTERCODE=18002d4ff0a2
IFMODE=1

FlashAirとArduinoを接続

こんな感じのSDカードスロットを使ってArduinoと接続します。 今回はルンバの起動ができれば良いだけなので1ビット送れれば十分です。
そのため、信号ピン1つと3.3V、GNDの3ピンだけ接続します。

FlashAirのSDIをArduinoの11ピンに繋ぎました。
3.3VとGNDは適宜。

HTTPからFlashAirのGPIOを書き換える

GPIO機能のHTTP APIについてはFlashAir Developersの「SDインターフェース端子のI/O利用(op=190)」に書かれています。
今回はSDOピン(CMD)をGPIOとして使うので、0x01のHIGH/LOWを切り替えられれば良いです。

つまり、URLパラメータとGPIOの組み合わせは次の通りです。
  • SDO=HIGH → op=190&CTRL=0x1f&DATA=0x01
  • SDO=LOW → op=190&CTRL=0x1f&DATA=0x00

ArduinoでFlashAirのGPIOを読む

Arduinoからピンの状態を読むのは普通に「digitalRead」で良いです。
ライブラリも何も必要ありません。
注意点としては、FlashAir起動時は全てのピンがHIGHになっていることでしょうか。

つまり、SDOピンがHIGHの状態を検知してルンバを起動させようと考えた場合、
FlashAirの電源投入時にHIGHなのでいきなりルンバが起動してしまいます。

電子回路的にHIGH/LOWを反転させても良いのですが、部品点数が増えて面倒なので、
Node-RED側で一旦LOWに落としてからHIGHにするようなリクエストを投げることで回避しました。
もしかしたら、Arduinoで一旦pinModeをOUTPUTにしてLOWに落とせるのかもしれませんが試していません。

Arduinoのコードはこんな感じです。
#define PIN_SD_CLEAN 11

void setup() {
  Serial.begin(9600);
  pinMode(PIN_SD_CLEAN, INPUT);
}

// Arduino起動直後は全てHIGHになっているが無視させる
// 一旦LOWに落ちてからHIGHになると清掃開始
volatile bool clean_flag = HIGH;
void loop() {
  if ( clean_flag == LOW && digitalRead(PIN_SD_CLEAN) == HIGH ){
    Serial.println("Run Roomba with clean mode");
  }
  clean_flag = digitalRead(PIN_SD_CLEAN);
  delay(1000);
}

Node-REDからArduinoにHTTPリクエスト

少々ブサイクですが、次のようにしました。
Alexaに「ルンバで掃除して」と言うと左端の「ルンバ」ノードが呼ばれます。
まず最初に真ん中のフローで「Roomba reset」が呼ばれます。
「Roomba reset」はHTTPリクエストのノードです。
FlashAirに「op=190&CTRL=0x1f&DATA=0x00」を投げて全てのピンをゼロリセットしています。
FlashAirの起動後、2回目以降の「ルンバ掃除して」の場合は前回呼び出し時にゼロリセットされているので、この処理は無意味になります。

先ほどのRoomba resetが先に実行されるように、上部のフローでは2秒delayが入っています。
本当はdelayよりも、Roomba resetの実行後にフラグを書き換えるなどしてロックした方が良さそうに思いますが手抜きです。
この手抜きのせいで、「ルンバ掃除して」と言ってから実行されるまでに2秒掛かってしまうので微妙に違和感があります。

2秒後に「Roomba clean」が呼ばれます。
これは「op=190&CTRL=0x1f&DATA=0x01」でSDOピンをHIGHにしています。
この時点でArduino側ではルンバの制御に入ります。

その後、5秒delayを掛けて再度ゼロリセットしておきます。
5秒というのは、Arduino側がHIGHを確実に検知できるよう、長めに取っています。

最初のdelayについては、もし検知し損ねても処理に失敗するのは起動後最初の1回だけなのでイライラが少ないです。
また、長すぎると「ルンバ掃除して」からのタイムラグが長くなってしまうため、できるだけ短く。
2回目のdelayは短かすぎると毎回失敗し、長くてもユーザビリティに影響ないので長めに。

ここまででNode-REDからHTTP経由でArduinoに1ビット信号を送ることができるようになりました。
次回はArduinoからルンバを操作します。

2018年4月17日火曜日

Alexa と Raspberry Piでルンバに掃除してもらう:(4)Node-REDからテレビを操作

前回まででNode-REDでRaspberry PiとAlexaを連携させ、さらに赤外線リモコンのコードをコピーしました。
今回はついに、Alexa → Node-RED → 赤外線 → テレビ という流れを全部繋ぎます。
  1. 材料を揃える。
  2. 母艦のRaspberry PiにNode-REDをセットアップしてAmazon Echo Dotに認識させる
  3. Raspberry Pi Zero WをヘッドレスでWiFi対応セットアップする
  4. Raspberry Piで赤外線リモコンのコードをコピーする
  5. Node-REDから赤外線機能を呼び出してテレビを操作する【今回】
  6. FlashAirのGPIO機能でArduinoをHTTP API経由で制御する
  7. (おまけ)ArduinoからROI経由でルンバを動かす
  8. Arduinoから赤外線経由でルンバを動かす
  9. Node-REDからFlashAir/Arduino経由でルンバを動かす

Node-REDから赤外線機能を呼ぶ場合、次の二種類のケースがあると思います。
  • 母艦のRaspberry Piに赤外線LEDを付ける場合
  • 母艦とは別のRaspberry Piに赤外線LEDを付ける場合
まずは前者から。

母艦のNode-RED から母艦の lirc を呼ぶ

1台のRaspberry Piの上でNode-RED と lirc を連携させるのは非常に簡単です。
Node-REDをセットアップした時と同じ手順でノードの追加画面を開きます。
「lirc」というキーワードでノードを検索すると「node-red-contrib-lirc」というノードが見つかるので追加します。
すると、こんな感じで出力のリストに「lirc」が増えます。

この「lirc」をドラッグして右の広いところにドロップ。
ドロップした「lirc」ノードをダブルクリックして図のように入力します。

「Name」は何でも良いです。分かりやすい名前をつけます。

「Controller」はLinuxのlircコマンドの第一引数です。
今回は1回送信したいので「SEND_ONCE」です。

「Device Name」はirrecordコマンドで指定したデバイス名です。
前回「TV」というデバイスを登録したので「TV」。

「Output」は「1」固定です。
lirc が複数の赤外線LEDをサポートした時のために用意されているようですが、現状では1個しか使えないので「1」。

ここで勘の良い方は「あれ?」と思うかもしれません。
デバイス「TV」には複数のリモコンボタンがあるはずなのですがその設定がありません。
例えば「on」とか「ch1」とかをどこかで指定する必要があるはずです。
lircノードではメッセージペイロードでこれらのボタン名を指定します。

メッセージペイロードとは


Node-RED では前のノードから後のノードに対してメッセージが渡されます。
デフォルトではメッセージはJSON形式で書かれるようです。
このメッセージには「msg」というオブジェクト名がつけられています。
msgオブジェクト内の要素は上流の入力次第なのですが、どの入力も「payload」という要素を1つ持っています。
多くのノードで、この msg.payload を入力パラメータとして解釈する仕様になっています。

lirc ノードでは msg.payload の値をリモコンのボタン名として解釈します。
つまり、lirc ノードにフローを繋ぐ際、msg.payload に「on」を代入すればテレビに対してonの信号が飛びます。
(TV.lircd.conf に「on」が定義されていれば、ですが。)

この約束を頭の片隅に入れておいて、Node-RED の入力側からフローを繋いでいきます。

Alexa からの入力とつなぐ

Node-REDの「入力」のリストから「alexa local」を追加します。
名前は「テレビ」。
前にも説明しましたが、この名前がAlexaで呼びかける時のデバイス名になるので要注意です。

さらに、「機能」のリストから「template」を追加して、alexa local の右側からtemplate の左側に線を繋ぎます。
繋いだらtemplateノードをダブルクリック。
このtemplateノードで、lirc に渡す msg.payload を代入します。

「名前」は適当に分かりやすい名前を付けます。
「設定先」は代入したい変数。今回は「msg.payload」ですね。
今回は固定文字列を埋め込みたいので「形式」は「平文」で構いません。

テンプレートのところに書いた内容が msg.payload に代入されます。
今回は「on」です。
「{{hoge}}」などと書くと、入力に渡された msg.hoge 変数の値が埋め込まれます。
「構文」を指定するとJavaScriptで演算した結果を代入することもできます。

設定できたら、templateノードの右側(出力側)をlircノードの左側(入力側)に繋いで完成。
右上の「デプロイ」ボタンを押すと保存されます。

もし、Alexa に「テレビ」デバイスを認識させていなければ「Alexa デバイスを探して」と言うと見つけてくれます。


Alexa から実行

Raspberry Piの赤外線LEDをテレビの赤外線受光部の近くに貼り付けておきます。
この状態で「Alexa テレビをつけて」と言うとテレビがつく、、、はずです。

うまくいかない場合は、debug ノードを追加して msg オブジェクトの中身を確認していき、lirc に正しくメッセージが渡っていることを確認します。
メッセージが渡っているようであれば、今度は Linux 上で lirc がエラーになっていないかを確認するとか。
それでも問題ないようなら、赤外線LEDを普通のLEDに付け替えてみて、ちゃんと光るかどうかを確認。
ハード/ソフト/ネットワークが絡み合っているので、1つずつ接点を潰しながら不具合箇所を特定する地味な作業になります。


別々のRaspberry PiでNode-REDとlircを動かす場合

Node-RED から何らかの方法でネットワークに飛ばし、lirc側も何らかの方法でリクエストを受け取れば良いです。
lirc 側で Apache からShellコマンドを読んでも良いのですが、いっそのこともう1つNode-REDを立ち上げても良いです。
オーバースペックですが。

lirc 側の Node-RED では「入力」の「http」ノードを追加して、好きなパス名を指定。
ここで指定したパスを、母艦側から呼び出します。
http ノードと lirc ノードを接続すれば、lirc 用 HTTP API の完成。

母艦側では「機能」の「http request」ノードを追加。
ダブルクリックして、lirc 側の IP アドレスと、「http」ノードのパスを設定すれば呼び出せます。

その他は同じRaspberry Piで Node-REDとlircの両方を動かす場合と同じです。
まぁ、Node-REDを両方に立ち上げるなら、lirc 側のNode-REDにalexa localノードを作った方が手っ取り早いですが。。。

完成

そんなこんなで、Alexa とテレビの連携が完成しました。
頑張ってNode-REDを設定すると、テレビのON/OFFの他に録画リストから番組を選んで再生したり色々できます。

次回からはさらにルンバと連携させていきます。

2018年4月14日土曜日

Alexa と Raspberry Pi でルンバに掃除してもらう:(3)Raspberry Piを赤外線リモコンにする

前回まででRaspberry Pi上でNode-REDが動きました。
ここまでが母艦側になります。
今回は母艦からコントロールされるエッジ側の準備です。

余談ですが、、、
ちょっと前までのIoT分野ではデータ収集がメインだったので
「エッジ」→「ゲートウェイ」→「サーバ」という名前の付け方が
多かったように思います。

今回のように母艦が複数のエッジデバイスを制御するような場合、
サーバ/クライアントモデルとかデータフローがあまり関係ないので
「サーバ」とか「ゲートウェイ」という名付け方は不適切に感じます。

呼び方がよく分からないので、ここでは制御の頭脳側を「母艦」と呼ぶことにします。
母艦から制御されるデバイス側を「エッジ」と呼ぶことにします。


エッジ側は複数のデバイスそれぞれに対応させる必要があります。
最終的にはルンバを動かしたいのですが、実はルンバの赤外線は
結構面倒だということがわかったので、まずはテレビを動かしてみようと思います。

【注意】
今回のやり方ではルンバは動きません。
後日説明しますが、ルンバの場合はRaspberry PiではなくArduinoを使います。

2018年4月13日金曜日

Raspberry Pi Zero W をヘッドレスでWiFi対応セットアップする

Alexaでルンバを動かすことを目指していますが、今回はちょっと脱線します。
単にRaspberry Piのセットアップを簡単にする方法を紹介するだけなので必須ではありません。
  1. 材料を揃える。
  2. 母艦のRaspberry PiにNode-REDをセットアップしてAmazon Echo Dotに認識させる
  3. (おまけ)Raspberry Pi Zero WをヘッドレスでWiFi対応セットアップする(今回)
  4. Raspberry Piで赤外線リモコンのコードをコピーする
  5. Node-REDから赤外線機能を呼び出してテレビを操作する
  6. FlashAirのGPIOモードでArduinoをHTTP API経由で制御する/li>
  7. (おまけ)ArduinoからROI経由でルンバを動かす
  8. Node-REDからFlashAir/Arduino経由でルンバを動かす
Alexa で色々な家電の操作ができる仕組みを作成していく中でRaspberry Pi Zero Wは良い選択肢だと思います。
赤外線で家電を操作することを考えると、どうしても家電の近くにRaspberry PiかArduinoを置くことになりがちです。
その際の条件を考えていくと、次のような感じになります。
  1. 安い → 家電の数だけ設置する必要がある。
  2. 目立たない → あちこちに無骨な基板が転がっていると気になるので。。。
  3. 配線が少ない → 少なくとも無線化は必須。ACアダプタなしでUSBケーブル程度。
Raspberry Pi Model Bだと価格がネック。
Arduinoだと無線が貧弱なのでちょっと厳しい。
そんなこんなでRaspberry Pi Zero Wに行き着きます。

ただ、Raspberry Pi Zero WはHDMIやUSBがマイクロタイプなので、セットアップの際に配線がごちゃごちゃしがち。
そんな訳で、キーボードもモニタも無しでRaspberry Pi Zero Wをセットアップします。