2018年11月10日土曜日

Raspberry Piでジュークボックス(番外):現状

先日準備編だけ書いたままだったジュークボックスのその後。
リビングの家電を一通り音声操作できるようにしてリモコンを撲滅したところ、家族から次々とリクエストが出てきて意見箱が設置されることに。
意見箱の投書を粛々と実装するうち、何なのかよく分からないデバイスに進化してきました。
そもそも「デバイス」と言っても、家中に散らばったデバイスが連動して動いているので、どれが本体なのか誰も気にしていません。と言うか「本体」など存在しない状態に。

主な構成

  1. 音声入力
    ダイニングに置いてあるAmazonEcho。
  2. 音声出力
    メインはリビングのスピーカー(サウンドバー)
    AmazonEcho自体の機能に関する音声フィードバックはAmazonEchoから出るので、あちこちで違う声で喋りまくり。
  3. 制御系
    AmazonEchoとRaspberry Pi2台とArduinoとアンプが連動しているのでどれがどうとも。。。
    音声入力系については、基本的にはこの流れ。
    AmazonEcho → Node-RED(Raspberry Pi) → 赤外線(Raspberry Pi) → アンプ切り替え&各機器制御
    そのほか、時間での自動起動系はRaspberry Piとアンプが赤外線で連携。

主な実装済み機能

  1. テレビ/プロジェクター/ルンバの音声操作
  2. 携帯やPCからリビングのスピーカーで音楽再生
  3. 操作対象に合わせてアンプの音声入出力を自動切り替え
  4. 朝夕と夜に時報を喋る。
  5. 早朝にYoutubeからそれっぽいPlaylistを検索して自動再生。
  6. 誰かの誕生日には勝手に祝う。
  7. 学校に行く時間になったら音声で急かす。
  8. 週末も学校と同じ時間に学校と同じチャイムが鳴る。
  9. 時報などの音声案内は音声合成の話者をランダムに入れ替えて再生。

未実装の機能というか要望

  1. 音声合成の話者入れ替えに関して、OpenJTalkのおっさんの声が偶然私の声に似ていたらしく、娘から「もっとやれ」との指示。
  2. Logicoolのカメラをハードオフのジャンク箱から100円でゲットしたので画像入力を使いたい。
  3. 家電からのフィードバックパスが無く、細かい使い勝手に制限が出る。
    (家電用リモコンで電源を切ると音声入出力の組み合わせが連動しないとか)
  4. AmazonEchoからキーワード入力できないので、ジュークボックスにキーワード検索させようとすると携帯操作になる。
  5. 制御系を全部隠してあるのに、AmazonEchoが存在感を出しすぎ。どこかに隠したい。
  6. 音声入力を分散配置したいが質の良いマイクを自前で用意するのが厳しい。
  7. 対象機器にアナログ掛け時計を入れたい。Raspberry Piを組み込んでデジタル鳩時計にしたい。

2018年9月30日日曜日

Raspberry Piでネットワークルーティング

またしても需要がなさそうですが、Raspberry Piで2つのプライベートネットワーク間をルーティングします。

ググるとRaspberry PiをDHCP & default gateway にする方法はたくさん出てくるのですが、
DHCP と default gateway は普通の WiFi ルータに任せて、プライベートセグメントだけルーティングする、という面倒なことをやろうとしたら情報がなかったので。

やりたいこと

図のように、WiFiルータが2つあって、それぞれ LAN1(192.168.1.0/24) と LAN2(192.168.2.0/24) のネットワークを作っています。
LAN1 には NAS がぶら下がっていて、普段作業する Mac なども繋がっています。
一方、LAN2 には子供が使う Windows マシンが繋がっています。
このままでもそれぞれの WiFi ルータからネット接続できるのですが、
LAN2 側から NAS を使いたかったので Raspberry Pi でゲートウェイすることにしました。
ただし、DHCPはそれぞれのWiFiルータなので default gateway はWiFiルータです。

普通はWiFiルータ2をWiFiコンバータにすれば済む話なのですが、
たまたまWiFiルータが1台余っていたので。

しかも、自宅の壁面にマルチメディアコンセントが埋め込まれているのですが、
そこにLANケーブルを挿すとグローバルネットワークになってしまうのでこんなことに。
本当は天井裏のハブをルータに置き換えて、壁面配線をプライベートネットワークにしたいのですがそれはまた別の話。

Raspberry Piでルーティングする

Raspberry Piを有線と無線の両方につないだとしても、そのままではルーティングしてくれません。
IP forwardを許可して iptables を設定すればOKです。

まず、/etc/sysctl.conf に「#net.ipv4.ip_forward=1」という行があるのでコメントを外します。
その後、/proc/sys/net/ipv4/ip_forwardに「1」を書き込んで reboot。
root@raspberrypi:~ # echo -n 1 > /proc/sys/net/ipv4/ip_forward
root@raspberrypi:~ # reboot
さらに、ルーティングされるようにiptables に設定を追加。
素通しするだけだから「nat」とか要らないかなぁ、という気もしますが、細かい設定はよく分かりません。
誰かちゃんとした設定を教えて、エロい人。
root@raspberrypi:~ # iptables -F
root@raspberrypi:~ # iptables -X
root@raspberrypi:~ # iptables -t nat -F
root@raspberrypi:~ # iptables -t nat -X
root@raspberrypi:~ # iptables -t mangle -F
root@raspberrypi:~ # iptables -t mangle -X
root@raspberrypi:~ # iptables -P INPUT ACCEPT
root@raspberrypi:~ # iptables -P FORWARD ACCEPT
root@raspberrypi:~ # iptables -P OUTPUT ACCEPT
root@raspberrypi:~ # iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
root@raspberrypi:~ # iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
「iptables -L」でこんな感じの出力になれば成功です。
root@raspberrypi:~ # iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination  
この状態で Raspberry Pi はルーティングしてくれるはずですが、
Windows マシンから 192.168.1.x のアドレスに ping を打っても返ってきません。
それは Raspberry Pi とは別の理由なので後で対応します。

iptables が起動時に設定されるようにする

Raspbian は標準で iptables が入っていますが、起動時に設定を反映してくれないようです。
「iptables-persistent」というパッケージを入れれば良い模様。

インストールは単に「sudo apt-get install iptables-persistent」でOK。
インストール時に現在のiptablesを保存するか聞かれるので「YES」。
その後、再起動すれば iptables が再設定されます。

Windowsから別セグメントのファイルサーバにアクセスできるようにする

これでルーティングされているはずなのですが Windows から NAS に ping が通りません。
なぜなら、Windows の default gateway が WiFi router2 になっているから。
WiFi router2 は NAS のアドレスを知らないため、インターネット側にルーティングしてしまいます。

もし、WiFi router2 の設定で、192.168.1.0/24 を Raspberry Pi にルーティングできるなら設定します。
普通の家庭用 WiFi ルータではそんな細かい設定ができないことが多いので、今回は Windows 側で頑張ります。

とは言え、Windows でも通常のコントロールパネルからは設定できません。
管理者権限でDOSプロンプトを開いて次のコマンドを実行。
# route -p add 192.168.1.0/24 192.168.2.10
「192.168.2.10」は Raspberry Piの LAN2 側アドレスです。
「-p」オプションは persistent のことで、このオプションを付けると再起動後にも反映されます。
もしいきなり試すのが怖ければ「-p」無しで実行すれば再起動で元に戻ります。

ここまで設定できたら Windows から ping を打ってみます。
# ping 192.168.2.10
PING 192.168.2.10 (192.168.2.10): 56 data bytes
64 bytes from 192.168.2.10: icmp_seq=0 ttl=64 time=2.546 ms
  :
# ping 192.168.1.10
PING 192.168.1.10 (192.168.1.10): 56 data bytes
64 bytes from 192.168.1.10: icmp_seq=0 ttl=64 time=2.546 ms
  :
# ping 192.168.1.2
PING 192.168.1.2 (192.168.1.2): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
・・・あれ?
Raspberry Pi の LAN1 側アドレスからは応答があるので、ルーティングはできています。
ところが、LAN1に繋がっているNASから応答がありません。

これは、NAS までは ping が通っています。
NAS から戻ってくるときに、NAS 側の default gateway が WiFi router1 になっているからです。
またかよ。

基本的に NAS からインターネットアクセスすることはないと思いますので、
NAS の default gateway を Raspberry Pi (192.168.1.10) にすれば大丈夫です。
もちろん、192.168.2.0/24 だけを振っても良いですが、家庭用NASではそんな設定はできないと思います。

Windowsのhostsファイルを編集する

これで、 LAN2 側の Windows から LAN1 側の NAS につながるようになりました。
ただし、このままだと名前解決できないので常に IP アドレスで繋ぐことになります。

Raspberry Pi で DNS を動かしても良いですが面倒なので Windows の hosts ファイルに直接書き込んでしまいます。
メモ帳を管理者権限で開いて hosts ファイルの末尾に設定を追加します。
Windows10 の hosts ファイルは「C:\Windows\System32\drivers\etc\hosts」です。
# cat C:\Windows\System32\drivers\etc\hosts
   :
192.168.1.10  NAS
これでホスト名でファイルサーバにアクセスできるようになりました。

おわり。

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をスリープから復帰させるように改造します。