MPAndroidChartを使用してストレージオシロスコープの様なリアルタイム更新ラインチャートを作る(Android Studio Kotlin)

, ,

電子工作品のセンサ等のデータをリアルタイムにスマホにグラフ表示出来たら良いなと思い、手始めにスマホ自体に内蔵されている加速度センサ(重力を含むもの)の現在値をストレージオシロスコープ的に表示するアプリを作ってみましたので紹介します。

ソフトウェア開発ツールはAndroid Studio(バージョン:Giraffe/2022.3.1 Patch3)を使用し、プログラミング言語はKotlinを使用しました。

1.作成アプリの動作動画

以下は今回作成したアプリの動作動画です。「START」ボタンを押すと加速度(X軸、Y軸、Z軸方向、重力を含むもの)の線グラフがリアルタイムに表示されます。(縦軸は加速度でm/s²単位、横軸は「START」ボタンを押してからの時間でmsec単位です)

「STOP」ボタンを押すとグラフの表示が停止します。その後、過去のデータをスクロールして見たり、拡大したりすることが出来ます。

2.MPAndroidChartに関して

Kotlinでグラフを描画する方法は様々あると思われますが、ここではMPAndroidChartを使用しました。
MPAndroidChartはグラフを作成するためのライブラリで、これを使用することによって、折れ線グラフや棒グラフや円グラフ等のグラフを作成することが出来ます。
ここでは、折れ線グラフ(LineChart)のライブラリを使用して、ストレージオシロスコープ的なリアルタイムグラフを作成してみました。

3.MPAndroidChartを使用する手順

MPAndroidChartを使用する手順に関して以下に記します。

1.Gladle Scripts→settings.gradle.kts(Project Settings)のdependencyResolutionManagementのrepositories内に、
maven { url = uri(“https://jitpack.io”) }
を追加します。

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}

2.Gladle Scripts→build.gradle.kts(Module:app)のdependencies内に
implementation(“com.github.PhilJay:MPAndroidChart:v3.1.0”)
を追加します。

dependencies {

    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.appcompat:appcompat:1.7.0")
    implementation("com.google.android.material:material:1.12.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.2.1")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}

4.加速度センサに関して

折れ線グラフにプロットするリアルタイムデータとして、スマホに内蔵している加速度センサ値を使用しました。

Androidでサポートされている加速度センサのタイプとしては、表1に示すように重力を含む加速度を測定するTYPE_ACCELEROMETERと、重力を含まない加速度を測定するTYPE_LINIEAR_ACCELERATIONがありますが、ここでは重力を含むTYPE_ACCELEROMETERを使用します。
これはスマホの加速度をX軸方向、Y軸方向、Z軸方向に関してm/s²で検出します。重力も含むのでスマホの傾きを変えるとそれぞれの値は変化します。

定数名内容
TYPE_ACCELEROMETERX軸、Y軸、Z軸方向の加速度(重力含む)をm/s²単位で測定する。
TYPE_LINEAR_ACCELERATIONX軸、Y軸、Z軸方向の加速度(重力除く)をm/s²単位で測定する。
表1.加速度センサタイプ
図1.スマホの加速度の検出座標

5.アプリ画面のレイアウト

作製したアプリ画面のレイアウトを以下に示します。

アプリ画面のレイアウトは、ラインチャートとButtonを3個とTextViewを3個配置しました。表2に各Viewのidと役割に関して記載します。

名称種類id役割
ラインチャートcom.github.mikephil.
charting.charts.LineChart
line_chartX軸、Y軸、Z軸の加速度をリアルタイムにラインチャート表示
STARTボタンButtonstart_button加速度のラインチャート表示を開始する
STOPボタンButtonstop_button加速度のラインチャート表示を停止する
CLEARボタンButtonclear_button描画したラインチャートを消去する
X軸加速度表示TextViewXaccTextViewX軸方向の加速度を表示する
Y軸加速度表示TextViewYaccTextViewY軸方向の加速度を表示する
Z軸加速度表示TextViewZaccTextViewZ軸方向の加速度を表示する
表2.各Viewの役割とid

画面レイアウトに対応したXMLのコード(app→res→layout→activity_main.xml)を以下に示します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/XaccTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="8dp"
        android:text="X: 0.0 m/s²"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/YaccTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="8dp"
        android:text="Y: 0.0 m/s²"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/XaccTextView" />

    <TextView
        android:id="@+id/ZaccTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="8dp"
        android:text="Z: 0.0 m/s²"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/YaccTextView" />

    <com.github.mikephil.charting.charts.LineChart
        android:id="@+id/line_chart"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_marginTop="24dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/ZaccTextView" />

    <Button
        android:id="@+id/start_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="32dp"
        android:text="START"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/line_chart" />

    <Button
        android:id="@+id/stop_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="32dp"
        android:text="STOP"
        app:layout_constraintStart_toEndOf="@+id/start_button"
        app:layout_constraintTop_toBottomOf="@+id/line_chart" />

    <Button
        android:id="@+id/clear_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="32dp"
        android:text="CLEAR"
        app:layout_constraintStart_toEndOf="@+id/stop_button"
        app:layout_constraintTop_toBottomOf="@+id/line_chart" />

</androidx.constraintlayout.widget.ConstraintLayout>

6.Kotlinプログラム

6-1.Kotlinプログラムコードの主要要素の説明

今回作成したアプリのKotlinプログラム(MainActivity.kt)のソースコードの主要要素部分を抜き出して説明します。

①プロパティ

MainActivityクラス内で、共通に使用するプロパティを以下に示します。

    private var startFlag: Boolean = false  //スタートフラグ
    private var stopFlag: Boolean = false   //停止要求フラグ
    private var scrollStartFlag = false     //グラフの横スクロールスタートフラグ

    //センサーマネージャ
    private lateinit var sensManager: SensorManager

    //センサから得た加速度データ用変数
    private var accXvalue: Float? = 0f
    private var accYvalue: Float? = 0f
    private var accZvalue: Float? = 0f

    //加速度データのロック
    private val accDataLock= ReentrantLock()

    //X軸表示最大値 (5000 msec)
    private val X_DISP_MAX: Float = 5000f

【説明】
6行目:
private lateinit var sensManager: SensorManager
センサーマネージャーのプロパティの定義です。lateinitとして、onCreate()関数内で初期化します。

9行目~11行目:
accXvalue, accYvalue, accZvalueは、加速度センサの値を記憶する変数です。accXvalueがX軸方向の加速度、 accYvalueがY軸方向の加速度、 accZvalueがZ軸方向の加速度です。
加速度センサ値が変化した場合に呼び出されるイベントリスナで、それぞれのセンサ値を記憶して、チャートにデータをプロットするスレッドで読み出しています。

14行目:
private val accDataLock= ReentrantLock()
排他処理をするためのReentrantLockクラスのインスタンスの定義です。
accXvalue, accYvalue, accZvalueは、加速度センサ値が変化した場合に呼び出されるイベントリスナで書込み、チャートにデータをプロットするスレッドで読み出しているので、これらの変数の使用時はでaccDataLock.lock()とaccDataLock.unlock()で挟んで排他処理をしています。

②onCleate()メソッド

アプリが起動された時に最初に実行されるonCleate()メソッドのソースコードを以下に示します。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //センサーマネージャー取得
        sensManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager

        //---チャート関係設定----
        val chart: LineChart = findViewById<LineChart>(R.id.line_chart)
        //チャートのバックカラーの指定
        chart.setBackgroundColor(Color.LTGRAY)

        //左側の縦軸を-30~30とする
        chart.axisLeft.axisMinimum = -30f
        chart.axisLeft.axisMaximum = 30f
        chart.axisLeft.textColor = Color.BLUE   //縦軸の色指定
        chart.axisLeft.setDrawGridLines(true)   //グリッドライン表示

        //右側の縦軸は無
        chart.axisRight.isEnabled = false

        //Descriptionテキストの表示無
        chart.description.isEnabled = false

        //X軸(横軸)の最小値と最大値の初期設定
        chart.xAxis.axisMinimum = 0f         //X軸最小値
        chart.xAxis.axisMaximum = X_DISP_MAX //X軸最大値
        chart.xAxis.textColor = Color.BLUE   //X軸のテキストの色指定
        chart.xAxis.setLabelCount(6,true)    //X軸に6個のラベルを付ける

        //ラインの凡例の設定
        //凡例文字の前にラインを表す線を色別で添付する。
        chart.legend.form = com.github.mikephil.charting.components.Legend.LegendForm.LINE
        chart.legend.textColor = Color.BLUE  //凡例文字の色指定

        //-----ラインチャート設定------
        //点情報ArrayList
        var value1: ArrayList<Entry> = ArrayList()
        var value2: ArrayList<Entry> = ArrayList()
        var value3: ArrayList<Entry> = ArrayList()

        //折れ線グラフ1本の情報を格納
        val dataSet1 = LineDataSet(value1, "X軸")
        dataSet1.color = Color.RED
        dataSet1.setDrawValues(false)   //値表示無
        dataSet1.setDrawCircles(false)  //プロット点の表示無

        val dataSet2 = LineDataSet(value2, "Y軸")
        dataSet2.color =Color.BLUE
        dataSet2.setDrawValues(false)   //値表示無
        dataSet2.setDrawCircles(false)  //プロット点の表示無

        val dataSet3 = LineDataSet(value3, "Z軸")
        dataSet3.color =Color.YELLOW
        dataSet3.setDrawValues(false)   //値表示無
        dataSet3.setDrawCircles(false)  //プロット点の表示無

        //折れ線グラフの全体情報をセット
        val dataSets: MutableList<ILineDataSet> = ArrayList()
        dataSets.add(dataSet1)
        dataSets.add(dataSet2)
        dataSets.add(dataSet3)

        //グラフにデータをセット
        chart.data = LineData(dataSets)
        chart.invalidate()  //リフレッシュ

        //スタートボタンのonClickListenerを設定
        val staButton = findViewById<Button>(R.id.start_button)
        staButton.setOnClickListener(Start_buttonOnClickListener())

        //ストップボタンonClickListenerを設定
        val stpButton = findViewById<Button>(R.id.stop_button)
        stpButton.setOnClickListener(Stop_buttonOnClickListener())

        //クリアボタンのonClickListenerを設定
        val clrButton = findViewById<Button>(R.id.clear_button)
        clrButton.setOnClickListener(Clear_buttonOnClickListener())
    }

【説明】
6行目:
加速度センサを使用するので、センサーマネージャーを取得しています。加速度センサの指定はonResume()メソッド内で行います。

14行目~17行目:
グラフの左側の縦軸の目盛りが-30~30になるように設定しています。
縦軸には左側の縦軸と右側の縦軸が存在しますが、右側の縦軸は20行目でchart.axisRight.isEnabled = falseとして、表示しないようにしています。

26行目~29行目:
chart.xAxis.axisMinimum = 0fとchart.xAxis.axisMaximum = X_DISP_MAXでX軸(横軸)の表示設定をしています。X軸の最小値は0で、最大値はX_DISP_MAXで5000msecです。

chart.xAxis.setLabelCount(6,true)
で6個のラベル (0, 1000, 2000, 3000, 4000, 5000)を表示します。

33行目~34行目:
チャートのラインの凡例の設定をしています。ライン色別の短い線の後に”X軸”等の文字を表示します。

37行目~66行目:
凡例が「X軸」、「Y軸」、「Z軸」でそれぞれ赤線、青線、黄線の3本のラインチャートの設定をしています。
ストレージオシロスコープ的に見せるために、.setDrawValues(false)でプロット点での値表示を無としています。
また、.setDrawCircles(false)でプロットした点の円表示は無としています。

③onResume()、onPause()、onSensorChanged()メソッド

主に加速度センサ関係の処理を記載したonResume()、onPause()、onSensorChanged()メソッドのソースコードを以下に示します。

override fun onResume() {
        super.onResume()
        //重力を含む加速度センサ設定
        val accSensor = sensManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        sensManager.registerListener(this, accSensor, SensorManager.SENSOR_DELAY_NORMAL)
    }

    override fun onPause() {
        super.onPause()
        sensManager.unregisterListener(this)
    }

    //センサの値が変更された
    override fun onSensorChanged(event: SensorEvent?) {
        val valX = event?.values?.get(0)    //X軸加速度
        if (valX != null){
            accDataLock.lock()              //データロック
            accXvalue = valX               //X軸加速度設定
            accDataLock.unlock()            //データアンロック
        }
        val valY = event?.values?.get(1)    //Y軸加速度
        if (valY != null){
            accDataLock.lock()              //データロック
            accYvalue = valY               //Y軸加速度設定
            accDataLock.unlock()            //データアンロック
        }
        val valZ = event?.values?.get(2)    //Z軸加速度
        if (valZ != null){
            accDataLock.lock()              //データロック
            accZvalue = valZ               //Z軸加速度設定
            accDataLock.unlock()            //データアンロック
        }
    }

    //センサの精度が変更された
    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {

    }

【説明】
4行目~5行目:
アプリが起動されて、画面が表示された時に呼び出されるonResume()メソッド内に加速度センサの設定を記載します。

val accSensor = sensManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
の「Sensor.TYPE_ACCELEROMETER」で重力を含む加速度(X,Y,Z方向)を指定しています。

sensManager.registerListener(this, accSensor, SensorManager.SENSOR_DELAY_NORMAL)
で加速度センサ値が変化した場合に呼び出されるイベントリスナを登録しています。
SENSOR_DELAY_NORMALは以下の様に通常の通知頻度を表します。

定数の定義は以下の通りです。

定数名内容
SENSOR_DELAY_NORMAL通常の通知頻度
SENSOR_DELAY_FASTEST最も早い通知頻度
SENSOR_DELAY_GAMEゲームに適した通知頻度
SENSOR_DELAY_UIユーザーインターフェイス利用に適した通知頻度

10行目:
アプリが一時停止した時に呼び出されるonPause()メソッド内でセンサ-マネージャーのイベントリスナの登録抹消を行っています。

14行目:
加速度センサの値が変化した時にonSensorChanged()メソッドが呼び出されます。

15行目:
val valX = event?.values?.get(0)
でX軸方向の加速度値をm/s²単位で呼び出します。同様に.get(1)でY軸方向、.get(2)でZ軸方向の加速度値を呼び出すことができます。

17行目~19行目
呼び出したX軸方向の加速度値をプロパティaccXvalueに代入しています。
accXvalueはチャートにデータをプロットするスレッドで読み出されるので、ReentrantLockクラスのインスタンスaccDataLockを使用してロックして、排他処理をしています。
Y軸方向、Z軸方向の加速度値も同様の方法でaccYvalue、accZvalueに代入します。

④スタートボタンのオンクリックリスナとラインチャートにデータプロットするスレッド

本プログラムでは、リアルタイムにデータをプロットするのにスレッドを使用しています。STARTボタンをクリックすると、そのスレッドをスタートして、一定時間間隔毎にデータのプロットを行います。

    //スタートボタンのonClickListener
    private inner class Start_buttonOnClickListener : View.OnClickListener{
        override fun onClick(v: View?) {
            //データ更新スレッドスタート
            if (startFlag == false) { //スタート中?
                stopFlag = false
                //プロットスレッドスタート
                val plotThread = PlotThread()
                plotThread.start()
            }
        }
    }
    
    //プロットスレッド(一定時間毎に加速度値をプロット)
    private inner class PlotThread() : Thread() {
        public override fun run() {
            val chart: LineChart = findViewById<LineChart>(R.id.line_chart)
            val xaccTextView = findViewById<TextView>(R.id.XaccTextView)
            val yaccTextView = findViewById<TextView>(R.id.YaccTextView)
            val zaccTextView = findViewById<TextView>(R.id.ZaccTextView)

            scrollStartFlag = false
            startFlag = true

            //スタート時間記憶
            val startTime = System.currentTimeMillis()
            while (true) {
                //加速度データ呼出し
                accDataLock.lock()              //データロック
                val xAcc: Float = accXvalue!!
                val yAcc: Float = accYvalue!!
                val zAcc: Float = accZvalue!!
                accDataLock.unlock()            //データアンロック

                //TextViewへの加速度値表示用(小数点以下3桁まで表示)
                val xAccStr = "X: " + (Math.round(xAcc * 1000f) / 1000f).toString() + " mm/s²"
                val yAccStr = "Y: " + (Math.round(yAcc * 1000f) / 1000f).toString() + " mm/s²"
                val zAccStr = "Z: " + (Math.round(zAcc * 1000f) / 1000f).toString() + " mm/s²"

                //経過時間計算
                val elapsedTime = (System.currentTimeMillis() - startTime).toFloat()

                runOnUiThread {     //UIスレッド
                    if (elapsedTime > X_DISP_MAX){            //初期のX軸の表示範囲を越す
                        if (scrollStartFlag == false) { //横スクロールスタートしていない
                            scrollStartFlag = true      //横スクロールスタートフラグセット
                            //X軸の最小値と最大値を解消する
                            chart.xAxis.resetAxisMinimum()   //X軸最小値解除
                            chart.xAxis.resetAxisMaximum()   //X軸最大値解除
                        }
                    }

                    var lineData = chart.data      //チャートのデータ取得
                    lineData.addEntry(Entry(elapsedTime, xAcc), 0)   //ライン1(X軸)のデータの追加
                    lineData.addEntry(Entry(elapsedTime, yAcc), 1)   //ライン2(Y軸)のデータの追加
                    lineData.addEntry(Entry(elapsedTime, zAcc), 2)   //ライン3(Z軸)のデータの追加

                    chart.notifyDataSetChanged()    //データチェンジをチャートに知らせる
                    chart.setVisibleXRangeMaximum(X_DISP_MAX)
                    chart.moveViewToX(elapsedTime)      //グラフ表示の横移動

                    //textViewに加速度表示
                    xaccTextView.text = xAccStr
                    yaccTextView.text = yAccStr
                    zaccTextView.text = zAccStr
                }

                if (stopFlag == true){  //停止要求?
                    stopFlag = false
                    break
                }

                sleep(5)                //5msec待ち
            }

            startFlag = false
        }
    }

【説明】
8行目~9行目:
STARTボタンを押した時に呼び出されるStart_buttonOnClickListenerのonClick()メソッドでデータをプロットするPlotThreadを起動しています。
PlotThreadではwhileループ内で、定期的に加速度センサの値を確認して、その値をチャートデータに追加しています。

26行目:
val startTime = System.currentTimeMillis()
グラフの横軸はスタートしてからの経過時間なので、その値を得るためにスタート時の時間を記憶しています。単位はmsecです。

29行目~33行目:
加速度センサ値が変化した時に呼び出されるonSensorChanged()メソッドで取得したX軸、Y軸、Z軸方向の加速度accXvalue,accYvalue,accZvalueを変数xAcc,yAcc,zAccに代入しています。ReentrantLockクラスのインスタンスaccDataLockを使用してロックして排他処理を行って、センサのイベントリスナと処理がかぶらないようにしています。

41行目:
val elapsedTime = (System.currentTimeMillis() – startTime).toFloat()
グラフの横軸の値であるスタートしてからの経過時間を得るために、現在の時間からスタート時の時間を引き算しています。

43行目:
ラインチャートへのアクセスとTextViewへのアクセスはUIスレッドで行わないと、実行時にエラーが発生してしまいます。
runOnUiThread { }を使用してUIスレッドでラインチャートとTextViewへのアクセス処理を行っています。

44行目、48行目、49行目:
スタート時は、グラフの横軸は0msec~X_DISP_MAX (5000msec)となっており、その範囲でチャートが表示されます。

チャートの表示がX_DISP_MAX (5000msec)を超えた以降、チャートはリアルタイムに横方向にスクロールして常に5000msec幅の範囲が表示されるようにしています。

そのために横軸のデータが初期表示のX_DISP_MAX (5000msec)を超えた時点で
chart.xAxis.resetAxisMinimum()
chart.xAxis.resetAxisMaximum()
を実効して横軸の最小値と最大値の設定をリセットしています。

リセット後は、59行目の
chart.setVisibleXRangeMaximum(X_DISP_MAX)
の実行によってX_DISP_MAXの5000msecの横幅でチャートが表示されます。

53行目~60行目:
チャートに新たな加速度のデータを追加しています。
lineData.addEntry(Entry(elapsedTime, xAcc), 0)
lineData.addEntry(Entry(elapsedTime, yAcc), 1)
lineData.addEntry(Entry(elapsedTime, zAcc), 2)
で、X軸、Y軸、Z軸のチャートのラインに加速度のデータを追加しています。Entryの第一引数は横軸データでelapsedTimeで現在の経過時間を指定しています。

58行目の
chart.notifyDataSetChanged()
はチャートのデータの変更を行ったので、そのことをチャートに知らせます。

59行目の
chart.setVisibleXRangeMaximum(X_DISP_MAX)
はX軸(横軸)の最大表示範囲をX_DISP_MAXで5000msecに指定します。

60行目の
chart.moveViewToX(elapsedTime)
はチャート表示の位置を最新のデータ位置(現在の時刻位置)に移動します。

6-2.MainActivity.ktの全プログラムコードあ

以下に作成したKotlinプログラム(MainActivity.kt)の全プログラムコードを示します。
MainActivityクラス宣言にSensorEventListenerインターフェースを記載することが必要です。

class MainActivity : AppCompatActivity(), SensorEventListener {
    private var startFlag: Boolean = false  //スタートフラグ
    private var stopFlag: Boolean = false   //停止要求フラグ
    private var scrollStartFlag = false     //グラフの横スクロールスタートフラグ

    //センサーマネージャ
    private lateinit var sensManager: SensorManager

    //センサから得た加速度データ用変数
    private var accXvalue: Float? = 0f
    private var accYvalue: Float? = 0f
    private var accZvalue: Float? = 0f

    //加速度データのロック
    private val accDataLock= ReentrantLock()

    //X軸表示最大値 (5000 msec)
    private val X_DISP_MAX: Float = 5000f

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //センサーマネージャ設定
        sensManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager

        //---チャート関係設定----
        val chart: LineChart = findViewById<LineChart>(R.id.line_chart)
        //チャートのバックカラーの指定
        chart.setBackgroundColor(Color.LTGRAY)

        //左側の縦軸を-30~30とする
        chart.axisLeft.axisMinimum = -30f
        chart.axisLeft.axisMaximum = 30f
        chart.axisLeft.textColor = Color.BLUE   //縦軸の色指定
        chart.axisLeft.setDrawGridLines(true)   //グリッドライン表示

        //右側の縦軸は無
        chart.axisRight.isEnabled = false

        //Descriptionテキストの表示無
        chart.description.isEnabled = false

        //X軸(横軸)の最小値と最大値の初期設定
        chart.xAxis.axisMinimum = 0f         //X軸最小値
        chart.xAxis.axisMaximum = X_DISP_MAX //X軸最大値
        chart.xAxis.textColor = Color.BLUE   //X軸のテキストの色指定
        chart.xAxis.setLabelCount(6,true)    //X軸に6個のラベルを付ける

        //ラインの凡例の設定
        //凡例文字の前にラインを表す線を色別で添付する。
        chart.legend.form = com.github.mikephil.charting.components.Legend.LegendForm.LINE
        chart.legend.textColor = Color.BLUE  //凡例文字の色指定

        //----ラインチャート設定----
        //点情報ArrayList
        var value1: ArrayList<Entry> = ArrayList()
        var value2: ArrayList<Entry> = ArrayList()
        var value3: ArrayList<Entry> = ArrayList()

        //折れ線グラフ1本の情報を格納
        val dataSet1 = LineDataSet(value1, "X軸")
        dataSet1.color = Color.RED
        dataSet1.setDrawValues(false)   //値表示無
        dataSet1.setDrawCircles(false)  //プロット点の表示無

        val dataSet2 = LineDataSet(value2, "Y軸")
        dataSet2.color =Color.BLUE
        dataSet2.setDrawValues(false)   //値表示無
        dataSet2.setDrawCircles(false)  //プロット点の表示無

        val dataSet3 = LineDataSet(value3, "Z軸")
        dataSet3.color =Color.YELLOW
        dataSet3.setDrawValues(false)   //値表示無
        dataSet3.setDrawCircles(false)  //プロット点の表示無

        //折れ線グラフの全体情報をセット
        val dataSets: MutableList<ILineDataSet> = ArrayList()
        dataSets.add(dataSet1)
        dataSets.add(dataSet2)
        dataSets.add(dataSet3)

        //グラフにデータをセット
        chart.data = LineData(dataSets)
        chart.invalidate()  //リフレッシュ

        //スタートボタンのonClickListenerを設定
        val staButton = findViewById<Button>(R.id.start_button)
        staButton.setOnClickListener(Start_buttonOnClickListener())

        //ストップボタンonClickListenerを設定
        val stpButton = findViewById<Button>(R.id.stop_button)
        stpButton.setOnClickListener(Stop_buttonOnClickListener())

        //クリアボタンのonClickListenerを設定
        val clrButton = findViewById<Button>(R.id.clear_button)
        clrButton.setOnClickListener(Clear_buttonOnClickListener())
    }

    override fun onResume() {
        super.onResume()
        //重力を含む加速度センサ設定
        val accSensor = sensManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        sensManager.registerListener(this, accSensor, SensorManager.SENSOR_DELAY_NORMAL)
    }

    override fun onPause() {
        super.onPause()
        sensManager.unregisterListener(this)
    }

    //センサの値が変更された
    override fun onSensorChanged(event: SensorEvent?) {
        val valX = event?.values?.get(0)    //X軸加速度
        if (valX != null){
            accDataLock.lock()              //データロック
            accXvalue = valX               //X軸加速度設定
            accDataLock.unlock()            //データアンロック
        }
        val valY = event?.values?.get(1)    //Y軸加速度
        if (valY != null){
            accDataLock.lock()              //データロック
            accYvalue = valY               //Y軸加速度設定
            accDataLock.unlock()            //データアンロック
        }
        val valZ = event?.values?.get(2)    //Z軸加速度
        if (valZ != null){
            accDataLock.lock()              //データロック
            accZvalue = valZ               //Z軸加速度設定
            accDataLock.unlock()            //データアンロック
        }
    }

    //センサの精度が変更された
    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {

    }

    //スタートボタンのonClickListener
    private inner class Start_buttonOnClickListener : View.OnClickListener{
        override fun onClick(v: View?) {
            //データ更新スレッドスタート
            if (startFlag == false) { //スタート中?
                stopFlag = false
                //プロットスレッドスタート
                val plotThread = PlotThread()
                plotThread.start()
            }
        }
    }

    //ストップボタンのonClickListener
    private inner class Stop_buttonOnClickListener : View.OnClickListener{
        override fun onClick(v: View?) {
            stopFlag = true
        }
    }

    //クリアボタンのonClickListener
    private inner class Clear_buttonOnClickListener : View.OnClickListener{
        override fun onClick(v: View?) {
              runOnUiThread {     //UIスレッド
                val chart: LineChart = findViewById<LineChart>(R.id.line_chart)

                //X軸(横軸)の最小値と最大値の初期設定
                chart.xAxis.axisMinimum = 0f         //X軸最小値
                chart.xAxis.axisMaximum = X_DISP_MAX //X軸最大値
                chart.xAxis.textColor = Color.BLUE   //X軸のテキストカラーブラック
                chart.xAxis.setLabelCount(6,true)    //横軸に6個のラベルを付ける

                //ラインの凡例の設定
                //凡例文字の前にラインを現す線を色別で添付する。
                chart.legend.form = com.github.mikephil.charting.components.Legend.LegendForm.LINE
                chart.legend.textColor = Color.BLUE  //凡例文字は黒

                //点情報ArrayList
                var value1: ArrayList<Entry> = ArrayList()
                var value2: ArrayList<Entry> = ArrayList()
                var value3: ArrayList<Entry> = ArrayList()

                //折れ線グラフ1本の情報を格納
                val dataSet1 = LineDataSet(value1, "X")
                dataSet1.color = Color.RED
                dataSet1.setDrawValues(false)   //値表示無
                dataSet1.setDrawCircles(false)  //プロット点の表示無

                val dataSet2 = LineDataSet(value2, "Y")
                dataSet2.color =Color.BLUE
                dataSet2.setDrawValues(false)   //値表示無
                dataSet2.setDrawCircles(false)  //プロット点の表示無

                val dataSet3 = LineDataSet(value3, "Z")
                dataSet3.color =Color.YELLOW
                dataSet3.setDrawValues(false)   //値表示無
                dataSet3.setDrawCircles(false)  //プロット点の表示無

                //折れ線グラフの全体情報をセット
                val dataSets: MutableList<ILineDataSet> = ArrayList()
                dataSets.add(dataSet1)
                dataSets.add(dataSet2)
                dataSets.add(dataSet3)

                //グラフ全体のデータを格納
                chart.data = LineData(dataSets)
                chart.invalidate()  //リフレッシュ
                chart.fitScreen()   //ズームをリセットする
            }

            scrollStartFlag = false
        }
    }

    //プロットスレッド(一定時間毎に加速度値をプロット))
    private inner class PlotThread() : Thread() {
        public override fun run() {
            val chart: LineChart = findViewById<LineChart>(R.id.line_chart)
            val xaccTextView = findViewById<TextView>(R.id.XaccTextView)
            val yaccTextView = findViewById<TextView>(R.id.YaccTextView)
            val zaccTextView = findViewById<TextView>(R.id.ZaccTextView)

            scrollStartFlag = false
            startFlag = true

            //スタート時間記憶
            val startTime = System.currentTimeMillis()
            while (true) {
                //加速度データ呼出し
                accDataLock.lock()              //データロック
                val xAcc: Float = accXvalue!!
                val yAcc: Float = accYvalue!!
                val zAcc: Float = accZvalue!!
                accDataLock.unlock()            //データアンロック

                //TextViewへの加速度値表示用(小数点以下3桁まで表示)
                val xAccStr = "X: " + (Math.round(xAcc * 1000f) / 1000f).toString() + " mm/s²"
                val yAccStr = "Y: " + (Math.round(yAcc * 1000f) / 1000f).toString() + " mm/s²"
                val zAccStr = "Z: " + (Math.round(zAcc * 1000f) / 1000f).toString() + " mm/s²"

                //経過時間計算
                val elapsedTime = (System.currentTimeMillis() - startTime).toFloat()

                runOnUiThread {     //UIスレッド
                    if (elapsedTime > X_DISP_MAX){            //初期のX軸の表示範囲を越す
                        if (scrollStartFlag == false) { //横スクロールスタートしていない
                            scrollStartFlag = true      //横スクロールスタートフラグセット
                            //X軸の最小値と最大値を解消する
                            chart.xAxis.resetAxisMinimum()   //X軸最小値解除
                            chart.xAxis.resetAxisMaximum()   //X軸最大値解除
                        }
                    }

                    var lineData = chart.data      //チャートのデータ取得
                    lineData.addEntry(Entry(elapsedTime, xAcc), 0)   //ライン1(X軸)のデータの追加
                    lineData.addEntry(Entry(elapsedTime, yAcc), 1)   //ライン2(Y軸)のデータの追加
                    lineData.addEntry(Entry(elapsedTime, zAcc), 2)   //ライン3(Z軸)のデータの追加

                    chart.notifyDataSetChanged()    //データチェンジをチャートに知らせる
                    chart.setVisibleXRangeMaximum(X_DISP_MAX)
                    chart.moveViewToX(elapsedTime)      //グラフ表示の横移動

                    //textViewに加速度表示
                    xaccTextView.text = xAccStr
                    yaccTextView.text = yAccStr
                    zaccTextView.text = zAccStr
                }

                if (stopFlag == true){  //停止要求?
                    stopFlag = false
                    break
                }

                sleep(5)                //5msec待ち
            }

            startFlag = false
        }
    }
}

7.最後に

スマホ自体に内蔵する加速度センサ(重力を含むもの)から得た加速度値をリアルタイムにストレージオシロスコープ的に表示することが出来ました。

今後はマイコンボードに搭載したセンサのデータをスマホに転送して、リアルタイムにグラフ表示する応用等を考えたいと思います。

尚、著者は電子工作の一環として、工作品の操作やモニタ用に使用することを前提として、スマホのアプリを作成していて、スマホアプリ作成を専門に行っている者ではありません。プログラムにバグや、より効率的なアルゴリズムや、簡潔なコーディングがあるかもしれませんがご容赦下さい。







PAGE TOP