Files
vc-kirby-app/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt
2024-10-28 15:22:11 +08:00

461 lines
14 KiB
Kotlin

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<String, List<BleOperationType>>) {
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<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"))
)
private fun payloadToMeasurements(payload: Payload): List<Measurement> {
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
}
}
)
}