feat: support BLE low power mode
This commit is contained in:
5
.idea/gradle.xml
generated
5
.idea/gradle.xml
generated
@@ -4,16 +4,15 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="GRADLE" />
|
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="jbr-17" />
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -292,10 +292,6 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
val scanCallback = object : ScanCallback() {
|
val scanCallback = object : ScanCallback() {
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
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) }
|
notifyListeners(result.device.address) { it.onScanResult(callbackType, result) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ object DecoderIaq {
|
|||||||
parseUnsignedInt(inputBytes, 8, 9) ushr 4 and INVALID_OCCUPANCY
|
parseUnsignedInt(inputBytes, 8, 9) ushr 4 and INVALID_OCCUPANCY
|
||||||
measurement.occupancy = if (occupancy == INVALID_OCCUPANCY) null else 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) {
|
if (msgType == 0) {
|
||||||
val pm25 =
|
val pm25 =
|
||||||
parseUnsignedInt(inputBytes, 8, 10) ushr 2 and INVALID_PM25
|
parseUnsignedInt(inputBytes, 8, 10) ushr 2 and INVALID_PM25
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import com.android.volley.toolbox.JsonObjectRequest
|
|||||||
import com.android.volley.toolbox.Volley
|
import com.android.volley.toolbox.Volley
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
@@ -35,6 +33,7 @@ enum class DeviceStatus {
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
class KirbyDevice(
|
class KirbyDevice(
|
||||||
|
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val connectionManager: ConnectionManager,
|
private val connectionManager: ConnectionManager,
|
||||||
private val bleDevice: BluetoothDevice,
|
private val bleDevice: BluetoothDevice,
|
||||||
@@ -45,7 +44,11 @@ class KirbyDevice(
|
|||||||
|
|
||||||
|
|
||||||
) : BleListener(bleDevice.address), DeviceListEntry {
|
) : BleListener(bleDevice.address), DeviceListEntry {
|
||||||
|
private val tag = "KirbyDevice"
|
||||||
|
private var lastSeen: Long = 0
|
||||||
private val queue: RequestQueue = Volley.newRequestQueue(context)
|
private val queue: RequestQueue = Volley.newRequestQueue(context)
|
||||||
|
private val reconnectionDelayMs = 10_000
|
||||||
|
private val settings = settingsRepository.getSettings()
|
||||||
|
|
||||||
fun subscribe() {
|
fun subscribe() {
|
||||||
if(statuses.contains(DeviceStatus.CONNECTED)) {
|
if(statuses.contains(DeviceStatus.CONNECTED)) {
|
||||||
@@ -56,8 +59,14 @@ class KirbyDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun connect() {
|
fun connect() {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastSeen > reconnectionDelayMs) {
|
||||||
|
Log.i(tag, "Connecting to device " + bleDevice.address)
|
||||||
connectionManager.connect(bleDevice)
|
connectionManager.connect(bleDevice)
|
||||||
connectionManager.discoverServices(bleDevice)
|
connectionManager.discoverServices(bleDevice)
|
||||||
|
} else{
|
||||||
|
Log.i(tag, "Waiting before reconnecting to device " + bleDevice.address)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readIaq() {
|
fun readIaq() {
|
||||||
@@ -69,7 +78,9 @@ class KirbyDevice(
|
|||||||
gatt: BluetoothGatt,
|
gatt: BluetoothGatt,
|
||||||
characteristic: BluetoothGattCharacteristic
|
characteristic: BluetoothGattCharacteristic
|
||||||
) {
|
) {
|
||||||
|
|
||||||
addMeasurement(characteristic)
|
addMeasurement(characteristic)
|
||||||
|
|
||||||
onStateChange(this)
|
onStateChange(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,10 +107,18 @@ class KirbyDevice(
|
|||||||
statuses.remove(DeviceStatus.CONNECTED)
|
statuses.remove(DeviceStatus.CONNECTED)
|
||||||
statuses.remove(DeviceStatus.SUBSCRIBED)
|
statuses.remove(DeviceStatus.SUBSCRIBED)
|
||||||
onStateChange(this)
|
onStateChange(this)
|
||||||
|
Log.i(tag, "Disconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||||
addMeasurement(characteristic)
|
addMeasurement(characteristic)
|
||||||
|
lastSeen = System.currentTimeMillis()
|
||||||
|
|
||||||
|
if(settings.lowPowerMode){
|
||||||
|
Log.i(tag, "Received data, closing connection")
|
||||||
|
connectionManager.teardownConnection(bleDevice)
|
||||||
|
}
|
||||||
|
|
||||||
onStateChange(this)
|
onStateChange(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +164,7 @@ class KirbyDevice(
|
|||||||
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
|
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
|
||||||
val hexPayload = characteristic.value.toHexString().substring(2)
|
val hexPayload = characteristic.value.toHexString().substring(2)
|
||||||
val measurement = DecoderIaq.parseMeasurement(hexPayload)
|
val measurement = DecoderIaq.parseMeasurement(hexPayload)
|
||||||
var payload : Payload
|
val payload : Payload
|
||||||
if (measurement == null) {
|
if (measurement == null) {
|
||||||
payload = Payload(hexPayload)
|
payload = Payload(hexPayload)
|
||||||
} else {
|
} else {
|
||||||
@@ -167,7 +186,6 @@ class KirbyDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun publishMeasurement(payload: String) {
|
private fun publishMeasurement(payload: String) {
|
||||||
val settings = settingsRepository.readSettings()
|
|
||||||
val accessKey = settings.apiKey
|
val accessKey = settings.apiKey
|
||||||
val url = settings.apiUrl
|
val url = settings.apiUrl
|
||||||
|
|
||||||
@@ -376,26 +394,11 @@ data class Payload(
|
|||||||
.format(DateTimeFormatter.ofPattern("dd.MM.yy HH:mm:ss"))
|
.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> {
|
private fun payloadToMeasurements(payload: Payload): List<Measurement> {
|
||||||
return listOf(object : Measurement {
|
return listOf(object : Measurement {
|
||||||
override fun getLabel(): String {
|
override fun getLabel(): String {
|
||||||
return payload.ts.toString()
|
return payload.ts
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFormattedValue(): String {
|
override fun getFormattedValue(): String {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
newConnectionManager()
|
newConnectionManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val kirbyDevices = mutableListOf<DeviceListEntry>()
|
private val kirbyDevices = mutableListOf<KirbyDevice>()
|
||||||
|
|
||||||
private val deviceListAdapter: DeviceListAdapter by lazy {
|
private val deviceListAdapter: DeviceListAdapter by lazy {
|
||||||
DeviceListAdapter(kirbyDevices)
|
DeviceListAdapter(kirbyDevices)
|
||||||
@@ -139,14 +139,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
if (hasRequiredRuntimePermissions()) {
|
if (hasRequiredRuntimePermissions()) {
|
||||||
connectionManager.startScan()
|
connectionManager.startScan()
|
||||||
|
/*
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
delay(5000L)
|
delay(5000L)
|
||||||
connectionManager.stopScan()
|
connectionManager.stopScan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer().schedule(object : TimerTask() {
|
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 {
|
private fun newConnectionManager(): ConnectionManager {
|
||||||
val mngr = ConnectionManager(applicationContext, bluetoothAdapter)
|
val mngr = ConnectionManager(applicationContext, bluetoothAdapter)
|
||||||
mngr.register(object : BleListener(null) {
|
mngr.register(object : BleListener(null) {
|
||||||
@@ -237,19 +229,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
"ScanCallback",
|
"ScanCallback",
|
||||||
"Found Kirby device with name ${result.device.name} (address: ${result.device.address}, rssi: ${result.rssi})"
|
"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) {
|
if (kirbyDevice == null) {
|
||||||
val kirby = newKirbyDevice(result.device)
|
kirbyDevice = newKirbyDevice(result.device)
|
||||||
kirby.onScanResult(callbackType, result)
|
kirbyDevice.onScanResult(callbackType, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bondedDevices.contains(result.device.address)) {
|
if (bondedDevices.contains(result.device.address)) {
|
||||||
Log.i("KirbyDevice", "Connecting to " + result.device.address)
|
Log.i("KirbyDevice", "Auto connecting to bonded device" + result.device.address)
|
||||||
|
kirbyDevice.connect()
|
||||||
connectionManager.connect(result.device)
|
|
||||||
|
|
||||||
//connectionManager.readRemoteRssi(result.device)
|
|
||||||
connectionManager.discoverServices(result.device)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import android.content.Context
|
|||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.CheckBox
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.logitech.vc.kirbytest.BuildConfig
|
import com.logitech.vc.kirbytest.BuildConfig
|
||||||
import com.logitech.vc.kirbytest.R
|
import com.logitech.vc.kirbytest.R
|
||||||
@@ -33,30 +35,35 @@ class SettingsRepository(context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
val API_URL = stringPreferencesKey("api_url")
|
val API_URL = stringPreferencesKey("api_url")
|
||||||
val API_KEY = stringPreferencesKey("api_key")
|
val API_KEY = stringPreferencesKey("api_key")
|
||||||
|
val BLE_LOW_POWER_MODE = booleanPreferencesKey("ble_low_power_mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun saveSettings(apiUrl: String, apiKey: String) {
|
fun saveSettings(apiUrl: String, apiKey: String, isLowPowerMode: Boolean) {
|
||||||
settings.apiKey = apiKey
|
settings.apiKey = apiKey
|
||||||
settings.apiUrl = apiUrl
|
settings.apiUrl = apiUrl
|
||||||
|
settings.lowPowerMode = isLowPowerMode
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
coroutineScope.launch(Dispatchers.Main) {
|
||||||
dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
preferences[API_URL] = apiUrl
|
preferences[API_URL] = apiUrl
|
||||||
preferences[API_KEY] = apiKey
|
preferences[API_KEY] = apiKey
|
||||||
|
preferences[BLE_LOW_POWER_MODE] = isLowPowerMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readSettings(): Settings {
|
fun getSettings(): Settings {
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
private val settingsFlow: Flow<Settings> = dataStore.data.map {
|
private val settingsFlow: Flow<Settings> = dataStore.data.map {
|
||||||
Settings(
|
Settings(
|
||||||
apiUrl = it[API_URL] ?: BuildConfig.API_BASE_URL,
|
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) {
|
if (result != null) {
|
||||||
settings.apiKey = result.apiKey
|
settings.apiKey = result.apiKey
|
||||||
settings.apiUrl = result.apiUrl
|
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 {
|
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 root = layoutInflater.inflate(R.layout.settings_dialog, null)
|
||||||
val urlField = root.findViewById<EditText>(R.id.apiUrl)
|
val urlField = root.findViewById<EditText>(R.id.apiUrl)
|
||||||
val keyField = root.findViewById<EditText>(R.id.apiKey)
|
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)
|
urlField.setText(settings.apiUrl)
|
||||||
keyField.setText(settings.apiKey)
|
keyField.setText(settings.apiKey)
|
||||||
|
lowPowerMode.isChecked = settings.lowPowerMode
|
||||||
|
|
||||||
return androidx.appcompat.app.AlertDialog.Builder(context)
|
return androidx.appcompat.app.AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.settings)
|
.setTitle(R.string.settings)
|
||||||
@@ -92,8 +102,9 @@ fun settingsDialog(context: Context, settingsRepo: SettingsRepository): AlertDia
|
|||||||
|
|
||||||
val url = urlField.text.toString()
|
val url = urlField.text.toString()
|
||||||
val key = keyField.text.toString()
|
val key = keyField.text.toString()
|
||||||
|
val isLowPowerMode = lowPowerMode.isChecked
|
||||||
if (isFullPath(url) || url.isEmpty()) {
|
if (isFullPath(url) || url.isEmpty()) {
|
||||||
settingsRepo.saveSettings(url, key)
|
settingsRepo.saveSettings(url, key, isLowPowerMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,12 @@
|
|||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:text="" />
|
android:text="" />
|
||||||
|
|
||||||
|
<CheckBox android:id="@+id/checkboxLowPowerMode"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="BLE low power mode"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.1.2' apply false
|
id 'com.android.application' version '8.5.0' apply false
|
||||||
id 'com.android.library' version '8.1.2' apply false
|
id 'com.android.library' version '8.5.0' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
|
id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
|
||||||
}
|
}
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Wed Jun 14 12:17:09 CEST 2023
|
#Wed Jun 14 12:17:09 CEST 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
Reference in New Issue
Block a user