Belajar Membaca Nilai Sensor Accelerometer Di Android


Perangkat Android biasanya dilengkapi dengan satu atau lebih sensor. Dari semua sensor yang ada, accelerometer termasuk sebuah sensor yang paling umum dijumpai di perangkat modern. Accelerometer dipakai untuk mengukur percepatan (a). Tentu saja sebuah sensor perangkat keras seperti ini tidak berguna bila tidak ada kode program untuk mengambil nilainya. Dengan menggunakan Sensor API di Android SDK, saya bisa membaca nilai percepatan di sumbu x, y, dan z dalam satuan m/s^2. Nilai ini bisa dipakai untuk mendeteksi guncangan pada perangkat dan gerakan memiringkan perangkat (misalnya untuk kendali pada game).

Agar bisa memahami nilai yang dikembalikan dari sensor secara mudah, pada latihan kali ini, saya akan membuat sebuah aplikasi sederhana yang menampilkan nilai yang dibaca dari accelerometer dalam bentuk grafis. Seperti biasa, saya mulai dengan membuat proyek baru di Android Studio. Kali ini saya akan menggunakan bahasa Groovy.

Nilai yang dikembalikan sensor (berlaku untuk semua sensor) akan disimpan dalam bentuk array sebagai nilai untuk attribute values di object SensorEvent. Bila saya ingin menampung nilai yang dikembalikan dari waktu ke waktu, saya tidak bisa begitu saja menyimpan referensi ke values karena array tersebut akan selalu di-isi ulang dengan nilai terbaru. Oleh sebab itu, saya akan membuat sebuah class baru yang berguna untuk menyimpan nilai sensor seperti berikut ini:

@CompileStatic
public class NilaiSensor {

    float[][] nilai;
    int maxN;
    int n;

    NilaiSensor(int maxN) {
        this.maxN = maxN
        nilai = new float[maxN][3]
        n = 0
    }

    void tambah(SensorEvent event) {
        nilai[n][0] = event.values[0]
        nilai[n][1] = event.values.length >= 2? event.values[1]: 0
        nilai[n][2] = event.values.length >= 3? event.values[2]: 0
        n++
        if (n >= maxN) {
            n = 0
        }
    }

    float getX(int n) {
        if (n >= nilai.length) return 0
        nilai[n][0]
    }

    float getY(int n) {
        if (n >= nilai.length) return 0
        nilai[n][1]
    }

    float getZ(int n) {
        if (n >= nilai.length) return 0
        nilai[n][2]
    }

}

Khusus untuk accelerometer, nilai untuk sumbu x diwakili oleh event.values[0], sumbu y diwakili oleh event.values[1] dan sumbu z diwakili oleh event.values[2].

Untuk menghindari aktifitas garbage collection yang berlebihan, saya membuat sebuah array besar di memori untuk menampung seluruh nilai yang akan dibuat grafisnya. Nilai ini mulai dari nilai n=0 sampai n=maxN. Setiap kali ada data baru dari sensor, method tambah() akan dipanggil. Bila array besar ini sudah tidak muat, maka nilai n akan dikembalikan ke semula (0) sehingga menimpa nilai yang berada di posisi awal.

Nilai yang dikembalikan sensor dapat berupa angka negatif atau positif. Saya akan menganggap tengah layar sebagai nilai 0. Bagian di atas tengah layar akan dialokasikan untuk nilai positif dan sebaliknya, bagian di bawah tengah layar akan dialokasikan untuk nilai negatif. Agar lebih menghemat memori, nilai yang disimpan di NilaiSensor harusnya adalah nilai yang sudah diterjemahkan ke koordinat layar sehingga siap untuk dipakai. Untuk itu, saya mengubah kode program NilaiSensor sehingga menjadi seperti berikut ini:

@CompileStatic
public class NilaiSensor {

    float[][] nilai;
    int maxN, height, mid;
    int n;

    NilaiSensor(int width, int height) {
        this.maxN = width
        this.height = height
        this.mid = (int)(height / 2)
        nilai = new float[maxN][3]
        n = 0
    }

    float translate(float nilai) {
        mid - nilai * 20
    }

    void tambah(SensorEvent event) {
        nilai[n][0] = translate(event.values[0])
        nilai[n][1] = translate(event.values.length >= 2? event.values[1]: 0f)
        nilai[n][2] = translate(event.values.length >= 3? event.values[2]: 0f)
        n++
        if (n >= maxN) {
            n = 0
        }
    }

    float getX(int n) {
        if (n >= nilai.length) return mid
        nilai[n][0]
    }

    float getY(int n) {
        if (n >= nilai.length) return mid
        nilai[n][1]
    }

    float getZ(int n) {
        if (n >= nilai.length) return mid
        nilai[n][2]
    }

}

Langkah berikutnya, untuk menggambar nilai yang telah dihitung, saya membuat sebuah turunan dari SurfaceView seperti berikut ini:

@CompileStatic
class PlotterView extends SurfaceView implements SurfaceHolder.Callback, SensorEventListener {

    Timer timer
    TimerTask timerTask
    volatile int width
    volatile int height
    NilaiSensor nilaiSensor
    Paint paintX, paintY, paintZ, paintSumbu

    PlotterView(Context context) {
        super(context)
        getHolder().addCallback(this)
        paintX = new Paint()
        paintX.setColor(Color.YELLOW)
        paintY = new Paint()
        paintY.setColor(Color.BLUE)
        paintZ = new Paint()
        paintZ.setColor(Color.CYAN)
        paintSumbu = new Paint()
        paintSumbu.setColor(Color.WHITE)
    }

    @Override
    void surfaceCreated(SurfaceHolder holder) {
        width = getWidth()
        height = getHeight()
        setup()
    }

    @Override
    void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        setWidth(width)
        this.width = width
        setHeight(height)
        setup()
    }

    void setup() {
        timerTask?.cancel()
        nilaiSensor = new NilaiSensor(width, height)
        timerTask = new TimerTask() {
            @Override
            void run() {
                updateAction()
            }
        }
        timer = new Timer()
        timer.schedule(timerTask, 0, 10)
    }

    @Override
    void surfaceDestroyed(SurfaceHolder holder) {
        timerTask.cancel()
        timer.cancel()
        timer.purge()
    }

    void updateAction() {
        SurfaceHolder holder = getHolder()
        Canvas c
        try {
            c = holder.lockCanvas()
            if (c) {
                c.drawColor(Color.BLACK)
                int n
                for (n=1; n <= nilaiSensor.n; n++) {
                    c.drawLine(n-1, nilaiSensor.getX(n-1), n, nilaiSensor.getX(n), paintX)
                    c.drawLine(n-1, nilaiSensor.getY(n-1), n, nilaiSensor.getY(n), paintY)
                    c.drawLine(n-1, nilaiSensor.getZ(n-1), n, nilaiSensor.getZ(n), paintZ)
                }
            }
        } finally {
            if (c) {
                holder.unlockCanvasAndPost(c)
            }
        }
    }

    @Override
    void onSensorChanged(SensorEvent event) {
        if (nilaiSensor) {
            nilaiSensor.tambah(event)
        }
    }

    @Override
    void onAccuracyChanged(Sensor sensor, int accuracy) {}

}

Class di atas mengimplementasikan SensorEventListener sehingga memiliki method onSensorChanged() dan onAccuracyChanged(). Bila method ini didaftarkan melalui SensorManager, maka method onSensorChanged() akan dikerjakan setiap kali ada nilai baru yang dapat dibaca dari sensor. Saya menggunakan TimerTask untuk memperbaharui layar setiap 10 ms. Method updateAction() berisi kode program yang akan dikerjakan setiap 10 detik tersebut. Pada dasarnya, method tersebut akan menggambar 3 garis masing-masing untuk mewakili nilai X, Y, dan Z yang diperoleh dari sensor.

Sebagai langkah terakhir, saya membuat sebuah Activity yang dapat dipanggil oleh pengguna, misalnya seperti berikut ini:

@CompileStatic
class MainActivity extends Activity {

    SensorManager sensorManager
    PlotterView plotterView

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState)
        plotterView = new PlotterView(this)
        setContentView(plotterView)
    }

    @Override
    protected void onStart() {
        super.onStart()
        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE)
        Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        if (!sensor) {
            throw new RuntimeException("Tidak ada accelerometer")
        }
        sensorManager.registerListener(plotterView, sensor, SensorManager.SENSOR_DELAY_UI)
    }

    @Override
    protected void onPause() {
        super.onPause()
        sensorManager.unregisterListener(plotterView)
    }

}

Pada activity di atas, saya mendapatkan sebuah instance Sensor dengan memanggil method getDefaultSensor() dari SensorManager. Pada method ini, saya menyertakan jenis sensor yang dikehendaki yaitu Sensor.TYPE_ACCELEROMETER. Karena tidak semua perangkat memiliki sensor tersebut, method getDefaultSensor() bisa saja mengembalikan nilai null. Setelah itu, saya mendaftarkan PlotterView yang sudah saya buat sebelumnya sebagai listener untuk sensor yang baru saja saya dapatkan dengan menggunakan method registerListener() dari SensorManager. Agar tidak mengkonsumsi baterai berlebihan, saat aplikasi di-pause, saya memanggil method unregisterListener() dari SensorManager sehingga pembacaan nilai dari sensor tidak dilakukan lagi.

Bila saya menjalankan aplikasi, saya akan memperoleh tampilan seperti pada gambar berikut ini:

Tampilan awal

Tampilan awal

Agar lebih mudah dibaca, saya akan menambahkan informasi nilai. Untuk itu, saya mengubah kode program PlotterView sehingga menjadi seperti berikut ini:

@CompileStatic
class PlotterView extends SurfaceView implements SurfaceHolder.Callback, SensorEventListener {

  ...

  Map<String, Float> nilaiSumbu = [:]

  ...

  void setup() {
    ...
    int y = (int)((height / 2) / 20)
    while (nilaiSensor.translate(y) < height) {
      nilaiSumbu[y.toString()] = nilaiSensor.translate(y)
      y -= 1
    }
    ...
  }

  ...

  void updateAction() {
    SurfaceHolder holder = getHolder()
    Canvas c
    try {
      c = holder.lockCanvas()
      if (c) {
        ...
        //Gambar sumbu
        n = nilaiSensor.n
        c.drawLine(n , 0,  n, height, paintSumbu)
        for (String k: nilaiSumbu.keySet()) {
          float v = nilaiSumbu[k]
          c.drawText(k, n, v, paintSumbu)
          c.drawLine(n-5, v, n, v, paintSumbu)
        }
      }
    } finally {
      if (c) {
        holder.unlockCanvasAndPost(c)
      }
    }
  }

  ...

}

Sekarang, bila saya menjalankan aplikasi, saya akan memperoleh informasi sumbu. Sebagai contoh, bila saya meletakkan perangkat secara mendatar di atas meja, saya memperoleh hasil seperti pada gambar berikut ini:

Nilai accelerometer saat perangkat diletakkan terbaring diam dengan layar menghadap ke atas

Nilai accelerometer saat perangkat diletakkan terbaring diam dengan layar menghadap ke atas

Terlihat bahwa pada posisi diam pun, accelerometer tidak serta merta mengembalikan nilai 0. Hal ini karena nilai yang dikembalikan dipengaruhi oleh gaya gravitasi. Sebagai contoh, garis warna biru terang mewakili nilai percepatan pada sumbu yang menghadap ke layar (pada sensor Android, ini disebut sumbu Z). Pada saat perangkat berada dalam keadaan dibaringkan (dimana layar menghadap ke atas), maka sumbu Z sepenuhnya dipengaruhi oleh gaya gravitasi bumi (sekitar 9,8 m/s^2).

Bila saya menegakkan perangkat Android di atas meja dimana perangkat tersebut dalam keadaan diam, saya memperoleh hasil dari accelerometer seperti pada gambar berikut ini:

Nilai accelerometer saat perangkat tegak dalam keadaan diam

Nilai accelerometer saat perangkat tegak dalam keadaan diam

Walaupun saya mengubah posisi perangkat dari terbaring di atas meja menjadi berdiri tegak, lokasi sumbu tidak berubah. Sebagai contoh, sumbu Z tetap adalah sumbu yang menghadap ke arah layar. Pengaruh gravitasi padanya kini menjadi lebih kecil pada sumbu tersebut. Sebaliknya, sumbu Y yang menghadap ke arah atas perangkat (vertikal) kini memperoleh pengaruh besar dari gravitasi.

Contoh lainnya, bila saya menggoyangkan perangkat dengan keras, tampilan grafis akan terlihat seperti pada gambar berikut ini:

Nilai accelerometer saat perangkat diguncang dengan keras ke kiri dan ke kanan

Nilai accelerometer saat perangkat diguncang dengan keras ke kiri dan ke kanan

Pada gambar di atas, saya mengguncang perangkat sebanyak 4 kali (ke kiri dan ke kanan). Terlihat pada percepatan pada sumbu X naik turun secara drastis pada saat saya menggoyang perangkat. Berdasarkan informasi ini, saya dapat membuat aplikasi yang mendeteksi apakah perangkat bergoyang (dengan memeriksa apakah ada perubahan drastis nilai X positif menjadi negatif dalam waktu singkat).

Pola lain yang menarik adalah nilai yang diperoleh pada saat pengguna berjalan. Sebagai contoh, bila saya merekam nilai dari accelerometer pada saat saya melangkahkan kaki sambil memegang perangkat di tangan, saya memperoleh hasil seperti pada gambar berikut ini:

Nilai accelerometer saat perangkat dipegang di tangan sambil berjalan

Nilai accelerometer saat perangkat dipegang di tangan sambil berjalan

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: