diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ae388c2..0897082 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,15 @@ diff --git a/app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt b/app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt index 74ff75c..f6eb816 100644 --- a/app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt +++ b/app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt @@ -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) } } diff --git a/app/src/main/java/com/logitech/vc/kirbytest/DecoderIaq.kt b/app/src/main/java/com/logitech/vc/kirbytest/DecoderIaq.kt index f385f22..7c4a14b 100644 --- a/app/src/main/java/com/logitech/vc/kirbytest/DecoderIaq.kt +++ b/app/src/main/java/com/logitech/vc/kirbytest/DecoderIaq.kt @@ -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 diff --git a/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt b/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt index 4ae4a4e..f612a93 100644 --- a/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt +++ b/app/src/main/java/com/logitech/vc/kirbytest/KirbyDevice.kt @@ -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 { return listOf(object : Measurement { override fun getLabel(): String { - return payload.ts.toString() + return payload.ts } override fun getFormattedValue(): String { diff --git a/app/src/main/java/com/logitech/vc/kirbytest/MainActivity.kt b/app/src/main/java/com/logitech/vc/kirbytest/MainActivity.kt index b719f01..f84c808 100644 --- a/app/src/main/java/com/logitech/vc/kirbytest/MainActivity.kt +++ b/app/src/main/java/com/logitech/vc/kirbytest/MainActivity.kt @@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() { newConnectionManager() } - private val kirbyDevices = mutableListOf() + private val kirbyDevices = mutableListOf() 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() } } diff --git a/app/src/main/java/com/logitech/vc/kirbytest/Settings.kt b/app/src/main/java/com/logitech/vc/kirbytest/Settings.kt index 049acd6..a5c86ec 100644 --- a/app/src/main/java/com/logitech/vc/kirbytest/Settings.kt +++ b/app/src/main/java/com/logitech/vc/kirbytest/Settings.kt @@ -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 = 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(R.id.apiUrl) val keyField = root.findViewById(R.id.apiKey) + val lowPowerMode = root.findViewById(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) } } diff --git a/app/src/main/res/layout/settings_dialog.xml b/app/src/main/res/layout/settings_dialog.xml index e068cb8..e358e88 100644 --- a/app/src/main/res/layout/settings_dialog.xml +++ b/app/src/main/res/layout/settings_dialog.xml @@ -51,6 +51,12 @@ android:inputType="text" android:text="" /> + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index c2745b0..6498050 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.1.2' apply false - id 'com.android.library' version '8.1.2' apply false + id 'com.android.application' version '8.5.0' apply false + id 'com.android.library' version '8.5.0' apply false id 'org.jetbrains.kotlin.android' version '1.8.20' apply false } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f1cb267..8c90bab 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Jun 14 12:17:09 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists