package com.logitech.vc.kirbytest import SettingsRepository import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.BOND_BONDED import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.le.ScanResult import android.content.Context import android.util.Log import com.android.volley.Request import com.android.volley.RequestQueue import com.android.volley.toolbox.JsonObjectRequest import com.android.volley.toolbox.Volley import org.json.JSONException import org.json.JSONObject import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.Base64 import java.util.EnumSet import java.util.UUID import java.util.stream.Collectors private val SERVICE_UUID = UUID.fromString(BuildConfig.SERVICE_UUID) private val CHAR_UUID = UUID.fromString(BuildConfig.CHAR_UUID) enum class DeviceStatus { CONNECTED, BONDED, SUBSCRIBED, MISSING } @SuppressLint("MissingPermission") class KirbyDevice( private val context: Context, private val connectionManager: ConnectionManager, private val bleDevice: BluetoothDevice, private val loggerDb: LoggerContract.LoggerDb, private val settingsRepository: SettingsRepository, private val onStateChange: (device: KirbyDevice) -> Unit, ) : BleListener(bleDevice.address), DeviceListEntry { private val tag = "KirbyDevice" private var lastSeen: Long = 0 private val queue: RequestQueue = Volley.newRequestQueue(context) private val reconnectionDelayMs = 10_000 private val settings = settingsRepository.getSettings() fun subscribe() { if(statuses.contains(DeviceStatus.CONNECTED)) { connectionManager.enableNotification( bleDevice, SERVICE_UUID, CHAR_UUID ) } } fun connect() { val now = System.currentTimeMillis() if (now - lastSeen > reconnectionDelayMs) { Log.i(tag, "Connecting to device " + bleDevice.address) connectionManager.connect(bleDevice) connectionManager.discoverServices(bleDevice) } else{ Log.i(tag, "Waiting before reconnecting to device " + bleDevice.address) } } fun readIaq() { connectionManager.readChar(bleDevice, SERVICE_UUID, CHAR_UUID) } override fun onSuccessfulCharRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { addMeasurement(characteristic) onStateChange(this) } override fun onScanResult(callbackType: Int, result: ScanResult) { rssi = result.rssi onStateChange(this) } override fun onConnect(gatt: BluetoothGatt) { statuses.add(DeviceStatus.CONNECTED) statuses.remove(DeviceStatus.MISSING) subscribe() onStateChange(this) } override fun onConnectToBondedFailed(gatt: BluetoothGatt) { statuses.add(DeviceStatus.MISSING) onStateChange(this) } override fun onDisconnect(gatt: BluetoothGatt) { statuses.remove(DeviceStatus.CONNECTED) statuses.remove(DeviceStatus.SUBSCRIBED) onStateChange(this) Log.i(tag, "Disconnected") } override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { addMeasurement(characteristic) lastSeen = System.currentTimeMillis() if(settings.lowPowerMode){ Log.i(tag, "Received data, closing connection") connectionManager.teardownConnection(bleDevice) } onStateChange(this) } override fun onSubscribe( gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, ) { statuses.add(DeviceStatus.SUBSCRIBED) onStateChange(this) } override fun onUnsubscribe(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor) { statuses.remove(DeviceStatus.SUBSCRIBED) onStateChange(this) } override fun onQueueSizeChange(groupedOps: Map>) { hasRunningOp = groupedOps.getOrDefault(bleDevice.address, emptyList()).isNotEmpty() onStateChange(this) } override fun onBonded(device: BluetoothDevice) { statuses.add(DeviceStatus.BONDED) onStateChange(this) connect() } override fun onUnbonded(device: BluetoothDevice) { statuses.remove(DeviceStatus.BONDED) onStateChange(this) } override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int) { this.rssi = rssi onStateChange(this) } override var hasRunningOp: Boolean = false override var rssi: Int? = null private fun addMeasurement(characteristic: BluetoothGattCharacteristic) { val hexPayload = characteristic.value.toHexString().substring(2) val measurement = DecoderIaq.parseMeasurement(hexPayload) val payload : Payload if (measurement == null) { payload = Payload(hexPayload) } else { measurement.deviceId = bleDevice.address payload = Payload(measurement.toString()) Log.i("BleListener", "Char received: $payload") val base64Payload = Base64.getEncoder().encodeToString(characteristic.value) publishMeasurement(base64Payload) publishMeasurementAutoServer(hexPayload, measurement as DecoderIaq.Measurement) loggerDb.writeLog(measurement) } measurements.add(payload) if (measurements.size > maxMeasurements) { measurements.removeFirst() } } private fun publishMeasurement(payload: String) { val accessKey = settings.apiKey val url = settings.apiUrl if(url.isEmpty() || accessKey.isEmpty()) { return } val eui = "0000${bleDevice.address.replace(":", "")}" val postData = JSONObject() try { Log.i("POST", "Transmitting for $eui: $payload") postData.put("accessKey", accessKey) postData.put("metricPayload", payload) postData.put("eui", eui) } catch (e: JSONException) { e.printStackTrace() } val request = JsonObjectRequest( Request.Method.POST, url, postData, { response -> Log.i("sendDataResponse", "Response is: $response") } ) { error -> error.printStackTrace() } queue.add(request) } private fun publishMeasurementAutoServer(hexPayload: String, measurement: DecoderIaq.Measurement) { // Read url from SharedPreferences val sharedPref = context.getSharedPreferences(context.getString(R.string.app_name), Context.MODE_PRIVATE) val url = sharedPref.getString( "kirby_data_post_url", context.getString(R.string.kirby_data_post_url_default)) ?: context.getString(R.string.kirby_data_post_url_default) val accessKey = BuildConfig.API_KEY if(url.isEmpty()) { return } val eui = "0000${bleDevice.address.replace(":", "")}" val postData = JSONObject() try { // Log.i("POST", "Transmitting for $eui: $payload") postData.put("accessKey", "${accessKey}_fromAndroid") postData.put("eui", eui) postData.put("deviceId", measurement.deviceId) postData.put("msgType", measurement.msgType) postData.put("rawData", hexPayload) postData.put("co2", measurement.co2) postData.put("voc", measurement.voc) postData.put("humidity", measurement.humidity) postData.put("temperature", measurement.temperature) postData.put("pressure", measurement.pressure) postData.put("occupancy", measurement.occupancy) postData.put("pm25", measurement.pm25) postData.put("pm10", measurement.pm10) postData.put("decodeSuccess", true) postData.put("source", "bt") } catch (e: JSONException) { e.printStackTrace() } val request = JsonObjectRequest( Request.Method.POST, url, postData, { response -> Log.i("sendDataResponse", "Response is: $response") } ) { error -> error.printStackTrace() } queue.add(request) } private val measurements = ArrayList() private val maxMeasurements = 20 private val statuses = EnumSet.noneOf(DeviceStatus::class.java) init { if (bleDevice.bondState == BOND_BONDED) { statuses.add(DeviceStatus.BONDED) } } override val address: String get() = bleDevice.address override val name: String? get() = bleDevice.name override val status: String? get() = statuses.stream().map { it.name }.collect(Collectors.joining(", ")) override fun getMeasurements(): List { val result = mutableListOf() measurements.reversed().forEach { m -> result.addAll(payloadToMeasurements(m))} /* var pl = Payload(payload = "006b04ab74a1ed0d101404", ts = "2000") result.addAll(payloadToMeasurements(pl)) result.addAll(payloadToMeasurements(pl)) */ return result } override fun getActions(): List { val actions = mutableListOf() if (!statuses.contains(DeviceStatus.BONDED)) { actions.add(object : Action { override fun getLabel(): String { return "Bond" } override fun getIcon(): Int { return R.drawable.action_icon_bond } override fun execute() { connectionManager.bond(bleDevice) } }) } if (!statuses.contains(DeviceStatus.CONNECTED) ) { actions.add(object : Action { override fun getLabel(): String { return "Connect" } override fun getIcon(): Int { return R.drawable.action_icon_connect } override fun execute() { connectionManager.connect(bleDevice) connectionManager.readRemoteRssi(bleDevice) connectionManager.discoverServices(bleDevice) } }) } if (statuses.contains(DeviceStatus.CONNECTED)) { actions.add(object : Action { override fun getLabel(): String { return "Disconnect" } override fun getIcon(): Int { return R.drawable.action_icon_disconnect } override fun execute() { connectionManager.teardownConnection(bleDevice) } }) actions.add(object : Action { override fun getLabel(): String { return "Fetch Measurement" } override fun getIcon(): Int { return R.drawable.action_icon_fetch_measurement } override fun execute() { readIaq() } }) if (!statuses.contains(DeviceStatus.SUBSCRIBED)) { actions.add(object : Action { override fun getLabel(): String { return "Subscribe" } override fun getIcon(): Int { return R.drawable.action_icon_subscribe } override fun execute() { subscribe() } }) } actions.add(object : Action { override fun getLabel(): String { return "Update Signal Strength" } override fun getIcon(): Int { return R.drawable.action_icon_update_signal_strength } override fun execute() { connectionManager.readRemoteRssi(bleDevice) } }) } if (statuses.contains(DeviceStatus.SUBSCRIBED)) { actions.add(object : Action { override fun getLabel(): String { return "Unsubscribe" } override fun getIcon(): Int { return R.drawable.action_icon_subscribe_disable } override fun execute() { connectionManager.disableNotification( bleDevice, SERVICE_UUID, CHAR_UUID ) } }) } if (statuses.contains(DeviceStatus.BONDED) ) { actions.add(object : Action { override fun getLabel(): String { return "Unbond" } override fun getIcon(): Int { return R.drawable.action_icon_unbond } override fun execute() { connectionManager.unbond(bleDevice) } }) } return actions } } data class Payload( val payload: String, val ts: String = LocalDateTime.now() .format(DateTimeFormatter.ofPattern("dd.MM.yy HH:mm:ss")) ) private fun payloadToMeasurements(payload: Payload): List { return listOf(object : Measurement { override fun getLabel(): String { return payload.ts } override fun getFormattedValue(): String { return payload.payload } override fun getIcon(): Int { return R.drawable.baseline_numbers_24 } } ) }