2️⃣

Androidでセンシングをします2 ~加速度をCSV出力する~

2023/07/19に公開

はじめに

前回はAndroidで加速度を取得しました。
今回は取得した加速度をCSVファイルとして出力してPCに移動させましょう。

なぜわざわざCSVを作ってPCに移動するのか

Androidで加速度を取ってAndroidアプリを作るのでこのステップはなくてもアプリは作れます。

しかし私たちはセンサを使ったアプリを作成するとき、

  1. Androidでセンシング
  2. CSV出力してPCに移動
  3. Pythonで処理
  4. Androidでpythonの処理と同じ処理を実装
    という流れで実装します。

わざわざPythonの処理を含むのは試行錯誤をするからです。

携帯を一回振った加速度(x,y,z)をグラフ表示するとこのようになります。

例えば携帯ポケットに入れて歩いた時の加速度から、歩数を推定するアプリを作るとします。
この時フィルター処理や閾値決定など様々な試行錯誤をします。

この試行錯誤をAndroidで行うとなかなか難しいです。
Pythonで行う方が圧倒的に楽です。

Pythonでセンサ処理をする方法はJupyter(matplotlib)でセンサデータの分析をするに書いてあります。

内部ストレージと外部ストレージ

今回は外部ストレージにCSVファイルを作成、書き込みを行います。

  • 内部ストレージ: Internal Storage
    • 常に使用できる
    • ここに保存されたファイルは、自分のアプリからのみアクセスできます。
    • ユーザーがアプリをアンインストールすると、システムは内部ストレージから当該アプリのファイルをすべて削除します。
    • ユーザーからも他のアプリからも、自分のファイルにアクセスできないようにしたい場合、内部ストレージが最適です。
  • 外部ストレージ:External Storage
    • 誰でも読み取り可能なため、ここに保存されたファイルは自分のコントロールの及ばない所で読み取られる可能性があります。
    • 以前はSDカードを共有メモリとしていたが、最近では内部にある共有メモリ領域がむしろメイン領域となっている

この通り、センサデータを取り出す場合は外部ストレージに保存するのがいいですね。

まずは適当なファイルを作ってみよう

座学はこのへんにしてプログラムを作っていきましょう。
今回は外部ストレージにテキストを書き込むプログラムを書きます。
前回同様MainActivityにも書けますが、MainActivityに書きすぎるとソースコードの可読性(読みやすさ)が下がります。
一つのクラスにいろんな種類の仕事を任せるのは良くないです。
(加速度を取得するコードも別クラスにするのが望ましいと思います)

新しいクラスを作る

ということで”外部ストレージに書き込みを行う”クラスを作りましょう。
File → New → kotlin Class

クラス名は外部ストレージ関連の名前をつけましょう。
クラス名なので先頭大文字です!
画像ではOtherFileStorageという名前になっています。
が、ExternalFileStorageの方がいいと思います。

外部ストレージに適当な文字を保存してみる

OtherFileStorage
import android.os.Bundle
import android.os.Environment
import androidx.appcompat.app.AppCompatActivity
import java.io.BufferedWriter
import java.io.FileWriter
import java.io.PrintWriter

class OtherFileStorage: AppCompatActivity() {

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

        val filePath: String = getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).toString().plus("/Log.csv") //内部ストレージのDocumentのURL
        val fileAppend: Boolean = true //true=追記, false=上書き
        val fil = FileWriter(filePath,fileAppend)
        val pw = PrintWriter(BufferedWriter(fil))
        pw.println("3")
        pw.println("1")
        pw.println("4")
        pw.close()
    }
}
AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.k18054.practice">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Practice">
<!--        ↓ここが変わってるよ! ここだけ変えてね!-->
        <activity android:name=".OtherFileStorage">
<!--        ↑ここが変わってるよ! ここだけ変えてね!-->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

<activity android:name=".(AppCompatActivityを継承したクラス)">では、アプリを起動したときに最初に開くActivityを設定できます。
今回はMainActivityではなくAppCompatActivityを継承させたOtherFileStorageを実行させましょう。
(この場合MainActivityは全く動作しません)
今回は動作の確認のためこのようにしていますが、すぐに戻します。

作ったCSVファイルを確認しよう。

Android File Transferをインストールします。
Androidが入っている端末を接続。
設定から↓

Androidのバージョンによっては
設定 → 開発者向けオプション → デフォルトのUSB設定
とかでできると思います。

そしたらAndroid File Transefarを開いて
Android → data → パッケージ名 → files → Documents → ファイル名

これを取り出しましょう。
PCのデスクトップなど好きなとこにドラッグ&ドロップで取り出せます。

取り出したらNumberやExcelなど表計算ツールで開きましょう。
VSCodeやAtomなどのテキストエディタでもOKです。
3
1
4
と表示されていればOKです。

加速度をCSVに書き込もう

まずAndroidManifestの起動時に実行するActivityをMainActivityに戻しましょう。

AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.k18054.practice">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Practice">
<!--        ↓ここが変わってるよ! ここだけ変えてね!-->
        <activity android:name=".MainActivity">
<!--        ↑ここが変わってるよ! ここだけ変えてね!-->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

次はOtherFileStorageを書き換えていきます。
OtherFileStorageがAppCompatActivityを継承しないようにします。
これでActivityではなく、ただのクラスになりました。

import android.content.Context
import android.os.Environment
import java.io.BufferedWriter
import java.io.FileWriter
import java.io.PrintWriter

class OtherFileStorage(context: Context) {

    val fileAppend : Boolean = true //true=追記, false=上書き
    val context:Context = context
    var fileName : String = "SensorLog"
    val extension : String = ".csv"
    val filePath: String = context.getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).toString().plus("/").plus(fileName).plus(extension) //内部ストレージのDocumentのURL

    fun writeText(text:String){
        val fil = FileWriter(filePath,fileAppend)
        val pw = PrintWriter(BufferedWriter(fil))
        pw.println(text)
        pw.close()
    }
}

それでは解説していきます。

class OtherFileStorage(context: Context)

class OtherFileStorage(context: Context)

  • AppCompatActivity()が消えました。
  • AppCompatActivity()が消えたのでContextがない。
  • でもファイル保存をするためにContextが必要
  • なのでクラスをインスタンス化する時、Contextをもらう必要がある。

val context:Context = context

val context:Context = context

  • 引数でもらったcontextをちゃんと定義する。
  • class OtherFileStorage(val context: Context)でも良い

val filePath: String

val filePath: String = context.getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).toString().plus("/").plus(fileName).plus(extension) //内部ストレージのDocumentのURL

  • getApplicationContext()はAppCompatActivity()を継承した場合使える。
  • 今回はAppCompatActivity()を継承していないが、Contextから持ってこれる。

OtherFileStorageをMainActivityで使う

※本来はOtherFileStorageのグローバル変数を用意し、onCreatedでインスタンス化します!
※contextやlateinitの説明はここではしないので次のステップで説明します!

センサーの値が変かしたらログを書き込む

    //センサーに何かしらのイベントが発生したときに呼ばれる
    override fun onSensorChanged(event: SensorEvent) {
        var sensorX: Float
        var sensorY: Float
        var sensorZ: Float
        // 全て
        if (event.sensor.type === Sensor.TYPE_LINEAR_ACCELERATION) {
            sensorX = event.values[0]
            sensorY = event.values[1]
            sensorZ = event.values[2]
            val strTmp = """加速度センサー
                         X: $sensorX
                         Y: $sensorY
                         Z: $sensorZ"""
            val textView: TextView = findViewById(R.id.textView)
            textView.setText(strTmp)
            //追加
            val log:String = sensorX.toString().plus(",").plus(sensorY).plus(",").plus(sensorZ)
            OtherFileStorage(this).writeText(log)
        }
    }

val log:String

val log:String = sensorX.toString().plus(",").plus(sensorY).plus(",").plus(sensorZ)

  • CSV(カンマ区切り)のテキストを準備

OtherFileStorage(this).writeText(log)

OtherFileStorage(this).doLog(log)

  • OtherFileStorageは引数にContextを求めているのでContextを持っているこのMainActivityを渡してあげる。
  • OtherFileStorageの中に作ったwriteTextにStringを渡してあげる。

センシングしてみよう!

アプリを起動すると加速度が記録されます!

もうちょっと綺麗に書く

実はさっきのプログラム結構やばい事書いてあります。
加速度は1秒間に100回、200回値が変わります。
なのでonSensorChangedもその頻度で呼ばれます。
OtherFileStorage(this).writeText(log)も同じ回数実行されます。
クラスのインスタンス化 → 関数の実行 → クラスの破棄 をこの頻度で実行するのは良くない。
なのでクラスのインスタンス化は1度だけするようにしましょう。

MainActivity
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity(), SensorEventListener {

    private lateinit var sensorManager: SensorManager
    private var AccSensor: Sensor? = null

    lateinit var otherFileStorage: OtherFileStorage

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

        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        AccSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)

        otherFileStorage = OtherFileStorage(this)
    }

    //センサーに何かしらのイベントが発生したときに呼ばれる
    override fun onSensorChanged(event: SensorEvent) {
        var sensorX: Float
        var sensorY: Float
        var sensorZ: Float
        // Remove the gravity contribution with the high-pass filter.
        if (event.sensor.type === Sensor.TYPE_LINEAR_ACCELERATION) {
            sensorX = event.values[0]
            sensorY = event.values[1]
            sensorZ = event.values[2]
            val strTmp = """加速度センサー
                         X: $sensorX
                         Y: $sensorY
                         Z: $sensorZ"""
            val sensorText: TextView = findViewById(R.id.textView)
            sensorText.setText(strTmp)

            val log:String = sensorX.toString().plus(",").plus(sensorY).plus(",").plus(sensorZ)
            otherFileStorage.writeText(log)
        }
    }
    //センサの精度が変更されたときに呼ばれる
    override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
    }

    override fun onResume() {
        super.onResume()
        //リスナーとセンサーオブジェクトを渡す
        //第一引数はインターフェースを継承したクラス、今回はthis
        //第二引数は取得したセンサーオブジェクト
        //第三引数は更新頻度 UIはUI表示向き、FASTはできるだけ早く、GAMEはゲーム向き
        sensorManager.registerListener(this, AccSensor, SensorManager.SENSOR_DELAY_UI)
    }

    //アクティビティが閉じられたときにリスナーを解除する
    override fun onPause() {
        super.onPause()
        //リスナーを解除しないとバックグラウンドにいるとき常にコールバックされ続ける
        sensorManager.unregisterListener(this)
    }
}

lateinit var otherFileStorage: OtherFileStorage

'lateinit var otherFileStorage: OtherFileStorage'

  • まずクラス内で扱えるグローバル変数を宣言します。
  • OtherFileStorageのインスタンス化にはContextが必要です。しかしContextはこの段階では呼べません。
  • なので後で宣言します。という意味でlateinitと書きます。

otherFileStorage = OtherFileStorage(this)

'otherFileStorage = OtherFileStorage(this)'

  • onCreatedの中ではContextを扱えるのでここでインスタンス化します。
  • lateinitをインスタンス化する前に呼ぶとエラーが出ます。

otherFileStorage.writeText(log)

'otherFileStorage.writeText(log)'

  • インスタンス化した変数から関数を呼びましょう。

時間情報を追加する

Android(というよりJava)で現在時間を入手するには様々な方法があります。

ZonedDateTimeOffsetDateTimeLocalDateTime

その中でこの3つを見てみましょう。

val zonedDateTime: ZonedDateTime = ZonedDateTime.now()
// 2021-07-20T12:25:12.257+09:00[Asia/Tokyo]

val offsetDateTime: OffsetDateTime = OffsetDateTime.now()
// 2021-07-20T12:25:12.257+09:00

val localDateTime: LocalDateTime = LocalDateTime.now()
// 2021-07-20T12:25:12.257

val offsetDateTime2: OffsetDateTime = ZonedDateTime.of(LocalDateTime.now(), ZoneId.systemDefault()).toOffsetDateTime()
// 2021-07-20T12:25:12.257+09:00

このようなデータがもらえます。
が、しかし今回はUnixTimeを使いましょう。
UnixTimeだとPythonで処理する時に楽になります。
UnixTimeは'System.currentTimeMillis()'と書くとLong型でもらえます。

最終的にこのようなCSVができるようにしましょう。

必要なのは

  1. CSVの1行目はラベル(数値ではなく何を表すデータかを示す情報)
  2. CSVの1列目に時間情報を追加する

OtherFileStorageを書き換えましょう。

OtherFileStorage
import android.content.Context
import android.os.Environment
import java.io.BufferedWriter
import java.io.FileWriter
import java.io.PrintWriter

class OtherFileStorage(context: Context) {

    val fileAppend : Boolean = true //true=追記, false=上書き
    val context:Context = context
    var fileName : String = "SensorLog"
    val extension : String = ".csv"
    val filePath: String = context.getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).toString().plus("/").plus(fileName).plus(extension) //内部ストレージのDocumentのURL

    init {
        val fil = FileWriter(filePath,fileAppend)
        val pw = PrintWriter(BufferedWriter(fil))
        val text = "time,x,y,z"
        pw.println(text)
        pw.close()
    }

    fun writeText(text:String){
        val fil = FileWriter(filePath,fileAppend)
        val pw = PrintWriter(BufferedWriter(fil))
        val addUnixTimeCsv = System.currentTimeMillis().toString().plus(",").plus(text)
        pw.println(addUnixTimeCsv)
        pw.close()
    }
}

init

init { val fil = FileWriter(filePath,fileAppend) val pw = PrintWriter(BufferedWriter(fil)) val text = "time,x,y,z" pw.println(text) pw.close() }

  • インスタンス化された時に呼ばれる。
  • 今回はインスタンス化された時にヘッダを1行だけ書き込む

val addUnixTimeCsv = System.currentTimeMillis().toString().plus(",").plus(text)

val addUnixTimeCsv = System.currentTimeMillis().toString().plus(",").plus(text)

  • 受け取ったtextの左にUnixTimeとカンマをつけます。
  • これでCSVの一列目に時間情報が入ります。

おわりに

今回は取得した加速度をCSVとして出力しました。
携帯だけでなく多くのセンサ情報はCSVで公開されています。

CSVを取るのはデータを準備する段階で、最もメインなのはどのように処理するかの部分です。
なので身近なセンサである携帯を使って自分の欲しいセンサデータを集めましょう。

また加速度だけでなくジャイロ、気圧、地磁気、重力、照度、GPS、Wi-Fi、、、
いろんなセンサがあります。
皆さんもこれらを使って面白いものを作りましょう!

それでは.また.
Happy Hacking

Discussion