こちらは、Android Thingsハンズオン(GDG京都、GDG神戸共催)の資料です。
Android ThingsのDeveloper Preview3から提供されているUSBホスト機能を利用して、センサーデータを受信するまでの流れを記載します。
利用するUSBドングルとセンサーは省電力無線の1つであるEnOcean対応のものを利用します。
最終的な成果物のサンプルコードはこちらです。
0. 前準備
準備するもの
- Raspberry Pi3(16GB以上のmicroSDカード)
- Android Studioをインストールしたマシン
- EnOcean USBドングル
- EnOcean対応センサー(Switch Science等で購入できます)
今回はCO2センサー(A5-09-04)を使用します。
EnOceanの技術資料はこちらからご確認ください
Android Thingsのインストール
こちらの手順を参照して、Android ThingsのイメージがRaspberry Pi3上で動作することを確認します。
Android Thingsへのインストール方法は、こちらの記事が参考になります。
Android Studioのインストールが必要なので、最初にこちらからダウンロードしてインストールしてください。
1. Hello World
Android SDKのアップデート
SDK toolsを25.0.3以上に、SDKをAPIレベル24以上にアップデートする必要があります。
Project作成
こちらから、Android Thingsのアプリケーション向けにのプロジェクトファイルをクローンもしくはダウンロードして読み込みます。
Android StudioからNew Projectで作成しても大丈夫ですが、Android Thingsには必要のないライブラリ依存(Support Library)があることにご注意ください。
既存プロジェクトからの修正
既存のプロジェクトをAndroid Things向けに修正する場合や、Android StudioからNew Projectで作成した場合は、以下のように必要な記載を行います。
ライブラリの追加
app下のbuild.gradleに依存を追加します。
※下記ではDeveloper Preview4.1を利用していますが、最新のものに読み替えてください
dependencies {
...
provided 'com.google.android.things:androidthings:0.4.1-devpreview'
}
<application ...>
<uses-library android:name="com.google.android.things"/>
...
</application>
Homeアプリとしての宣言
Android Things上のアプリケーションはHomeアプリとして宣言した1つが動作するため、
Homeアプリ(IoTランチャー)としての宣言を行います。
<application
android:label="@string/app_name">
<!-- ↑の手順で追加したもの -->
<uses-library android:name="com.google.android.things"/>
<activity android:name=".HomeActivity">
<!-- Android Studioで自動的に追加されるもの -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Android Thingsのランチャーとしての宣言。起動時に自動的に起動します -->
<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>
2. USBホスト機能の実装
USBドングルをRaspberry Pi3に挿入し、データをUSBホスト機能を利用してシリアル通信で受信する部分を実装します。
FTDIドライバーの入手
EnOceanのUSBドングルはFTDIのチップを使っているため、FTDIからAndroid用のドライバー(jar)が提供されています。
こちらからjarファイル(Android_Java_D2xx_n.nn.zip)をダウンロードします。
zipを解凍し、d2xx.jarを利用します。
FTDIドライバーをProjectから参照する
ドライバーを利用するために、Android Studioで参照を追加します。
jarファイルの配置
app直下(srcと同じパス)に、「libs」ディレクトリを作成し、d2xx.jarをおいてください。
jarファイルの参照
配置したjarファイルを参照するためにapp下のbuild.gradleを編集します。
dependencies {
...
compile fileTree(dir: 'libs', include: ['*.jar']) // ここを追加
provided 'com.google.android.things:androidthings:0.4.1-devpreview'
}
参照を記載後、Android StudioでRebuildを実施してください。
USBイベントの受信を宣言する
アプリケーションがシステムからUSBのイベントを受信するための設定を行います。
USBの挿抜イベントの受信宣言
manifestにイベントを受信するためのintentフィルターを追記します。
<application
android:label="@string/app_name">
<!-- ↑の手順で追加したもの -->
<uses-library android:name="com.google.android.things"/>
<activity android:name=".HomeActivity">
<!-- Android Studioで自動的に追加されるもの -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Android Thingsのランチャーとしての宣言。起動時に自動的に起動します -->
<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>
<!-- USBデバイスの挿抜イベントを受信するための宣言 -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
</intent-filter
</activity>
</application>
対象とするUSBのフィルタリング
続いて、対象とするUSBデバイスのVIDとPIDを記載したxmlファイルを追加します。
これによって、対象のUSBデバイスのイベントのみでアプリケーションが反応します。
以下のxmlファイルを、res下にxmlディレクトリを作成して追加します。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device vendor-id="1027" product-id="24577" /> <!-- FT232RL -->
<usb-device vendor-id="1027" product-id="24596" /> <!-- FT232H -->
<usb-device vendor-id="1027" product-id="24592" /> <!-- FT2232C/D/HL -->
</resources>
追加したxmlをmanifestから参照します。
<application
android:label="@string/app_name">
<!-- ↑の手順で追加したもの -->
<uses-library android:name="com.google.android.things"/>
<activity android:name=".HomeActivity">
<!-- Android Studioで自動的に追加されるもの -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Android Thingsのランチャーとしての宣言。起動時に自動的に起動します -->
<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>
<!-- USBデバイスの挿抜イベントを受信するための宣言 -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
</intent-filter
<!-- フィルターファイルへの参照を追加 -->
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter"/>
</activity>
</application>
USBドングルと接続してデータを受信する
USBドングルと接続してデータ受信するする処理を実装します。
MainActivityと同じパスに、「usb」パッケージを追加し、「usb」パッケージに、「USBManager」という名称でJavaクラスを新規に作成してください。
package com.nissha.android.things.sample.usb;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.ftdi.j2xx.D2xxManager;
import com.ftdi.j2xx.FT_Device;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* USB Accessory Management class.
*/
public class USBManager {
/**
* USBからのデータ受信リスナー定義
*/
public interface IUSBDataListener {
void onReceivedData(byte[] data);
}
/**
* Log用TAG.
*/
private static final String TAG = USBManager.class.getSimpleName();
private Context mContext;
private D2xxManager mInstance;
private FT_Device mFTDevice;
private boolean mIsRunning = false;
private IUSBDataListener mIUSBDataListener;
public USBManager(Context context) {
mContext = context;
try {
mInstance = D2xxManager.getInstance(context);
} catch (D2xxManager.D2xxException e) {
e.printStackTrace();
}
}
public void setListener(IUSBDataListener listener) {
mIUSBDataListener = listener;
}
/**
* USBデバイスと接続
*
* @return 成否
*/
public boolean openDevice() {
if (mFTDevice != null) {
if (mFTDevice.isOpen()) {
if (!mIsRunning) {
setConfig();
mIsRunning = true;
new Thread(mReadRunner).start();
}
return true;
}
}
int devCount = mInstance.createDeviceInfoList(mContext);
// Toastで接続中のデバイス数を通知
Toast.makeText(mContext, "device count" + devCount, Toast.LENGTH_SHORT).show();
if (devCount > 0) {
D2xxManager.FtDeviceInfoListNode deviceList = mInstance.getDeviceInfoListDetail(0);
if (mFTDevice == null) {
mFTDevice = mInstance.openByIndex(mContext, 0);
} else {
synchronized (mFTDevice) {
mFTDevice = mInstance.openByIndex(mContext, 0);
}
}
if (mFTDevice.isOpen()) {
// 接続に成功したらToastで通知
Toast.makeText(mContext, "Succeeded Open Device!!", Toast.LENGTH_SHORT).show();
if (!mIsRunning) {
setConfig();
mIsRunning = true;
// データ受信スレッドを起動
new Thread(mReadRunner).start();
}
return true;
} else {
Toast.makeText(mContext, "Failed Open Device...", Toast.LENGTH_SHORT).show();
return false;
}
} else {
// error...
return false;
}
}
/**
* USBデバイスを切断
*/
public void closeDevice() {
mIsRunning = false;
if (mFTDevice != null) {
mFTDevice.close();
}
}
private void setConfig() {
if ((mFTDevice == null) || (!mFTDevice.isOpen())) {
return;
}
mFTDevice.setBitMode((byte) 0, D2xxManager.FT_BITMODE_RESET);
mFTDevice.setBaudRate(57600);
}
/**
* データ受信処理のRunnable
*/
private Runnable mReadRunner = new Runnable() {
private byte[] receivedData = new byte[4096 * 10];
private byte[] buf = new byte[4096 * 2];
private ExecutorService pool = Executors.newCachedThreadPool();
@Override
public void run() {
int receivedSize = 0;
while (mIsRunning) {
synchronized (mFTDevice) {
// ドングルから受信できるデータサイズを取得
int readSize = mFTDevice.getQueueStatus();
// CPU負荷低減
if (readSize == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (readSize > 0) {
// 受信データを読み込む
Arrays.fill(buf, (byte) 0x00);
mFTDevice.read(buf, readSize);
// 別バッファーに詰め替え(次の手順のため...)
final byte[] packet = new byte[readSize];
System.arraycopy(buf, 0, packet, 0, readSize);
// 受信したデータをリスナーに通知
pool.execute(new Runnable() {
@Override
public void run() {
if (mIUSBDataListener != null) {
mIUSBDataListener.onReceivedData(packet);
}
}
});
}
}
}
}
};
}
続いて、作成したUSBデータ受信処理を利用するための処理をMainActivityに実装します。
public class MainActivity extends Activity implements USBManager.IUSBDataListener {
// Log出力用のTAG
private static final String TAG = MainActivity.class.getSimpleName();
// USBデバイス受信処理クラス
private USBManager mUSBManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// USBデバイス受信クラスを生成
mUSBManager = new USBManager(this);
// Activityにリスナーをimplementsしているので、thisをセットします
mUSBManager.setListener(this);
}
@Override
protected void onStart() {
super.onStart();
// USBの挿抜イベントを受信するためのBroadcastレシーバーを登録します。
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
registerReceiver(mUsbReceiver, filter);
// すでにUSBドングルがささっていれば、ここで接続できます。
mUSBManager.openDevice();
}
@Override
protected void onStop() {
super.onStop();
// USBドングルとの接続を終了します
mUSBManager.closeDevice();
// Broadcastレシーバーを解除します
unregisterReceiver(mUsbReceiver);
}
// --------------------------------
@Override
public void onReceivedData(byte[] data) {
// USBManagerでデータ受信したさいのコールバックです。
// ここでブレークを貼って確認します。
Log.d(TAG, "received data!!");
}
// --------------------------------
/**
* USBイベントのBroadcastレシーバーの実装
*/
private BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(MainActivity.this, "Catch USB Receiver", Toast.LENGTH_SHORT).show();
String action = intent.getAction();
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
// USBが挿入されたさいの通知
// 接続
mUSBManager.openDevice();
} else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
// USBが抜かれた際の通知
// 接続を解除
mUSBManager.closeDevice();
}
}
};
}
ここで、デバッグ実行して、MainActivityのonReceiveData()にブレークポイントを設定して、データが送信されてくることを確認しましょう。
3. センサーデータのパース
USBドングルが受信したデータをアプリケーション上で受信できたので、続いては受信したデータをセンサーデータとしてパースします。
EnOceanのプロトコルを参照して、データのパースを行います。
こちらから以下のプロトコル仕様書とプロファイル仕様書を参照します。
- EnOcean Serial Protocol(ESP3)
- EnOcean Equipment Profiles(EEP)
ESPはUSBドングルからのデータ通信(シリアル通信)用のプロトコル仕様書で、
まずは、シリアル通信で受信したデータをこの仕様書にしたがって、パケットに分割していきます。
分割したパケットのデータ部分はセンサー種別ごとにプロファイルとして定義されており、
EEPを参照して、1byteもしくは4byteのデータを解析することになります。
プロトコルに対する細かいお話は、こちらの書籍(無関係です笑)や、
いくつかのWebサイトに技術情報がありますので、そちらを参照してください。
パケット解析処理の実装
ESP3にしたがって、データをパケットに分割するための処理を実装します。
MainActivityと同じパスに「enocean」というパッケージを作成してください。
そのパッケージに「EnOceanMessage」という名称でJavaクラスを作成します。
package com.nissha.android.things.sample.enocean;
import android.content.Context;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* EnOcean Message(packet data) class.
*/
public class EnOceanMessage {
/**
* EnOcean Sync. Byte
*/
public static final byte SYNC_BYTE = 0x55;
/**
* PacketType : ERP2
*/
public static final int PACKET_TYPE_ERP2 = 0x0A;
public static final int MIN_DATA_LEN = 9;
private static final int HEADER_SIZE = 4;
private static final int DATA_OFFSET = 6;
private byte[] mMessage;
/**
* コンストラクタ.
*/
public EnOceanMessage() {
}
/**
* コンストラクタ.
*
* @param data 受信したデータ.
* @throws Exception 例外.
*/
public EnOceanMessage(byte[] data) throws Exception {
if (data == null) {
throw new Exception("Need data.");
}
if (!isEnOceanData(data)) {
throw new Exception("Data is NOT EnOcean packet...");
}
int packetSize = getPacketSize(data);
if (packetSize == 0) {
throw new Exception("Data is too short...");
}
mMessage = new byte[packetSize];
System.arraycopy(data, 0, mMessage, 0, packetSize);
}
/**
* RSSIを取得する.
*
* @return RSSI.
*/
public static int getRSSI(byte[] data) {
if (data == null) {
return 0;
}
if (data.length < 2) {
return 0;
}
int offset = data.length - 2;
int rssi = data[offset] & 0xFF;
rssi = -rssi; // 符号が付いてない値が送信されてくる
return rssi;
}
public static boolean isTargetData(byte[] data) {
if (data == null) {
return false;
}
if (data.length < (HEADER_SIZE + 1)) {
return false;
}
if (!isEnOceanData(data)) {
return false;
}
int packetType = data[4];
if (packetType != PACKET_TYPE_ERP2) {
return false;
}
return true;
}
private static boolean isEnOceanData(byte[] data) {
int sync = data[0];
if (sync == SYNC_BYTE) {
return true;
}
return false;
}
/**
* ESPのパケット全体長を取得する.
*
* @param data USBドングルで受信したデータ配列
* @return パケット長
*/
public static int getPacketSize(byte[] data) {
int packetSize = 0;
if (data == null) {
return packetSize;
}
if (data.length < 5) {
return packetSize;
}
// Headerからデータ長を取得
int offset = 1;
int dataLen = getDataLen(data);
// ESP HeaderからOptional Lengthを取得
offset += 2;
int optDataLen = data[offset];
packetSize = 7 + dataLen + optDataLen; // 7=SyncByte + Header(4byte) + CRC8 Header, CRC8 Data
return packetSize;
}
/**
* ESP HeaderからERPのデータ長を取得する.
*
* @param data ESPのデータ.
* @return ERPのデータ長
*/
public static int getDataLen(byte[] data) {
int dataLen = 0;
if (data == null) {
return dataLen;
}
if (data.length < 3) {
return dataLen;
}
// ESP Headerからデータ長を取得
int offset = 1;
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
byteBuffer.order(ByteOrder.BIG_ENDIAN);
dataLen = byteBuffer.getShort(offset);
return dataLen;
}
}
つづいて、「USBManager」クラスを修正して、データ配列をEnOceanのパケットごとに分割します。
以下のようにRunnableを修正してください。
(環境にたくさんのセンサーがある場合の考慮です)
private Runnable mReadRunner = new Runnable() {
private byte[] receivedData = new byte[4096 * 10];
private byte[] buf = new byte[4096 * 2];
private ExecutorService pool = Executors.newCachedThreadPool();
@Override
public void run() {
int receivedSize = 0;
while (mIsRunning) {
synchronized (mFTDevice) {
int readSize = mFTDevice.getQueueStatus();
// CPU負荷低減
if (readSize == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (readSize > 0) {
// 受信データを読み込む
Arrays.fill(buf, (byte) 0x00);
if (readSize > buf.length) {
readSize = buf.length;
}
mFTDevice.read(buf, readSize);
// 前回読み込んだ途中のデータの後ろにコピーする
// (読み込み済みデータがなければ、receivedDataの先頭にコピーされる)
System.arraycopy(buf, 0, receivedData, receivedSize, readSize);
receivedSize += readSize;
// 受信したデータをパケット単位に切り出して通知する
while (receivedSize >= EnOceanMessage.MIN_DATA_LEN) {
int firstPos = findFirstPos(receivedSize);
if (firstPos > 0) {
// 先頭がSync Byteでないので、Sync Byteまで移動
receivedSize -= firstPos;
System.arraycopy(receivedData, firstPos, receivedData, 0, receivedSize);
} else if (firstPos == -1) {
// Sync Byteが存在しないので再読み込み
receivedSize = 0;
break;
}
// 対象データでなければ次のデータを取得する
if (!EnOceanMessage.isTargetData(receivedData)) {
// EnOceanデータではないので再読み込み
receivedSize = 0;
break;
}
// データからパケットサイズを取得する
int packetSize = EnOceanMessage.getPacketSize(receivedData);
if (packetSize <= 0) {
// Packet Sizeが異常なので再読み込み
receivedSize = 0;
break;
}
// 取得データがパケットサイズより大きければ1パケットずつに分割
if (receivedSize >= packetSize) {
// 1パケット分をコピー
final byte[] packet = new byte[packetSize];
System.arraycopy(receivedData, 0, packet, 0, packetSize);
// 通知
pool.execute(new Runnable() {
@Override
public void run() {
if (mIUSBDataListener != null) {
mIUSBDataListener.onReceivedData(packet);
}
}
});
// 通知した分をつめる
receivedSize -= packetSize;
System.arraycopy(receivedData, packetSize, receivedData, 0, receivedSize);
} else {
// 1パケットに足りないので再読み込みする
break;
}
}
}
}
}
}
private int findFirstPos(int maxSize) {
int pos = -1;
for (int index = 0; index < maxSize; index++) {
byte b = receivedData[index];
if (b == EnOceanMessage.SYNC_BYTE) {
pos = index;
break;
}
}
return pos;
}
};
ここまでで、さきほど同様にMainActivityでブレークすると、1パケットずつのデータが受信できるはずです。
EnOceanのパケットはESP3によって、0x55から開始することが決まっているので、そちらを確認してください。
センサーデータのパース
1パケットずつに分割したデータをEEPで解析していきます。
EnOceanの仕様では、対象のセンサーのEEPの判別は、Teach-inとよばれる特別な操作を行った際に送信されるLearnデータを解析することで判別ができるようになっています。
(一部例外があり、RPSというタイプのみ、Teach-inが存在しません)
ここでは、Teach-inの処理は実装せず、受信したセンサーIDが、自分が解析しようとしているものと一致していたら、
対象のEEPとしてパースを行っていきます。
Teach-in対応を行う際は、SQLite等を利用して、受信したセンサーIDとEEPとの紐付けを保存するような考慮が必要になります。
まずは、「enocean」パッケージ下に、「EnOceanModule」という名称で、Javaクラスを新規作成します。
複数の種別のセンサーに対応することを考慮した基底クラスの定義です。
package com.nissha.android.things.sample.enocean;
/**
* EnOcean module common abstract class.
*/
public abstract class EnOceanModule {
private EnOceanSensorData mSensorData;
public EnOceanSensorData getSensorData() {
return mSensorData;
}
public void setSensorData(EnOceanSensorData sensorData) {
mSensorData = sensorData;
}
}
同じく、「enocean」パッケージ下に、「EnOceanSensorData」という名称で、Javaクラスを新規作成します。
こちらも、複数の種別のセンサーデータを扱うことを考慮した基底クラスの定義です。
package com.nissha.android.things.sample.enocean;
/**
* Sensor Data common abstract class.
*/
public abstract class EnOceanSensorData {
public abstract float getValues(final int index);
public abstract String getXDataLabel();
}
つづいて、「enocean」パッケージ下に、「CO2SensorData」という名称で、Javaクラスを新規作成します。
パースしたセンサーデータを格納するクラスです。
package com.nissha.android.things.sample.enocean;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* CO2 sensor data class.
*/
public class CO2SensorData extends EnOceanSensorData {
public String mSensorId;
/**
* ガス濃度(ppm).
*/
public int mConcentration;
/**
* 供給電圧(0 - 5.1V).
*/
public double mVoltage;
/**
* 温度(0 - 51℃).
*/
public double mTemperature;
/**
* 湿度(0 - 100%)
*/
public double mHumidity;
/**
* データ受信時刻(Unix Time)
*/
public long mTime;
/**
* RSSI
*/
public int mRSSI;
/**
* コンストラクタ.
*/
public CO2SensorData(String sensorId) {
this(new java.util.Date().getTime(), sensorId);
}
/**
* コンストラクタ.
*
* @param time データ取得時間.
*/
public CO2SensorData(long time, String sensorId) {
mTime = time;
mSensorId = sensorId;
}
@Override
public float getValues(int index) {
switch (index) {
case 0:
return (float) mConcentration;
case 1:
return (float) mTemperature;
case 2:
return (float) mHumidity;
}
return 0;
}
@Override
public String getXDataLabel() {
java.util.Date date = new Date(mTime);
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
return sdf.format(date);
}
}
そして、「enocean」パッケージ下に、「CO2Sensor」という名称で、Javaクラスを新規作成します。
こちらは、センサーデータを格納するクラスです。
package com.nissha.android.things.sample.enocean;
/**
* CO2 sensor module class.
*/
public class CO2Sensor extends EnOceanModule {
public CO2SensorData mSensorData;
public CO2Sensor(CO2SensorData sensorData) {
super.setSensorData(sensorData);
mSensorData = sensorData;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
int concentration = mSensorData.mConcentration;
sb.append(concentration)
.append("ppm");
double voltage = mSensorData.mVoltage;
sb.append(" / ")
.append(voltage)
.append("V");
double temperature = mSensorData.mTemperature;
sb.append(" / ")
.append(temperature)
.append("°C");
double humidity = mSensorData.mHumidity;
sb.append(" / ")
.append(humidity)
.append("%");
return sb.toString();
}
}
そして、「enocean」パッケージ下に、「EEP」という名称で、Javaクラスを新規作成します。
(ビルドエラーがでますが、つぎの手順で解消されます)
作成後、「MY_SENSOR_DATA」に使用するセンサーのIDを記載してください。
package com.nissha.android.things.sample.enocean;
import android.content.Context;
import java.util.Locale;
/**
* EEP
*/
public abstract class EEP {
/**
* 対象のセンサーID
*/
private static final String MY_SENSOR_ID = "04123456";
/**
* Minimum packet data length
*/
public static final int MIN_PACKET_LEN = 14;
/**
* Offset to payload data
*/
private static final int OFFSET_PAYLOAD = 6;
/**
* ペイロードデータ.
*/
protected byte[] mPayloadData;
/**
* センダーID.
*/
protected byte[] mSenderID;
/**
* コンストラクタ.
*
* @param payloadData ペイロードデータ.
* @param senderID センダーID.
*/
public EEP(byte[] payloadData, byte[] senderID) {
mPayloadData = new byte[payloadData.length];
System.arraycopy(payloadData, 0, mPayloadData, 0, payloadData.length);
mSenderID = new byte[senderID.length];
System.arraycopy(senderID, 0, mSenderID, 0, senderID.length);
}
/**
* センダーIDを文字列で取得する.
*
* @return センダーID文字列.
*/
public String getSensorID() {
return getSensorID(mSenderID);
}
/**
* センダーIDを文字列に変換する.
*
* @param data センダーIDのbyte配列.
* @return センダーID文字列.
*/
public static String getSensorID(byte[] data) {
StringBuilder sb = new StringBuilder();
for (byte b : data) {
sb.append(String.format(Locale.getDefault(), "%02X", b));
}
String sensorId = sb.toString();
sensorId = sensorId.substring(0, sensorId.length());
return sensorId;
}
/**
* EEPに応じてデータを解析し、EnOceanのデバイス情報を生成する.
*
* @param context コンテキスト.
* @param rssi RSSI
* @return 生成したEnOceanデバイス情報.
*/
public abstract EnOceanModule analyze(Context context, int rssi);
/**
* センサーから受信したデータを解析して対象のEEPを取得する.
*
* @param data 受信データ.
* @return EEP.
*/
public static EEP getEEP(byte[] data) {
EEP eep = null;
if ((data == null) || (data.length < MIN_PACKET_LEN)) {
return null;
}
if (data[4] != EnOceanMessage.PACKET_TYPE_ERP2) {
return null;
}
try {
// ERPのデータ長
int dataLen = EnOceanMessage.getDataLen(data);
int offset = OFFSET_PAYLOAD;
// ERPヘッダー
int erpHeader = data[offset];
// 拡張ヘッダー有無
boolean existExtHeader = existExtHeader(erpHeader);
int optLen = 0;
if (existExtHeader(erpHeader)) {
offset += 1;
}
// 拡張テレグラム有無
boolean existExtTelegram = existExtTelegram(erpHeader);
if (existExtTelegram) {
offset += 1;
}
offset += 1;
int originatorIDLen = getOriginatorIDLen(erpHeader);
// センサーIDは常に4byteとして使用する
byte[] senderId = new byte[4];
if (originatorIDLen == 3) {
// 3byteのときは最初に00が入る
System.arraycopy(data, offset, senderId, 1, originatorIDLen);
} else if (originatorIDLen == 6) {
// 6byteのときは3byte目から利用する
System.arraycopy(data, (offset + 2), senderId, 0, 4);
} else {
// 4byteのときはそのまま利用する
System.arraycopy(data, offset, senderId, 0, originatorIDLen);
}
offset += originatorIDLen;
int destinationIDLen = getDestinationIDLen(erpHeader);
offset += destinationIDLen;
// 実データ長
int payloadLen = dataLen -
(1 + // ERP Header
(existExtHeader ? 1 : 0) + // Ext Header(Option)
(existExtTelegram ? 1 : 0) + // Ext Telegram(Option)
originatorIDLen + // OriginatorID
destinationIDLen + // DestinationID(Option)
optLen + // OptionData(Option)
1); // CRC8
if (payloadLen < 0) {
return null;
}
// 実データ
if (data.length <= (offset + payloadLen)) {
return null; // データ長が不正
}
byte[] payload = new byte[payloadLen];
System.arraycopy(data, offset, payload, 0, payloadLen);
// EEPを取得
eep = getEEP(payload, senderId);
} catch (Exception e) {
e.printStackTrace();
}
return eep;
}
/**
* センサーIDからEEPを取得する
*
* @param payload EEPのペイロードデータ.
* @param senderId センダーID.
* @return EEP.
*/
public static EEP getEEP(byte[] payload, byte[] senderId) {
EEP eep = null;
final String sensorID = getSensorID(senderId);
if (sensorID.equals(MY_SENSOR_ID)) {
eep = new A50904(payload, senderId);
}
return eep;
}
/**
* EEPの拡張ヘッダーが存在するか判別する.
*
* @param erpHeader erpヘッダー.
* @return true : 存在する.
*/
private static boolean existExtHeader(int erpHeader) {
boolean existExtHeader = false;
int extHeaderAvailable = erpHeader >> 4;
if ((extHeaderAvailable & 0x01) == 0x01) {
existExtHeader = true;
}
return existExtHeader;
}
/**
* EEPの拡張テレグラムが存在するか判別する.
*
* @param erpHeader erpヘッダー.
* @return true : 存在する.
*/
private static boolean existExtTelegram(int erpHeader) {
boolean existExtTelegram = false;
int telegramType = erpHeader & 0x0F;
if (telegramType == 0x0F) {
existExtTelegram = true;
}
return existExtTelegram;
}
/**
* EEPのデバイスIDの長さを判別する.
*
* @param erpHeader erpヘッダー.
* @return デバイスID長.
*/
private static int getOriginatorIDLen(int erpHeader) {
int originatorIDLen;
int addressCtrl = (erpHeader >> 5) & 0x07;
switch (addressCtrl) {
default:
case 0:
originatorIDLen = 3;
break;
case 1:
case 2:
originatorIDLen = 4;
break;
case 3:
originatorIDLen = 6;
break;
}
return originatorIDLen;
}
/**
* EEPのデスティネーションIDの長さを判別する.
*
* @param erpHeader erpヘッダー.
* @return デスティネーションID長.
*/
private static int getDestinationIDLen(int erpHeader) {
int destinationIDLen = 0;
int addressCtrl = (erpHeader >> 5) & 0x07;
if (addressCtrl == 2) {
destinationIDLen = 4;
}
return destinationIDLen;
}
}
最後に、「enocean」パッケージ下に、「A50904」という名称で、Javaクラスを新規作成します。
こちらは、CO2のプロファイルである「4BS - A5-09-04」に対応したクラスです。
別のプロファイルを利用する際には、このクラス同様のクラスを実装してください。
package com.nissha.android.things.sample.enocean;
import android.content.Context;
/**
* EEP - A5-09-04
*/
public class A50904 extends EEP {
/**
* コンストラクタ.
*
* @param payloadData EEPデータ.
* @param senderID デバイスID.
*/
public A50904(byte[] payloadData, byte[] senderID) {
super(payloadData, senderID);
}
@Override
public EnOceanModule analyze(Context context, int rssi) {
byte[] data = mPayloadData;
String sensorId = super.getSensorID();
CO2SensorData sensorData = new CO2SensorData(sensorId);
// 湿度
int humData = data[0] & 0xFF;
sensorData.mHumidity = calcHumidity(humData);
// 濃度
int contData = data[1] & 0xFF;
sensorData.mConcentration = calcConcentration(contData);
// 温度
int tempData = data[2] & 0xFF;
sensorData.mTemperature = calcTemperature(tempData);
// RSSI
sensorData.mRSSI = rssi;
return new CO2Sensor(sensorData);
}
private double calcHumidity(int data) {
return data * 0.5;
}
private int calcConcentration(int data) {
return (data * 10);
}
private double calcTemperature(int data) {
double dt = (double) 51 / 255;
double temp = (dt * data);
return roundValue(temp);
}
private double roundValue(double val) {
double tempVal = val * 10;
tempVal = Math.round(tempVal);
return (tempVal / 10);
}
}
解析したセンサーデータの表示
実装した解析処理を実行して、センサーデータを表示してみましょう。
EnOceanMessageクラスに以下のメソッドを追加します。
/**
* Messageからセンサー情報を取得する
*
* @param context Context
* @return モジュール
*/
public EnOceanModule getEnOceanModule(final Context context) {
final EEP eep = EEP.getEEP(mMessage);
final int rssi = getRSSI(mMessage);
if (eep != null) {
return eep.analyze(context, rssi);
}
return null;
}
MainActivityのonReceivedData()を以下のように修正します。
@Override
public void onReceivedData(byte[] data) {
// データ受信したのでセンサーデータをパースする
try {
final EnOceanMessage enOceanMessage = new EnOceanMessage(data);
// パケットからセンサーを取得
final EnOceanModule enOceanModule = enOceanMessage.getEnOceanModule(this);
if (enOceanModule != null) {
// センサーデータをテキストに変換
final String sensorData = enOceanModule.toString();
// ログにセンサーデータを出力
Log.d(TAG, sensorData);
}
} catch (Exception e) {
e.printStackTrace();
}
}
プログラムを実行して、センサーデータがLogcatに出力されることを確認してください。
4. センサーデータを表示する
Android Thingsでも通常のAndroidと同じUIコンポーネントが提供されていますので、
それを利用して受信したセンサーデータを表示してみます。
レイアウトファイルの変更
activity_main.xmlを以下のように編集します。
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
tools:context="com.nissha.android.things.sample.MainActivity">
<TextView
android:id="@+id/text_sensor_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="28sp"/>
</ScrollView>
Activityの修正
続いて、修正したレイアウトに合わせてコードを変更し、
受信したセンサーデータをレイアウトに配置したTextViewに表示します。
MainActivity.javaを以下のように修正します。
public class MainActivity extends Activity implements USBManager.IUSBDataListener {
private static final String TAG = MainActivity.class.getSimpleName();
private USBManager mUSBManager;
private TextView mSensorDataText; // 追加
private Handler mHandler; // 追加
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUSBManager = new USBManager(this);
mUSBManager.setListener(this);
mHandler = new Handler(Looper.getMainLooper()); // 追加
mSensorDataText = (TextView) findViewById(R.id.text_sensor_data); // 追加
}
// 省略
@Override
public void onReceivedData(byte[] data) {
// データ受信したのでセンサーデータのパースする
try {
final EnOceanMessage enOceanMessage = new EnOceanMessage(data);
final EnOceanModule enOceanModule = enOceanMessage.getEnOceanModule(this);
if (enOceanModule != null) {
final String sensorData = enOceanModule.toString();
Log.d(TAG, sensorData);
// 追加
mHandler.post(new Runnable() {
@Override
public void run() {
String currentText = sensorDataText.getText().toString();
currentText += sensorData;
currentText += "\n";
sensorDataText.setText(currentText);
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
プログラムを実行して、センサーデータを受信する度に、データが1行ずつ表示されることを確認してください。
画面の確認の際に、HDMIモニターがない場合には、adbのscreenrecordコマンドで動画キャプチャーを行うことで画面の確認ができます。
(静止画は取得できないようです)
adb shell screenrecord /sdcard/text.mp4
// Ctrl + Cで中断
adb pull /sdcard/text.mp4
5. センサーデータをグラフ表示する
最後に、受信したセンサーデータをグラフ表示させます。
グラフの描画には、HelloChartsを利用します。
ライブラリ参照を追加
app下のbuild.gradleを編集して、HelloChartsのライブラリを参照します。
dependencies {
// USB操作に必要なFTDI提供のjar
compile fileTree(include: ['*.jar'], dir: 'libs')
// Android Things
provided 'com.google.android.things:androidthings:0.4.1-devpreview'
// HelloCharts
compile 'com.github.lecho:hellocharts-library:1.5.5@aar'
compile 'com.android.support:support-v4:25.3.0' // HelloChartsが参照するサポートライブラリ
}
レイアウト編集
レイアウトをグラフ表示用に編集していきます。
layout下に、「fragment_linechart.xml」という名称で、レイアウトファイルを作成してください。
作成したファイルの内容を以下のように変更します。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<lecho.lib.hellocharts.view.LineChartView
android:id="@+id/chart_line_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:padding="8dp"/>
</FrameLayout>
既存の、「activity_main.xml」を以下のように編集します。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/view_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
グラフ表示機能実装
追加したグラフ表示レイアウトに、HelloChartsの機能でグラフ表示を行うように実装を行います。
「MainActivity.java」を以下のように編集します。
HelloChartsのAPIおよび、サンプルはGitHubで確認してください。
package com.nissha.android.things.sample;
import android.app.Activity;
import android.app.Fragment;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.nissha.android.things.sample.enocean.EnOceanMessage;
import com.nissha.android.things.sample.enocean.EnOceanModule;
import com.nissha.android.things.sample.enocean.EnOceanSensorData;
import com.nissha.android.things.sample.usb.USBManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lecho.lib.hellocharts.formatter.SimpleAxisValueFormatter;
import lecho.lib.hellocharts.model.Axis;
import lecho.lib.hellocharts.model.AxisValue;
import lecho.lib.hellocharts.model.Line;
import lecho.lib.hellocharts.model.LineChartData;
import lecho.lib.hellocharts.model.PointValue;
import lecho.lib.hellocharts.model.Viewport;
import lecho.lib.hellocharts.view.LineChartView;
public class MainActivity extends Activity implements USBManager.IUSBDataListener {
private static final String TAG = MainActivity.class.getSimpleName();
private USBManager mUSBManager;
private LineChartFragment mLineChartFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUSBManager = new USBManager(this);
mUSBManager.setListener(this);
mHandler = new Handler(Looper.getMainLooper());
// グラフ表示用のレイアウト(Fragment)を生成して配置
mLineChartFragment = new LineChartFragment();
getFragmentManager().beginTransaction().add(R.id.view_holder, mLineChartFragment).commit();
}
@Override
protected void onStart() {
super.onStart();
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
registerReceiver(mUsbReceiver, filter);
mUSBManager.openDevice();
}
@Override
protected void onStop() {
super.onStop();
unregisterReceiver(mUsbReceiver);
}
// --------------------------------
private Handler mHandler;
private List<EnOceanSensorData> mSensorDataList = new ArrayList<>();
@Override
public void onReceivedData(byte[] data) {
// データ受信したのでセンサーデータのパースする
try {
final EnOceanMessage enOceanMessage = new EnOceanMessage(data);
final EnOceanModule enOceanModule = enOceanMessage.getEnOceanModule(this);
if (enOceanModule != null) {
final String sensorData = enOceanModule.toString();
Log.d(TAG, sensorData);
mSensorDataList.add(enOceanModule.getSensorData());
mHandler.post(new Runnable() {
@Override
public void run() {
// 受信したデータリストを渡してグラフを更新
mLineChartFragment.setData(mSensorDataList);
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
// --------------------------------
/**
* グラフ表示用Fragment
*/
public static class LineChartFragment extends Fragment {
private static int[] LINE_COLORS = {
Color.CYAN,
Color.YELLOW,
Color.GREEN,
Color.MAGENTA,
Color.BLACK
};
private LineChartView mLineChartView;
private float mMinValue;
private float mMaxValue;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_linechart, container, false);
}
@Override
public void onStart() {
super.onStart();
final View view = getView();
if (view != null) {
mLineChartView = (LineChartView) view.findViewById(R.id.chart_line_view);
}
}
private void setViewPort(final long top, final long bottom, final long first, final long last) {
Viewport viewport = new Viewport(mLineChartView.getMaximumViewport());
// Viewの最大領域
viewport.bottom = bottom;
viewport.top = top;
viewport.left = 0;
viewport.right = last;
mLineChartView.setMaximumViewport(viewport);
// カレントの表示領域
viewport.left = first;
mLineChartView.setCurrentViewport(viewport);
}
public void setData(List<EnOceanSensorData> dataList) {
// 線グラフ用データ生成
final LineChartData lineChartData = createLineChartData(dataList);
// グラフにデータセット
mLineChartView.setLineChartData(lineChartData);
// 表示領域設定
int dataNum = dataList.size();
long first = 0;
long last = dataNum - 1;
if (dataNum > 10) {
// カレントの表示エリアは10件に絞る
first = (last - 10);
}
int top = (int) (mMaxValue + 1);
int bottom = (int) (mMinValue - 1);
int diff = (top - bottom);
if (diff <= 0) {
top += (Math.abs(diff) + 5);
}
setViewPort(top, bottom, first, last);
}
private LineChartData createLineChartData(List<EnOceanSensorData> dataList) {
if (dataList == null) {
return null;
}
if (dataList.size() == 0) {
return null;
}
int dataNum = dataList.size();
int axisNum = 3; // CO2濃度、温度、湿度の3軸
Map<Integer, List<PointValue>> valueMap = new HashMap<>();
for (int axisIndex = 0; axisIndex < axisNum; axisIndex++) {
valueMap.put(axisIndex, new ArrayList<PointValue>());
}
List<AxisValue> axisValues = new ArrayList<>();
// 異なるレンジのデータを表示する場合(CO2濃度と温度とか)の
// データのスケーリング値
int maxValue = 2550; // CO2濃度の最大値(2550ppm)
int maxValue2 = 100; // 温度・湿度の最大値
int minValue = 0;
int minValue2 = 0;
float scale = (maxValue - minValue) / maxValue2;
float sub = (minValue2 * scale) / 2;
// データをセットする
for (int index = 0; index < dataNum; index++) {
EnOceanSensorData data = dataList.get(index);
for (int axisIndex = 0; axisIndex < axisNum; axisIndex++) {
float orgVal = (float) data.getValues(axisIndex);
float v = orgVal;
if ((axisIndex != 0) && (scale != 1)) {
v = (orgVal * scale) - sub;
}
if (mMaxValue < v) {
mMaxValue = v;
}
if (mMinValue > v) {
mMinValue = v;
}
PointValue val = new PointValue(index, v);
// 表示用ラベルをセット
val.setLabel("" + orgVal);
valueMap.get(axisIndex).add(val);
}
// X軸データに時間文字列を追加
final String dateLabel = data.getXDataLabel();
axisValues.add(new AxisValue(index).setLabel(dateLabel));
}
List<Line> lines = new ArrayList<>();
// データライン設定
for (int axisIndex = 0; axisIndex < axisNum; axisIndex++) {
List<PointValue> values = valueMap.get(axisIndex);
Line line = new Line(values);
int color = Color.BLACK;
if (axisIndex < LINE_COLORS.length) {
color = LINE_COLORS[axisIndex];
}
line.setHasLabels(true);
line.setColor(color);
line.setPointRadius(0);
lines.add(line);
}
LineChartData data = new LineChartData(lines);
// X軸設定
Axis axisX = new Axis(axisValues);
axisX.setName("時間");
axisX.setTextColor(Color.BLACK);
data.setAxisXBottom(axisX);
// Y軸設定
int axisYColor = Color.BLACK;
if (scale != 1) {
axisYColor = LINE_COLORS[0];
}
Axis axisY = new Axis().setHasLines(true).setName("データ")
.setHasTiltedLabels(false).setTextColor(axisYColor);
data.setAxisYLeft(axisY);
// Y軸設定2
if (scale != 1) {
// 異なるレンジのデータを表示する際の右側のY軸情報
data.setAxisYRight(new Axis().setFormatter(new HeightValueFormatter(scale, sub, 0)));
}
data.setBaseValue(0);
return data;
}
private static class HeightValueFormatter extends SimpleAxisValueFormatter {
private float scale;
private float sub;
private int decimalDigits;
HeightValueFormatter(float scale, float sub, int decimalDigits) {
this.scale = scale;
this.sub = sub;
this.decimalDigits = decimalDigits;
}
@Override
public int formatValueForAutoGeneratedAxis(char[] formattedValue, float value, int autoDecimalDigits) {
float scaledValue = (value + sub) / scale;
return super.formatValueForAutoGeneratedAxis(formattedValue, scaledValue, this.decimalDigits);
}
}
}
// --------------------------------
private BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(MainActivity.this, "Catch USB Receiver", Toast.LENGTH_SHORT).show();
String action = intent.getAction();
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
mUSBManager.openDevice();
} else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
mUSBManager.closeDevice();
}
}
};
}
プログラムを実行することで、センサーデータがグラフに表示されることを確認します。
adb shell screenrecord /sdcard/chart.mp4
// Ctrl + Cで中断
adb pull /sdcard/chart.mp4