Intro
이제 마지막이다. 이전에 받아온 센서 데이터를 차트 형태로 출력해보자.
차트 라이브러리는 MPAndroidChart를 사용했다.
GitHub - PhilJay/MPAndroidChart: A powerful 🚀 Android chart view / graph view library, supporting line- bar- pie- radar- bubb
A powerful 🚀 Android chart view / graph view library, supporting line- bar- pie- radar- bubble- and candlestick charts as well as scaling, panning and animations. - GitHub - PhilJay/MPAndroidChart:...
github.com
Gradle 설정
settings.gradle
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
...
maven { url 'https://jitpack.io' }
}
}
build.gradle
dependencies {
...
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}
일정 주기로 데이터를 요청해야 하기 때문에 coroutine도 추가했다.
MainActivity UI 작성
단순히 모든 차트를 수직으로 배열했다.
Height가 화면 크기 이상으로 나올것이 당연하기 때문에 ScrollView 안에 넣었다.
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">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tvECO2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ECO2"
android:textSize="25sp"
android:textAlignment="center"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chartECO2"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_marginTop="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvECO2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/constraintLayout1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tvTVOC"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TVOC"
android:textSize="25sp"
android:textAlignment="center"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chartTVOC"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_marginTop="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTVOC" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/constraintLayout2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tvTemp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Temperature"
android:textSize="25sp"
android:textAlignment="center"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chartTemp"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_marginTop="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTemp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/constraintLayout3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tvAccel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Accelerometer"
android:textSize="25sp"
android:textAlignment="center"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chartAccel"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_marginTop="30dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAccel" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
Chart Extension Function 생성
차트를 일일이 다 초기화하면 코드가 많이 더러워질 것이다.
때문에 Kotlin에서 제공하는 확장함수를 사용해 LineChart를 초기화하는 함수를 작성했다.
차트 내부의 격자는 연한 회색, 축과 다른 텍스트는 진한 회삭으로 설정했다.
X축 텍스트는 230142와 같은 실수를 HH:mm:ss 형태로 출력하도록 커스텀 ValueFormatter를 설정해줬다.
LineChart.initializeChart
private fun LineChart.initializeChart() {
val colorGrid = Color.parseColor("#CCCCCC")
val colorText = Color.parseColor("#666666")
// 차트 초기화
this.apply {
invalidate()
clear()
description = null
}
this.xAxis.apply {
position = XAxis.XAxisPosition.BOTTOM
// 230142 형태의 실수를 23:01:42 형태의 String으로 변환
valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
val time = value.toInt()
return "${(time / 10000).toString().padStart(2, '0')}:" +
"${((time % 10000) / 100).toString().padStart(2, '0')}:" +
"${(time % 100).toString().padStart(2, '0')}"
}
}
setLabelCount(CHART_SIZE, true)
textColor = colorText
gridColor = colorGrid
}
this.axisLeft.apply {
textColor = colorText
gridColor = colorGrid
}
// 오른쪽 Y축 비활성화
this.axisRight.apply {
setDrawLabels(false)
setDrawAxisLine(false)
setDrawGridLines(false)
}
this.legend.apply {
textColor = colorText
}
}
이제 아래와 같이 모든 차트들을 깔끔하게 초기화할 수 있다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
binding.chartECO2.initializeChart()
binding.chartTVOC.initializeChart()
binding.chartTemp.initializeChart()
binding.chartAccel.initializeChart()
setContentView(binding.root)
}
Entry 리스트, 차트 제목과 선 색상을 받아 차트에 선을 그려주는 함수다.
단점은 Accel과 같이 여러 개의 Entry 리스트를 받아야 하는 경우는 처리를 못해준다.
LineChart.setData
private fun LineChart.setData(data: ArrayList<Entry>, label: String, colorString: String) {
val color = Color.parseColor(colorString)
val lineDataSet = LineDataSet(data, label)
lineDataSet.color = color
lineDataSet.setCircleColor(color)
lineDataSet.circleHoleColor = ContextCompat.getColor(applicationContext, R.color.white)
val lineData = LineData().apply {
addDataSet(lineDataSet)
setValueTextColor(R.color.black)
setValueTextSize(9f)
}
this.invalidate()
this.clear()
this.data = lineData
}
UI 업데이트를 위한 Handler 작성
Coroutine에서 바로 UI를 업데이트하면 좋겠지만, 안드로이드는 메인 스레드에서만 UI를 변경할 수 있다.
이럴 경우 Handler를 사용해 메인이 아닌 스레드에서 Handler에 메시지를 보내고, Handler에서 메시지를 받아 메인 스레드에서 UI를 업데이트 시키는 것이 가능하다.
Accel 데이터는 LineChart.setData함수로 처리가 안돼서 직접 해줬다.
private fun String.ISOStringToFloat(): Float {
return this.substring(11, 19).replace(":", "").toFloat()
}
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
val response = msg.obj as JSONObject
val eco2 = ArrayList<Entry>()
val tvoc = ArrayList<Entry>()
val temp = ArrayList<Entry>()
val accelX = ArrayList<Entry>()
val accelY = ArrayList<Entry>()
val accelZ = ArrayList<Entry>()
val array = response.getJSONArray("sensorData")
for(i in 0 until array.length()) {
val row = array.getJSONObject(i)
val date = row.getString("createdDate").ISOStringToFloat()
eco2.add(0, Entry(date, row.getInt("eco2").toFloat()))
tvoc.add(0, Entry(date, row.getInt("tvoc").toFloat()))
temp.add(0, Entry(date, row.getDouble("temp").toFloat()))
val accel = row.getJSONObject("accel")
accelX.add(0, Entry(date, accel.getDouble("x").toFloat()))
accelY.add(0, Entry(date, accel.getDouble("y").toFloat()))
accelZ.add(0, Entry(date, accel.getDouble("z").toFloat()))
}
binding.chartECO2.setData(eco2, "eCO2", "#7900FF")
binding.chartTVOC.setData(tvoc, "TVOC", "#548CFF")
binding.chartTemp.setData(temp, "Temperature", "#FF00BE")
val lineDataSetX = LineDataSet(accelX, "X")
lineDataSetX.color = Color.RED
lineDataSetX.setCircleColor(Color.RED)
lineDataSetX.circleHoleColor = ContextCompat.getColor(applicationContext, R.color.white)
val lineDataSetY = LineDataSet(accelY, "Y")
lineDataSetY.color = Color.GREEN
lineDataSetY.setCircleColor(Color.GREEN)
lineDataSetY.circleHoleColor = ContextCompat.getColor(applicationContext, R.color.white)
val lineDataSetZ = LineDataSet(accelZ, "Z")
lineDataSetZ.color = Color.BLUE
lineDataSetZ.setCircleColor(Color.BLUE)
lineDataSetZ.circleHoleColor = ContextCompat.getColor(applicationContext, R.color.white)
binding.chartAccel.invalidate()
binding.chartAccel.clear()
binding.chartAccel.data = LineData().apply {
addDataSet(lineDataSetX)
addDataSet(lineDataSetY)
addDataSet(lineDataSetZ)
setValueTextColor(R.color.black)
setValueTextSize(9f)
}
}
}
일정 간격으로 센서 데이터 Request
이전에 적용한 Coroutine이 여기서 쓰인다.
서버에 데이터를 요청하고 Callback함수를 등록하여 응답이 오면 위에 만들었던 Handler에 메시지를 보내 차트를 업데이트한다.
간격은 React와 동일하게 2초로 설정했다.
GlobalScope.launch {
val onSensorData = { response: JSONObject ->
handler.obtainMessage(0, response).sendToTarget()
}
while(true) {
RequestHandler.request("/api/1/sensor?size=$CHART_SIZE", null, onSensorData, null, false, Request.Method.GET)
Thread.sleep(2000)
}
}
동작 테스트
프로젝트를 실행하며 아래와 같이 차트가 정상적으로 뜨는것을 볼 수 있다.
당연하겠지만 안드로이드와 서버가 같은 네트워크 안에 있어야 정상동작 한다.
이번 프로젝트는 이쯤에서 마무리하는 것이 좋을 것 같다.
그동안 공부했던 다양한 기술들을 사용할 수 있어서 좋았고, 결과물도 3일 한 것 치곤 상당히 잘 나온것 같아 만족스럽다.
모든 소스코드는 Github에 올라가있으니 필요하면 복사해서 사용해도 괜찮다.
https://github.com/DevJaewoo/sensor-monitoring-system
GitHub - DevJaewoo/sensor-monitoring-system
Contribute to DevJaewoo/sensor-monitoring-system development by creating an account on GitHub.
github.com
'Projects > 센서 모니터링 시스템' 카테고리의 다른 글
[센서 모니터링 시스템] 12. Android Request 구현 (0) | 2022.08.19 |
---|---|
[센서 모니터링 시스템] 11. Rechart를 사용한 센서 데이터 시각화 (0) | 2022.08.19 |
[센서 모니터링 시스템] 10. React 개발환경 구성 (0) | 2022.08.19 |
[센서 모니터링 시스템] 9. 라즈베리파이 Request 구현 (0) | 2022.08.18 |
[센서 모니터링 시스템] 8. Service, REST Controller 개발 (0) | 2022.08.18 |