405 lines
12 KiB
Kotlin
405 lines
12 KiB
Kotlin
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() {
|
|
if(statuses.contains(DeviceStatus.CONNECTED)) {
|
|
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)
|
|
|
|
if(result.isConnectable) {
|
|
connectionManager.connect(bleDevice)
|
|
connectionManager.discoverServices(bleDevice)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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<String, List<BleOperationType>>) {
|
|
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 {
|
|
measurement.deviceId = bleDevice.address
|
|
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
|
|
|
|
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)
|
|
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<Payload>()
|
|
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<Measurement> {
|
|
val result = mutableListOf<Measurement>()
|
|
|
|
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<Action> {
|
|
val actions = mutableListOf<Action>()
|
|
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<Measurement> {
|
|
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
|
|
}
|
|
}
|
|
)
|
|
} |