Android ThingsのUSBホスト機能でEnOceanのセンサーデータを受信する

こちらは、[Android Thingsハンズオン](https://gdgkobe.doorkeeper.jp/events/62386)（GDG京都、GDG神戸共催）の資料です。

Android ThingsのDeveloper Preview3から提供されているUSBホスト機能を利用して、センサーデータを受信するまでの流れを記載します。
利用するUSBドングルとセンサーは省電力無線の１つである[EnOcean](https://www.enocean.com/jp/)対応のものを利用します。

最終的な成果物のサンプルコードは[こちら](https://github.com/kubotaku1119/AndroidThingsEnOceanSample)です。

## 0. 前準備
### 準備するもの
- Raspberry Pi3(16GB以上のmicroSDカード)
- Android Studioをインストールしたマシン
- [EnOcean USBドングル](https://www.amazon.co.jp/gp/product/B0107UUUGG)
- EnOcean対応センサー(Switch　Science等で購入できます)

今回はCO2センサー（A5-09-04）を使用します。

EnOceanの技術資料は[こちら](https://www.enocean.com/jp/enocean_modules_928mhz/esk-400j/)からご確認ください

### Android Thingsのインストール
[こちら](https://developer.android.com/things/hardware/raspberrypi.html)の手順を参照して、Android ThingsのイメージがRaspberry Pi3上で動作することを確認します。

Android Thingsへのインストール方法は、[こちら](http://qiita.com/satoshi_noda/items/9cd40383dc30a9326e6f)の記事が参考になります。

Android Studioのインストールが必要なので、最初に[こちら](https://developer.android.com/studio/index.html)からダウンロードしてインストールしてください。

## 1. Hello World
### Android SDKのアップデート
SDK toolsを25.0.3以上に、SDKをAPIレベル24以上にアップデートする必要があります。

### Project作成
[こちら](https://github.com/androidthings/new-project-template)から、Android Thingsのアプリケーション向けにのプロジェクトファイルをクローンもしくはダウンロードして読み込みます。

Android StudioからNew Projectで作成しても大丈夫ですが、Android Thingsには必要のないライブラリ依存（Support Library）があることにご注意ください。

### 既存プロジェクトからの修正
既存のプロジェクトをAndroid Things向けに修正する場合や、Android StudioからNew Projectで作成した場合は、以下のように必要な記載を行います。

#### ライブラリの追加
app下のbuild.gradleに依存を追加します。
※下記ではDeveloper Preview4.1を利用していますが、最新のものに読み替えてください

```:build.gradle
dependencies {
    ...
    provided 'com.google.android.things:androidthings:0.4.1-devpreview'
}
```

```xml:AndroidManifest.xml
<application ...>
    <uses-library android:name="com.google.android.things"/>
    ...
</application>
```

#### Homeアプリとしての宣言
Android Things上のアプリケーションはHomeアプリとして宣言した１つが動作するため、
Homeアプリ（IoTランチャー）としての宣言を行います。

```xml:AndroidManifest.xml
<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)が提供されています。
[こちら](http://www.ftdichip.com/Android.htm)からjarファイル(Android_Java_D2xx_n.nn.zip)をダウンロードします。
zipを解凍し、d2xx.jarを利用します。

### FTDIドライバーをProjectから参照する

ドライバーを利用するために、Android Studioで参照を追加します。

#### jarファイルの配置

app直下（srcと同じパス）に、「libs」ディレクトリを作成し、d2xx.jarをおいてください。
<img width="285" alt="スクリーンショット 2017-07-02 13.19.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/13974/28eeb559-b217-e77c-e25c-021dce598680.png">


#### jarファイルの参照

配置したjarファイルを参照するためにapp下のbuild.gradleを編集します。

```: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フィルターを追記します。

```xml:AndroidManifest.xml
<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:device_filter.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>
```

<img width="362" alt="スクリーンショット 2017-07-02 13.35.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/13974/5aef15e7-b372-cf57-44c3-df9dede6ce74.png">

追加したxmlをmanifestから参照します。

```xml:AndroidManifest.xml
<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クラスを新規に作成してください。

```java:USBManager
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に実装します。

```java:MainActivity.java
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のプロトコルを参照して、データのパースを行います。
[こちら](https://www.enocean.com/jp/enocean_modules_928mhz/esk-400j/)から以下のプロトコル仕様書とプロファイル仕様書を参照します。
- EnOcean Serial Protocol(ESP3)
- EnOcean Equipment Profiles(EEP)　


ESPはUSBドングルからのデータ通信（シリアル通信）用のプロトコル仕様書で、
まずは、シリアル通信で受信したデータをこの仕様書にしたがって、パケットに分割していきます。

分割したパケットのデータ部分はセンサー種別ごとにプロファイルとして定義されており、
EEPを参照して、1byteもしくは4byteのデータを解析することになります。

プロトコルに対する細かいお話は、[こちら](http://shop.cqpub.co.jp/hanbai/books/MTR/MTRZ201704.html)の書籍（無関係です笑）や、
いくつかのWebサイトに技術情報がありますので、そちらを参照してください。

### パケット解析処理の実装

ESP3にしたがって、データをパケットに分割するための処理を実装します。

MainActivityと同じパスに「enocean」というパッケージを作成してください。
そのパッケージに「EnOceanMessage」という名称でJavaクラスを作成します。

```java: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を修正してください。
（環境にたくさんのセンサーがある場合の考慮です）

```java:USBManager.java

    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;
                            }


                            // 取得データがパケットサイズより大きければ１パケットずつに分割
                            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クラスを新規作成します。
複数の種別のセンサーに対応することを考慮した基底クラスの定義です。

```java: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クラスを新規作成します。
こちらも、複数の種別のセンサーデータを扱うことを考慮した基底クラスの定義です。

```java: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クラスを新規作成します。
パースしたセンサーデータを格納するクラスです。

```java:CO2SensorData
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クラスを新規作成します。
こちらは、センサーデータを格納するクラスです。

```java: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を記載してください。

```java:EEP.java
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」に対応したクラスです。
別のプロファイルを利用する際には、このクラス同様のクラスを実装してください。

```java:A50904.java
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クラスに以下のメソッドを追加します。

```java:EnOceanMessage.java
    /**
     * 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()を以下のように修正します。

```java:MainActivity.java
    @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: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を以下のように修正します。

```java: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行ずつ表示されることを確認してください。

![スクリーンショット 2017-07-10 16.11.44.png](https://qiita-image-store.s3.amazonaws.com/0/13974/2c5b4829-8734-685d-aaf6-090151eeee9d.png)

画面の確認の際に、HDMIモニターがない場合には、adbのscreenrecordコマンドで動画キャプチャーを行うことで画面の確認ができます。
（静止画は取得できないようです）

```shell-session
adb shell screenrecord /sdcard/text.mp4

// Ctrl + Cで中断

adb pull /sdcard/text.mp4
```




## 5. センサーデータをグラフ表示する

最後に、受信したセンサーデータをグラフ表示させます。
グラフの描画には、[HelloCharts](https://github.com/lecho/hellocharts-android)を利用します。

### ライブラリ参照を追加

app下のbuild.gradleを編集して、HelloChartsのライブラリを参照します。

```build.gradle
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: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: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で確認してください。
- [線グラフのサンプルとAPI](https://github.com/lecho/hellocharts-android/blob/master/hellocharts-samples/src/lecho/lib/hellocharts/samples/LineChartActivity.java)
- [サンプルアプリ](https://play.google.com/store/apps/details?id=lecho.lib.hellocharts.samples)

```java:MainActivity.java
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();

            }
        }
    };
}
```
プログラムを実行することで、センサーデータがグラフに表示されることを確認します。

![スクリーンショット 2017-07-10 16.03.49.png](https://qiita-image-store.s3.amazonaws.com/0/13974/e2962f05-c887-6984-027f-bddd1936f747.png)

```shell-session
adb shell screenrecord /sdcard/chart.mp4

// Ctrl + Cで中断

adb pull /sdcard/chart.mp4
```