package com.logitech.vc.kirbytest 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.nio.ByteBuffer import java.nio.ByteOrder 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 onStateChange: (device: KirbyDevice) -> Unit, ) : BleListener(bleDevice.address), DeviceListEntry { private val queue: RequestQueue = Volley.newRequestQueue(context) fun subscribe() { connectionManager.enableNotification( bleDevice, SERVICE_UUID, CHAR_UUID ) } 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) 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) } override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { addMeasurement(characteristic) 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) } 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) var payload : Payload if (measurement == null) { payload = Payload(hexPayload) } else { payload = Payload(measurement.toString()) Log.i("BleListener", "Char received: $payload") val base64Payload = Base64.getEncoder().encodeToString(characteristic.value) publishMeasurement(base64Payload) loggerDb.writeLog( measurement) } measurements.add(payload) if (measurements.size > maxMeasurements) { measurements.removeFirst() } } private fun publishMeasurement(payload: String) { val accessKey = BuildConfig.API_KEY val url = BuildConfig.API_BASE_URL 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 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")) ) fun bytesToUInt16(arr: ByteArray, start: Int): Int { return ByteBuffer.wrap(arr, start, 2) .order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF } fun bytesToInt16(arr: ByteArray, start: Int): Short { return ByteBuffer.wrap(arr, start, 2) .order(ByteOrder.LITTLE_ENDIAN).short } fun bytesToInt32(arr: ByteArray, start: Int): Int { return ByteBuffer.wrap(arr, start, 4) .order(ByteOrder.LITTLE_ENDIAN).int } private fun payloadToMeasurements(payload: Payload): List { return listOf(object : Measurement { override fun getLabel(): String { return payload.ts.toString() } override fun getFormattedValue(): String { return payload.payload } override fun getIcon(): Int? { return R.drawable.baseline_numbers_24 } } ) }