LoginSignup
6
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-07-16

こちらは、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を利用していますが、最新のものに読み替えてください

build.gradle
dependencies {
    ...
    provided 'com.google.android.things:androidthings:0.4.1-devpreview'
}
AndroidManifest.xml
<application ...>
    <uses-library android:name="com.google.android.things"/>
    ...
</application>

Homeアプリとしての宣言

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

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)が提供されています。
こちらからjarファイル(Android_Java_D2xx_n.nn.zip)をダウンロードします。
zipを解凍し、d2xx.jarを利用します。

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

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

jarファイルの配置

app直下(srcと同じパス)に、「libs」ディレクトリを作成し、d2xx.jarをおいてください。
スクリーンショット 2017-07-02 13.19.45.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フィルターを追記します。

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ディレクトリを作成して追加します。

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>

スクリーンショット 2017-07-02 13.35.43.png

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

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

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に実装します。

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のプロトコルを参照して、データのパースを行います。
こちらから以下のプロトコル仕様書とプロファイル仕様書を参照します。
- EnOcean Serial Protocol(ESP3)
- EnOcean Equipment Profiles(EEP) 

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

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

プロトコルに対する細かいお話は、こちらの書籍(無関係です笑)や、
いくつかのWebサイトに技術情報がありますので、そちらを参照してください。

パケット解析処理の実装

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

MainActivityと同じパスに「enocean」というパッケージを作成してください。
そのパッケージに「EnOceanMessage」という名称で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を修正してください。
(環境にたくさんのセンサーがある場合の考慮です)

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


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

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

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

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

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

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

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

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()を以下のように修正します。

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を以下のように編集します。

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を以下のように修正します。

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

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

adb shell screenrecord /sdcard/text.mp4

// Ctrl + Cで中断

adb pull /sdcard/text.mp4

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

最後に、受信したセンサーデータをグラフ表示させます。
グラフの描画には、HelloChartsを利用します。

ライブラリ参照を追加

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」という名称で、レイアウトファイルを作成してください。

作成したファイルの内容を以下のように変更します。

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」を以下のように編集します。

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
- サンプルアプリ

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

adb shell screenrecord /sdcard/chart.mp4

// Ctrl + Cで中断

adb pull /sdcard/chart.mp4
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4