461 lines
14 KiB
Kotlin
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
|
|
}
|
|
}
|
|
)
|
|
} |