2018年8月16日木曜日

Raspberry Pi に日本語を喋らせる

Alexa でルンバに掃除させる際の試行錯誤で、色々な家電を音声コマンドで動かせるようになりました。

便利にはなったものの、Alexa Home Skill は返事が毎回「はい」なので無愛想に感じてしまいます。
そこで、Raspberry Piで家電を操作する際にもう少し丁寧に喋ってもらうようにしました。

Raspberry Piから音声出力できるようにする

Raspberry Piにはスピーカーがありません。
そのため、何らかの方法で音声出力できるようにする必要があります。

今回は家電が近くにあるのでHDMI出力を使いました。
HDMIから音声出力する方法は前回の「Raspberry PiをAirPlayレシーバにする」をご参照ください。

特にHDMIにこだわらない場合は、USBスピーカーを使うのが音質が良く、動作も確実なようです。
音声設定についてはこのサイトが詳しいです。
ちなみに、今回のように音声合成の結果をHDMI出力すると次の問題があってちょっと面倒です。
  • 音声をデジタル変換する処理が追いつかず、合成音の最初の2〜3文字分の音が出ない。
  • 家電操作の対象が出力先の音声デバイスとバッティングすると厄介なことになる。
前者については、合成音の冒頭に「はい」「えーと」など無意味なセリフを入れて回避するしかなさそうです。
後者は、対象デバイスのHDMI入力を一旦Raspberry Piに切り替えてから合成音を再生し、その後で家電操作する、という流れになります。

単に「切り替えてから」と書きましたが、大抵のデバイスは音声再生程度で自動切り替えしない仕様なので、
自分で赤外線リモコンコードをコピーして操作することになると思います。
方法はルンバの掃除の際に書いたものが使えると思います。

Open JTalkをインストールする

次のようにOpen JTalkをインストールします。
$ sudo apt-get install open-jtalk
$ sudo apt-get install open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001
hts-voice-nitech-jp-atr503-m001 はおっさんの声の辞書です。
音声合成と言えば女性の声だろう、ということで女性の声の辞書を入れます。
$ wget http://downloads.sourceforge.net/project/mmdagent/MMDAgent_Example/MMDAgent_Example-1.7/MMDAgent_Example-1.7.zip
$ MMDAgent_Example-1.7.zip
$ sudo cp -r ./MMDAgent_Example-1.7/Voice/mei /usr/share/hts-voice/

Open JTalkに喋ってもらう

コマンド引数にテキストと音声辞書を指定すると合成してくれるのですが、
次のようなスクリプトを作っておくと便利です。
#!/bin/sh

msg="$1"

voice_type=/usr/share/hts-voice/mei/mei_normal.htsvoice

echo "$msg" |
open_jtalk  -m $voice_type -x /var/lib/mecab/dic/open-jtalk/naist-jdic -ow /dev/stdout |
aplay -
こんな感じで、このスクリプトの引数に日本語を渡せば再生してくれます。
$ ./voice.sh "抵抗は無意味だ"

使ってみる

open_jtalk には合成時のオプションを色々と指定できるのですが、
あまりいじっても音質が劣化して残念なことに。
よほど意味が分かっていない限りは、デフォルトのまま使うのがおすすめです。

crontab に次のようにスクリプトを仕込んでおくと、毎日7時〜22時に鳴る時報になります。
(タイムゾーンがUTCの場合は9時間引くなりなんなり)
$ crontab -e
0 7-22 * * * /home/pi/bin/tone.sh
呼び出されるスクリプトはこんな感じで適当に。
#!/bin/sh

DIR=`/usr/bin/dirname $0`

y=`/bin/date +%Y`
m=`/bin/date +%-m`
d=`/bin/date +%-d`
h=`/bin/date +%-h`

msg="$y年 $m月 $d日 h時です。"

$DIR/voice.sh "$msg"

Alexaと連携する

Alexa Home Skill と Raspberry Pi の連携については以前の説明をご参照ください。
NodeREDで家電の起動コマンドを送る前に音声合成のShell Execを挟み込めばOKです。

例えば、こんな感じです。
これでプロジェクターの起動待ちの間に「しばらくお待ちください」などのセリフを挟み込めます。


2018年8月13日月曜日

Raspberry PiをAirPlayレシーバにする

まるでニーズがないと思いますが、Raspberry PiとAVアンプを組み合わせてiPhoneの音楽をリモート再生する方法。

なぜそんな構成にしたのか

我が家のリビングはAVアンプのHDMI入力にAmazon Fire TV stickを挿しています。
出力はHDMIでプロジェクター。

Amazon Fire TV はアプリを入れるとAirPlayレシーバになるので、
iPhoneから音楽を飛ばしてAVアンプのスピーカーで音楽を鳴らすことができます。

ところが、AirPlayで飛ばした瞬間にAmazon Fire TV stick がスリープから起き上がってきます。
すると、プロジェクターが状態変化を自動的に検知して起動してしまいます。

音楽を聴きたいだけなのに、フルスペックで起きがってくるAV機器ども。

さすがにちょっと鬱陶しいので、Raspberry PiをAirPlayの音楽用レシーバに仕立てあげることにしました。
こうすると、Amazon Fire TV stickを使わないのでスリープから起き上がってきません。
また、Raspberry PiはAirPlayを受け取っても画面は切り替わらないのでプロジェクターにも影響ありません。

ただし、AVアンプも自動で入力切り替えしてくれなかったので、そのあたりはRaspberry Piでごにょごにょします。

Raspberry Piの音をHDMIで出力する

Raspberry PiはデフォルトではHDMIから音が出ないようです。
「/boot/config.txt」で次の行のコメントを外して再起動すればOK。
hdmi_drive=2

次のコマンドを実行して、HDMI出力先のスピーカーから音が出れば成功です。
$ speaker-test -t sine -f 600

Raspberry Piにshairport-syncをインストールする

shairport-syncというAirPlayレシーバをインストールします。
「shairport」というツールもあるようですが、全く別物らしいので注意。

まず、依存するパッケージをインストール。
$ sudo apt-get install -y build-essential git xmltoman autoconf automake libtool libdaemon-dev libasound2-dev libpopt-dev libconfig-dev avahi-daemon libavahi-client-dev libssl-dev libsoxr-dev

次に、gitからshairport-syncをcloneしてビルド。
$ git clone https://github.com/mikebrady/shairport-sync.git
$ autoreconf -i -f
$ ./configure --with-alsa --with-avahi --with-ssl=openssl --with-metadata --with-soxr --with-systemd
$ make
$ sudo make install

インストールできたら自動起動を設定します。
$ sudo systemctl enable shairport-sync

設定変更

ここまででiPhoneからAirPlayを飛ばせるようになりますが、音量が妙に小さいです。
そこで、iPhone側の音量を無視してRaspberry Pi側(正確には、Raspberry PiからHDMI出力したAVアンプ側)で音量設定できるようにします。

まず、音量を100%に設定。
$ sudo amixer cset numid=1 100%

shairpoint-sync の設定ファイルが「/usr/local/etc/sharepoint-sync.conf」にあります。
サンプルの設定ファイルから次の箇所を変更します。
コメントアウトされていた場合はコメントを外します。
general = {
    :
interpolation = "soxr";
ignore_volume_control = "yes";
interpolationをsoxrにした方が音質が良いらしいです。
ただし、Raspberry Pi Zero Wで有効にした場合、CPU処理が追いつかなくなって音飛びしました。
音飛びしてしまう場合はinterpolationの行をコメントアウトすればデフォルトに戻ります。

ignore_volume_control を yes にするとiPhoneの設定を無視するようになります。

AVアンプの設定を切り替える

AVアンプの機種によると思いますが、私が使っているものだと、Raspberry Piが音声をHDMI出力しても自動で音声入力が切り替わりませんでした。
そこで、AVアンプの赤外線リモコンコードを読み取って、Raspberry PiからHDMI入力を切り替える信号を送ります。
赤外線リモコンのコピーの仕方は以前掲載した説明をご参照ください。

問題はどのタイミングで赤外線信号を送るかです。
実は、sharepoint-sync.confの設定の中に用意されています。
次の行のコメントを外して設定値を記載します。
sessioncontrol = {
       :
        run_this_before_play_begins = "/usr/bin/irsend SEND_ONCE AV hdmi2"; 
        run_this_after_play_ends = "/usr/bin/irsend SEND_ONCE AV tv";

run_this_before_play_begins はiPhoneからAirPlayで送信先として指定したタイミングで実行されます。
run_this_after_play_ends はAirPlayの終了(音楽生成終了)の際に実行されます。

設定を変更したあと、サービスを再起動すれば反映されます。
$ sudo service shairport-sync restart

一通り設定が終わったら、iPhoneからAirplayでRaspberryPiに音楽を飛ばすと AVアンプの入力系統が自動的に切り替わって再生が始まります。

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の他に録画リストから番組を選んで再生したり色々できます。

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