feat: add csv auto export
This commit is contained in:
@@ -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()}"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user