feat: support BLE low power mode

This commit is contained in:
Stefan Zollinger
2024-08-16 11:06:01 +02:00
parent 83b120b1ce
commit 48c6b2c314
9 changed files with 65 additions and 58 deletions

View File

@@ -292,10 +292,6 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
@SuppressLint("MissingPermission")
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
Log.d(
"ScanCallback",
"Found BLE device with address ${result.device.address} (name: ${result.device.name}, rssi: ${result.rssi})"
)
notifyListeners(result.device.address) { it.onScanResult(callbackType, result) }
}

View File

@@ -55,6 +55,10 @@ object DecoderIaq {
parseUnsignedInt(inputBytes, 8, 9) ushr 4 and INVALID_OCCUPANCY
measurement.occupancy = if (occupancy == INVALID_OCCUPANCY) null else occupancy
val occupancy2 =
parseUnsignedInt(inputBytes, 10, 11) ushr 4 and INVALID_OCCUPANCY
measurement.occupancy = if (occupancy == INVALID_OCCUPANCY) null else occupancy
if (msgType == 0) {
val pm25 =
parseUnsignedInt(inputBytes, 8, 10) ushr 2 and INVALID_PM25

View File

@@ -16,8 +16,6 @@ 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
@@ -35,6 +33,7 @@ enum class DeviceStatus {
@SuppressLint("MissingPermission")
class KirbyDevice(
private val context: Context,
private val connectionManager: ConnectionManager,
private val bleDevice: BluetoothDevice,
@@ -45,7 +44,11 @@ class KirbyDevice(
) : 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)) {
@@ -56,8 +59,14 @@ class KirbyDevice(
}
fun connect() {
connectionManager.connect(bleDevice)
connectionManager.discoverServices(bleDevice)
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() {
@@ -69,7 +78,9 @@ class KirbyDevice(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
addMeasurement(characteristic)
onStateChange(this)
}
@@ -96,10 +107,18 @@ class KirbyDevice(
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)
}
@@ -145,7 +164,7 @@ class KirbyDevice(
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
val hexPayload = characteristic.value.toHexString().substring(2)
val measurement = DecoderIaq.parseMeasurement(hexPayload)
var payload : Payload
val payload : Payload
if (measurement == null) {
payload = Payload(hexPayload)
} else {
@@ -167,7 +186,6 @@ class KirbyDevice(
}
private fun publishMeasurement(payload: String) {
val settings = settingsRepository.readSettings()
val accessKey = settings.apiKey
val url = settings.apiUrl
@@ -376,26 +394,11 @@ data class Payload(
.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()
return payload.ts
}
override fun getFormattedValue(): String {

View File

@@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
newConnectionManager()
}
private val kirbyDevices = mutableListOf<DeviceListEntry>()
private val kirbyDevices = mutableListOf<KirbyDevice>()
private val deviceListAdapter: DeviceListAdapter by lazy {
DeviceListAdapter(kirbyDevices)
@@ -139,14 +139,14 @@ class MainActivity : AppCompatActivity() {
if (hasRequiredRuntimePermissions()) {
connectionManager.startScan()
/*
lifecycleScope.launch {
withContext(Dispatchers.Main) {
delay(5000L)
connectionManager.stopScan()
}
}
*/
}
Timer().schedule(object : TimerTask() {
@@ -209,14 +209,6 @@ class MainActivity : AppCompatActivity() {
}
@SuppressLint("NotifyDataSetChanged")
private fun addDummyDevices() {
for (i in 0..14) {
kirbyDevices.add(DummyListEntry("$i"))
}
deviceListAdapter.notifyDataSetChanged()
}
private fun newConnectionManager(): ConnectionManager {
val mngr = ConnectionManager(applicationContext, bluetoothAdapter)
mngr.register(object : BleListener(null) {
@@ -237,19 +229,15 @@ class MainActivity : AppCompatActivity() {
"ScanCallback",
"Found Kirby device with name ${result.device.name} (address: ${result.device.address}, rssi: ${result.rssi})"
)
val kirbyDevice = kirbyDevices.find { it.address == result.device.address }
var kirbyDevice = kirbyDevices.find { it.address == result.device.address }
if (kirbyDevice == null) {
val kirby = newKirbyDevice(result.device)
kirby.onScanResult(callbackType, result)
kirbyDevice = newKirbyDevice(result.device)
kirbyDevice.onScanResult(callbackType, result)
}
if (bondedDevices.contains(result.device.address)) {
Log.i("KirbyDevice", "Connecting to " + result.device.address)
connectionManager.connect(result.device)
//connectionManager.readRemoteRssi(result.device)
connectionManager.discoverServices(result.device)
Log.i("KirbyDevice", "Auto connecting to bonded device" + result.device.address)
kirbyDevice.connect()
}
}

View File

@@ -5,8 +5,10 @@ import android.content.Context
import android.content.DialogInterface
import android.util.Log
import android.view.LayoutInflater
import android.widget.CheckBox
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.logitech.vc.kirbytest.BuildConfig
import com.logitech.vc.kirbytest.R
@@ -33,30 +35,35 @@ class SettingsRepository(context: Context) {
companion object {
val API_URL = stringPreferencesKey("api_url")
val API_KEY = stringPreferencesKey("api_key")
val BLE_LOW_POWER_MODE = booleanPreferencesKey("ble_low_power_mode")
}
@OptIn(DelicateCoroutinesApi::class)
fun saveSettings(apiUrl: String, apiKey: String) {
fun saveSettings(apiUrl: String, apiKey: String, isLowPowerMode: Boolean) {
settings.apiKey = apiKey
settings.apiUrl = apiUrl
settings.lowPowerMode = isLowPowerMode
coroutineScope.launch(Dispatchers.Main) {
dataStore.edit { preferences ->
preferences[API_URL] = apiUrl
preferences[API_KEY] = apiKey
preferences[BLE_LOW_POWER_MODE] = isLowPowerMode
}
}
}
fun readSettings(): Settings {
fun getSettings(): Settings {
return settings
}
private val settingsFlow: Flow<Settings> = dataStore.data.map {
Settings(
apiUrl = it[API_URL] ?: BuildConfig.API_BASE_URL,
apiKey = it[API_KEY] ?: BuildConfig.API_KEY
apiKey = it[API_KEY] ?: BuildConfig.API_KEY,
lowPowerMode = it[BLE_LOW_POWER_MODE] ?: true
)
}
@@ -65,11 +72,12 @@ class SettingsRepository(context: Context) {
if (result != null) {
settings.apiKey = result.apiKey
settings.apiUrl = result.apiUrl
settings.lowPowerMode = result.lowPowerMode
}
}
}
data class Settings(var apiUrl: String, var apiKey: String)
data class Settings(var apiUrl: String, var apiKey: String, var lowPowerMode: Boolean = true)
fun settingsDialog(context: Context, settingsRepo: SettingsRepository): AlertDialog {
@@ -79,10 +87,12 @@ fun settingsDialog(context: Context, settingsRepo: SettingsRepository): AlertDia
val root = layoutInflater.inflate(R.layout.settings_dialog, null)
val urlField = root.findViewById<EditText>(R.id.apiUrl)
val keyField = root.findViewById<EditText>(R.id.apiKey)
val lowPowerMode = root.findViewById<CheckBox>(R.id.checkboxLowPowerMode)
val settings = settingsRepo.readSettings()
val settings = settingsRepo.getSettings()
urlField.setText(settings.apiUrl)
keyField.setText(settings.apiKey)
lowPowerMode.isChecked = settings.lowPowerMode
return androidx.appcompat.app.AlertDialog.Builder(context)
.setTitle(R.string.settings)
@@ -92,8 +102,9 @@ fun settingsDialog(context: Context, settingsRepo: SettingsRepository): AlertDia
val url = urlField.text.toString()
val key = keyField.text.toString()
val isLowPowerMode = lowPowerMode.isChecked
if (isFullPath(url) || url.isEmpty()) {
settingsRepo.saveSettings(url, key)
settingsRepo.saveSettings(url, key, isLowPowerMode)
}
}