diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml new file mode 100644 index 0000000..8997d1b --- /dev/null +++ b/.idea/androidTestResultsUserPreferences.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 1ccc02f..80bbd6a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,5 +58,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation 'androidx.recyclerview:recyclerview:1.2.0' + implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0' + implementation "com.android.volley:volley:1.2.1" } \ No newline at end of file diff --git a/app/src/main/java/com/example/sensortestingapp/DecoderIaq.kt b/app/src/main/java/com/example/sensortestingapp/DecoderIaq.kt new file mode 100644 index 0000000..abfb42b --- /dev/null +++ b/app/src/main/java/com/example/sensortestingapp/DecoderIaq.kt @@ -0,0 +1,102 @@ +package com.example.sensortestingapp + +import org.apache.commons.codec.DecoderException +import org.apache.commons.codec.binary.Hex +import kotlin.math.roundToInt + +object DecoderIaq { + private const val INVALID_CO2 = 16383 + private const val INVALID_VOC = 2047 + private const val INVALID_HUMIDITY = 1023 + private const val INVALID_TEMPERATURE = 2047 + private const val INVALID_PRESSURE = 65535 + private const val INVALID_OCCUPANCY = 3 + private const val INVALID_PM25 = 1023 + private const val INVALID_PM10 = 1023 + fun parseMeasurement(input: String): Measurement { + val measurement = Measurement() + val inputBytes = hexStringToByteArray(input) + val msgType = inputBytes[0].toInt() and 0xFF ushr 4 + measurement.msgType = msgType + + val co2 = parseUnsignedInt(inputBytes, 0, 3) ushr 6 and INVALID_CO2 + measurement.co2 = if (co2 == INVALID_CO2) null else co2 + + val voc = parseUnsignedInt(inputBytes, 2, 4) ushr 3 and INVALID_VOC + measurement.voc = if (co2 == INVALID_VOC) null else voc + + val humidity = + parseUnsignedInt(inputBytes, 3, 5) ushr 1 and INVALID_HUMIDITY + measurement.humidity = if (humidity == INVALID_HUMIDITY) null else humidity / 10 + + val temperature = + parseUnsignedInt(inputBytes, 4, 7) ushr 6 and INVALID_TEMPERATURE + measurement.temperature = + if (temperature == INVALID_TEMPERATURE) null else ((temperature / 10.0 - 40) * 10.0).roundToInt() / 10.0 + + val pressure = + parseUnsignedInt(inputBytes, 6, 9) ushr 6 and INVALID_PRESSURE + measurement.pressure = + if (pressure == INVALID_PRESSURE) null else (30000 + 19000.0 * pressure / 13107).roundToInt() + + val occupancy = + parseUnsignedInt(inputBytes, 8, 9) ushr 4 and INVALID_OCCUPANCY + measurement.occupancy = if (occupancy == INVALID_OCCUPANCY) null else occupancy + + if (msgType == 0) { + val pm25 = + parseUnsignedInt(inputBytes, 8, 10) ushr 2 and INVALID_PM25 + measurement.pm25 = if (pm25 == INVALID_PM25) null else pm25 + + val pm10 = parseUnsignedInt(inputBytes, 9, 11) and INVALID_PM10 + measurement.pm10 = if (pm10 == INVALID_PM10) null else pm10 + } + return measurement + } + + private fun parseUnsignedInt(bytes: ByteArray, startIncl: Int, endExcl: Int): Int { + val section = bytes.copyOfRange(startIncl, endExcl) + var unsignedInt = 0 + for (i in section.indices) { + unsignedInt = unsignedInt shl 8 + unsignedInt = unsignedInt or (section[i].toInt() and 0xFF) + } + return unsignedInt + } + + private fun hexStringToByteArray(encoded: String): ByteArray { + return try { + Hex.decodeHex(encoded) + } catch (e: DecoderException) { + throw RuntimeException(e) + } + } + + data class Measurement ( + var msgType: Number? = null, + var co2: Number? = null, + var voc: Number? = null, + var humidity: Number? = null, + var temperature: Number? = null, + var pressure: Number? = null, + var occupancy: Number? = null, + var pm25: Number? = null, + var pm10: Number? = null + ) { + + + override fun toString(): String { + return "M{" + + "type=" + msgType + + "co2=" + co2 + + ", voc=" + voc + + ", hum=" + humidity + + ", temp=" + temperature + + ", press=" + pressure + + ", occ=" + occupancy + + ", pm25=" + pm25 + + ", pm10=" + pm10 + + '}' + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt b/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt index 541f5d8..e840b51 100644 --- a/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt +++ b/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt @@ -128,7 +128,7 @@ class KirbyDevice( override var rssi: Int? = null private fun addMeasurement(characteristic: BluetoothGattCharacteristic) { - val hexPayload = characteristic.value.toHexString() + val hexPayload = characteristic.value.toHexString().substring(2) val payload = Payload(hexPayload) val base64Payload = Base64.getEncoder().encodeToString(characteristic.value) Log.i("BleListener", "Char received: $payload") @@ -183,25 +183,10 @@ class KirbyDevice( get() = statuses.stream().map { it.name }.collect(Collectors.joining(", ")) override fun getMeasurements(): List { - if (measurements.isEmpty()) { - return emptyList() - } - val latest = measurements.last() - val result = mutableListOf(object : Measurement { - override fun getLabel(): String { - return "Index" - } + val result = mutableListOf() - override fun getIcon(): Int { - return R.drawable.baseline_numbers_24 - } - - override fun getFormattedValue(): String { - return measurements.size.toString() - } - }) - result.addAll(payloadToMeasurements(latest)) - return result + measurements.forEach { m -> result.addAll(payloadToMeasurements(m))} + return result.reversed() } override fun getActions(): List { @@ -345,10 +330,9 @@ class KirbyDevice( data class Payload( val payload: String, val ts: String = LocalDateTime.now() - .format(DateTimeFormatter.ofPattern("HH:mm:ss")) + .format(DateTimeFormatter.ofPattern("dd.MM.yy HH:mm:ss")) ) - fun bytesToUInt16(arr: ByteArray, start: Int): Int { return ByteBuffer.wrap(arr, start, 2) .order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF @@ -376,15 +360,15 @@ private fun payloadToMeasurements(payload: Payload): List { } override fun getIcon(): Int? { - return R.drawable.baseline_access_time_24 + return R.drawable.baseline_numbers_24 } }, object : Measurement { override fun getLabel(): String { - return "Payload" + return "" } override fun getFormattedValue(): String { - return payload.payload + return DecoderIaq.parseMeasurement(payload.payload).toString(); } override fun getIcon(): Int? { diff --git a/app/src/main/res/layout/row_measurements_list.xml b/app/src/main/res/layout/row_measurements_list.xml index 4cd3b3e..5e2b941 100644 --- a/app/src/main/res/layout/row_measurements_list.xml +++ b/app/src/main/res/layout/row_measurements_list.xml @@ -10,22 +10,26 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawablePadding="2dp" - android:textSize="18sp" + android:textSize="16sp" app:drawableStartCompat="@drawable/baseline_device_thermostat_24" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + + app:layout_constraintTop_toTopOf="parent" tools:text="Temperature" /> + tools:text="android:layout_margin="18dp" android:maxWidth="250dp" android:text="@string/year" android:maxLines="24" android:textSize="18sp" app:layout_constraintBottom_toBottomOf="parent"" /> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 125df87..7895a17 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,3 +1,4 @@ 16dp + 0dp \ No newline at end of file diff --git a/app/src/test/java/com/example/sensortestingapp/DecoderTest.kt b/app/src/test/java/com/example/sensortestingapp/DecoderTest.kt new file mode 100644 index 0000000..a52aa8f --- /dev/null +++ b/app/src/test/java/com/example/sensortestingapp/DecoderTest.kt @@ -0,0 +1,27 @@ +package com.example.sensortestingapp + +import org.junit.Assert.* +import org.junit.Test + + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class DecoderTest { + @Test + fun message_type_0_decodes_correctly() { + val res2 = DecoderIaq.parseMeasurement("006b04ab74a1ed0d101404"); + val testMeasurement = DecoderIaq.Measurement(msgType = 0, co2 = 428, voc = 149, humidity = 44, pressure = 96873, occupancy = 1, pm10 = 4, pm25 = 5, temperature = 24.7 ) + assertEquals(testMeasurement, res2) + } + + + @Test + fun message_type_1_decodes_correctly() { + val res2 = DecoderIaq.parseMeasurement("106b04ab74a1ed0d10"); + val testMeasurement = DecoderIaq.Measurement(msgType = 1, co2 = 428, voc = 149, humidity = 44, pressure = 96873, occupancy = 1, pm10 = null, pm25 = null, temperature = 24.7 ) + assertEquals(testMeasurement, res2) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/example/sensortestingapp/ExampleUnitTest.kt b/app/src/test/java/com/example/sensortestingapp/ExampleUnitTest.kt deleted file mode 100644 index 7a120f7..0000000 --- a/app/src/test/java/com/example/sensortestingapp/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.sensortestingapp - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file