feat: add csv auto export

This commit is contained in:
Stefan Zollinger
2024-04-12 12:13:02 +02:00
parent 62ce221860
commit a5425083da
8 changed files with 189 additions and 89 deletions

View File

@@ -90,7 +90,7 @@ open class BleListener(private val deviceAddress: String?) {
open fun isRelevantMessage(address: String?): Boolean { open fun isRelevantMessage(address: String?): Boolean {
if (deviceAddress != null && deviceAddress == address) { if (deviceAddress != null && deviceAddress == address) {
return true; return true
} }
return deviceAddress == null return deviceAddress == null
} }
@@ -165,7 +165,7 @@ private fun BluetoothGatt.printGattTable() {
separator = "\n|--", separator = "\n|--",
prefix = "|--" prefix = "|--"
) { ) {
"${it.uuid.toString()} | " + "${it.uuid} | " +
"readable: ${it.isReadable()}, " + "readable: ${it.isReadable()}, " +
"writable: ${it.isWritable()}, " + "writable: ${it.isWritable()}, " +
"writableWithoutResponse: ${it.isWritableWithoutResponse()}, " + "writableWithoutResponse: ${it.isWritableWithoutResponse()}, " +
@@ -325,7 +325,11 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
fun connect(device: BluetoothDevice) { fun connect(device: BluetoothDevice) {
if (device.isConnected()) { Log.i("ConnectionManager", "connecting to " +device.address)
val isConnecting = pendingOperation != null && pendingOperation is Connect && (pendingOperation as Connect).device.address == device.address
if(device.isConnected() or isConnecting or operationQueue.any { it.device.address === device.address && it is Connect }) {
Log.e("ConnectionManager", "Already connected to ${device.address}!") Log.e("ConnectionManager", "Already connected to ${device.address}!")
} else { } else {
enqueueOperation(Connect(device, context)) enqueueOperation(Connect(device, context))
@@ -528,7 +532,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
} }
is SetNotification -> with(operation) { is SetNotification -> with(operation) {
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId); val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId)
if (characteristic == null) { if (characteristic == null) {
Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!") Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!")
@@ -560,7 +564,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
payload = payload =
if (characteristic.isIndicatable()) if (characteristic.isIndicatable())
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
else BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; else BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
} else { } else {
if (!gatt.setCharacteristicNotification(characteristic, false)) { if (!gatt.setCharacteristicNotification(characteristic, false)) {
@@ -613,7 +617,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
it.onConnectToBondedFailed(gatt) it.onConnectToBondedFailed(gatt)
} }
signalEndOfOperation(operation) signalEndOfOperation(operation)
} else if (status == BluetoothGatt.GATT_SUCCESS) { } else if (status == GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) { if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i( Log.i(
"ConnectionManager", "ConnectionManager",
@@ -649,7 +653,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
with(gatt) { with(gatt) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == GATT_SUCCESS) {
Log.w( Log.w(
"ConnectionManager", "ConnectionManager",
"Discovered ${services.size} services for ${device.address}." "Discovered ${services.size} services for ${device.address}."
@@ -673,7 +677,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
Log.w( Log.w(
"ConnectionManager", "ConnectionManager",
"ATT MTU changed to $mtu, success: ${status == BluetoothGatt.GATT_SUCCESS}" "ATT MTU changed to $mtu, success: ${status == GATT_SUCCESS}"
) )
val operation = pendingOperation val operation = pendingOperation
@@ -689,7 +693,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
) { ) {
with(characteristic) { with(characteristic) {
when (status) { when (status) {
BluetoothGatt.GATT_SUCCESS -> { GATT_SUCCESS -> {
Log.i( Log.i(
"ConnectionManager", "ConnectionManager",
"Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}" "Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}"

View File

@@ -10,7 +10,11 @@ import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import java.io.File
import java.time.Instant import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.SortedMap
object LoggerContract { object LoggerContract {
@@ -30,6 +34,7 @@ object LoggerContract {
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${LogEntry.TABLE_NAME}" private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${LogEntry.TABLE_NAME}"
class LoggerDbHelper(context: Context) : class LoggerDbHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
@@ -60,6 +65,9 @@ object LoggerContract {
private val dbHelper = LoggerDbHelper(context) private val dbHelper = LoggerDbHelper(context)
private val dbWrite = dbHelper.writableDatabase private val dbWrite = dbHelper.writableDatabase
private val dbRead = dbHelper.writableDatabase private val dbRead = dbHelper.writableDatabase
private val tag = "LoggerDb"
val context: Context = context val context: Context = context
fun writeLog(payload: Any): Long? { fun writeLog(payload: Any): Long? {
@@ -67,8 +75,6 @@ object LoggerContract {
val jsonString = gson.toJson(payload) val jsonString = gson.toJson(payload)
val ts = Instant.now().toString() val ts = Instant.now().toString()
Log.i("Database", jsonString)
val values = ContentValues().apply { val values = ContentValues().apply {
put(LogEntry.COLUMN_NAME_TS, ts) put(LogEntry.COLUMN_NAME_TS, ts)
put(LogEntry.COLUMN_NAME_PAYLOAD, jsonString) put(LogEntry.COLUMN_NAME_PAYLOAD, jsonString)
@@ -77,6 +83,21 @@ object LoggerContract {
return dbWrite?.insert(LogEntry.TABLE_NAME, null, values) return dbWrite?.insert(LogEntry.TABLE_NAME, null, values)
} }
fun getExportFileUri(): Uri? {
val file = File(context.filesDir, "export.csv")
if (!file.exists()) {
file.createNewFile()
}
file.setReadable(true, false)
return Uri.fromFile(file)
}
fun exportToCsv() {
val uri = getExportFileUri() ?: return
exportToUri(uri)
}
fun exportToUri(uri: Uri) { fun exportToUri(uri: Uri) {
val projection = val projection =
@@ -97,7 +118,7 @@ object LoggerContract {
try { try {
val gson = Gson() val gson = Gson()
val type = object : TypeToken<HashMap<String, Any>>() {}.type
var headerWritten = false var headerWritten = false
val sep = "," val sep = ","
@@ -109,30 +130,26 @@ object LoggerContract {
val ts = getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_TS)) val ts = getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_TS))
val storedField = val storedField =
getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_PAYLOAD)) getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_PAYLOAD))
val payload = parsePayload(storedField)
try { try {
val payload: HashMap<String, Any> = gson.fromJson(storedField, type)
if (!headerWritten) { if (!headerWritten) {
val headerRow = val headerRow =
"timestamp" + sep + payload.keys.joinToString(sep) + newLine "timestamp" + sep + "local_time" + sep + payload.keys.joinToString(sep) + newLine
writer.write(headerRow.toByteArray()) writer.write(headerRow.toByteArray())
headerWritten = true headerWritten = true
} }
val localTime = convertIsoToLocalTime(ts)
val row = ts + sep + payload.values.joinToString(sep) + newLine val row = ts + sep + localTime + sep + payload.values.joinToString(sep) + newLine
writer.write(row.toByteArray()) writer.write(row.toByteArray())
} catch (exception: JsonSyntaxException) { } catch (exception: JsonSyntaxException) {
Log.e("db", exception.toString()) Log.e(tag, exception.toString())
} }
} }
} }
} }
truncate()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -140,9 +157,80 @@ object LoggerContract {
cursor.close() cursor.close()
} }
private fun truncate() { fun exportToMultipleCSV() {
dbWrite.execSQL("DELETE FROM ${LogEntry.TABLE_NAME}"); val projection =
dbWrite.execSQL("VACUUM"); arrayOf(BaseColumns._ID, LogEntry.COLUMN_NAME_PAYLOAD, LogEntry.COLUMN_NAME_TS)
val sortOrder = "${BaseColumns._ID} ASC"
val cursor = dbRead.query(
LogEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
null, // The columns for the WHERE clause
null, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
)
val files = HashMap<String, File>()
try {
val sep = ","
val newLine = '\n'
with(cursor) {
while (moveToNext()) {
val ts = getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_TS))
val storedField =
getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_PAYLOAD))
try {
val payload = parsePayload(storedField)
val deviceId = payload.getOrDefault("bleAddress", "unknown") as String
val fileName = "kirby_export_${deviceId.replace(":", "")}.csv"
val f = files.getOrElse(deviceId) {
val file = File(context.filesDir, fileName)
if (!file.exists()) {
file.createNewFile()
}
val headerRow =
"timestamp" + sep + "local_time" + sep + payload.keys.joinToString(
sep
) + newLine
file.writeText(headerRow)
files[deviceId] = file
Log.i(tag, file.absolutePath)
file.setReadable(true, false)
file
}
val localTime = convertIsoToLocalTime(ts)
val row =
ts + sep + localTime + sep + payload.values.joinToString(sep) + newLine
f.appendText(row)
} catch (exception: JsonSyntaxException) {
Log.e(tag, exception.toString())
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
cursor.close()
}
fun reset() {
dbWrite.execSQL("DELETE FROM ${LogEntry.TABLE_NAME}")
dbWrite.execSQL("VACUUM")
} }
fun close() { fun close() {
@@ -152,7 +240,16 @@ object LoggerContract {
} }
} }
fun parsePayload(payload: String): SortedMap<String, Any> {
val type = object : TypeToken<HashMap<String, Any>>() {}.type
val gson = Gson()
val parsed : HashMap<String, Any> = gson.fromJson(payload, type)
return parsed.toSortedMap()
}
fun convertIsoToLocalTime(isoDateTime: String): String {
val systemZone = ZoneId.systemDefault()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return Instant.parse(isoDateTime).atZone(systemZone).format(formatter)
}

View File

@@ -1,6 +1,7 @@
package com.logitech.vc.kirbytest package com.logitech.vc.kirbytest
import android.util.Log import android.util.Log
import com.google.gson.annotations.SerializedName
import org.apache.commons.codec.DecoderException import org.apache.commons.codec.DecoderException
import org.apache.commons.codec.binary.Hex import org.apache.commons.codec.binary.Hex
import kotlin.math.min import kotlin.math.min
@@ -25,7 +26,7 @@ object DecoderIaq {
if(!supportedMessageTypes.contains(msgType)) { if(!supportedMessageTypes.contains(msgType)) {
Log.i("Decoder", "Invalid message type: $msgType") Log.i("Decoder", "Invalid message type: $msgType")
return null; return null
} }
measurement.msgType = msgType measurement.msgType = msgType
@@ -84,6 +85,7 @@ object DecoderIaq {
} }
data class Measurement ( data class Measurement (
@SerializedName("bleAddress")
var deviceId: String? = null, var deviceId: String? = null,
var msgType: Number? = null, var msgType: Number? = null,
var co2: Number? = null, var co2: Number? = null,

View File

@@ -52,6 +52,11 @@ class KirbyDevice(
} }
} }
fun connect() {
connectionManager.connect(bleDevice)
connectionManager.discoverServices(bleDevice)
}
fun readIaq() { fun readIaq() {
connectionManager.readChar(bleDevice, SERVICE_UUID, CHAR_UUID) connectionManager.readChar(bleDevice, SERVICE_UUID, CHAR_UUID)
} }
@@ -68,11 +73,6 @@ class KirbyDevice(
override fun onScanResult(callbackType: Int, result: ScanResult) { override fun onScanResult(callbackType: Int, result: ScanResult) {
rssi = result.rssi rssi = result.rssi
onStateChange(this) onStateChange(this)
if(result.isConnectable) {
connectionManager.connect(bleDevice)
connectionManager.discoverServices(bleDevice)
}
} }
override fun onConnect(gatt: BluetoothGatt) { override fun onConnect(gatt: BluetoothGatt) {
@@ -122,6 +122,7 @@ class KirbyDevice(
override fun onBonded(device: BluetoothDevice) { override fun onBonded(device: BluetoothDevice) {
statuses.add(DeviceStatus.BONDED) statuses.add(DeviceStatus.BONDED)
onStateChange(this) onStateChange(this)
connect()
} }
override fun onUnbonded(device: BluetoothDevice) { override fun onUnbonded(device: BluetoothDevice) {
@@ -361,7 +362,7 @@ class KirbyDevice(
}) })
} }
return actions; return actions
} }
} }
@@ -397,7 +398,7 @@ private fun payloadToMeasurements(payload: Payload): List<Measurement> {
return payload.payload return payload.payload
} }
override fun getIcon(): Int? { override fun getIcon(): Int {
return R.drawable.baseline_numbers_24 return R.drawable.baseline_numbers_24
} }
} }

View File

@@ -40,6 +40,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Timer
import java.util.TimerTask
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1 private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1
@@ -66,7 +68,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var loggerDb: LoggerContract.LoggerDb private lateinit var loggerDb: LoggerContract.LoggerDb
private lateinit var createFileLauncher: ActivityResultLauncher<String> private lateinit var createFileLauncher: ActivityResultLauncher<String>
private val bondedDevices = HashSet<String>()
private val bluetoothAdapter: BluetoothAdapter by lazy { private val bluetoothAdapter: BluetoothAdapter by lazy {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
@@ -78,7 +80,7 @@ class MainActivity : AppCompatActivity() {
newConnectionManager() newConnectionManager()
} }
private val kirbyDevices = mutableListOf<DeviceListEntry>(); private val kirbyDevices = mutableListOf<DeviceListEntry>()
private val deviceListAdapter: DeviceListAdapter by lazy { private val deviceListAdapter: DeviceListAdapter by lazy {
DeviceListAdapter(kirbyDevices) DeviceListAdapter(kirbyDevices)
@@ -144,6 +146,12 @@ class MainActivity : AppCompatActivity() {
connectionManager.stopScan() connectionManager.stopScan()
} }
} }
Timer().schedule(object : TimerTask() {
override fun run() {
loggerDb.exportToMultipleCSV()
}
}, 1000, 10000)
} }
private fun toggleScanning(): Unit { private fun toggleScanning(): Unit {
@@ -186,6 +194,7 @@ class MainActivity : AppCompatActivity() {
private fun addBondedDevices(): Unit { private fun addBondedDevices(): Unit {
bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach { bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach {
bondedDevices.add(it.address)
newKirbyDevice(it) newKirbyDevice(it)
} }
} }
@@ -204,7 +213,7 @@ class MainActivity : AppCompatActivity() {
mngr.register(object : BleListener(null) { mngr.register(object : BleListener(null) {
override fun onScanningStateChange(isScanning: Boolean) { override fun onScanningStateChange(isScanning: Boolean) {
runOnUiThread { runOnUiThread {
binding.fab.setText(if (isScanning) "Stop Scan" else "Start Scan") binding.fab.text = if (isScanning) "Stop Scan" else "Start Scan"
binding.fab.setIconResource( binding.fab.setIconResource(
if (isScanning) R.drawable.action_icon_disconnect else R.drawable.action_icon_scan if (isScanning) R.drawable.action_icon_disconnect else R.drawable.action_icon_scan
) )
@@ -221,7 +230,16 @@ class MainActivity : AppCompatActivity() {
) )
val kirbyDevice = kirbyDevices.find { it.address == result.device.address } val kirbyDevice = kirbyDevices.find { it.address == result.device.address }
if (kirbyDevice == null) { if (kirbyDevice == null) {
newKirbyDevice(result.device).onScanResult(callbackType, result) val kirby = newKirbyDevice(result.device)
kirby.onScanResult(callbackType, result)
}
if (bondedDevices.contains(result.device.address)) {
Log.i("KirbyDevice", "Connecting to " + result.device.address)
connectionManager.connect(result.device)
connectionManager.readRemoteRssi(result.device)
connectionManager.discoverServices(result.device)
} }
} }
@@ -231,8 +249,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun newKirbyDevice( private fun newKirbyDevice(
bleDevice: BluetoothDevice, bleDevice: BluetoothDevice
autoConnect: Boolean = false
): KirbyDevice { ): KirbyDevice {
val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb) { val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb) {
@@ -244,13 +261,6 @@ class MainActivity : AppCompatActivity() {
Log.i("MainActivity", bleDevice.address) Log.i("MainActivity", bleDevice.address)
connectionManager.register(device) connectionManager.register(device)
if (autoConnect) {
connectionManager.connect(bleDevice)
connectionManager.discoverServices(bleDevice)
}
kirbyDevices.add(device) kirbyDevices.add(device)
deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1) deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1)
return device return device
@@ -276,6 +286,22 @@ class MainActivity : AppCompatActivity() {
return true return true
} }
R.id.action_reset_log -> {
val builder = AlertDialog.Builder(this@MainActivity)
builder.setMessage("Are you sure you want to reset the log?")
.setCancelable(false)
.setPositiveButton("Yes") { dialog, id ->
loggerDb.reset()
}
.setNegativeButton("No") { dialog, id ->
// Dismiss the dialog
dialog.dismiss()
}
val alert = builder.create()
alert.show()
return true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
@@ -447,7 +473,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
return "Test action 1" return "Test action 1"
} }
override fun getIcon(): Int? { override fun getIcon(): Int {
return R.drawable.action_icon_disconnect return R.drawable.action_icon_disconnect
} }
@@ -459,7 +485,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
return "Test action 2" return "Test action 2"
} }
override fun getIcon(): Int? { override fun getIcon(): Int {
return R.drawable.action_icon_connect return R.drawable.action_icon_connect
} }
@@ -479,7 +505,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
return "21.2 °C" return "21.2 °C"
} }
override fun getIcon(): Int? { override fun getIcon(): Int {
return R.drawable.baseline_device_thermostat_24 return R.drawable.baseline_device_thermostat_24
} }
}, object : Measurement { }, object : Measurement {
@@ -491,7 +517,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
return "232 bar" return "232 bar"
} }
override fun getIcon(): Int? { override fun getIcon(): Int {
return R.drawable.baseline_compress_24 return R.drawable.baseline_compress_24
} }
}) })

View File

@@ -7,4 +7,9 @@
android:orderInCategory="100" android:orderInCategory="100"
android:title="@string/action_export" android:title="@string/action_export"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_reset_log"
android:orderInCategory="200"
android:title="@string/action_reset_log"
app:showAsAction="never" />
</menu> </menu>

View File

@@ -1,46 +1,11 @@
<resources> <resources>
<string name="app_name">Kirby Test App</string> <string name="app_name">Kirby Test App</string>
<string name="action_export">Export to csv</string> <string name="action_export">Export to csv</string>
<string name="action_reset_log">Reset log</string>
<!-- Strings used for fragments for navigation --> <!-- Strings used for fragments for navigation -->
<string name="first_fragment_label">First Fragment</string> <string name="first_fragment_label">First Fragment</string>
<string name="second_fragment_label">Second Fragment</string> <string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string> <string name="next">Next</string>
<string name="previous">Previous</string> <string name="previous">Previous</string>
<string name="lorem_ipsum">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris
volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus
dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad
litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend
diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a,
ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n
Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus
egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed
neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada
fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae,
molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor
bibendum, vel congue leo egestas.\n\n
Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit
amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel,
molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer
interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at
lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula,
in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque
est.\n\n
Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh.
Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui
non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In
eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a
placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus
convallis.\n\n
Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa
gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper,
libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper
sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus
libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
</string>
</resources> </resources>

View File

@@ -12,7 +12,7 @@ import org.junit.Test
class DecoderTest { class DecoderTest {
@Test @Test
fun message_type_0_decodes_correctly() { fun message_type_0_decodes_correctly() {
val res2 = DecoderIaq.parseMeasurement("006b04ab74a1ed0d101404"); 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 ) 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) assertEquals(testMeasurement, res2)
} }
@@ -20,7 +20,7 @@ class DecoderTest {
@Test @Test
fun message_type_1_decodes_correctly() { fun message_type_1_decodes_correctly() {
val res2 = DecoderIaq.parseMeasurement("106b04ab74a1ed0d10"); 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 ) 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) assertEquals(testMeasurement, res2)
} }