電子工作や製品のプロトタイピング (例『地球規模で遠隔操作できるブルドーザー』) で利用される Raspberry Pi 3 について、こちらのページで構築した環境で Android Things アプリケーションを開発できます。
本ページでは、簡単な例として LED を点灯させるアプリケーションを扱います。より実用的なアプリケーションを開発する際には GitHub (Android Things) のサンプルコードを参考にします。
関連する公式ドキュメント
Raspberry Pi 3 に Android Things イメージを書き込み、Wi-Fi 設定を行う方法は環境によって異なります。公式ドキュメントにすべての場合について記載がありますが、特にここでは以下の場合を想定した例を記載します。
必要なものは以下のとおりです。
Android Things Console に Google アカウントでログインして「CREATE A PRODUCT」ボタンをクリックします。
情報を入力します。SOM (System on Module) type は Raspberry Pi 3 を選択します。また、OEM パーティションには Bundle とよばれるアプリケーション関連ファイル一式が格納されます。
登録した Product ページ内の「CREATE BUILD CONFIGURATION」をクリックすると、ページ内下部の Build configuration list に一行追加されます。
追加された行内の Download build をクリックすると zip ファイルのダウンロードが開始されます。少々時間がかかるため、待ちます。
ダウンロードした zip ファイルを解凍します。unzip コマンドではエラーが出るため、The Unarchiver (Mac OS) を利用します。約 250 MB の zip ファイル myproduct_Raspberry Pi 3_0_OIR1.170720.015_userdebug_build.zip
が解凍されて 4 GB 程の iot_rpi3.img
になります。
8 GB 以上の microSD カードを Mac に認識させた後、デバイスの識別番号を確認します。以下の例では disk3
が microSD カードです。
$ diskutil list
/dev/disk0
#: TYPE NAME SIZE IDENTIFIER
0: GUID_partition_scheme *121.3 GB disk0
1: EFI EFI 209.7 MB disk0s1
2: Apple_CoreStorage 120.5 GB disk0s2
3: Apple_Boot Recovery HD 650.0 MB disk0s3
/dev/disk1
#: TYPE NAME SIZE IDENTIFIER
0: Apple_HFS Macintosh HD *120.1 GB disk1
Logical Volume on disk0s2
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Unencrypted
/dev/disk2
#: TYPE NAME SIZE IDENTIFIER
0: Apple_partition_scheme *20.0 MB disk2
1: Apple_partition_map 32.3 KB disk2s1
2: Apple_HFS Flash Player 20.0 MB disk2s2
/dev/disk3
#: TYPE NAME SIZE IDENTIFIER
0: FDisk_partition_scheme *7.9 GB disk3
1: Windows_FAT_16 RECOVERY 2.2 GB disk3s1
2: Linux 33.6 MB disk3s5
3: Windows_FAT_32 boot 66.1 MB disk3s6
4: Linux 5.6 GB disk3s7
アンマウントします。
$ diskutil unmountDisk /dev/disk3
Unmount of all volumes on disk3 was successful
以下のコマンドで img を書き込みます。時間がかかるため気長に待ちます。
$ sudo dd bs=1m if=iot_rpi3.img of=/dev/rdisk3 conv=sync
4352+0 records in
4352+0 records out
4563402752 bytes transferred in 452.560177 secs (10083527 bytes/sec)
書き込みが終わると Mac からは認識できないためポップアップが表示されますが、そのまま取り出して Raspberry Pi にセットします。
今回、モバイル Wi-Fi ルータを利用しており、有線 LAN で Raspberry Pi に接続できない場合を想定しているため、初期の Wi-Fi パスワード設定等を別の方法で行う必要があります。ここでは、公式ドキュメントにも記載のあるシリアルデバッグコンソールを利用します。接続のためのケーブルは Amazon 等で安価に入手できます。電気街が近場にある方は実際の店舗で購入するのもよさそうです。その際、TTL 電圧レベルは 5V ではなく 3.3V のものを選択することに注意します。
OS によって USB to TTL Serial Cable のデバイスドライバをインストールする必要があります。上記 Amazon リンクのケーブルは PL2303 です。Adafruit の商品ページにドライバのインストール方法が記載されています。例えば、macOS について以下のように記載されています。
Prolific Chipset
と SiLabs CP210X Drivers
の二種類あり、どちらか分からない場合は両方インストールしておけば問題ない。Prolific Chipset
については macOS のバージョンによってダウンロードすべきドライバのバージョンが異なるため注意する。ただし、これらのドライバは OS のバージョンによってはシステムのクラッシュにつながります。特に Prolific Chipset
のドライバは不具合報告がなされており、以下のコマンドでドライバを削除できます。
保存場所の確認
$ kextfind -b com.prolific.driver.PL2303
/Library/Extensions/ProlificUsbSerial.kext
適当なディレクトリに移動
$ sudo mv /Library/Extensions/ProlificUsbSerial.kext ~/Desktop/
PC 再起動
$ sudo reboot
以下のページの画像と見比べて作業するなどし、接続するピンを間違えて Raspberry Pi を壊さないように注意します。
黒 → GND ピン、白 → TXD ピン、緑 → RXD ピンにそれぞれ接続します。赤 (5V) は接続しません。
デバイスドライバのインストールに成功していれば、USB ケーブルを PC に接続すると以下のコマンドで識別子を確認できます。以下の例では /dev/tty.usbserial
と表示されています。
$ ls -l /dev/tty.*
crw-rw-rw- 1 root wheel 18, 0 8 24 02:17 /dev/tty.Bluetooth-Incoming-Port
crw-rw-rw- 1 root wheel 18, 2 8 24 02:17 /dev/tty.Bluetooth-Modem
crw-rw-rw- 1 root wheel 18, 4 8 24 02:31 /dev/tty.usbserial
macOS の場合、screen を利用して以下のコマンドで接続できます。何も表示されない場合はエンターキーを押してみます。
screen /dev/tty.usbserial 115200
以下のように表示されれば接続成功です。
rpi3:/ $
別ターミナルから以下のコマンドで pid を確認して終了できます。環境によってはドライバが合わず、ここで正常にデバイスを解放できない可能性があります。別のドライバをインストールするか、後述の Wi-Fi 設定を行ってしまい、シリアル通信は以降利用しないようにして回避します。
screen -ls
screen -S 1983 -X quit
以下のコマンドで Wi-Fi 認証情報を設定します。特殊な記号がパスワードに含まれる場合は passphrase64 を利用します。
rpi3:/ $ am startservice -n com.google.wifisetup/.WifiSetupService -a WifiSetupService.Connect -e ssid 利用するSSID名 -e passphrase パスワード
Starting service: Intent { act=WifiSetupService.Connect cmp=com.google.wifisetup/.WifiSetupService (has extras) }
ログに Successfully connected to
と記載されていることを確認します。途中で NTP に接続できたため時刻が現在時刻に変更されていることが確認できます。
rpi3:/ $ logcat -d | grep Wifi | tail
01-01 00:43:52.083 876 876 V WifiWatcher: SSID changed: "SSID_NAME"
01-01 00:43:52.083 876 893 I WifiConfigurator: Successfully connected to SSID_NAME
01-01 00:43:52.089 309 378 E WifiStateMachine: Did not find remoteAddress {192.168.179.1} in /proc/net/arp
01-01 00:43:52.112 309 378 E WifiVendorHal: getSupportedFeatureSet(l.856) failed {.code = ERROR_NOT_AVAILABLE, .description = }
01-01 00:43:52.122 309 378 D WifiNetworkAgent: NetworkAgent: Received signal strength thresholds: []
01-01 00:43:52.125 309 378 E WifiVendorHal: stopRssiMonitoring(l.2115) failed {.code = ERROR_NOT_AVAILABLE, .description = }
01-01 00:43:52.924 309 378 D WifiStateMachine: NETWORK_STATUS_UNWANTED_VALIDATION_FAILED
08-23 17:47:13.903 309 378 W AlarmManager: Unrecognized alarm listener com.android.server.wifi.WifiConfigStore$1@ad923b1
08-23 17:47:13.965 309 378 D WifiConfigStore: Writing to stores completed in 61 ms.
08-23 17:47:14.647 309 540 D WificondControl: Scan result ready event
時刻が同期されていることを確認します。
rpi3:/ $ date
Wed Aug 23 17:49:07 GMT 2017
シリアル通信中は ping コマンドを実行するために root 権限が必要です。8.8.8.8
は Google Public DNS です。
rpi3:/ $ su
rpi3:/ # ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=55 time=197 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=55 time=121 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=55 time=120 ms
ip コマンド等で IP アドレスを確認しておきます。以下の例では 192.168.179.7
がポケット Wi-Fi ネットワーク内の IP です。
rpi3:/ $ ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1
link/sit 0.0.0.0 brd 0.0.0.0
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether b8:27:eb:79:1e:23 brd ff:ff:ff:ff:ff:ff
inet 192.168.179.7/24 brd 192.168.179.255 scope global wlan0
valid_lft forever preferred_lft forever
inet6 fe80::ba27:ebff:fe79:1e23/64 scope link
valid_lft forever preferred_lft forever
4: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
link/ether b8:27:eb:2c:4b:76 brd ff:ff:ff:ff:ff:ff
Wi-Fi 設定をリセットするためには以下のコマンドを実行します。
am startservice -n com.google.wifisetup/.WifiSetupService -a WifiSetupService.Reset
Wi-Fi で無線接続できるようになれば、以降はシリアルデバイスコンソールを利用せずに adb コマンドを利用して Raspberry Pi に接続できます。
Android Studio で SDK と同封されてインストールされる adb (Android Debug Bridge) コマンドを利用すると Android 端末に shell で接続できます。「Preferences → Appearance & Behavior → System Settings → Android SDK → Android SDK Location」で SDK の場所を確認して PATH を通しておきます。
$ /Users/username/Library/Android/sdk/platform-tools/adb connect 192.168.179.7
connected to 192.168.179.7:5555
$ /Users/username/Library/Android/sdk/platform-tools/adb devices
List of devices attached
192.168.179.7:5555 device
$ /Users/username/Library/Android/sdk/platform-tools/adb shell
『Android アプリケーション hello world』にも記載の Android Studio をインストールします。その後、インストールされている「SDK tools」と「SDK Platforms」を SDK Manager でアップデートします。
新規プロジェクトを作成します。
アプリケーション名を入力します。
2017/08/19 現在のところ Android Things 専用の項目はなく、Phone and Tablet を選択します。
Empty Activity を選択します。
Activity Name は MainActivity
のままでも問題ありませんが、ここでは Android Things のドキュメントで散見される HomeActivity
に変更しています。また、UI を持たないアプリケーション開発を想定しているため Layout File 生成のチェックは外します。更に、モバイルアプリケーション開発で必要になる後方互換性は不要であり Homeactivity は AppCompatActivity ではなく Activity を直接継承すればよいため二つ目のチェックも外しておきます。
2017/08/19 現在のところ Android Things 専用の項目はなく、Phone and Tablet で生成された内容を流用しているため、Android Things アプリ化するために以下の箇所を手動で設定する必要があります。
アプリケーションのルートディレクトリに存在する build.gradle ではなく app ディレクトリ直下の build.gradle です。Bintray / androidthings-supportlibrary で確認できる最新のバージョンを指定して追記します。外部 JAR に依存しているだけでありコンパイル時に APK ファイルに含める必要はないため compile
ではなく provided
で設定します。Gradle についてはこちらのページもご参照ください。
apply plugin: 'com.android.application'
android {
...
}
dependencies {
....
provided 'com.google.android.things:androidthings:0.5-devpreview' ←追記
}
build.gradle で provided
設定した外部 JAR を実行時に参照するための classpath 設定を uses-library
で行います。また、Android Things デバイスブート時に起動される Activity であることをインテントフィルタで設定しています。インテントについてはこちらのページもご参照ください。
...
<manifest ...>
<application ...>
<uses-library android:name="com.google.android.things" /> ←追記
<activity android:name=".HomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter> ←↓追記
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.IOT_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
汎用の入出力ピンである GPIO ピンを制御して LED を一定間隔で点灯させます。
package com.example.mycompany.myapp;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import com.google.android.things.pio.Gpio;
import com.google.android.things.pio.PeripheralManagerService;
import java.io.IOException;
public class HomeActivity extends Activity {
// ログ出力で使用するタグ名
private static final String TAG = "HomeActivity";
// LED の点滅間隔
private static final int INTERVAL_BETWEEN_BLINKS_MS = 1000;
// LED 用の出力ピン識別子
// https://developer.android.com/things/hardware/raspberrypi-io.html
private static final String LED_PIN_NAME = "BCM6";
// main スレッドとは別のスレッド、およびそのスレッドで順番に実行するためのキューを有します。
// https://developer.android.com/reference/android/os/Handler.html
private Handler mHandler = new Handler();
// GPIO を表現するオブジェクト
private Gpio mLedGpio;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 周辺機器との接続を管理するオブジェクト
PeripheralManagerService service = new PeripheralManagerService();
try {
// LED 点灯用の GPIO ピンをオープン
mLedGpio = service.openGpio(LED_PIN_NAME);
// 出力ピンとして設定、電圧の初期状態は 0V
mLedGpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW);
// LED 点灯処理を別スレッド実行用のキューに積みます。
mHandler.post(mBlinkRunnable);
} catch (IOException e) {
Log.e(TAG, "Error on PeripheralIO API", e);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 終了時は別スレッド実行用のキューを空にします。
mHandler.removeCallbacks(mBlinkRunnable);
// GPIO ピンを閉じます。
if (mLedGpio != null) {
try {
mLedGpio.close();
} catch (IOException e) {
Log.e(TAG, "Error on PeripheralIO API", e);
}
}
}
// 別スレッドで実行する LED 点灯処理です。
private Runnable mBlinkRunnable = new Runnable() {
@Override
public void run() {
// 何らかの原因で GPIO が閉じられている場合はそのまま処理を終えます。
if (mLedGpio == null) {
return;
}
try {
// 「点灯」と「消灯」をトグルします。
mLedGpio.setValue(!mLedGpio.getValue());
Log.d(TAG, mLedGpio.getValue() ? "ON" : "OFF");
// 一定時間経過してから実行する設定で再びキューに積みます。
mHandler.postDelayed(mBlinkRunnable, INTERVAL_BETWEEN_BLINKS_MS);
} catch (IOException e) {
Log.e(TAG, "Error on PeripheralIO API", e);
}
}
};
}