feat: add decoded payloads

This commit is contained in:
Stefan Zollinger
2023-11-28 13:40:38 +01:00
parent 93c941cf7c
commit ae0d1ff921
8 changed files with 172 additions and 47 deletions

View File

@@ -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"
}

View File

@@ -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 +
'}'
}
}
}

View File

@@ -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<Measurement> {
if (measurements.isEmpty()) {
return emptyList()
}
val latest = measurements.last()
val result = mutableListOf<Measurement>(object : Measurement {
override fun getLabel(): String {
return "Index"
}
val result = mutableListOf<Measurement>()
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<Action> {
@@ -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<Measurement> {
}
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? {

View File

@@ -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" />
<TextView
android:id="@+id/measurement_value"
android:layout_width="wrap_content"
android:layout_width="@dimen/match_constraint"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="20 °C" />
tools:text="android:layout_margin=&quot;18dp&quot; android:maxWidth=&quot;250dp&quot; android:text=&quot;@string/year&quot; android:maxLines=&quot;24&quot; android:textSize=&quot;18sp&quot; app:layout_constraintBottom_toBottomOf=&quot;parent&quot;" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,3 +1,4 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
<dimen name="match_constraint">0dp</dimen>
</resources>

View File

@@ -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)
}
}

View File

@@ -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)
}
}