Compare commits
14 Commits
c94ca3f40f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcd501a44b | ||
|
|
d210a2754d | ||
|
|
48c6b2c314 | ||
|
|
83b120b1ce | ||
|
|
803ff2e41a | ||
|
|
91638d6ccd | ||
|
|
01bf2693ca | ||
|
|
0aed2068ec | ||
|
|
a5425083da | ||
|
|
62ce221860 | ||
|
|
7311ba335f | ||
|
|
bf7722747e | ||
|
|
a3b2944e03 | ||
|
|
e53a269b4f |
60
.github/workflows/push.yaml
vendored
Normal file
60
.github/workflows/push.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Push
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Generate App Bundle
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: set up JDK
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: "17"
|
||||||
|
distribution: "temurin"
|
||||||
|
cache: gradle
|
||||||
|
- name: Make gradle executable
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Create env props
|
||||||
|
run: |
|
||||||
|
echo "apiUrl=${{ secrets.API_URL }}" > env.properties
|
||||||
|
echo "apiKey=${{ secrets.API_KEY }}" >> env.properties
|
||||||
|
- name: Bundle 'release' with Gradle
|
||||||
|
run: |
|
||||||
|
./gradlew assembleDebug
|
||||||
|
- name: Setup build tool version variable
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
|
||||||
|
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||||
|
echo Last build tool version is: $BUILD_TOOL_VERSION
|
||||||
|
- name: Sign package
|
||||||
|
id: sign
|
||||||
|
uses: r0adkll/sign-android-release@v1
|
||||||
|
with:
|
||||||
|
releaseDirectory: app/build/outputs/apk/debug
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }}
|
||||||
|
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
||||||
|
alias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||||
|
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||||
|
- run: echo "Build status report=${{ job.status }}."
|
||||||
|
- name: Get tag or commit id
|
||||||
|
id: get-version-id
|
||||||
|
uses: iawia002/get-tag-or-commit-id@v1
|
||||||
|
with:
|
||||||
|
length: 7
|
||||||
|
- name: Rename
|
||||||
|
run: mv ${{steps.sign.outputs.signedReleaseFile}} app/build/outputs/apk/debug/kirby_${{steps.get-version-id.outputs.id}}.apk
|
||||||
|
- name: Upload App Bundle
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: signed_apk
|
||||||
|
path: app/build/outputs/apk/debug/kirby_${{steps.get-version-id.outputs.id}}.apk
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
files: app/build/outputs/apk/debug/kirby_${{steps.get-version-id.outputs.id}}.apk
|
||||||
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
|||||||
Sensor Testing App
|
Kirby Test App
|
||||||
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>
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.example.sensortestingapp'
|
namespace 'com.logitech.vc.kirbytest'
|
||||||
compileSdk 32
|
compileSdk 32
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.example.sensortestingapp"
|
applicationId "com.logitech.vc.kirbytest"
|
||||||
minSdk 29
|
minSdk 29
|
||||||
targetSdk 32
|
targetSdk 32
|
||||||
versionCode 1
|
versionCode 1
|
||||||
@@ -59,8 +59,10 @@ dependencies {
|
|||||||
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||||
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.8'
|
implementation 'com.google.code.gson:gson:2.8.8'
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
|
||||||
|
|
||||||
implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0'
|
implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0'
|
||||||
|
|
||||||
implementation "com.android.volley:volley:1.2.1"
|
implementation "com.android.volley:volley:1.2.1"
|
||||||
|
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.sensortestingapp
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
@@ -17,6 +17,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("com.example.sensortestingapp", appContext.packageName)
|
assertEquals("com.logitech.vc.kirbytest", appContext.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package com.example.sensortestingapp
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import org.apache.commons.codec.DecoderException
|
|
||||||
import org.apache.commons.codec.binary.Hex
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
object DecoderIaq {
|
|
||||||
private const val INVALID_CO2 = 16383
|
|
||||||
private const val INVALID_VOC = 2047
|
|
||||||
private const val INVALID_HUMIDITY = 1023
|
|
||||||
private const val INVALID_TEMPERATURE = 2047
|
|
||||||
private const val INVALID_PRESSURE = 65535
|
|
||||||
private const val INVALID_OCCUPANCY = 3
|
|
||||||
private const val INVALID_PM25 = 1023
|
|
||||||
private const val INVALID_PM10 = 1023
|
|
||||||
|
|
||||||
private val supportedMessageTypes = listOf<Number>(0, 1)
|
|
||||||
|
|
||||||
fun parseMeasurement(input: String): Measurement? {
|
|
||||||
val measurement = Measurement()
|
|
||||||
val inputBytes = hexStringToByteArray(input)
|
|
||||||
val msgType = inputBytes[0].toInt() and 0xFF ushr 4
|
|
||||||
|
|
||||||
if(!supportedMessageTypes.contains(msgType)) {
|
|
||||||
Log.i("Decoder", "Invalid message type: $msgType")
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
measurement.msgType = msgType
|
|
||||||
|
|
||||||
val co2 = parseUnsignedInt(inputBytes, 0, 3) ushr 6 and INVALID_CO2
|
|
||||||
measurement.co2 = if (co2 == INVALID_CO2) null else co2
|
|
||||||
|
|
||||||
val voc = parseUnsignedInt(inputBytes, 2, 4) ushr 3 and INVALID_VOC
|
|
||||||
measurement.voc = if (co2 == INVALID_VOC) null else voc
|
|
||||||
|
|
||||||
val humidity =
|
|
||||||
parseUnsignedInt(inputBytes, 3, 5) ushr 1 and INVALID_HUMIDITY
|
|
||||||
measurement.humidity = if (humidity == INVALID_HUMIDITY) null else humidity / 10
|
|
||||||
|
|
||||||
val temperature =
|
|
||||||
parseUnsignedInt(inputBytes, 4, 7) ushr 6 and INVALID_TEMPERATURE
|
|
||||||
measurement.temperature =
|
|
||||||
if (temperature == INVALID_TEMPERATURE) null else ((temperature / 10.0 - 40) * 10.0).roundToInt() / 10.0
|
|
||||||
|
|
||||||
val pressure =
|
|
||||||
parseUnsignedInt(inputBytes, 6, 9) ushr 6 and INVALID_PRESSURE
|
|
||||||
measurement.pressure =
|
|
||||||
if (pressure == INVALID_PRESSURE) null else (30000 + 19000.0 * pressure / 13107).roundToInt()
|
|
||||||
|
|
||||||
val occupancy =
|
|
||||||
parseUnsignedInt(inputBytes, 8, 9) 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
|
|
||||||
measurement.pm25 = if (pm25 == INVALID_PM25) null else pm25
|
|
||||||
|
|
||||||
val pm10 = parseUnsignedInt(inputBytes, 9, 11) and INVALID_PM10
|
|
||||||
measurement.pm10 = if (pm10 == INVALID_PM10) null else pm10
|
|
||||||
}
|
|
||||||
return measurement
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseUnsignedInt(bytes: ByteArray, startIncl: Int, endExcl: Int): Int {
|
|
||||||
val section = bytes.copyOfRange(startIncl, min(bytes.size, endExcl))
|
|
||||||
var unsignedInt = 0
|
|
||||||
for (i in section.indices) {
|
|
||||||
unsignedInt = unsignedInt shl 8
|
|
||||||
unsignedInt = unsignedInt or (section[i].toInt() and 0xFF)
|
|
||||||
}
|
|
||||||
return unsignedInt
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hexStringToByteArray(encoded: String): ByteArray {
|
|
||||||
return try {
|
|
||||||
Hex.decodeHex(encoded)
|
|
||||||
} catch (e: DecoderException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Measurement (
|
|
||||||
var msgType: Number? = null,
|
|
||||||
var co2: Number? = null,
|
|
||||||
var voc: Number? = null,
|
|
||||||
var humidity: Number? = null,
|
|
||||||
var temperature: Number? = null,
|
|
||||||
var pressure: Number? = null,
|
|
||||||
var occupancy: Number? = null,
|
|
||||||
var pm25: Number? = null,
|
|
||||||
var pm10: Number? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "M{" +
|
|
||||||
"type=" + msgType +
|
|
||||||
", co2=" + co2 +
|
|
||||||
", voc=" + voc +
|
|
||||||
", hum=" + humidity +
|
|
||||||
", temp=" + temperature +
|
|
||||||
", press=" + pressure +
|
|
||||||
", pm25=" + pm25 +
|
|
||||||
", pm10=" + pm10 +
|
|
||||||
", occ=" + occupancy +
|
|
||||||
'}'
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toCsv() : String {
|
|
||||||
return "${msgType},${co2},${voc},${humidity},${temperature},${pressure}${pm25},${pm10},${occupancy}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.sensortestingapp
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.sensortestingapp
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
@@ -18,6 +18,7 @@ import android.bluetooth.le.ScanCallback
|
|||||||
import android.bluetooth.le.ScanFilter
|
import android.bluetooth.le.ScanFilter
|
||||||
import android.bluetooth.le.ScanResult
|
import android.bluetooth.le.ScanResult
|
||||||
import android.bluetooth.le.ScanSettings
|
import android.bluetooth.le.ScanSettings
|
||||||
|
import android.bluetooth.le.ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -90,7 +91,7 @@ open class BleListener(private val deviceAddress: String?) {
|
|||||||
|
|
||||||
open fun isRelevantMessage(address: String?): Boolean {
|
open fun isRelevantMessage(address: String?): Boolean {
|
||||||
if (deviceAddress != null && deviceAddress == address) {
|
if (deviceAddress != null && deviceAddress == address) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
return deviceAddress == null
|
return deviceAddress == null
|
||||||
}
|
}
|
||||||
@@ -165,7 +166,7 @@ private fun BluetoothGatt.printGattTable() {
|
|||||||
separator = "\n|--",
|
separator = "\n|--",
|
||||||
prefix = "|--"
|
prefix = "|--"
|
||||||
) {
|
) {
|
||||||
"${it.uuid.toString()} | " +
|
"${it.uuid} | " +
|
||||||
"readable: ${it.isReadable()}, " +
|
"readable: ${it.isReadable()}, " +
|
||||||
"writable: ${it.isWritable()}, " +
|
"writable: ${it.isWritable()}, " +
|
||||||
"writableWithoutResponse: ${it.isWritableWithoutResponse()}, " +
|
"writableWithoutResponse: ${it.isWritableWithoutResponse()}, " +
|
||||||
@@ -210,10 +211,9 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
|
|
||||||
private val scanSettings = ScanSettings.Builder()
|
private val scanSettings = ScanSettings.Builder()
|
||||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||||
|
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val scanFilters = listOf( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BuildConfig.SERVICE_UUID)).build())
|
|
||||||
|
|
||||||
private val bleScanner by lazy {
|
private val bleScanner by lazy {
|
||||||
bleAdapter.bluetoothLeScanner
|
bleAdapter.bluetoothLeScanner
|
||||||
}
|
}
|
||||||
@@ -294,10 +294,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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +310,8 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
fun startScan() {
|
fun startScan() {
|
||||||
if (!isScanning) {
|
if (!isScanning) {
|
||||||
isScanning = true
|
isScanning = true
|
||||||
bleScanner.startScan( null, scanSettings, scanCallback)
|
val filter = ScanFilter.Builder().setDeviceName("KIRBY").build()
|
||||||
|
bleScanner.startScan(listOf(filter), scanSettings, scanCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +322,11 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
|
|
||||||
|
|
||||||
fun connect(device: BluetoothDevice) {
|
fun connect(device: BluetoothDevice) {
|
||||||
if (device.isConnected()) {
|
Log.i("ConnectionManager", "connecting to " +device.address)
|
||||||
|
|
||||||
|
val isConnecting = pendingOperation != null && pendingOperation is Connect && (pendingOperation as Connect).device.address == device.address
|
||||||
|
|
||||||
|
if(device.isConnected() or isConnecting or operationQueue.any { it.device.address === device.address && it is Connect }) {
|
||||||
Log.e("ConnectionManager", "Already connected to ${device.address}!")
|
Log.e("ConnectionManager", "Already connected to ${device.address}!")
|
||||||
} else {
|
} else {
|
||||||
enqueueOperation(Connect(device, context))
|
enqueueOperation(Connect(device, context))
|
||||||
@@ -349,6 +350,9 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun discoverServices(device: BluetoothDevice) {
|
fun discoverServices(device: BluetoothDevice) {
|
||||||
|
if(operationQueue.any { it.device.address === device.address && it is DiscoverServicesRequest }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
enqueueOperation(DiscoverServicesRequest(device))
|
enqueueOperation(DiscoverServicesRequest(device))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,6 +377,9 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun readRemoteRssi(device: BluetoothDevice) {
|
fun readRemoteRssi(device: BluetoothDevice) {
|
||||||
|
if(operationQueue.any { it.device.address === device.address && it is ReadRemoteRssi }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
enqueueOperation(ReadRemoteRssi(device))
|
enqueueOperation(ReadRemoteRssi(device))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,9 +401,11 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun enqueueOperation(operation: BleOperationType) {
|
private fun enqueueOperation(operation: BleOperationType) {
|
||||||
|
/*
|
||||||
if (isScanning) {
|
if (isScanning) {
|
||||||
stopScan()
|
stopScan()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
operationQueue.add(operation)
|
operationQueue.add(operation)
|
||||||
notifyListenersOfQueueChange(operation.device.address)
|
notifyListenersOfQueueChange(operation.device.address)
|
||||||
if (pendingOperation == null) {
|
if (pendingOperation == null) {
|
||||||
@@ -526,7 +535,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is SetNotification -> with(operation) {
|
is SetNotification -> with(operation) {
|
||||||
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId);
|
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId)
|
||||||
|
|
||||||
if (characteristic == null) {
|
if (characteristic == null) {
|
||||||
Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!")
|
Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!")
|
||||||
@@ -558,7 +567,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
payload =
|
payload =
|
||||||
if (characteristic.isIndicatable())
|
if (characteristic.isIndicatable())
|
||||||
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
|
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
|
||||||
else BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
|
else BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (!gatt.setCharacteristicNotification(characteristic, false)) {
|
if (!gatt.setCharacteristicNotification(characteristic, false)) {
|
||||||
@@ -611,7 +620,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
it.onConnectToBondedFailed(gatt)
|
it.onConnectToBondedFailed(gatt)
|
||||||
}
|
}
|
||||||
signalEndOfOperation(operation)
|
signalEndOfOperation(operation)
|
||||||
} else if (status == BluetoothGatt.GATT_SUCCESS) {
|
} else if (status == GATT_SUCCESS) {
|
||||||
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
||||||
Log.i(
|
Log.i(
|
||||||
"ConnectionManager",
|
"ConnectionManager",
|
||||||
@@ -647,7 +656,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
|
|
||||||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||||
with(gatt) {
|
with(gatt) {
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == GATT_SUCCESS) {
|
||||||
Log.w(
|
Log.w(
|
||||||
"ConnectionManager",
|
"ConnectionManager",
|
||||||
"Discovered ${services.size} services for ${device.address}."
|
"Discovered ${services.size} services for ${device.address}."
|
||||||
@@ -671,7 +680,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||||
Log.w(
|
Log.w(
|
||||||
"ConnectionManager",
|
"ConnectionManager",
|
||||||
"ATT MTU changed to $mtu, success: ${status == BluetoothGatt.GATT_SUCCESS}"
|
"ATT MTU changed to $mtu, success: ${status == GATT_SUCCESS}"
|
||||||
)
|
)
|
||||||
|
|
||||||
val operation = pendingOperation
|
val operation = pendingOperation
|
||||||
@@ -687,7 +696,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
|||||||
) {
|
) {
|
||||||
with(characteristic) {
|
with(characteristic) {
|
||||||
when (status) {
|
when (status) {
|
||||||
BluetoothGatt.GATT_SUCCESS -> {
|
GATT_SUCCESS -> {
|
||||||
Log.i(
|
Log.i(
|
||||||
"ConnectionManager",
|
"ConnectionManager",
|
||||||
"Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}"
|
"Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.sensortestingapp
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -10,8 +10,11 @@ import android.util.Log
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.JsonSyntaxException
|
import com.google.gson.JsonSyntaxException
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import java.time.LocalDateTime
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.SortedMap
|
||||||
|
|
||||||
|
|
||||||
object LoggerContract {
|
object LoggerContract {
|
||||||
@@ -31,6 +34,7 @@ object LoggerContract {
|
|||||||
|
|
||||||
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${LogEntry.TABLE_NAME}"
|
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${LogEntry.TABLE_NAME}"
|
||||||
|
|
||||||
|
|
||||||
class LoggerDbHelper(context: Context) :
|
class LoggerDbHelper(context: Context) :
|
||||||
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
@@ -61,16 +65,15 @@ object LoggerContract {
|
|||||||
private val dbHelper = LoggerDbHelper(context)
|
private val dbHelper = LoggerDbHelper(context)
|
||||||
private val dbWrite = dbHelper.writableDatabase
|
private val dbWrite = dbHelper.writableDatabase
|
||||||
private val dbRead = dbHelper.writableDatabase
|
private val dbRead = dbHelper.writableDatabase
|
||||||
|
|
||||||
|
private val tag = "LoggerDb"
|
||||||
|
|
||||||
val context: Context = context
|
val context: Context = context
|
||||||
|
|
||||||
fun writeLog(payload: Any): Long? {
|
fun writeLog(payload: Any): Long? {
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
val jsonString = gson.toJson(payload)
|
val jsonString = gson.toJson(payload)
|
||||||
|
val ts = Instant.now().toString()
|
||||||
val date = LocalDateTime.now()
|
|
||||||
val ts = date.format(DateTimeFormatter.ISO_DATE_TIME)
|
|
||||||
|
|
||||||
Log.i("Database", jsonString)
|
|
||||||
|
|
||||||
val values = ContentValues().apply {
|
val values = ContentValues().apply {
|
||||||
put(LogEntry.COLUMN_NAME_TS, ts)
|
put(LogEntry.COLUMN_NAME_TS, ts)
|
||||||
@@ -80,6 +83,21 @@ object LoggerContract {
|
|||||||
return dbWrite?.insert(LogEntry.TABLE_NAME, null, values)
|
return dbWrite?.insert(LogEntry.TABLE_NAME, null, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getExportFileUri(): Uri? {
|
||||||
|
val file = File(context.filesDir, "export.csv")
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.createNewFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
file.setReadable(true, false)
|
||||||
|
|
||||||
|
return Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportToCsv() {
|
||||||
|
val uri = getExportFileUri() ?: return
|
||||||
|
exportToUri(uri)
|
||||||
|
}
|
||||||
|
|
||||||
fun exportToUri(uri: Uri) {
|
fun exportToUri(uri: Uri) {
|
||||||
val projection =
|
val projection =
|
||||||
@@ -100,7 +118,7 @@ object LoggerContract {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
val type = object : TypeToken<HashMap<String, Any>>() {}.type
|
|
||||||
var headerWritten = false
|
var headerWritten = false
|
||||||
val sep = ","
|
val sep = ","
|
||||||
|
|
||||||
@@ -112,30 +130,26 @@ object LoggerContract {
|
|||||||
val ts = getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_TS))
|
val ts = getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_TS))
|
||||||
val storedField =
|
val storedField =
|
||||||
getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_PAYLOAD))
|
getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_PAYLOAD))
|
||||||
|
val payload = parsePayload(storedField)
|
||||||
try {
|
try {
|
||||||
val payload: HashMap<String, Any> = gson.fromJson(storedField, type)
|
|
||||||
|
|
||||||
if (!headerWritten) {
|
if (!headerWritten) {
|
||||||
val headerRow =
|
val headerRow =
|
||||||
"timestamp" + sep + payload.keys.joinToString(sep) + newLine
|
"timestamp" + sep + "local_time" + sep + payload.keys.joinToString(sep) + newLine
|
||||||
writer.write(headerRow.toByteArray())
|
writer.write(headerRow.toByteArray())
|
||||||
|
|
||||||
headerWritten = true
|
headerWritten = true
|
||||||
}
|
}
|
||||||
|
val localTime = convertIsoToLocalTime(ts)
|
||||||
val row = ts + sep + payload.values.joinToString(sep) + newLine
|
val row = ts + sep + localTime + sep + payload.values.joinToString(sep) + newLine
|
||||||
|
|
||||||
writer.write(row.toByteArray())
|
writer.write(row.toByteArray())
|
||||||
} catch (exception: JsonSyntaxException) {
|
} catch (exception: JsonSyntaxException) {
|
||||||
Log.e("db", exception.toString())
|
Log.e(tag, exception.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
truncate()
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -143,9 +157,79 @@ object LoggerContract {
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun truncate() {
|
fun exportToMultipleCSV() {
|
||||||
dbWrite.execSQL("DELETE FROM ${LogEntry.TABLE_NAME}");
|
val projection =
|
||||||
dbWrite.execSQL("VACUUM");
|
arrayOf(BaseColumns._ID, LogEntry.COLUMN_NAME_PAYLOAD, LogEntry.COLUMN_NAME_TS)
|
||||||
|
|
||||||
|
val sortOrder = "${BaseColumns._ID} ASC"
|
||||||
|
|
||||||
|
val cursor = dbRead.query(
|
||||||
|
LogEntry.TABLE_NAME, // The table to query
|
||||||
|
projection, // The array of columns to return (pass null to get all)
|
||||||
|
null, // The columns for the WHERE clause
|
||||||
|
null, // The values for the WHERE clause
|
||||||
|
null, // don't group the rows
|
||||||
|
null, // don't filter by row groups
|
||||||
|
sortOrder // The sort order
|
||||||
|
)
|
||||||
|
|
||||||
|
val files = HashMap<String, File>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val sep = ","
|
||||||
|
val newLine = '\n'
|
||||||
|
|
||||||
|
with(cursor) {
|
||||||
|
while (moveToNext()) {
|
||||||
|
val ts = getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_TS))
|
||||||
|
val storedField =
|
||||||
|
getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_PAYLOAD))
|
||||||
|
|
||||||
|
try {
|
||||||
|
val payload = parsePayload(storedField)
|
||||||
|
val deviceId = payload.getOrDefault("bleAddress", "unknown") as String
|
||||||
|
val fileName = "kirby_export_${deviceId.replace(":", "")}.csv"
|
||||||
|
|
||||||
|
val f = files.getOrElse(deviceId) {
|
||||||
|
val file = File(context.filesDir, fileName)
|
||||||
|
if (!file.exists()) {
|
||||||
|
file.createNewFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
val headerRow =
|
||||||
|
"timestamp" + sep + "local_time" + sep + payload.keys.joinToString(
|
||||||
|
sep
|
||||||
|
) + newLine
|
||||||
|
file.writeText(headerRow)
|
||||||
|
|
||||||
|
files[deviceId] = file
|
||||||
|
|
||||||
|
file.setReadable(true, false)
|
||||||
|
file
|
||||||
|
}
|
||||||
|
|
||||||
|
val localTime = convertIsoToLocalTime(ts)
|
||||||
|
|
||||||
|
val row =
|
||||||
|
ts + sep + localTime + sep + payload.values.joinToString(sep) + newLine
|
||||||
|
|
||||||
|
f.appendText(row)
|
||||||
|
} catch (exception: JsonSyntaxException) {
|
||||||
|
Log.e(tag, exception.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
dbWrite.execSQL("DELETE FROM ${LogEntry.TABLE_NAME}")
|
||||||
|
dbWrite.execSQL("VACUUM")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
@@ -155,7 +239,16 @@ object LoggerContract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parsePayload(payload: String): SortedMap<String, Any> {
|
||||||
|
val type = object : TypeToken<HashMap<String, Any>>() {}.type
|
||||||
|
val gson = Gson()
|
||||||
|
val parsed : HashMap<String, Any> = gson.fromJson(payload, type)
|
||||||
|
return parsed.toSortedMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertIsoToLocalTime(isoDateTime: String): String {
|
||||||
|
val systemZone = ZoneId.systemDefault()
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||||
|
return Instant.parse(isoDateTime).atZone(systemZone).format(formatter)
|
||||||
|
}
|
||||||
|
|
||||||
181
app/src/main/java/com/logitech/vc/kirbytest/DecoderIaq.kt
Normal file
181
app/src/main/java/com/logitech/vc/kirbytest/DecoderIaq.kt
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|
||||||
|
object DecoderIaq {
|
||||||
|
private const val INVALID_CO2 = 16383
|
||||||
|
private const val INVALID_VOC = 2047
|
||||||
|
private const val INVALID_HUMIDITY = 1023
|
||||||
|
private const val INVALID_TEMPERATURE = 2047
|
||||||
|
private const val INVALID_PRESSURE = 65535
|
||||||
|
private const val INVALID_OCCUPANCY = 3
|
||||||
|
private const val INVALID_PM25 = 1023
|
||||||
|
private const val INVALID_PM10 = 1023
|
||||||
|
|
||||||
|
private val supportedMessageTypes = listOf<Number>(0, 1, 5)
|
||||||
|
|
||||||
|
fun parseMeasurement(input: String): Uplink? {
|
||||||
|
|
||||||
|
val inputBytes = hexStringToByteArray(input)
|
||||||
|
val msgType = inputBytes[0].toInt() and 0xFF ushr 4
|
||||||
|
|
||||||
|
if (!supportedMessageTypes.contains(msgType)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType < 3) {
|
||||||
|
return parseIaq(msgType, inputBytes)
|
||||||
|
} else if (msgType == 5) {
|
||||||
|
return parseDeviceStatus(msgType, inputBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDeviceStatus(msgType: Number, inputBytes: ByteArray): DeviceStatus {
|
||||||
|
val status = DeviceStatus(msgType = msgType)
|
||||||
|
|
||||||
|
val fwMajor = parseUnsignedIntFromBytes(inputBytes, 4, 8)
|
||||||
|
val fwMinor = parseUnsignedIntFromBytes(inputBytes, 12, 8)
|
||||||
|
val fwPatch = parseUnsignedIntFromBytes(inputBytes, 20, 8)
|
||||||
|
val fwTweak = parseUnsignedIntFromBytes(inputBytes, 28, 8)
|
||||||
|
|
||||||
|
status.firmware = "$fwMajor.$fwMinor.$fwPatch"
|
||||||
|
if(fwTweak > 0u) {
|
||||||
|
status.firmware += "-$fwTweak"
|
||||||
|
}
|
||||||
|
|
||||||
|
status.operationMode = parseUnsignedIntFromBytes(inputBytes, 36, 4).toInt()
|
||||||
|
val batteryVoltage = parseUnsignedIntFromBytes(inputBytes, 40, 16).toInt()
|
||||||
|
if (batteryVoltage == 0) {
|
||||||
|
status.usbPowered = true
|
||||||
|
} else {
|
||||||
|
status.batteryVoltage = batteryVoltage / 1000f
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (inputBytes.size > 7) {
|
||||||
|
val errors = inputBytes[7].toInt()
|
||||||
|
val errorCo2 = (errors shr 0) and 1
|
||||||
|
val errorVoc = (errors shr 1) and 1
|
||||||
|
val errorPm = (errors shr 2) and 1
|
||||||
|
val errorRadar = (errors shr 3) and 1
|
||||||
|
val errorTemp = (errors shr 4) and 1
|
||||||
|
|
||||||
|
status.errorCo2 = errorCo2 == 1
|
||||||
|
status.errorVoc = errorVoc == 1
|
||||||
|
status.errorPm = errorPm == 1
|
||||||
|
status.errorRadar = errorRadar == 1
|
||||||
|
status.errorTemp = errorTemp == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun parseIaq(msgType: Number, inputBytes: ByteArray): Measurement {
|
||||||
|
val measurement = Measurement()
|
||||||
|
measurement.msgType = msgType
|
||||||
|
|
||||||
|
val co2 = parseUnsignedInt(inputBytes, 0, 3) ushr 6 and INVALID_CO2
|
||||||
|
measurement.co2 = if (co2 == INVALID_CO2) null else co2
|
||||||
|
|
||||||
|
val voc = parseUnsignedInt(inputBytes, 2, 4) ushr 3 and INVALID_VOC
|
||||||
|
measurement.voc = if (co2 == INVALID_VOC) null else voc
|
||||||
|
|
||||||
|
val humidity =
|
||||||
|
parseUnsignedInt(inputBytes, 3, 5) ushr 1 and INVALID_HUMIDITY
|
||||||
|
measurement.humidity = if (humidity == INVALID_HUMIDITY) null else humidity / 10
|
||||||
|
|
||||||
|
val temperature =
|
||||||
|
parseUnsignedInt(inputBytes, 4, 7) ushr 6 and INVALID_TEMPERATURE
|
||||||
|
measurement.temperature =
|
||||||
|
if (temperature == INVALID_TEMPERATURE) null else ((temperature / 10.0 - 40) * 10.0).roundToInt() / 10.0
|
||||||
|
|
||||||
|
val pressure =
|
||||||
|
parseUnsignedInt(inputBytes, 6, 9) ushr 6 and INVALID_PRESSURE
|
||||||
|
measurement.pressure =
|
||||||
|
if (pressure == INVALID_PRESSURE) null else (30000 + 19000.0 * pressure / 13107).roundToInt()
|
||||||
|
|
||||||
|
val occupancy =
|
||||||
|
parseUnsignedInt(inputBytes, 8, 9) 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
|
||||||
|
measurement.pm25 = if (pm25 == INVALID_PM25) null else pm25
|
||||||
|
|
||||||
|
val pm10 = parseUnsignedInt(inputBytes, 9, 11) and INVALID_PM10
|
||||||
|
measurement.pm10 = if (pm10 == INVALID_PM10) null else pm10
|
||||||
|
}
|
||||||
|
return measurement
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface Uplink {
|
||||||
|
var deviceId: String?
|
||||||
|
var msgType: Number?
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Measurement(
|
||||||
|
@SerializedName("bleAddress")
|
||||||
|
override var deviceId: String? = null,
|
||||||
|
override var msgType: Number? = null,
|
||||||
|
var co2: Number? = null,
|
||||||
|
var voc: Number? = null,
|
||||||
|
var humidity: Number? = null,
|
||||||
|
var temperature: Number? = null,
|
||||||
|
var pressure: Number? = null,
|
||||||
|
var occupancy: Number? = null,
|
||||||
|
var pm25: Number? = null,
|
||||||
|
var pm10: Number? = null
|
||||||
|
) : Uplink {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "M{" +
|
||||||
|
"type=" + msgType +
|
||||||
|
", co2=" + co2 +
|
||||||
|
", voc=" + voc +
|
||||||
|
", hum=" + humidity +
|
||||||
|
", temp=" + temperature +
|
||||||
|
", press=" + pressure +
|
||||||
|
", pm25=" + pm25 +
|
||||||
|
", pm10=" + pm10 +
|
||||||
|
", occ=" + occupancy +
|
||||||
|
'}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeviceStatus(
|
||||||
|
@SerializedName("bleAddress")
|
||||||
|
override var deviceId: String? = null,
|
||||||
|
override var msgType: Number? = null,
|
||||||
|
var firmware: String? = null,
|
||||||
|
var operationMode: Number? = null,
|
||||||
|
var batteryVoltage: Number? = null,
|
||||||
|
var usbPowered: Boolean = false,
|
||||||
|
var errorCo2: Boolean = false,
|
||||||
|
var errorVoc: Boolean = false,
|
||||||
|
var errorPm: Boolean = false,
|
||||||
|
var errorRadar: Boolean = false,
|
||||||
|
var errorTemp: Boolean = false,
|
||||||
|
) : Uplink {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "S{" +
|
||||||
|
"type=" + msgType +
|
||||||
|
", fw=" + firmware +
|
||||||
|
", op=" + operationMode +
|
||||||
|
", usb=" + usbPowered +
|
||||||
|
", batt=" + batteryVoltage +
|
||||||
|
", errTemp=" + errorTemp +
|
||||||
|
", errCo2=" + errorCo2 +
|
||||||
|
", errVoc=" + errorVoc +
|
||||||
|
", errPm=" + errorPm +
|
||||||
|
", errRad=" + errorRadar +
|
||||||
|
'}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.sensortestingapp
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.sensortestingapp
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
|
import SettingsRepository
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothDevice.BOND_BONDED
|
import android.bluetooth.BluetoothDevice.BOND_BONDED
|
||||||
@@ -15,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
|
||||||
@@ -34,32 +33,54 @@ 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,
|
||||||
private val loggerDb: LoggerContract.LoggerDb,
|
private val loggerDb: LoggerContract.LoggerDb,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
private val onStateChange: (device: KirbyDevice) -> Unit,
|
private val onStateChange: (device: KirbyDevice) -> Unit,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
) : 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)) {
|
||||||
|
connectionManager.enableNotification(
|
||||||
|
bleDevice, SERVICE_UUID, CHAR_UUID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connectionManager.enableNotification(
|
fun connect() {
|
||||||
bleDevice, SERVICE_UUID, CHAR_UUID
|
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() {
|
fun readIaq() {
|
||||||
connectionManager.readChar(bleDevice, SERVICE_UUID, CHAR_UUID)
|
connectionManager.readChar(bleDevice, SERVICE_UUID, CHAR_UUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onSuccessfulCharRead(
|
override fun onSuccessfulCharRead(
|
||||||
gatt: BluetoothGatt,
|
gatt: BluetoothGatt,
|
||||||
characteristic: BluetoothGattCharacteristic
|
characteristic: BluetoothGattCharacteristic
|
||||||
) {
|
) {
|
||||||
|
|
||||||
addMeasurement(characteristic)
|
addMeasurement(characteristic)
|
||||||
|
|
||||||
onStateChange(this)
|
onStateChange(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +93,9 @@ class KirbyDevice(
|
|||||||
statuses.add(DeviceStatus.CONNECTED)
|
statuses.add(DeviceStatus.CONNECTED)
|
||||||
statuses.remove(DeviceStatus.MISSING)
|
statuses.remove(DeviceStatus.MISSING)
|
||||||
|
|
||||||
|
subscribe()
|
||||||
onStateChange(this)
|
onStateChange(this)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConnectToBondedFailed(gatt: BluetoothGatt) {
|
override fun onConnectToBondedFailed(gatt: BluetoothGatt) {
|
||||||
@@ -84,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +144,7 @@ class KirbyDevice(
|
|||||||
override fun onBonded(device: BluetoothDevice) {
|
override fun onBonded(device: BluetoothDevice) {
|
||||||
statuses.add(DeviceStatus.BONDED)
|
statuses.add(DeviceStatus.BONDED)
|
||||||
onStateChange(this)
|
onStateChange(this)
|
||||||
|
connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnbonded(device: BluetoothDevice) {
|
override fun onUnbonded(device: BluetoothDevice) {
|
||||||
@@ -132,16 +164,17 @@ 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 {
|
||||||
|
measurement.deviceId = bleDevice.address
|
||||||
payload = Payload(measurement.toString())
|
payload = Payload(measurement.toString())
|
||||||
Log.i("BleListener", "Char received: $payload")
|
Log.i("BleListener", "Char received: $payload")
|
||||||
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
|
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
|
||||||
publishMeasurement(base64Payload)
|
publishMeasurement(base64Payload)
|
||||||
|
|
||||||
loggerDb.writeLog( measurement)
|
loggerDb.writeLog(measurement)
|
||||||
}
|
}
|
||||||
|
|
||||||
measurements.add(payload)
|
measurements.add(payload)
|
||||||
@@ -153,8 +186,13 @@ class KirbyDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun publishMeasurement(payload: String) {
|
private fun publishMeasurement(payload: String) {
|
||||||
val accessKey = BuildConfig.API_KEY
|
val accessKey = settings.apiKey
|
||||||
val url = BuildConfig.API_BASE_URL
|
val url = settings.apiUrl
|
||||||
|
|
||||||
|
if(url.isEmpty() || accessKey.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val eui = "0000${bleDevice.address.replace(":", "")}"
|
val eui = "0000${bleDevice.address.replace(":", "")}"
|
||||||
|
|
||||||
val postData = JSONObject()
|
val postData = JSONObject()
|
||||||
@@ -346,7 +384,7 @@ class KirbyDevice(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return actions;
|
return actions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,33 +394,18 @@ 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 {
|
||||||
return payload.payload
|
return payload.payload
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Int? {
|
override fun getIcon(): Int {
|
||||||
return R.drawable.baseline_numbers_24
|
return R.drawable.baseline_numbers_24
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.sensortestingapp
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
|
import SettingsRepository
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
@@ -8,9 +9,11 @@ import android.bluetooth.BluetoothAdapter
|
|||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.bluetooth.le.ScanResult
|
import android.bluetooth.le.ScanResult
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -20,17 +23,27 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View.INVISIBLE
|
import android.view.View.INVISIBLE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.example.sensortestingapp.databinding.ActivityMainBinding
|
import com.logitech.vc.kirbytest.databinding.ActivityMainBinding
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import settingsDialog
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
|
|
||||||
|
|
||||||
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1
|
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1
|
||||||
@@ -57,7 +70,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private lateinit var loggerDb: LoggerContract.LoggerDb
|
private lateinit var loggerDb: LoggerContract.LoggerDb
|
||||||
private lateinit var createFileLauncher: ActivityResultLauncher<String>
|
private lateinit var createFileLauncher: ActivityResultLauncher<String>
|
||||||
|
private val bondedDevices = HashSet<String>()
|
||||||
|
private lateinit var settings: SettingsRepository
|
||||||
|
|
||||||
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
||||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
@@ -69,7 +83,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)
|
||||||
@@ -82,7 +96,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
settings = SettingsRepository(applicationContext)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
@@ -108,12 +122,56 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setupDevicesList()
|
setupDevicesList()
|
||||||
|
|
||||||
createFileLauncher =
|
createFileLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri: Uri? ->
|
registerForActivityResult(CreateDocument("text/csv")) { uri: Uri? ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
// Use the URI to write your CSV content
|
// Use the URI to write your CSV content
|
||||||
loggerDb.exportToUri(uri)
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
loggerDb.exportToUri(uri)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Log export completed.",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasRequiredRuntimePermissions()) {
|
||||||
|
connectionManager.startScan()
|
||||||
|
/*
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
delay(5000L)
|
||||||
|
connectionManager.stopScan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer().schedule(object : TimerTask() {
|
||||||
|
override fun run() {
|
||||||
|
loggerDb.exportToMultipleCSV()
|
||||||
|
}
|
||||||
|
}, 1000, 10000)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
settings.loadSettings()
|
||||||
|
|
||||||
|
if(settings.getSettings().lowPowerMode) {
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Register for broadcasts on BluetoothAdapter state change
|
||||||
|
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||||
|
registerReceiver(mReceiver, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleScanning(): Unit {
|
private fun toggleScanning(): Unit {
|
||||||
@@ -124,6 +182,21 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val action = intent.action
|
||||||
|
|
||||||
|
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
||||||
|
val state = intent.getIntExtra(
|
||||||
|
BluetoothAdapter.EXTRA_STATE,
|
||||||
|
BluetoothAdapter.ERROR
|
||||||
|
)
|
||||||
|
Log.i("statechanged", "got state " + state.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun isKirbyDevice(device: BluetoothDevice): Boolean {
|
private fun isKirbyDevice(device: BluetoothDevice): Boolean {
|
||||||
val deviceName = (device.name ?: "").lowercase()
|
val deviceName = (device.name ?: "").lowercase()
|
||||||
return deviceName.contains("kirby") || deviceName.contains("krby")
|
return deviceName.contains("kirby") || deviceName.contains("krby")
|
||||||
@@ -155,26 +228,20 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addBondedDevices(): Unit {
|
private fun addBondedDevices(): Unit {
|
||||||
|
bondedDevices.clear()
|
||||||
bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach {
|
bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach {
|
||||||
val kirbyDevice = newKirbyDevice(it)
|
bondedDevices.add(it.address)
|
||||||
kirbyDevice.subscribe()
|
newKirbyDevice(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) {
|
||||||
override fun onScanningStateChange(isScanning: Boolean) {
|
override fun onScanningStateChange(isScanning: Boolean) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
binding.fab.setText(if (isScanning) "Stop Scan" else "Start Scan")
|
binding.fab.text = if (isScanning) "Stop Scan" else "Start Scan"
|
||||||
binding.fab.setIconResource(
|
binding.fab.setIconResource(
|
||||||
if (isScanning) R.drawable.action_icon_disconnect else R.drawable.action_icon_scan
|
if (isScanning) R.drawable.action_icon_disconnect else R.drawable.action_icon_scan
|
||||||
)
|
)
|
||||||
@@ -183,34 +250,39 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
if (isKirbyDevice(result.device)) {
|
if (isKirbyDevice(result.device) && result.isConnectable) {
|
||||||
|
|
||||||
Log.i(
|
Log.i(
|
||||||
"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) {
|
||||||
newKirbyDevice(result.device).onScanResult(callbackType, result)
|
kirbyDevice = newKirbyDevice(result.device)
|
||||||
|
kirbyDevice.onScanResult(callbackType, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kirbyDevice.connect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return mngr
|
return mngr
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice {
|
private fun newKirbyDevice(
|
||||||
|
bleDevice: BluetoothDevice
|
||||||
|
): KirbyDevice {
|
||||||
|
|
||||||
val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb) {
|
val device =
|
||||||
val i = kirbyDevices.indexOfFirst { d -> d === it }
|
KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb, settings) {
|
||||||
runOnUiThread {
|
val i = kirbyDevices.indexOfFirst { d -> d === it }
|
||||||
deviceListAdapter.notifyItemChanged(i)
|
runOnUiThread {
|
||||||
|
deviceListAdapter.notifyItemChanged(i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Log.i("MainActivity", bleDevice.address)
|
||||||
connectionManager.register(device)
|
connectionManager.register(device)
|
||||||
connectionManager.connect(bleDevice)
|
|
||||||
connectionManager.discoverServices(bleDevice)
|
|
||||||
kirbyDevices.add(device)
|
kirbyDevices.add(device)
|
||||||
deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1)
|
deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1)
|
||||||
return device
|
return device
|
||||||
@@ -236,6 +308,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.action_reset_log -> {
|
||||||
|
val builder = AlertDialog.Builder(this@MainActivity)
|
||||||
|
builder.setMessage("Are you sure you want to reset the log?")
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton("Yes") { dialog, id ->
|
||||||
|
loggerDb.reset()
|
||||||
|
}
|
||||||
|
.setNegativeButton("No") { dialog, id ->
|
||||||
|
// Dismiss the dialog
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
val alert = builder.create()
|
||||||
|
alert.show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.action_settings -> {
|
||||||
|
val settingsDialog = settingsDialog(this, settings)
|
||||||
|
settingsDialog.show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,6 +344,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
loggerDb.close()
|
loggerDb.close()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
|
unregisterReceiver(mReceiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@@ -407,7 +503,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
|
|||||||
return "Test action 1"
|
return "Test action 1"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Int? {
|
override fun getIcon(): Int {
|
||||||
return R.drawable.action_icon_disconnect
|
return R.drawable.action_icon_disconnect
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +515,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
|
|||||||
return "Test action 2"
|
return "Test action 2"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Int? {
|
override fun getIcon(): Int {
|
||||||
return R.drawable.action_icon_connect
|
return R.drawable.action_icon_connect
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +535,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
|
|||||||
return "21.2 °C"
|
return "21.2 °C"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Int? {
|
override fun getIcon(): Int {
|
||||||
return R.drawable.baseline_device_thermostat_24
|
return R.drawable.baseline_device_thermostat_24
|
||||||
}
|
}
|
||||||
}, object : Measurement {
|
}, object : Measurement {
|
||||||
@@ -451,7 +547,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
|
|||||||
return "232 bar"
|
return "232 bar"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIcon(): Int? {
|
override fun getIcon(): Int {
|
||||||
return R.drawable.baseline_compress_24
|
return R.drawable.baseline_compress_24
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.sensortestingapp;
|
package com.logitech.vc.kirbytest;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
128
app/src/main/java/com/logitech/vc/kirbytest/Settings.kt
Normal file
128
app/src/main/java/com/logitech/vc/kirbytest/Settings.kt
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
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
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
|
||||||
|
val Context.dataStore by preferencesDataStore(name = "app_settings")
|
||||||
|
|
||||||
|
class SettingsRepository(context: Context) {
|
||||||
|
private val dataStore = context.dataStore
|
||||||
|
|
||||||
|
private val settings = Settings(apiUrl = BuildConfig.API_BASE_URL, apiKey = BuildConfig.API_KEY)
|
||||||
|
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
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, 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 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,
|
||||||
|
lowPowerMode = it[BLE_LOW_POWER_MODE] ?: true
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadSettings() {
|
||||||
|
val result = settingsFlow.firstOrNull()
|
||||||
|
if (result != null) {
|
||||||
|
settings.apiKey = result.apiKey
|
||||||
|
settings.apiUrl = result.apiUrl
|
||||||
|
settings.lowPowerMode = result.lowPowerMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Settings(var apiUrl: String, var apiKey: String, var lowPowerMode: Boolean = true)
|
||||||
|
|
||||||
|
|
||||||
|
fun settingsDialog(context: Context, settingsRepo: SettingsRepository): AlertDialog {
|
||||||
|
|
||||||
|
val layoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
|
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.getSettings()
|
||||||
|
urlField.setText(settings.apiUrl)
|
||||||
|
keyField.setText(settings.apiKey)
|
||||||
|
lowPowerMode.isChecked = settings.lowPowerMode
|
||||||
|
|
||||||
|
return androidx.appcompat.app.AlertDialog.Builder(context)
|
||||||
|
.setTitle(R.string.settings)
|
||||||
|
.setView(root)
|
||||||
|
.setPositiveButton(R.string.save) { dialog: DialogInterface?, whichButton: Int ->
|
||||||
|
Log.d("SettingsDialog", "save settings")
|
||||||
|
|
||||||
|
val url = urlField.text.toString()
|
||||||
|
val key = keyField.text.toString()
|
||||||
|
val isLowPowerMode = lowPowerMode.isChecked
|
||||||
|
if (isFullPath(url) || url.isEmpty()) {
|
||||||
|
settingsRepo.saveSettings(url, key, isLowPowerMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel) { dialog: DialogInterface?, whichButton: Int ->
|
||||||
|
//Do something
|
||||||
|
Log.d("SettingsDialog", "cancel settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
.create()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isFullPath(potentialUrl: String): Boolean {
|
||||||
|
try {
|
||||||
|
URL(potentialUrl).toURI()
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
49
app/src/main/java/com/logitech/vc/kirbytest/parseUtils.kt
Normal file
49
app/src/main/java/com/logitech/vc/kirbytest/parseUtils.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
|
import org.apache.commons.codec.DecoderException
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
fun parseUnsignedInt(bytes: ByteArray, startIncl: Int, endExcl: Int): Int {
|
||||||
|
val section = bytes.copyOfRange(startIncl, min(bytes.size, endExcl))
|
||||||
|
var unsignedInt = 0
|
||||||
|
for (i in section.indices) {
|
||||||
|
unsignedInt = unsignedInt shl 8
|
||||||
|
unsignedInt = unsignedInt or (section[i].toInt() and 0xFF)
|
||||||
|
}
|
||||||
|
return unsignedInt
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hexStringToByteArray(encoded: String): ByteArray {
|
||||||
|
return try {
|
||||||
|
Hex.decodeHex(encoded)
|
||||||
|
} catch (e: DecoderException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseUnsignedIntFromBytes(byteArray: ByteArray, offset: Int, bits: Int): UInt {
|
||||||
|
require(bits in 1..32) { "Bits should be between 1 and 32" }
|
||||||
|
require(offset >= 0 && offset < byteArray.size * 8) { "Offset out of bounds" }
|
||||||
|
|
||||||
|
var result: UInt = 0u
|
||||||
|
var remainingBits = bits
|
||||||
|
var currentOffset = offset
|
||||||
|
|
||||||
|
while (remainingBits > 0) {
|
||||||
|
val byteIndex = currentOffset / 8
|
||||||
|
val bitIndex = currentOffset % 8
|
||||||
|
val bitsToRead = minOf(remainingBits, 8 - bitIndex)
|
||||||
|
|
||||||
|
val byteValue = byteArray[byteIndex].toInt() and 0xFF
|
||||||
|
val mask = (1 shl bitsToRead) - 1
|
||||||
|
val value = (byteValue shr (8 - bitIndex - bitsToRead)) and mask
|
||||||
|
|
||||||
|
result = (result shl bitsToRead) or value.toUInt()
|
||||||
|
|
||||||
|
currentOffset += bitsToRead
|
||||||
|
remainingBits -= bitsToRead
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
android:layout_width="150dp"
|
android:layout_width="150dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:text='Start Scan'
|
android:text='@string/start_scan'
|
||||||
app:icon="@drawable/action_icon_scan"
|
app:icon="@drawable/action_icon_scan"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<com.example.sensortestingapp.NonScrollListView
|
<com.logitech.vc.kirbytest.NonScrollListView
|
||||||
android:id="@+id/measurement_fields"
|
android:id="@+id/measurement_fields"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
62
app/src/main/res/layout/settings_dialog.xml
Normal file
62
app/src/main/res/layout/settings_dialog.xml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/apiUrlLabel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="10"
|
||||||
|
android:text="API url" />
|
||||||
|
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/apiUrl"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="10"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textUri"
|
||||||
|
android:text="" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/apiKeyLabel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="10"
|
||||||
|
android:text="API key" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/apiKey"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="20"
|
||||||
|
android:autofillHints="password"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="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>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:tools="http://schemas.android.com/tools"
|
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context="com.example.sensortestingapp.MainActivity" />
|
tools:context="com.logitech.vc.kirbytest.MainActivity" />
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:context="com.example.sensortestingapp.MainActivity">
|
tools:context="com.logitech.vc.kirbytest.MainActivity">
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_export"
|
android:id="@+id/action_export"
|
||||||
android:orderInCategory="100"
|
android:orderInCategory="100"
|
||||||
android:title="@string/action_export"
|
android:title="@string/action_export"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_reset_log"
|
||||||
|
android:orderInCategory="200"
|
||||||
|
android:title="@string/action_reset_log"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:orderInCategory="300"
|
||||||
|
android:title="@string/settings"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
@@ -1,46 +1,17 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Kirby Test App</string>
|
<string name="app_name">Kirby Test App</string>
|
||||||
<string name="action_export">Export to csv</string>
|
<string name="action_export">Export to csv</string>
|
||||||
|
<string name="action_reset_log">Reset log</string>
|
||||||
<!-- Strings used for fragments for navigation -->
|
<!-- Strings used for fragments for navigation -->
|
||||||
<string name="first_fragment_label">First Fragment</string>
|
<string name="first_fragment_label">First Fragment</string>
|
||||||
<string name="second_fragment_label">Second Fragment</string>
|
<string name="second_fragment_label">Second Fragment</string>
|
||||||
<string name="next">Next</string>
|
<string name="next">Next</string>
|
||||||
<string name="previous">Previous</string>
|
<string name="previous">Previous</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="api_url">API url</string>
|
||||||
|
<string name="api_key">API key</string>
|
||||||
|
<string name="save">Save</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="start_scan">Start Scan</string>
|
||||||
|
|
||||||
<string name="lorem_ipsum">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris
|
|
||||||
volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus
|
|
||||||
dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad
|
|
||||||
litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend
|
|
||||||
diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a,
|
|
||||||
ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n
|
|
||||||
Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus
|
|
||||||
egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed
|
|
||||||
neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada
|
|
||||||
fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae,
|
|
||||||
molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor
|
|
||||||
bibendum, vel congue leo egestas.\n\n
|
|
||||||
Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit
|
|
||||||
amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel,
|
|
||||||
molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer
|
|
||||||
interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at
|
|
||||||
lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula,
|
|
||||||
in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque
|
|
||||||
est.\n\n
|
|
||||||
Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh.
|
|
||||||
Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui
|
|
||||||
non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In
|
|
||||||
eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
|
|
||||||
quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
|
|
||||||
ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a
|
|
||||||
placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus
|
|
||||||
convallis.\n\n
|
|
||||||
Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et
|
|
||||||
malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa
|
|
||||||
gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper,
|
|
||||||
libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper
|
|
||||||
sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus
|
|
||||||
libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus
|
|
||||||
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
|
|
||||||
</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.example.sensortestingapp
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
class DecoderTest {
|
|
||||||
@Test
|
|
||||||
fun message_type_0_decodes_correctly() {
|
|
||||||
val res2 = DecoderIaq.parseMeasurement("006b04ab74a1ed0d101404");
|
|
||||||
val testMeasurement = DecoderIaq.Measurement(msgType = 0, co2 = 428, voc = 149, humidity = 44, pressure = 96873, occupancy = 1, pm10 = 4, pm25 = 5, temperature = 24.7 )
|
|
||||||
assertEquals(testMeasurement, res2)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun message_type_1_decodes_correctly() {
|
|
||||||
val res2 = DecoderIaq.parseMeasurement("106b04ab74a1ed0d10");
|
|
||||||
val testMeasurement = DecoderIaq.Measurement(msgType = 1, co2 = 428, voc = 149, humidity = 44, pressure = 96873, occupancy = 1, pm10 = null, pm25 = null, temperature = 24.7 )
|
|
||||||
assertEquals(testMeasurement, res2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
app/src/test/java/com/logitech/vc/kirbytest/DecoderTest.kt
Normal file
50
app/src/test/java/com/logitech/vc/kirbytest/DecoderTest.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package com.logitech.vc.kirbytest
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class DecoderTest {
|
||||||
|
@Test
|
||||||
|
fun message_type_0_decodes_correctly() {
|
||||||
|
val res2 = DecoderIaq.parseMeasurement("006b04ab74a1ed0d101404")
|
||||||
|
val testMeasurement = DecoderIaq.Measurement(msgType = 0, co2 = 428, voc = 149, humidity = 44, pressure = 96873, occupancy = 1, pm10 = 4, pm25 = 5, temperature = 24.7 )
|
||||||
|
assertEquals(testMeasurement, res2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun message_type_1_decodes_correctly() {
|
||||||
|
val res2 = DecoderIaq.parseMeasurement("106b04ab74a1ed0d10")
|
||||||
|
val testMeasurement = DecoderIaq.Measurement(msgType = 1, co2 = 428, voc = 149, humidity = 44, pressure = 96873, occupancy = 1, pm10 = null, pm25 = null, temperature = 24.7 )
|
||||||
|
assertEquals(testMeasurement, res2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun message_type_5() {
|
||||||
|
val res = DecoderIaq.parseMeasurement("501001C0430000")
|
||||||
|
|
||||||
|
val expected = DecoderIaq.DeviceStatus(msgType = 5, firmware = "1.0.28-4", operationMode = 3, batteryVoltage = 0, errorPm = null, errorVoc = null, errorTemp = null, errorCo2 = null, errorRadar = null)
|
||||||
|
assertEquals(expected, res)
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun message_type_5_battery() {
|
||||||
|
val res = DecoderIaq.parseMeasurement("501001C0430FF9")
|
||||||
|
val expected = DecoderIaq.DeviceStatus(msgType = 5, firmware = "1.0.28-4", operationMode = 3, batteryVoltage = 4089f/1000, errorPm = null, errorVoc = null, errorTemp = null, errorCo2 = null, errorRadar = null)
|
||||||
|
|
||||||
|
assertEquals(expected, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun message_type_5_with_errors() {
|
||||||
|
val res = DecoderIaq.parseMeasurement("501001C0430FF928")
|
||||||
|
val expected = DecoderIaq.DeviceStatus(msgType = 5, firmware = "1.0.28-4", operationMode = 3, batteryVoltage = 4089f/1000, errorPm = true, errorVoc = true, errorTemp = true, errorCo2 = true, errorRadar = true)
|
||||||
|
|
||||||
|
assertEquals(expected, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.2' apply false
|
||||||
id 'com.android.library' version '8.1.2' apply false
|
id 'com.android.library' version '8.5.2' 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
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ dependencyResolutionManagement {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootProject.name = "Sensor Testing App"
|
rootProject.name = "Kirby Test App"
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|||||||
Reference in New Issue
Block a user