2018年8月19日日曜日

Raspberry Piでジュークボックス:(1)準備

iPhoneでYoutubeから曲を選ぶと、Raspberry Pi経由で室内のスピーカーから流れるようにしました。

何をしたいのか

私よりも自宅にいることの多い顧客(家族)からのリクエストだったものの、
本人もどうなっていれば良いのか分からなかったようで試行錯誤しながらのヒアリングになりました。
結局のところ次のことを言っていたようです。
  • スピーカーでリビングに音楽を流して欲しい。
  • 楽曲指定は基本的にキーワード指定。お気に入りとかプレイリストとか要らない。
  • 簡単に再生開始したい。音声操作でもスマホでもどちらでも良いがハードウェアボタンを押しに行くのは嫌だ。
  • 再生中にスマホで他の操作をしたいので占有されたくない。
  • 1曲終わったら関連楽曲を連続再生して欲しい。
  • 途中、気に入らない楽曲が再生されたらスキップ or 他の関連曲に変えたい。
  • レスポンスが良いに越したことはないが、再生開始に時間がかかっても許容可能。(どうせ再生しっぱなしだから)
  • 再生時に余計な家電が動いてしまうのはNG。
  • コンテンツに定額が発生しても構わないが、気に入った曲が無いならダメ。

できるようになったこと


iPhoneでSiriに「Youtubeで◯◯を検索」と頼んで、検索結果からworkflowに共有するとRaspberry PiのNode-REDにリクエストが飛ぶようにしてあります。
最初からWorkflowにキーワードを入力してもOK。

Node-REDはリクエストされたYoutubeコンテンツIDまたはキーワードをShellScriptに渡します。
ShellScriptでは内部でmps-youtube(mpsyt)を呼び出して、Youtubeの楽曲を検索して再生。

その際、Raspberry Pi Zero Wを使うと再生開始まで非常に時間がかかる(1分くらい)ので、
Open JTalkで音声案内を入れるようにしました。

ここまでできてしまうと、mpsytの関連検索コマンドなどを駆使すれば連続再生なども簡単にできます。
処理が重すぎて、再生中のコマンドはうまく受け付けてくれないことがあるので、
停止やスキップについてはShellScriptから強制終了させるなどの対応にしました。

ついでにAmazon Echoに「ジュークボックスを消して」と言うとNodeREDから停止するようにしてあります。

試したこと

今回の構成に至るまでの試行錯誤の経緯。。。

Amazon Echo Dot + Amazon Music Unlimited

・選曲操作:Amazon Echo Dot
・音楽配信:Amazon Music
・再生端末:Amazon Echo Dot
・音声出力:Amazon Echo Dot

声を掛けるだけで再生されるしレスポンスも良く、再生デバイスとしてはGood。
ところが「Amazon Music Unlimitedが対応している曲がイマイチ」ということでお客様としてはNG。

Google Homeも買おうとしましたが、顧客から「なんでやねん」というツッコミが入って断念しました。

iPad + Youtube + AirPlay + Amazon Fire TV Stick

・選曲操作:iPad + Youtubeアプリ
・音楽配信:Youtube
・再生端末:iPad + AirPlay
・音声出力:Amazon Fire TV Stick

それで良いのか?と言いたくなりますが、Amazon MusicのコンテンツがイマイチならYoutubeで。。。
ちなみに、Apple Music は宗教的理由により見送りました。
死んだ爺さんから「月額課金はAmazon大明神の思し召しのままに」と言われているので。

Amazon Fire TV Stick にAirPlayのアプリを入れてiPadからYoutubeの音声を飛ばしました。
コンテンツ的にはOKらしいのですが、Fire TVにAirPlayを繋いだ瞬間にHDMI経由で画面(プロジェクター)が起き上がってしまいNG。
いや、だって、映像再生デバイスなんだから仕方ないじゃん。。。

また、iPadだとYoutubeが関連楽曲を連続再生してくれずご不満とのこと。
ふーん。

iPhone + Youtube + AirPlay + Raspberry Pi + スピーカー

・選曲操作:iPhone + Youtubeアプリ
・音楽配信:Youtube
・再生端末:iPhone + AirPlay
・音声出力:Raspberry Pi + スピーカー

Fire TVが画面出力してしまうならRaspberry Piでヘッドレスにしてみよう、ということで。
具体的な方法は以前説明した通り。
iPhoneであればYoutubeの連続再生も可能です。

しばらく良い感じに使っていたのですが、iPhoneで他の操作をしようとしてYoutubeアプリをバックグラウンドにすると音が切れてしまい、微妙なイライラが残ります。
ブラウザを駆使するとバックグラウンドで再生できるようですが、毎回この操作をするのは耐えられないとのことでNG。

iPhone + workflowアプリ + Raspberry Pi + Node-RED + Youtube + mpsyt + OpenJTalk + スピーカー

・選曲操作:iPhone + workflowアプリ(キーワード指定のみ)
・音楽配信:Youtube
・再生端末:Raspberry Pi + mpsyt
・音声出力:Raspberry Pi + スピーカー

最終的にたどり着いた構成がこれ。
結局、楽曲検索から再生から何から全部をRaspberry Piに詰め込んでしまい
「それミュージックプレイヤーやないかい」という状態。


前置きが長くなったので続きは次回。

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からルンバを操作します。