BLE の接続シーケンスを Wireshark で確認してみる

 前回 BLE Sniffer と Wireshark で BLE パケットをスニッフィングする方法を書きましたが、 Wireshark で確認できる内容から BLE の接続シーケンスを確認してみたいと思います。手元にあった BLE デバイスで簡単に確認してみた結果を書いてみます。

ADV_IND

 BLE デバイスがアドバタイジングを開始するとアドバタイジング・パケットを送出し始めます。 PDU(プロトコル・データ・ユニット)のタイプとしては4タイプありますが、今回使用したデバイスでは ADV_IND となっていました。

f:id:akanuma-hiroaki:20180414135756p:plain

 ADV_IND を含め、アドバタイジング・パケットのタイプは下記の4タイプになります。

  • ADV_IND:不特定多数のデバイスから接続可能
  • ADV_NONCONN_IND:デバイスからの接続は不可能
  • ADV_SCAN_IND:デバイスからのスキャン可能
  • ADV_DIRECT_IND:特定デバイスから接続可能

SCAN_REQ

 セントラルデバイスがペリフェラルデバイスからアドバタイジング・パケットを受け取った時に、アドバタイジング・パケットのペイロードのデータだけでは情報が不足している場合、 スキャン・リクエスト(SCAN_REQ)パケットを送信します。

f:id:akanuma-hiroaki:20180414135829p:plain

SCAN_RSP

 ペリフェラルデバイスが SCAN_REQ パケットを受け取ると、その応答として SCAN_RSP パケットを送信します。

f:id:akanuma-hiroaki:20180414135845p:plain

CONNECT_REQ

 アドバタイジング・パケットの送受信が完了し、セントラルデバイスからペリフェラルデバイスに対して接続要求が行われると、 CONNECT_REQ パケットが送信され、接続状態に切り替わります。セントラルとペリフェラルはそれぞれマスター(Master)とスレイブ(Slave)になり、 Wireshark での Source と Definition のデバイス表示も、 MAC アドレス表示から Master, Slave に切り替わります。

f:id:akanuma-hiroaki:20180414135907p:plain

LL_VERSION_IND

 リンク層の接続制御に使われるロジカル・リンク・コントロール PDU では、オペコード(Opcode)でその種別が表されています。 LL_VERSION_IND はコントローラのバージョン番号を示すためのものです。

f:id:akanuma-hiroaki:20180414135940p:plain

Exchange MTU Request/Response

 Exchange MTU Request/Reponse はアトリビュート・プロトコルの最大長(MTU: Maximum Transfer Unit)を決定するために使われます。まずはクライアント側から自身が扱える ATT_MTU をサーバ側に通知します。そのレスポンスとしてサーバからはサーバ側で扱える ATT_MTU を返します。

f:id:akanuma-hiroaki:20180414140002p:plain

f:id:akanuma-hiroaki:20180414140113p:plain

Read Request/Response

 サーバのアトリビュートをクライアントから読み取るには Read Request/Response が使われます。 Read リクエストには下記の5種類があります。

  • Read
  • Read Multiple
  • Read Blob
  • Read By Type
  • Read By Group Type

 Read By Group Type ではアトリビュートのグループを読み出します。下記の例では GATT の Primary Service Declaration を読み取っています。

f:id:akanuma-hiroaki:20180414140933p:plain

f:id:akanuma-hiroaki:20180414141110p:plain

 Read By Type ではアトリビュート・タイプを指定した読み出しに使用します。下記は GATT の Characteristic Declaration の例です。

f:id:akanuma-hiroaki:20180414141256p:plain

f:id:akanuma-hiroaki:20180414141334p:plain

 アトリビュートが存在しない場合は下記のように Error Response が返ってくるようです。

f:id:akanuma-hiroaki:20180414142229p:plain

 Read ではアトリビュート・ハンドルを指定して値を読み出します。

f:id:akanuma-hiroaki:20180414143245p:plain

f:id:akanuma-hiroaki:20180414143330p:plain

 長さが ATT_MTU を超えるアトリビュート・バリューを持つアトリビュートはロング・アトリビュートと呼ばれ、その値を読み出す場合には Read Blob が使われます。

f:id:akanuma-hiroaki:20180414143734p:plain

f:id:akanuma-hiroaki:20180414220339p:plain

Find Information Request/Response

 アトリビュート・ハンドルの範囲を指定してアトリビュート・ハンドルとアトリビュート・タイプを読み出すには Find Information が使われます。アトリビュートのタイプとバリューが指定できる Find By Type Value リクエストもありますが、今回は使われていなかったようです。下記は Find Information の例です。

f:id:akanuma-hiroaki:20180414141457p:plain

f:id:akanuma-hiroaki:20180414141531p:plain

Write Request/Response

 アトリビュート・ハンドルを指定して書き込みを行うには Write が使用されます。

f:id:akanuma-hiroaki:20180414141757p:plain

f:id:akanuma-hiroaki:20180414141902p:plain

 Write には他にも下記の4種類がありますが、今回は使われていなかったようです。

  • Prepare Write
  • Execute Write
  • Write Command
  • Signed Write Command

Handle Value Notification

 サーバからクライアントへの通知には Handle Value Notification が使われます。

f:id:akanuma-hiroaki:20180414143716p:plain

Connection Parameter Update Request

 コネクションのパラメータを変更する場合には Connection Parameter Update Request が送信されます。

f:id:akanuma-hiroaki:20180414144317p:plain

 オペコードとしては LL_CONNECTION_UPDATE_REQ が使われます。

f:id:akanuma-hiroaki:20180414144438p:plain

 パラメータが変更されると Connection Parameter Update Response が返されます。

f:id:akanuma-hiroaki:20180414144544p:plain

LL_CHANNEL_MAP_REQ

 コネクションのチャンネル・マッピングを変更する際には LL_CHANNEL_MAP_REQ というオペコードが使われます。

f:id:akanuma-hiroaki:20180414144818p:plain

LL_TERMINATE_IND

 コネクションを切断するには LL_TERMINATE_IND オペコードが使われます。

f:id:akanuma-hiroaki:20180414145133p:plain

まとめ

 ざっとですが手持ちの一デバイスで確認できた内容を書いてみました。 BLE デバイスを使ったサービスを開発する場合に、BLEデバイスとの接続に問題がありそうな場合には、今回のように接続シーケンスを確認することで何か解決への手がかりが見つかるかもしれません。使用方法もとても簡単なので、勉強のためにも今後色々と使ってみたいと思います。

 今回は下記のサイトを参考にさせていただきました。

blog.reinforce-lab.com

dsas.blog.klab.org

BLE Sniffer + Wireshark で BLE パケットをスニッフィングする

 BLE デバイスを使ったシステムを開発していると、BLE デバイスがどんな状態かを確認したいことが時々あります。アドバタイズメントを出しているかは LightBlue などのツールでも確認できますが、通信状態の詳細はわかりませんし、接続後の状態は接続しているデバイス側でデバッグ用の対応を入れる必要があります。

 そこで今回は、 BLE Sniffer を使ってみました。BLE Sniffer を使うと周囲のBLEデバイスの通信状況を確認することができますし、 Wireshark と連携することでさらに詳しくパケットを確認することができます。

 今回使ったのは Adafruit の Blueftuit LE Sniffer です。

Bluefruit LE Sniffer - Bluetooth Low Energy (BLE 4.0) - nRF51822 - v3.0
https://www.adafruit.com/product/2269

f:id:akanuma-hiroaki:20180407194943j:plain:w450

 スイッチサイエンスにも掲載はありましたが在庫切れだったので、 mouser で購入しました。

https://www.mouser.jp/ProductDetail/Adafruit/2269?qs=%2fha2pyFadujCcMq4sbQCcNuQKfV2ZrTBUyTuH62%2f2DAJK9B%252b%2f1bGJ3muPYq2CyZ9

www.switch-science.com

ユーティリティのインストールなど

 基本的な使用手順は下記のイントロダクションページで紹介されています。

Introducing the Adafruit Bluefruit LE Sniffer
https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-sniffer

 Windowsユーザの方であれば上記ページにて紹介されている手順通りに Nordic の公式ユーティリティをインストールして使用できるのですが、 Mac には対応していないので、 Mac ユーザ向けのページが別途用意されています。

OS X Support
https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-sniffer/os-x-support

 上記ページで Roland King さん提供の OS X 用のユーティリティが紹介されていますので、こちらを使用します。

nrf-ble-sniffer-osx
https://sourceforge.net/projects/nrfblesnifferosx/

 インストール手順や使用方法は上記プロダクトの Wiki ページに記載されていますので、そちらに従います。

https://sourceforge.net/p/nrfblesnifferosx/wiki/ble_sniffer/

 インストールはプロダクトのページからパッケージインストーラをダウンロードして実行するだけです。

ble-sniffer-osx Download
https://sourceforge.net/projects/nrfblesnifferosx/files/latest/download

Wireshark のインストール

 BLE Sniffer 単体で利用してもある程度の利用状況はわかるのですが、パケットアナライザを使うことでより詳細にパケットの状況を確認することができます。 Bluefruit LE Sniffer では Wireshark と連携することができるようになっています。

Wireshark · Go Deep.

 上記サイトからダウンロードしてインストールするのですが、 Bluefruit LE Sniffer の OS X Support ページには下記のように記載があります。 

Be sure to download Wireshark version 2.0.x NOT the new 2.2.7 that was released June 2017

 Wireshark の新しいバージョンではなく、 2.0系を使うようにということなので、下記から過去のバージョンを探します。

Wireshark All Versions
Index of /pub/network/security/wireshark/osx/all-versions

 2.0系の最新バージョンは 2.0.16 のようなので、こちらをダウンロードしてインストールします。

Wireshark 2.0.16 Intel 64.dmg
http://ftp.yz.yamagata-u.ac.jp/pub/network/security/wireshark/osx/all-versions/Wireshark%202.0.16%20Intel%2064.dmg

 下記のように tshark コマンドが使えるようになっていれば正しくインストールされています。

$ tshark -v
TShark (Wireshark) 2.0.16 (v2.0.16-0-gd60ab50 from refs/heads/master-2.0)

Copyright 1998-2017 Gerald Combs <gerald@wireshark.org> and contributors.
License GPLv2+: GNU GPL version 2 or later <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Compiled (64-bit) with libpcap, without POSIX capabilities, with libz 1.2.5,
with GLib 2.36.0, with SMI 0.4.8, with c-ares 1.12.0, with Lua 5.2, with GnuTLS
3.4.17, with Gcrypt 1.7.7, with MIT Kerberos, with GeoIP.

Running on Mac OS X 10.13.3, build 17D102 (Darwin 17.4.0), with locale
C/UTF-8/C/C/C/C, with libpcap version 1.8.1 -- Apple version 79.20.1, with libz
1.2.11, with GnuTLS 3.4.17, with Gcrypt 1.7.7.
Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz (with SSE4.2)

Built using llvm-gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build
2336.9.00).

スニッフィング実行

 必要なもののインストールが終わったので、スニッフィングを実行してみたいと思います。ユーティリティは Applications ディレクトリ内で下記のようなアイコンで表示されますので、起動します。

f:id:akanuma-hiroaki:20180403072004p:plain

 BLE Sniffer を接続していないと下記のような表示になります。

f:id:akanuma-hiroaki:20180403000143p:plain

 ここで BLE Sniffer を接続します。接続して少し待つとスニッフィングが始まり、アドバタイズメントを出しているデバイスのリストが表示されます。項目としては、デバイス名、MACアドレス、電波強度(RSSI)、アドバタイズ用の3つのチャネルそれぞれでのインターバルが表示されます。

f:id:akanuma-hiroaki:20180403010717p:plain

 デバイスリストの中から特定のデバイスについて詳しく見たい時には、そのデバイスを選択して Sniff Device をクリックします。

f:id:akanuma-hiroaki:20180407191901p:plain

 すると「Currently Sniffing」というフィールドの表示が追加になり、スニッフィング中のデバイスの情報が表示されます。

f:id:akanuma-hiroaki:20180403010958p:plain

 デバイスの情報までは確認できたので、パケットの状況を詳しく見るために、 Wireshark と連携してみます。 Capture to Wireshark ボタンをクリックします。

f:id:akanuma-hiroaki:20180407192235p:plain

 すると Wireshark が起動し、 スニッフィングしたパケットが Wireshark で表示されます。

f:id:akanuma-hiroaki:20180403011507p:plain

 起動時のデフォルトでは時系列で並んだ一番古いパケットの部分が表示されていますが、メニューの下記ボタンを押すことで常に最新のパケットが表示されるようにスクロールされます。

f:id:akanuma-hiroaki:20180407192436p:plain

 下側のペインではBLEパケットのさらに詳しい情報が表示されます。一つ目はフレームの詳細です。

f:id:akanuma-hiroaki:20180403011538p:plain

 二つ目は BLE Sniffer のメタデータです。

f:id:akanuma-hiroaki:20180403011601p:plain

 三つ目はデータリンク層の情報です。

f:id:akanuma-hiroaki:20180403011625p:plain

まとめ

 BLEデバイスを使うサービスを開発するときは、BLEデバイス側に問題があるのか、アプリ側に問題があるのかの切り分けが難しい場合がありますが、このツールを使うことでアドバタイズメントの状況や詳しいパケットの情報が見られるので、問題の切り分けがしやすくなりそうです。BLEでどういったパケットがどういう順番でやり取りされるかを知らないと見てもわからない部分もありますが、逆にそれを理解するためにもこういったツールは有効かと思います。

Arduino Cloud に Raspberry Pi を接続して Lチカ

 Arduino ではオンライン上でのオールインワンの開発プラットフォームとして Arduino Create というものを提供しています。

create.arduino.cc

f:id:akanuma-hiroaki:20180322081919p:plain

 以前の記事で使った Arduino Web Editor も Arduino Create で提供されているサービスです。そのほかのサービスとして Arduino Cloud という、 Arduino を簡単にインターネットに接続したり、ほかの Arduino と相互に接続したり、デバイスを管理したりできるサービスを提供しています。現在 Arduino Cloud の対応デバイスはまだ少なく、 YUN SHIELD、 MKR1000、 WIFI SHIELD 101 の3つのようで、いずれも技適を通っていないため日本ではまだ Arduino Cloud に対応している Arduino 端末はないようです。前回の記事で使用した WROOM-32 も Arduino 互換なので、なんとか接続できないかと色々試してみたのですが、今のところまだ接続できていません。

f:id:akanuma-hiroaki:20180321202749p:plain

 とりあえず対応している Arduino 端末が日本でも使えるようになるのを待ちたいと思いますが、 Arduino Cloud では Arduino 以外にも、 Raspberry Pi や BeagleBone などの Linux 端末も接続することができるので、今回は Raspberry Pi を接続してみたいと思います。

Arduino Cloud に Raspberry Pi を登録

 まずは Arduino Cloud に Raspberry Pi をデバイスとして登録します。Raspberry Pi の通常の初期セットアップは事前に終わらせておき、ネットワークに接続されている状態にしておきます。 Arduino Cloud にアクセスすると下記のようなメニュー画面になります。 Arduino Cloud 対応の Arduinoボードを登録する場合には右下の Arduino Cloud を選択すれば良いのですが、 Raspberry Pi などの Linux 端末を登録する場合には、左下の My Devices を選択します。

f:id:akanuma-hiroaki:20180321163526p:plain

 デバイスを登録するとできるようになることなどの説明が表示されるので、 ADD NEW DEVICE をクリックします。

f:id:akanuma-hiroaki:20180321163445p:plain

 登録するデバイスを選択する画面になりますので、今回は Set up a Raspberry Pi を選択します。

f:id:akanuma-hiroaki:20180321163507p:plain

 セットアップの手順内で行われる内容の説明が表示されます。セットアップでは PC 側にまだ Arduino Plugin がインストールされていなければインストールし、 Raspberry Pi 側に Arduino Connector がインストールされます。 NEXT をクリックして先に進みます。

f:id:akanuma-hiroaki:20180321163646p:plain

 Raspberry Pi がオンラインになっていることを確認するように促す画面が表示されますので、オンラインになっていれば MY DEVICE IS READY をクリックします。

f:id:akanuma-hiroaki:20180321163703p:plain

 セットアップの実行の仕方の選択肢として、 Raspberry Pi の IP アドレスがわかるか、 Raspberry Pi にモニタとキーボードが使えるようになっているかを選択します。今回はモニタとキーボードは繋いでいないですが、 IP アドレスはわかっているので、 I KNOW THE IP ADDRESS OF MY DEVICE を選択します。

f:id:akanuma-hiroaki:20180321201510p:plain

 すると IP アドレスの入力欄が表示されるので、 IP アドレスを入力して NEXT をクリックします。

f:id:akanuma-hiroaki:20180321201527p:plain

 Raspberry Pi にアクセスするためのアカウント情報が求められるので、フォームに入力して DONE をクリックします。

f:id:akanuma-hiroaki:20180321201543p:plain

 セットアップが開始され、処理中は下記のような画面が表示されます。ここで Arduino Connector のインストールなどが行われます。処理が終わると画面は自動的に切り替わりますので、このまま待ちます。

f:id:akanuma-hiroaki:20180321201600p:plain

 セットアップが無事終了すると下記のように Arduino Cloud に登録するデバイス名の入力フォームが表示されますので、わかりやすい名前をつけて SAVE をクリックします。

f:id:akanuma-hiroaki:20180321201616p:plain

 これでひとまず登録は完了ですので、 GO TO MY DEVICES をクリックしてデバイス一覧画面に移動します。

f:id:akanuma-hiroaki:20180321201631p:plain

 デバイス一覧には登録した Raspberry Pi が表示され、ネットワークに接続されていれば ONLINE という表示になっています。

f:id:akanuma-hiroaki:20180321201648p:plain

 ここでデバイスを選択すると下記のようにデバイスの状態についてのダッシュボードが表示されます。

f:id:akanuma-hiroaki:20180321202818p:plain

 ちなみに登録した Raspberry Pi でホームディレクトリを見てみると、Arduino Connector 関連の下記のようなファイルが追加されています。

$ ls -ltr ~
合計 28368
〜〜〜抜粋〜〜〜
-rwxr-xr-x  1 pi   pi   11346716  35 15:31 arduino-connector
-rw-------  1 root root      227  321 08:32 certificate.key
-rw-------  1 root root     1099  321 08:32 certificate.pem
-rw-r-----  1 pi   root      203  321 08:32 arduino-connector.cfg
-rw-r--r--  1 pi   pi        445  321 08:32 arduino-connector.log

 ログにはインストール時のものと思われるログが記録されています。 MQTT のサーバとしては AWS IoT を使っているっぽいです。

$ cat arduino-connector.log 
Version: 1.0.94
Generate private key
Generate csr
Request certificate
Request mqtt url
Write conf to arduino-connector.cfg
Check successful mqtt connection
setupMQTT certificate.pem certificate.key akanuma:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx xxxxxxxxxxxxxx.iot.us-east-1.amazonaws.com
WARN: memstore.go:137: [store]    memorystore wiped
WARN: net.go:328: [net]      errorWatch stopped
WARN: net.go:318: [net]      logic stopped
Setup completed

 設定ファイルは下記のような内容で作成されていました。

$ cat arduino-connector.cfg 
id=akanuma:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
url=xxxxxxxxxxxxxx.iot.us-east-1.amazonaws.com
http_proxy=
https_proxy=
all_proxy=
authurl=https://hydra.arduino.cc/
apiurl=https://api2.arduino.cc

スケッチの実行

 Arduino Cloud にデバイスを登録すると、 Arduino Uno 等の Arduino ボードと同様に、 Web Editor からスケッチをフラッシュして実行させることができます。そのデバイスがオンラインであれば Web Editor の接続先ボードとして選択できるようになりますので選択します。

f:id:akanuma-hiroaki:20180322072428p:plain

 今回はとりあえず下記のような Lチカのスケッチで試してみます。

const int ledPin = 12; // GPIOピン番号ではなく物理ピン番号

void setup()
{
  pinMode(ledPin, OUTPUT);
}

void loop()
{
  digitalWrite(ledPin, HIGH);
  delay(1000); // Wait for 1000 millisecond(s)
  digitalWrite(ledPin, LOW);
  delay(1000); // Wait for 1000 millisecond(s)
}

 上記コードを用意したら Arduinoボードの時と同様に、 Verify ボタンでコンパイル可能か確認して問題なければ Upload ボタンで Raspberry Pi にスケッチをアップロードして実行します。成功すると下記のように Web Editor のコンソールに Raspberry Pi で実行されているプロセスの ID が出力されます。

f:id:akanuma-hiroaki:20180322073650p:plain

 Raspberry Pi 上で確認してみると、確かにその PID でスケッチが実行されています。

$ ps aux | grep 1474 | grep -v grep
root      1474  0.1  0.2   4340  2520 pts/2    Ss+  22:35   0:00 /home/pi/sketches/CoolDensor1

 アップロードからLチカ実行の様子は下記動画のようになります。

 さらにプロセスの実行/停止もWeb上から操作できます。 Web Editor の RUN/STOP ボタンをクリックします。

f:id:akanuma-hiroaki:20180322075604p:plain

 すると登録済みのスケッチのリストと、各スケッチの実行状況がトグルボタンで表示されますので、このトグルを切り替えることでプロセスの実行/停止を行うことができます。

f:id:akanuma-hiroaki:20180322075620p:plain

 操作してみた様子は下記動画の通りです。トグルの切り替えでLチカの実行/停止を操作することができています。

まとめ

 Raspberry Pi 側では初期セットアップさえ終わっていれば、簡単に Arduino Cloud に登録できますし、登録してしまえば Raspberry Pi 側で直接操作することなく、他の Arduino ボードと同様に Web Editor から触ることができて便利ですね。 Arduino 以外にも mbed をはじめ、各社がクラウド環境でのプラットフォームを提供するようになってきていますが、それぞれできることが違っているので、デバイスを選定する場合はボード自体の機能だけでなく、プラットフォームの内容も含めて検討するのが良さそうです。

ESPr Developer 32 で WROOM-32 を Wi-Fi に繋いでみる

 前回 Seeeduino という Arduino Uno 互換ボードで Arduino に初めてさわってみたわけですが、 Arduino Uno は標準ではネットワークインタフェースを持っておらず、 Wi-Fi シールドも技適を通っているものを使うとなると選択肢は少なく、あっても結構高くついてしまうようでした。また、 ESP-WROOM-02 のような Wi-Fi モジュールを使おうと思うと、 Arduino Uno の 3.3V 出力では電流量が足りずに正しく動作しないようなので、それであれば Arduino Uno との組み合わせではなく単体でも Arduino 互換ボードとして動作し、 Wi-Fi だけでなく Bluetooth/BLE にも対応している ESP-WROOM-32 を使ってしまうのが良さそうということで、購入してみました。モジュールのサイトは下記になります。

www.espressif.com

 実際に使うにあたってはモジュール単体よりも開発ボードを買った方が色々とやりやすいので、今回はスイッチサイエンスで販売されている ESPr Developer 32 を購入しました。

www.switch-science.com

 ESP-WROOM-32 の開発ボードの選択肢としては他にも秋月電子で販売されている ESP32-DevKitC などもありますが、ESPr Developer 32 であれば突入電流対策が行われているということでしたので、値段的には ESP32-DevKitC よりも若干高いですが、 ESPr Developer 32 を選んでみました。

akizukidenshi.com

開発環境の準備から動作確認まで

 Seeeduino は Arduino Uno 互換ということで、 Arduino Web Editor を使ったのですが、 WROOM-32 は Web Editor では対応できないようだったので、 Arduino IDE を Mac にインストールして使用しました。インストール手順は下記リンク先で紹介されています。特に難しいことはないと思いますのでここでは詳細は割愛します。

Arduino - MacOSX

 次に、 Arduino IDE で WROOM-32 のファームウェアの開発ができるように、 Arduino core for ESP32 をインストールします。

github.com

$ mkdir -p ~/Documents/Arduino/hardware/espressif
$ cd ~/Documents/Arduino/hardware/espressif
$ git clone https://github.com/espressif/arduino-esp32.git esp32
$ cd esp32
$ git submodule update --init --recursive
$ cd tools
$ python get.py

 インストールが終わったら、 Mac と ESPr Developer 32 を USB ケーブルで接続します。

f:id:akanuma-hiroaki:20180317154120j:plain

 そして Arduino IDE を起動します。起動したら Tools メニューの Board の選択肢から、 ESP32 Dev Module を選択します。

f:id:akanuma-hiroaki:20180317152923p:plain

 これでひとまず環境の準備はできましたので、とりあえず正しく ESPr Developer 32 にファームウェアが書き込めるか確認してみます。下記スケッチを Arduino IDE で作成します。シリアル接続してシリアルモニタに文字を出力するだけのコードです。

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println("Hello World from ESPr Developer 32 :)");
  delay(1000);
}

 コードを入力したらまずは IDE の Verify ボタンでビルドできるか確認します。下記のような感じでエラーなくビルドされれば成功です。 

Sketch uses 162273 bytes (12%) of program storage space. Maximum is 1310720 bytes.
Global variables use 11052 bytes (3%) of dynamic memory, leaving 283860 bytes for local variables. Maximum is 294912 bytes.

 それでは IDE の Update ボタンで ESPr Developer 32 に書き込みます。

Sketch uses 162273 bytes (12%) of program storage space. Maximum is 1310720 bytes.
Global variables use 11052 bytes (3%) of dynamic memory, leaving 283860 bytes for local variables. Maximum is 294912 bytes.
esptool.py v2.1
Connecting....
Chip is ESP32D0WDQ6 (revision 0)
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 921600
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 8192 bytes to 47...

Writing at 0x0000e000... (100 %)
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.0 seconds (effective 4364.7 kbit/s)...
Hash of data verified.
Compressed 14128 bytes to 9196...

Writing at 0x00001000... (100 %)
Wrote 14128 bytes (9196 compressed) at 0x00001000 in 0.1 seconds (effective 1022.1 kbit/s)...
Hash of data verified.
Compressed 163424 bytes to 84070...

Writing at 0x00010000... (16 %)
Writing at 0x00014000... (33 %)
Writing at 0x00018000... (50 %)
Writing at 0x0001c000... (66 %)
Writing at 0x00020000... (83 %)
Writing at 0x00024000... (100 %)
Wrote 163424 bytes (84070 compressed) at 0x00010000 in 1.4 seconds (effective 929.0 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 122...

Writing at 0x00008000... (100 %)
Wrote 3072 bytes (122 compressed) at 0x00008000 in 0.0 seconds (effective 1712.6 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting...

 書き込みが成功すると自動的にプログラムが実行され、シリアルモニタでは下記のように文字列の出力が繰り返されます。

f:id:akanuma-hiroaki:20180318121723p:plain

Wi-Fi 接続確認

 ひとまず動作確認ができたので、 Wi-Fi への接続も確認してみます。とりあえず Wi-Fi に接続して、Webサーバへのリクエストを投げてみます。接続先としては同じ LAN に接続している Mac で下記のように HTTP サーバを起動しておきます。

$ python -m SimpleHTTPServer                                                                                                                                                                                                          
Serving HTTP on 0.0.0.0 port 8000 ...

 ESPr Developer 32 で動かすコードは下記のようになります。 Wi-Fi への接続自体は WiFi.begin() で SSID とパスワードを指定するだけでOKで、あとは WiFi.status() で接続ステータスが WL_CONNECTED になれば接続完了です。 loop() の処理としては先ほど動かした HTTP サーバに GET リクエストを投げてレスポンスを表示しているだけの単純なものです。

#include <WiFi.h>
#include <HTTPClient.h>

const char SSID[] = "aterm-xxxxxx-g";
const char PASSWORD[] = "xxxxxxxxxxxxx";
const char URL[] = "http://192.168.10.5:8000/";

void setup() {
  Serial.begin(115200);
  while(!Serial);

  WiFi.begin(SSID, PASSWORD);
  Serial.printf("Connecting to the WiFi AP: %s ", SSID);

  while(WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }

  Serial.println(" connected.");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  HTTPClient http;
  http.begin(URL);

  Serial.printf("Response: %d\n", http.GET());
  String body = http.getString();
  Serial.print("Response Body: ");
  Serial.println(body);

  delay(1000);
}

 これを Verify して問題なければ Upload で ESPr Developer 32 に書き込みます。書き込みが終わって起動すると、下記のようにシリアルモニタに出力され、 Wi-Fi へ接続して HTTP サーバからコンテンツを取得していることがわかります。

Connecting to the WiFi AP: aterm-xxxxxx-g ......... connected.
IP address: 192.168.10.10
Response: 200
Response Body: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
<title>Directory listing for /</title>
<body>
<h2>Directory listing for /</h2>
<hr>
<ul>
</ul>
<hr>
</body>
</html>

 HTTP サーバ側でも下記のようにリクエストを受信しているログが出力されます。

192.168.10.10 - - [17/Mar/2018 16:30:09] "GET / HTTP/1.1" 200 -
192.168.10.10 - - [17/Mar/2018 16:30:09] "GET / HTTP/1.1" 200 -
192.168.10.10 - - [17/Mar/2018 16:30:10] "GET / HTTP/1.1" 200 -

まとめ

 ネットワークインタフェースを搭載していないボードの場合、まずネットワークに接続できるようになるまでで結構苦労しそうですが、 ESPr Developer 32 のようなネットワークモジュールを積んだ開発ボードを使うことで簡単にネットワークに接続できてしまいます。値段的にも Arduino Uno やその互換ボードと比べても高くないですし、特に理由がなければ WROOM-32 などを最初から使ってしまう方が良さそうです。 ESPr Developer 32 にはピンヘッダがついていないので、今後自分ではんだ付けしてセンサー等との連携も試してみたいと思います。

Seeeduino(Arduino互換ボード)+ Tinkercad で Lチカ

 今まで Arduino は触ったことがなかったのですが、 Arduino 互換ボードの Seeeduino を買ったのでとりあえず Lチカしてみました。

www.switch-science.com

 Seeeduino は Arduino 互換というだけでなく、 GROVEコネクタが搭載されていますので、 Arduino よりも手軽にいろんなデバイスが接続できそうです。

f:id:akanuma-hiroaki:20180313074204j:plain:w450

 注意点として、PCと接続するためのUSBケーブルが、 Arduino Uno は A - B のタイプですが、 Seeeduino では A - microB ですので、そこはお気をつけください。

まずは Tinkercad でシミュレーション

 早速回路を組みたくなるところですが、まずは Tinkercad でシミュレーションしてみたいと思います。 Tinkercad は電子回路の作成と Arduino でのコード実行をシミュレートできるツールで、以前会社のブログで紹介させていただきましたので詳細はそちらをご覧ください。

tech.unifa-e.com

 Seeeduino は Tinkercad のパーツとしては用意されていないので、 Arduino を使っています。ブレッドボードに LED と抵抗(330Ω)を配置して、 Arduino に接続しています。コードはデフォルトで用意されている Lチカのコードそのままで、下記のようになります。これでシミュレーションを実行すると LED が点滅します。コードのダウンロードもできますので、あとで実機で動かすためにダウンロードしておきます。右上のダウンロードボタンをクリックすると、.ino形式のファイルとしてダウンロードすることができます。

f:id:akanuma-hiroaki:20180313082955p:plain:w450

Arduino Web Editor にコードをインポート

 Arduino ではプログラム(スケッチ)を開発するためのIDEが提供されています。

Arduino - Software

 IDE にはブラウザからアクセスしてオンラインで使える Arduino Web Editor と、MacやPCにインストールして使用する Arduino IDE があります。今回は Arduino Web Editor を使ってみます。

create.arduino.cc

 使用するにはアカウント登録が必要です。特に登録手順で難しいことはないのでここでは詳細は割愛します。

 Web Editor に初めてアクセスすると利用規約への同意が求められますので、名前と国を選んで NEXT をクリックします。

f:id:akanuma-hiroaki:20180313083730p:plain:w450

 Web Editor から実機にスケッチをフラッシュするにはプラグインをインストールする必要があり、その案内が表示されます。 NEXT をクリックして先へ進みます。

f:id:akanuma-hiroaki:20180313083747p:plain:w450

 左上で OS の種類を選択し、 DOWNLOAD PLUGIN ボタンをクリックするとインストーラがダウンロードされますので、 Mac であればダウンロードされた .dmg ファイルを実行し、案内に従ってインストールします。インストール自体は特に難しいことはないのでここでは割愛します。

f:id:akanuma-hiroaki:20180313083803p:plain:w450

 インストールが終了するとブラウザでの表示が下記のように変わりますので、 NEXT をクリックします。

f:id:akanuma-hiroaki:20180313083856p:plain:w450

 機能説明を実行するか選べるようになってます。機能説明を見るなら YES! をクリックします。

f:id:akanuma-hiroaki:20180313083916p:plain:w450

 一通り説明が終わると、下記のように手持ちのスケッチブックデータをインポートするか聞かれますので、 YES! をクリックします。

f:id:akanuma-hiroaki:20180313083936p:plain:w450

 アップロードできるのは .Zip .Ino .Pde 形式のいずれかという注意書きが表示されます。Tinkercad からダウンロードしたファイルは .Ino 形式ですので、 IMPORT をクリックしてファイルを選択してアップロードします。

f:id:akanuma-hiroaki:20180313083958p:plain:w450

 これでエラーにならなければ無事アップロード完了ということで、下記のように Tinkercad からダウンロードしたコードが Web Editor にインポートされて表示されます。

f:id:akanuma-hiroaki:20180313084014p:plain:w450

回路作成と接続

 それでは実際に回路を作成します。 Tinkercad で作ったものと同様の回路を構成して、 USB で接続します。

f:id:akanuma-hiroaki:20180313084132j:plain:w450

 Web Editor で、PCに接続しているボードとポートを選択します。プルダウンメニューから Board at 〜 もしくは Select Other Board & Port を選択します。

f:id:akanuma-hiroaki:20180313091600p:plain:w450

 Seeeduino は Arduino Uno 互換なので、 BOARDS から Arduino/Genuino Uno を選択します。 ポートは一台しか接続していなければ一つしか表示されないので、それを選択して、 OK をクリックします。

f:id:akanuma-hiroaki:20180313091616p:plain:w450

 画面上部の Verify ボタンを押して、ボードの選択が正しく行われているか確認します。 Success: Done verifying 〜 と表示されれば成功です。

f:id:akanuma-hiroaki:20180313091632p:plain:w450

動作確認

 それでは Upload ボタンを押して、ボードにスケッチをフラッシュします。 Success: Done uploading 〜 と表示されれば成功です。

f:id:akanuma-hiroaki:20180313091652p:plain:w450

 フラッシュ後は下記動画のようにLEDが点滅します。

まとめ

 Tinkercad で事前にシミュレーションすることで動作を確認した上で回路を組むことができますし、 Arduino Web Editor を使えばプラグインのインストール以外の環境構築も必要なく、手軽にプログラミングすることができました。 Seeeduino は価格的にも Arduino より安いですし、それでいて GROVE コネクタもついているので、色々と遊んでみたいと思います。

Voice Kit を Android Things で動かしてみる

f:id:akanuma-hiroaki:20180308074945p:plain:right

 Voice Kit は標準では Raspbian で動かしますが、 Android Things を使うこともできるということなので、試しに動かしてみました。 Voice Kit のページから Android Things のチュートリアルへのリンクが張られています。

*1


aiyprojects.withgoogle.com

 Android Things とはスマートフォンだけでなく IoT デバイスでも Android が動くように最適化されたもので、全てではないですが Android の API や SDK を使うことができます。逆に Android Things の API には各種センサーからデータを取得したり、ハードウェアを操作したりするための API が追加されていますので、 Android アプリ開発ができる方であれば同じスキルで IoT デバイスの開発ができそうです。ちなみに Android Things は現在まだ Developer Preview です。

developer.android.com

 Voice Kit を Android Things で動かすチュートリアルは下記で公開されています。

Android Things Assistant

Android Studio をインストール/アップデート

 Android アプリ開発には Android Studio を使いますが、 Android Things でも同様に Android Studio を使いますのでインストールします。 Android Things には Android Studio の 3.0 以上が必要なので、古いバージョンを使っている場合はアップデートします。私は 2系 だったのでアップデートしました。

developer.android.com

Android SDK をアップデート

 SDK も古い場合にはアップデートが必要です。 SDK Tools は 25.0.3 以上、 SDK Platforms は Android 7.0(API24)以上が必要となりますので、 SDK Manager でアップデートします。

Update the IDE and SDK Tools | Android Studio

f:id:akanuma-hiroaki:20180306081242p:plain

f:id:akanuma-hiroaki:20180306081303p:plain

Android Things イメージの書き込み

 次に Android Things のイメージを microSDカードに書き込みます。 手順は下記にも公開されています。

Raspberry Pi 3 | Android Things

 Android Things ではイメージのダウンロードや書き込みを行ってくれるセットアップツールが用意されているので、まずはそちらをダウンロードするために Android Things コンソールにアクセスします。初めてアクセスした時は規約への同意を求められるので、チェックボックスにチェックを入れて CONTINUE をクリックします。

f:id:akanuma-hiroaki:20180306081903p:plain:w450

 コンソールが表示されたら左上のメニューから Tools をクリックします。

f:id:akanuma-hiroaki:20180306081918p:plain:w450

 Setup Utility の説明が表示されますので、 DOWNLOAD ボタンをクリックしてツールをダウンロードします。

f:id:akanuma-hiroaki:20180306081933p:plain:w450

 ダウンロードしたツールを解凍すると、下記のように各プラットフォーム向けのツールが用意されています。

$ ls -l Downloads/android-things-setup-utility/
total 40024
-rw-r--r--@ 1 akanuma  staff     1721 Jan 26 03:55 README.md
-rwxr-xr-x@ 1 akanuma  staff  6339237 Jan 26 03:54 android-things-setup-utility-linux
-rwxr-xr-x@ 1 akanuma  staff  7687540 Jan 26 03:54 android-things-setup-utility-macos
-rwxr-xr-x@ 1 akanuma  staff  6456832 Jan 26 03:54 android-things-setup-utility-windows.exe

 Macの場合は下記のようにファイル名末尾が macos になっているものを実行します。

$ sudo ~/Downloads/android-things-setup-utility/android-things-setup-utility-macos 

Android Things Setup Utility (version 1.0.18)
============================
This tool will help you install Android Things on your board and set up Wi-Fi.

 対話形式で設定を選択していきます。まずは新たに Android Things をインストールするか、既存の Android Things デバイスの Wi-Fi をセットアップするか聞かれるので、前者(1)を選択します。

What do you want to do?
1 - Install Android Things and optionally set up Wi-Fi
2 - Set up Wi-Fi on an existing Android Things device
1

 次にハードウェアの選択です。 Voice Kit の場合は Raspberry Pi なので、1 の Raspberry Pi 3 を選択します。すると必要なツールのダウンロードと解凍が行われます。

What hardware are you using?
1 - Raspberry Pi 3
2 - NXP Pico i.MX7D
3 - NXP Pico i.MX6UL
1
You chose Raspberry Pi 3.

Setting up required tools...
Fetching additional configuration...
Downloading platform tools...
5.45 MB/5.45 MB
Unzipping platform tools...
Finished setting up required tools.

 次にデフォルトのイメージかカスタムイメージを使うか聞かれます。初めて使う場合はデフォルトイメージを選択します。するとイメージのダウンロードと解凍が行われ、さらに SD カードにイメージを書き込むためのツールのダウンロードと解凍が行われます。

Do you want to use the default image or a custom image?
1 - Default image: Used for development purposes. No access to the Android
Things Console features such as metrics, crash reports, and OTA updates.
2 - Custom image: Provide your own image, enter the path to an image generated
and from the Android Things Console.
1
Downloading Android Things image...
274 MB/274 MB 
Unzipping image...

Downloading Etcher-cli, a tool to flash your SD card...
20.5 MB/20.5 MB
Unzipping Etcher-cli...

 ここまででイメージの準備ができました。SDカードを Mac のSDカードスロットに入れて Enter キーを押すように促されますので、イメージを書き込む microSD カードをスロットに入れて Enter を押します。

Plug the SD card into your computer. Press [Enter] when ready

Running Etcher-cli...

 ドライブの選択プロンプトが表示されます。複数の SD カードスロットがある場合は複数のドライブが表示されるかと思いますが、一つの場合はそのまま Enter を押します。

? Select drive /dev/disk2 (32.0 GB) - SD Card Reader

 選択したドライブの SD カードの内容は消去されますので、問題なければ Yes を入力して Enter します。すると実際に書き込みが行われます。

? This will erase the selected drive. Are you sure? Yes
Flashing [========================] 100% eta 0s  
Validating [========================] 100% eta 0s  
iot_rpi3.img was successfully written to SD Card Reader (/dev/disk2)
Checksum: c1891a15

If you have successfully installed Android Things on your SD card, you can now
put the SD card into the Raspberry Pi and power it up. Otherwise you can abort
and run the tool again.

 書き込みが終わると、続けて Wi-Fi のセットアップを行うか聞かれます。行う場合は y を選択します。

Would you like to set up Wi-Fi on this device? (y/n) y

 ここまで来たら Mac から microSD カードを抜いて Voice Kit に挿し、 LAN ケーブルで LAN に接続して起動します。起動したら Enter を押すと、 Mac から LAN 内の Android Things(Voice Kit)に接続し、 Wi-Fi のセットアップを続行します。

Please plug your Raspberry Pi to your router with an Ethernet cable, then press [Enter].


Attempting to connect to your Raspberry Pi at Android.local...
Connected to device through Ethernet.

 正しく接続されると接続先APの SSID とパスワードを聞かれるので入力します。Wi-Fi セットアップが成功すれば一通りセットアップは完了ですので、 Enter を押してセットアップツールを終了します。

Enter the Wi-Fi network name: xxxxxxxxxxxxxx
Enter the Wi-Fi network password (leave empty if no password): 
Connecting to Wi-Fi network xxxxxxxxxxxxxx...
Looking for device... This can take up to 3 minutes.
Device found.
Waiting...
Successfully connected to Wifi
Stopping adb server...
Stopped adb server...

Now that you’re set up, try sample projects in Android Studio or in the sample
repository here: https://developer.android.com/things/sdk/samples.html

To learn more about features like over-the-air updates, visit the Android Things
Console: https://partner.android.com/things/console

Press [Enter] to quit.

 セットアップが終了したら一度 Voice Kit を再起動しておくのが良いかと思います。

adb での接続

 adb で Voice Kit に接続します。 IP アドレス指定でも接続できますが、マルチキャストDNSで Android.local というホスト名でも接続することができるようになっています。

$ adb connect Android.local
connected to Android.local:5555

サンプルコードの取得

 チュートリアルではサンプルコードが提供されていますので、今回はとりあえずそれを動かして見ます。 github から git clone で取得します。

$ git clone https://github.com/googlecodelabs/androidthings-googleassistant.git
Cloning into 'androidthings-googleassistant'...
remote: Counting objects: 338, done.
remote: Total 338 (delta 0), reused 0 (delta 0), pack-reused 338
Receiving objects: 100% (338/338), 117.37 KiB | 0 bytes/s, done.
Resolving deltas: 100% (101/101), done.
Checking connectivity... done.

認証情報の取得

 Google Assistant の API を使うためには API の有効化と認証情報の取得が必要になります。チュートリアル内でも紹介されていますが、私が最初に Voice Kit を試した時のサンプルコード実行時にも同様のことを行なっていますのでご参照ください。 assistant.json というファイル名で認証情報を保存しておきます。

blog.akanumahiroaki.com

認証実行

 まずは認証用のツールを pip でインストールします。

$ pip install google-auth-oauthlib[tool]

 続いてツールを使って認証を行います。 --client-secrets オプションには先ほど保存した認証情報のファイルパスを指定します。

$ google-oauthlib-tool --client-secrets assistant.json \
>                        --credentials shared/src/main/res/raw/credentials.json \
>                        --scope https://www.googleapis.com/auth/assistant-sdk-prototype \
>                        --save --headless
Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=XXXXXXXXXXXX-v9m9nb4bgdp58qk159cghm1ketnvin5t.apps.googleusercontent.com&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fassistant-sdk-prototype&state=QBeuos2rtESJnJ9TEjX6sp1xaAFKaV&prompt=consent&access_type=offline

 表示されたURLにブラウザからアクセスし、表示される認証コードをコピーして入力します。正しく認証されれば credentials.json というファイルが作成されます。

Enter the authorization code: 4/AAAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
credentials saved: shared/src/main/res/raw/credentials.json

サンプルコードの実行

 git clone したサンプルコード androidthings-googleassistant を Android Studio にインポートします。ライブラリの不足がなければ下記のように Gradle でのビルドが行われます。

21:20    Gradle sync started
21:21   Project setup started
21:21   Gradle sync finished in 45s 764ms
21:21   Executing tasks: [:shared:generateDebugSources, :shared:generateDebugAndroidTestSources, :shared:mockableAndroidJar, :step1-start-here:generateDebugSources, :step1-start-here:generateDebugAndroidTestSources, :step1-start-here:mockableAndroidJar, :step2-volume-control:generateDebugSources, :step2-volume-control:generateDebugAndroidTestSources, :step2-volume-control:mockableAndroidJar, :step3-conversational-state:generateDebugSources, :step3-conversational-state:generateDebugAndroidTestSources, :step3-conversational-state:mockableAndroidJar]
21:21   Gradle build finished in 22s 828ms

 ビルドが終わったらサンプルコードを実行してみます。 step1, 2, 3 が用意されていますが、今回はとりあえず step1 を実行してみます。 step1 は Google Assitant の基本的なデモです。 Android Studio の Run メニューから Run 'step1-start-here' を選択します。選択肢にないときは Run... から選択します。

f:id:akanuma-hiroaki:20180307083756p:plain:w300

 対象のデバイスとして Voice Kit を選択します。デバイス名は Google Iot_rpi3 となっています。

f:id:akanuma-hiroaki:20180307083809p:plain

 私の場合は初回実行時は下記のようにエラーになってしまいました。

03/04 21:32:50: Launching step1-start-here
$ adb shell am start -n "com.example.androidthings.assistant/com.example.androidthings.assistant.AssistantActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Client not ready yet..Waiting for process to come online
Connected to process 4115 on device google-iot_rpi3-Android.local:5555
Capturing and displaying logcat messages from application. This behavior can be disabled in the "Logcat output" section of the "Debugger" settings page.
I/InstantRun: starting instant run server: is main process
I/AssistantActivity: starting assistant demo
D/AssistantActivity: enumerating devices
I/AssistantActivity: initializing DAC trigger
I/AssistantActivity: setting volume to: 15
D/AudioTrack: Client defaulted notificationFrames to 460 for frameCount 1380
E/AudioRecord: AudioFlinger could not create record track, status: -1
E/AudioRecord-JNI: Error creating AudioRecord instance: initialization check failed with status -1.
E/android.media.AudioRecord: Error code -20 when initializing native AudioRecord object.
D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
                  Process: com.example.androidthings.assistant, PID: 4115
                  java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.androidthings.assistant/com.example.androidthings.assistant.AssistantActivity}: java.lang.UnsupportedOperationException: Cannot create AudioRecord
                      at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2778)
                      at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856)
                      at android.app.ActivityThread.-wrap11(Unknown Source:0)
                      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589)
                      at android.os.Handler.dispatchMessage(Handler.java:106)
                      at android.os.Looper.loop(Looper.java:164)
                      at android.app.ActivityThread.main(ActivityThread.java:6494)
                      at java.lang.reflect.Method.invoke(Native Method)
                      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
                      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
                   Caused by: java.lang.UnsupportedOperationException: Cannot create AudioRecord
                      at android.media.AudioRecord$Builder.build(AudioRecord.java:626)
                      at com.example.androidthings.assistant.AssistantActivity.onCreate(AssistantActivity.java:342)
                      at android.app.Activity.performCreate(Activity.java:7000)
                      at android.app.Activity.performCreate(Activity.java:6991)
                      at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214)
                      at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2731)
                      at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856) 
                      at android.app.ActivityThread.-wrap11(Unknown Source:0) 
                      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589) 
                      at android.os.Handler.dispatchMessage(Handler.java:106) 
                      at android.os.Looper.loop(Looper.java:164) 
                      at android.app.ActivityThread.main(ActivityThread.java:6494) 
                      at java.lang.reflect.Method.invoke(Native Method) 
                      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) 
                      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) 
I/Process: Sending signal. PID: 4115 SIG: 9
Application terminated.

 少し調べたところ、セットアップ後は一度 Voice Kit を再起動する必要があるようなので、再起動後に再度実行すると下記のようにエラーなく実行できました。

03/04 21:37:33: Launching step1-start-here
$ adb install-multiple -r -t /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/dep/dependencies.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_1.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_7.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_6.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_8.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_9.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_4.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_5.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_0.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_3.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/intermediates/split-apk/debug/slices/slice_2.apk /Users/akanuma/workspace/androidthings-googleassistant/step1-start-here/build/outputs/apk/debug/step1-start-here-debug.apk 
Split APKs installed
$ adb shell am start -n "com.example.androidthings.assistant/com.example.androidthings.assistant.AssistantActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Client not ready yet..Waiting for process to come online
Waiting for process to come online
Connected to process 1366 on device google-iot_rpi3-Android.local:5555
Capturing and displaying logcat messages from application. This behavior can be disabled in the "Logcat output" section of the "Debugger" settings page.
I/AssistantActivity: starting assistant demo
D/AssistantActivity: enumerating devices
I/AssistantActivity: initializing DAC trigger
I/AssistantActivity: setting volume to: 15
D/AudioTrack: Client defaulted notificationFrames to 460 for frameCount 1380
D/NetworkSecurityConfig: No Network Security Config specified, using platform default
D/vndksupport: Loading /vendor/lib/hw/android.hardware.graphics.mapper@2.0-impl.so from current namespace instead of sphal namespace.

 ここまで行けば Google Assistant を試すことができるようになっています。このサンプルでは Voice Kit のボタンを押している間は発話を受け付けてくれますので、ボタンを押したまま "What time is it now?" などと話してボタンを離すと、 Google Assistant が回答してくれます。

まとめ

 今回はとりあえずサンプルコードを一つ動かしてみただけですが、最初のセットアップさえ終わってしまえばコードを実行するときもスマートフォンアプリを開発するときと同じように Android Studio から動かすことができますので、 Android アプリ開発が得意な人には良さそうです。実際に作り込んでいった時にどこまで API が対応しているのかはまだわかってませんが、今後正式版になれば良い選択肢の一つになるかもしれません。

*1:Android ロボットは、Google が作成および提供している作品から複製または変更したものであり、Creative Commons 3.0 Attribution ライセンスに記載された条件に従って使用しています。

Voice Kit + Dialogflow で来客受付システム

 前回は Voice Kit と Web カメラで簡単な対話を行う処理を実装してみましたが、単語での回答に対してテキストマッチングするだけのものだったので、今回はもっと対話的な処理を行えるよう、 Dialogflow と組み合わせて会社の受付システムを想定したものを実装してみたいと思います。

 実装する処理としては、Webカメラで来客を検知したら、名前と社名、誰とのアポイントメントか、人数は何人かを聞いて、 Slack で画像をポストするというものです。

f:id:akanuma-hiroaki:20180303094131j:plain:w450

Dialogflow API 利用設定

 Dialogflow の API を初めて使う時は、利用を許可するための設定が必要になります。 GCP のコンソールのメニューから APIとサービス をクリックします。

f:id:akanuma-hiroaki:20180302201705p:plain:w450

 次に画面上部の API とサービスの有効化 をクリックします。

f:id:akanuma-hiroaki:20180302201813p:plain:w450

 API のリストの中から Dialogflow を検索してクリックします。

f:id:akanuma-hiroaki:20180302201830p:plain:w450

 API の詳細画面で 有効にする をクリックして API を有効化します。

f:id:akanuma-hiroaki:20180302201848p:plain:w450

 次に認証キーを作成する必要がありますので、メニューから 認証情報 をクリックします。

f:id:akanuma-hiroaki:20180302201907p:plain:w450

 認証情報を作成 プルダウンから、 サービスアカウントキー を選択します。

f:id:akanuma-hiroaki:20180302201926p:plain:w450

 下記のスクリーンショットのように情報を入力して 作成 をクリックします。すると認証情報の json ファイルがダウンロードされますので、ダウンロードして Voice Kit(Raspberry Pi)上に配置しておきます。

f:id:akanuma-hiroaki:20180302201944p:plain:w450

 以上で Dialogflow の API の有効化は完了です。

Dialogflow API の Python クライアント インストール

 今回は Dialogflow の API を Python スクリプトから使用します。 Python 用のクライアントが提供されていますので、こちらをインストールします。ちなみに Dialogflow の API には Version 1 と 2 があり、今回は 2 を使用します。

github.com

 ドキュメントはこちらにあります。

Dialogflow: Python Client — dialogflow 0.1.0 documentation

 AIY プロジェクトのスクリプト実行用のシェルを起動して、 pip でインストールします。

$ bin/AIY-projects-shell.sh 
$ pip3 install dialogflow

Dialogflow の Agent 作成

 それでは発話内容に対応して対話を行うための Dialogflow の設定をします。 Dialogflowコンソールから Agent を新規に作成します。作成した Agent の情報は下記のようになります。 API の Version は 2 を使うように設定しておきます。

f:id:akanuma-hiroaki:20180302210159p:plain:w450

 次に各発話に対応する Intent を作成していきます。今回はひとまず最低限の内容を実装して動作させてみたいと思います。まずは WelcomeIntent は下記のように設定します。単純に来訪者に名前を言ってもらえるように促しているだけになります。

f:id:akanuma-hiroaki:20180302210308p:plain:w450

 名前と社名を言ってもらった場合に対応するための NameIntent を作成します。実際は様々なパターンの発話を考慮すべきですが、今回はとりあえず1パターンだけ用意してみます。ゲストの名前と会社名は対話後にも使用したいので Output Context にパラメータを追加して、他の Intent と共有されるようにしておきます。そしてレスポンスで誰とのアポイントメントなのかを言ってもらえるように促します。

f:id:akanuma-hiroaki:20180302210326p:plain:w450

 誰とのアポイントメントなのかを言ってもらった場合に対応するための AppointmentIntent を作成します。こちらもとりあえず1パターンの発話だけ対応させておきます。 Output Context には訪問先の社員名を保存しておきます。

f:id:akanuma-hiroaki:20180302210346p:plain:w450

 最後に人数は何名かを言ってもらった場合の PeopleCountIntent を作成します。人数情報は Output Context に保存します。そしてこの Intent のレスポンスで対話は終了する想定なので、 Set this intent as end of confersation にチェックを入れておきます。

f:id:akanuma-hiroaki:20180302210408p:plain:w450

 以上が最低限の Intent の実装となります。

Voice Kit 上の Python スクリプトの実装

 上記で作成した Dialogflow の Agent に対応する Python スクリプトを実装します。まずはコード全体を掲載しておきます。

#!/usr/bin/env python3

import sys
import uuid
import cv2
import dialogflow
import aiy.audio
import aiy.cloudspeech
import aiy.voicehat
from slackclient import SlackClient

class Receptionist:
    def __init__(self):
        self.project_id = 'xxxxxx-xxxx-xxxxxx'
        self.client_access_token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
        self.language_code = 'en'
        self.session_client = dialogflow.SessionsClient()
        self.final_intent_name = 'PeopleCountIntent'

        self.face_cascade = cv2.CascadeClassifier('/home/pi/AIY-projects-python/src/examples/voice/haarcascade_frontalface_default.xml')
        self.guest_image_file = 'guest.jpg'

        self.recognizer = aiy.cloudspeech.get_recognizer()
        aiy.audio.get_recorder().start()

        self.status_ui = aiy.voicehat.get_status_ui()

        slack_token = 'xxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxx-xxxxxxxxxx'
        self.slack_client = SlackClient(slack_token)

    def send_welcome_request(self, session):
        event_input = dialogflow.types.EventInput(name = 'WELCOME', language_code = self.language_code)
        query_input = dialogflow.types.QueryInput(event = event_input)
        return self.send_request(session, query_input)

    def send_text_request(self, session, text):
        text_input = dialogflow.types.TextInput(text = text, language_code = self.language_code)
        query_input = dialogflow.types.QueryInput(text = text_input)
        return self.send_request(session, query_input)

    def send_request(self, session, query_input):
        response = self.session_client.detect_intent(session = session, query_input = query_input)

        print('=' * 20)
        print('Query text: {}'.format(response.query_result.query_text))
        print('Detected intent: {} (confidence: {})'.format(
            response.query_result.intent.display_name,
            response.query_result.intent_detection_confidence))
        print('Fulfillment text: {}'.format(response.query_result.fulfillment_text))
        print('-' * 20)

        return response

    def detect_face(self, cap):
        self.status_ui.status('ready')
        while(True):
            ret, frame = cap.read()
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            faces = self.face_cascade.detectMultiScale(gray, 1.3, 5)
            for (x, y, w, h) in faces:
                frame = cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2)
            cv2.imshow('frame', frame)
            if cv2.waitKey(1) & 0xff == ord('q'):
                cap.release()
                cv2.destroyAllWindows()
                sys.exit(1)
            if len(faces) > 0:
                cv2.imwrite(self.guest_image_file, frame)
                break

    def post_to_slack(self, response):
        output_contexts = response.query_result.output_contexts[0]
        guest_name = output_contexts.parameters['guest_name']
        company_name = output_contexts.parameters['company_name']
        dest_name = output_contexts.parameters['dest_name']
        people_count = output_contexts.parameters['people_count']
        comment = '%s from %s is coming for %s with %d people.' % (guest_name, company_name, dest_name, int(people_count))

        with open(self.guest_image_file, 'rb') as guest_image:
            self.slack_client.api_call(
                'files.upload',
                channels = '#akanuma_private',
                file = guest_image,
                filetype = 'jpg',
                filename = self.guest_image_file,
                initial_comment = comment
            )


    def main(self):
        while(True):
            cap = cv2.VideoCapture(0)
            self.detect_face(cap)

            session_id = uuid.uuid4().hex
            session = self.session_client.session_path(self.project_id, session_id)
            response = self.send_welcome_request(session)
            aiy.audio.say(response.query_result.fulfillment_text)

            while(True):
                self.status_ui.status('listening')
                text = self.recognizer.recognize()
                if not text:
                    aiy.audio.say('Sorry, I did not hear you.')
                    continue

                print('You said: %s' % text)
                response = self.send_text_request(session, text)
                aiy.audio.say(response.query_result.fulfillment_text)

                if response.query_result.intent.display_name == self.final_intent_name:
                    break

            self.post_to_slack(response)

            cv2.waitKey(3000)
            cap.release()

if __name__ == '__main__':
    receptionist = Receptionist()
    receptionist.main()

 まずコンストラクタでは各種トークン情報の保持や Dialogflow クライアントのインスタンス、顔認識用の検出器のインスタンス、 Slack のクライアントのインスタンスの作成などをしています。本当は各種トークンはコード内にハードコードすべきではありませんが、今回はとりあえず簡単に試すためにハードコードしてしまっています。

def __init__(self):
    self.project_id = 'xxxxxx-xxxx-xxxxxx'
    self.client_access_token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    self.language_code = 'en'
    self.session_client = dialogflow.SessionsClient()
    self.final_intent_name = 'PeopleCountIntent'

    self.face_cascade = cv2.CascadeClassifier('/home/pi/AIY-projects-python/src/examples/voice/haarcascade_frontalface_default.xml')
    self.guest_image_file = 'guest.jpg'

    self.recognizer = aiy.cloudspeech.get_recognizer()
    aiy.audio.get_recorder().start()

    self.status_ui = aiy.voicehat.get_status_ui()

    slack_token = 'xxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxx-xxxxxxxxxx'
    self.slack_client = SlackClient(slack_token)

 以降は main() メソッドの処理の順番に沿って説明します。まずは顔検出処理を実行し、受付にゲストが来て顔が検出されたらその時の画像を保存して、後続の処理に移ります。顔検出処理の内容については前回までとほぼ同様ですので、詳細は割愛します。

def detect_face(self, cap):
    self.status_ui.status('ready')
    while(True):
        ret, frame = cap.read()
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = self.face_cascade.detectMultiScale(gray, 1.3, 5)
        for (x, y, w, h) in faces:
            frame = cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2)
        cv2.imshow('frame', frame)
        if cv2.waitKey(1) & 0xff == ord('q'):
            cap.release()
            cv2.destroyAllWindows()
            sys.exit(1)
        if len(faces) > 0:
            cv2.imwrite(self.guest_image_file, frame)
            break

 顔が検出されたら対話処理を開始するために、 Dialogflow の WelcomeIntent へのリクエストを送信します。 WelcomeIntent のトリガーは他の Intent と違って発話ではないので、 WELCOME イベントで処理を開始するため、 EventInput のインスタンスを作成し、 QueryInput に渡します。

def send_welcome_request(self, session):
    event_input = dialogflow.types.EventInput(name = 'WELCOME', language_code = self.language_code)
    query_input = dialogflow.types.QueryInput(event = event_input)
    return self.send_request(session, query_input)

 そして Query の内容で Intent を判定する detect_intent() メソッドを実行してレスポンスを取得します。

def send_request(self, session, query_input):
    response = self.session_client.detect_intent(session = session, query_input = query_input)

 発話に応じたリクエストを投げる場合は EventInput の代わりに TextInput を使用します。それ以外の流れはイベントをトリガーする場合と同様です。

def send_text_request(self, session, text):
    text_input = dialogflow.types.TextInput(text = text, language_code = self.language_code)
    query_input = dialogflow.types.QueryInput(text = text_input)
    return self.send_request(session, query_input)

 発話に対応する処理は複数回になるため、ゲストからの発話を受け取ったら CloudSpeech API によってテキスト化し、 Dialogflow へのリクエスト送信、レスポンスとして受け取ったメッセージを発話するという流れをループで繰り返します。対話の終了判定は処理を返した Intent が最後の Intent (今回は PeopleCountIntent) だったらループを抜けるという処理にしています。対話の終了はレスポンスからもっとスマートに判断できないものかと思ったのですが、今回は判定方法がみつけられませんでした。

while(True):
    self.status_ui.status('listening')
    text = self.recognizer.recognize()
    if not text:
        aiy.audio.say('Sorry, I did not hear you.')
        continue

    print('You said: %s' % text)
    response = self.send_text_request(session, text)
    aiy.audio.say(response.query_result.fulfillment_text)

    if response.query_result.intent.display_name == self.final_intent_name:
        break

 対話が終了したらレスポンスに含まれる Output Context からゲストの名前、会社名、訪問先社員名、人数の情報を取得し、 Slack にポストするメッセージを作成します。そして顔検出時に保存しておいた画像をメッセージとともに Slack にアップロードします。

def post_to_slack(self, response):
    output_contexts = response.query_result.output_contexts[0]
    guest_name = output_contexts.parameters['guest_name']
    company_name = output_contexts.parameters['company_name']
    dest_name = output_contexts.parameters['dest_name']
    people_count = output_contexts.parameters['people_count']
    comment = '%s from %s is coming for %s with %d people.' % (guest_name, company_name, dest_name, int(people_count))

    with open(self.guest_image_file, 'rb') as guest_image:
        self.slack_client.api_call(
            'files.upload',
            channels = '#akanuma_private',
            file = guest_image,
            filetype = 'jpg',
            filename = self.guest_image_file,
            initial_comment = comment
        )

 Output Context についてはこちらに説明があります。

Types for Dialogflow API Client — dialogflow 0.1.0 documentation

 また、今回 Slack への Post にはこちらを使用しています。

github.com

スクリプトの実行

 ここまでで一通りの準備が終わったので、スクリプトを実行してみます。 GCP の API を使用するには認証を通す必要があります。認証については下記に説明があります。

Getting Started with Authentication  |  Documentation  |  Google Cloud Platform

 認証情報を含む json ファイルのパスを環境変数 GOOGLE_APPLICATION_CREDENTIALS に設定しておくことで認証情報が参照されますので、今回の冒頭でダウンロードした認証情報の json ファイルのパスを指定しておきます。

$ export GOOGLE_APPLICATION_CREDENTIALS="/home/pi/AIY-projects-python/src/examples/voice/service-account-file.json"

 そして下記のようにスクリプトを実行します。

$ src/examples/voice/receptionist.py

 実行した様子は下記の動画のようになります。ひとまず顔検出をトリガーにして想定した対話処理が行われているようです。

 また、 Slack には下記のように画像がアップロードされました。

f:id:akanuma-hiroaki:20180303013028p:plain:w450

まとめ

 今回は Dialogflow 側は最低限の設定しかしていないので、ほぼ決まった形での発話にしか対応できていませんが、 Dialogflow 側を作り込むことで様々な発話に柔軟に対応して対話することができるようになるかと思います。Slack とも連携できたので実用度もそれなりにありそうな気はしていますが、考えてみるとだいたい同じようなことはスマートフォンアプリでもできてしまうように思いますので、実際にはやはり Voice Kit のベースの Raspberry Pi の GPIO 等を活かした他のセンサーデバイス等との連携によって差別化できるのかなと思いました。とりあえず実験的に色々触ってみるのは面白いので、もう少し作り込めたら実際に会社の受付で試してみられると良いなと思っています。