Compare commits
10 Commits
c94ca3f40f
...
803ff2e41a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
@@ -4,11 +4,11 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.example.sensortestingapp'
|
||||
namespace 'com.logitech.vc.kirbytest'
|
||||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.sensortestingapp"
|
||||
applicationId "com.logitech.vc.kirbytest"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 1
|
||||
@@ -59,6 +59,7 @@ dependencies {
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||
|
||||
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'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.sensortestingapp
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
@@ -17,6 +17,6 @@ class ExampleInstrumentedTest {
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.sensortestingapp", appContext.packageName)
|
||||
assertEquals("com.logitech.vc.kirbytest", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.sensortestingapp
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.sensortestingapp
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
@@ -90,7 +90,7 @@ open class BleListener(private val deviceAddress: String?) {
|
||||
|
||||
open fun isRelevantMessage(address: String?): Boolean {
|
||||
if (deviceAddress != null && deviceAddress == address) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
return deviceAddress == null
|
||||
}
|
||||
@@ -165,7 +165,7 @@ private fun BluetoothGatt.printGattTable() {
|
||||
separator = "\n|--",
|
||||
prefix = "|--"
|
||||
) {
|
||||
"${it.uuid.toString()} | " +
|
||||
"${it.uuid} | " +
|
||||
"readable: ${it.isReadable()}, " +
|
||||
"writable: ${it.isWritable()}, " +
|
||||
"writableWithoutResponse: ${it.isWritableWithoutResponse()}, " +
|
||||
@@ -325,7 +325,11 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
|
||||
|
||||
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}!")
|
||||
} else {
|
||||
enqueueOperation(Connect(device, context))
|
||||
@@ -394,9 +398,11 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
|
||||
@Synchronized
|
||||
private fun enqueueOperation(operation: BleOperationType) {
|
||||
/*
|
||||
if (isScanning) {
|
||||
stopScan()
|
||||
}
|
||||
*/
|
||||
operationQueue.add(operation)
|
||||
notifyListenersOfQueueChange(operation.device.address)
|
||||
if (pendingOperation == null) {
|
||||
@@ -526,7 +532,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
}
|
||||
|
||||
is SetNotification -> with(operation) {
|
||||
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId);
|
||||
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId)
|
||||
|
||||
if (characteristic == null) {
|
||||
Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!")
|
||||
@@ -558,7 +564,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
payload =
|
||||
if (characteristic.isIndicatable())
|
||||
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
|
||||
else BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
|
||||
else BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
|
||||
} else {
|
||||
if (!gatt.setCharacteristicNotification(characteristic, false)) {
|
||||
@@ -611,7 +617,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
it.onConnectToBondedFailed(gatt)
|
||||
}
|
||||
signalEndOfOperation(operation)
|
||||
} else if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
} else if (status == GATT_SUCCESS) {
|
||||
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
||||
Log.i(
|
||||
"ConnectionManager",
|
||||
@@ -647,7 +653,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||
with(gatt) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
if (status == GATT_SUCCESS) {
|
||||
Log.w(
|
||||
"ConnectionManager",
|
||||
"Discovered ${services.size} services for ${device.address}."
|
||||
@@ -671,7 +677,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||
Log.w(
|
||||
"ConnectionManager",
|
||||
"ATT MTU changed to $mtu, success: ${status == BluetoothGatt.GATT_SUCCESS}"
|
||||
"ATT MTU changed to $mtu, success: ${status == GATT_SUCCESS}"
|
||||
)
|
||||
|
||||
val operation = pendingOperation
|
||||
@@ -687,7 +693,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
) {
|
||||
with(characteristic) {
|
||||
when (status) {
|
||||
BluetoothGatt.GATT_SUCCESS -> {
|
||||
GATT_SUCCESS -> {
|
||||
Log.i(
|
||||
"ConnectionManager",
|
||||
"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.Context
|
||||
@@ -10,8 +10,11 @@ import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonSyntaxException
|
||||
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.util.SortedMap
|
||||
|
||||
|
||||
object LoggerContract {
|
||||
@@ -31,6 +34,7 @@ object LoggerContract {
|
||||
|
||||
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${LogEntry.TABLE_NAME}"
|
||||
|
||||
|
||||
class LoggerDbHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
@@ -61,16 +65,15 @@ object LoggerContract {
|
||||
private val dbHelper = LoggerDbHelper(context)
|
||||
private val dbWrite = dbHelper.writableDatabase
|
||||
private val dbRead = dbHelper.writableDatabase
|
||||
|
||||
private val tag = "LoggerDb"
|
||||
|
||||
val context: Context = context
|
||||
|
||||
fun writeLog(payload: Any): Long? {
|
||||
val gson = Gson()
|
||||
val jsonString = gson.toJson(payload)
|
||||
|
||||
val date = LocalDateTime.now()
|
||||
val ts = date.format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
|
||||
Log.i("Database", jsonString)
|
||||
val ts = Instant.now().toString()
|
||||
|
||||
val values = ContentValues().apply {
|
||||
put(LogEntry.COLUMN_NAME_TS, ts)
|
||||
@@ -80,6 +83,21 @@ object LoggerContract {
|
||||
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) {
|
||||
val projection =
|
||||
@@ -100,7 +118,7 @@ object LoggerContract {
|
||||
try {
|
||||
|
||||
val gson = Gson()
|
||||
val type = object : TypeToken<HashMap<String, Any>>() {}.type
|
||||
|
||||
var headerWritten = false
|
||||
val sep = ","
|
||||
|
||||
@@ -112,30 +130,26 @@ object LoggerContract {
|
||||
val ts = getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_TS))
|
||||
val storedField =
|
||||
getString(getColumnIndexOrThrow(LogEntry.COLUMN_NAME_PAYLOAD))
|
||||
|
||||
val payload = parsePayload(storedField)
|
||||
try {
|
||||
val payload: HashMap<String, Any> = gson.fromJson(storedField, type)
|
||||
|
||||
if (!headerWritten) {
|
||||
val headerRow =
|
||||
"timestamp" + sep + payload.keys.joinToString(sep) + newLine
|
||||
"timestamp" + sep + "local_time" + sep + payload.keys.joinToString(sep) + newLine
|
||||
writer.write(headerRow.toByteArray())
|
||||
|
||||
headerWritten = true
|
||||
}
|
||||
|
||||
val row = ts + sep + payload.values.joinToString(sep) + newLine
|
||||
val localTime = convertIsoToLocalTime(ts)
|
||||
val row = ts + sep + localTime + sep + payload.values.joinToString(sep) + newLine
|
||||
|
||||
writer.write(row.toByteArray())
|
||||
} catch (exception: JsonSyntaxException) {
|
||||
Log.e("db", exception.toString())
|
||||
Log.e(tag, exception.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
truncate()
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -143,9 +157,80 @@ object LoggerContract {
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
private fun truncate() {
|
||||
dbWrite.execSQL("DELETE FROM ${LogEntry.TABLE_NAME}");
|
||||
dbWrite.execSQL("VACUUM");
|
||||
fun exportToMultipleCSV() {
|
||||
val projection =
|
||||
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
|
||||
|
||||
Log.i(tag, file.absolutePath)
|
||||
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() {
|
||||
@@ -155,7 +240,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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.sensortestingapp
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import org.apache.commons.codec.DecoderException
|
||||
import org.apache.commons.codec.binary.Hex
|
||||
import kotlin.math.min
|
||||
@@ -25,7 +26,7 @@ object DecoderIaq {
|
||||
|
||||
if(!supportedMessageTypes.contains(msgType)) {
|
||||
Log.i("Decoder", "Invalid message type: $msgType")
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
measurement.msgType = msgType
|
||||
@@ -84,6 +85,8 @@ object DecoderIaq {
|
||||
}
|
||||
|
||||
data class Measurement (
|
||||
@SerializedName("bleAddress")
|
||||
var deviceId: String? = null,
|
||||
var msgType: Number? = null,
|
||||
var co2: Number? = null,
|
||||
var voc: Number? = null,
|
||||
@@ -110,8 +113,5 @@ object DecoderIaq {
|
||||
'}'
|
||||
}
|
||||
|
||||
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.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.sensortestingapp
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothDevice
|
||||
@@ -45,16 +45,23 @@ class KirbyDevice(
|
||||
private val queue: RequestQueue = Volley.newRequestQueue(context)
|
||||
|
||||
fun subscribe() {
|
||||
if(statuses.contains(DeviceStatus.CONNECTED)) {
|
||||
connectionManager.enableNotification(
|
||||
bleDevice, SERVICE_UUID, CHAR_UUID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
connectionManager.enableNotification(
|
||||
bleDevice, SERVICE_UUID, CHAR_UUID
|
||||
)
|
||||
fun connect() {
|
||||
connectionManager.connect(bleDevice)
|
||||
connectionManager.discoverServices(bleDevice)
|
||||
}
|
||||
|
||||
fun readIaq() {
|
||||
connectionManager.readChar(bleDevice, SERVICE_UUID, CHAR_UUID)
|
||||
}
|
||||
|
||||
|
||||
override fun onSuccessfulCharRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic
|
||||
@@ -72,7 +79,9 @@ class KirbyDevice(
|
||||
statuses.add(DeviceStatus.CONNECTED)
|
||||
statuses.remove(DeviceStatus.MISSING)
|
||||
|
||||
subscribe()
|
||||
onStateChange(this)
|
||||
|
||||
}
|
||||
|
||||
override fun onConnectToBondedFailed(gatt: BluetoothGatt) {
|
||||
@@ -113,6 +122,7 @@ class KirbyDevice(
|
||||
override fun onBonded(device: BluetoothDevice) {
|
||||
statuses.add(DeviceStatus.BONDED)
|
||||
onStateChange(this)
|
||||
connect()
|
||||
}
|
||||
|
||||
override fun onUnbonded(device: BluetoothDevice) {
|
||||
@@ -136,12 +146,13 @@ class KirbyDevice(
|
||||
if (measurement == null) {
|
||||
payload = Payload(hexPayload)
|
||||
} else {
|
||||
measurement.deviceId = bleDevice.address
|
||||
payload = Payload(measurement.toString())
|
||||
Log.i("BleListener", "Char received: $payload")
|
||||
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
|
||||
publishMeasurement(base64Payload)
|
||||
|
||||
loggerDb.writeLog( measurement)
|
||||
loggerDb.writeLog(measurement)
|
||||
}
|
||||
|
||||
measurements.add(payload)
|
||||
@@ -155,6 +166,11 @@ class KirbyDevice(
|
||||
private fun publishMeasurement(payload: String) {
|
||||
val accessKey = BuildConfig.API_KEY
|
||||
val url = BuildConfig.API_BASE_URL
|
||||
|
||||
if(url.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val eui = "0000${bleDevice.address.replace(":", "")}"
|
||||
|
||||
val postData = JSONObject()
|
||||
@@ -346,7 +362,7 @@ class KirbyDevice(
|
||||
})
|
||||
}
|
||||
|
||||
return actions;
|
||||
return actions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,7 +398,7 @@ private fun payloadToMeasurements(payload: Payload): List<Measurement> {
|
||||
return payload.payload
|
||||
}
|
||||
|
||||
override fun getIcon(): Int? {
|
||||
override fun getIcon(): Int {
|
||||
return R.drawable.baseline_numbers_24
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.sensortestingapp
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
@@ -15,22 +15,33 @@ import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
|
||||
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1
|
||||
@@ -57,7 +68,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var loggerDb: LoggerContract.LoggerDb
|
||||
private lateinit var createFileLauncher: ActivityResultLauncher<String>
|
||||
|
||||
private val bondedDevices = HashSet<String>()
|
||||
|
||||
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
@@ -69,7 +80,7 @@ class MainActivity : AppCompatActivity() {
|
||||
newConnectionManager()
|
||||
}
|
||||
|
||||
private val kirbyDevices = mutableListOf<DeviceListEntry>();
|
||||
private val kirbyDevices = mutableListOf<DeviceListEntry>()
|
||||
|
||||
private val deviceListAdapter: DeviceListAdapter by lazy {
|
||||
DeviceListAdapter(kirbyDevices)
|
||||
@@ -108,12 +119,40 @@ class MainActivity : AppCompatActivity() {
|
||||
setupDevicesList()
|
||||
|
||||
createFileLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri: Uri? ->
|
||||
registerForActivityResult(CreateDocument("text/csv")) { uri: Uri? ->
|
||||
uri?.let {
|
||||
// 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)
|
||||
}
|
||||
|
||||
private fun toggleScanning(): Unit {
|
||||
@@ -156,11 +195,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun addBondedDevices(): Unit {
|
||||
bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach {
|
||||
val kirbyDevice = newKirbyDevice(it)
|
||||
kirbyDevice.subscribe()
|
||||
bondedDevices.add(it.address)
|
||||
newKirbyDevice(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun addDummyDevices() {
|
||||
for (i in 0..14) {
|
||||
@@ -174,7 +214,7 @@ class MainActivity : AppCompatActivity() {
|
||||
mngr.register(object : BleListener(null) {
|
||||
override fun onScanningStateChange(isScanning: Boolean) {
|
||||
runOnUiThread {
|
||||
binding.fab.setText(if (isScanning) "Stop Scan" else "Start Scan")
|
||||
binding.fab.text = if (isScanning) "Stop Scan" else "Start Scan"
|
||||
binding.fab.setIconResource(
|
||||
if (isScanning) R.drawable.action_icon_disconnect else R.drawable.action_icon_scan
|
||||
)
|
||||
@@ -183,7 +223,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
if (isKirbyDevice(result.device)) {
|
||||
if (isKirbyDevice(result.device) && result.isConnectable) {
|
||||
|
||||
Log.i(
|
||||
"ScanCallback",
|
||||
@@ -191,7 +231,16 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
val kirbyDevice = kirbyDevices.find { it.address == result.device.address }
|
||||
if (kirbyDevice == null) {
|
||||
newKirbyDevice(result.device).onScanResult(callbackType, result)
|
||||
val kirby = newKirbyDevice(result.device)
|
||||
kirby.onScanResult(callbackType, result)
|
||||
}
|
||||
|
||||
if (bondedDevices.contains(result.device.address)) {
|
||||
Log.i("KirbyDevice", "Connecting to " + result.device.address)
|
||||
|
||||
connectionManager.connect(result.device)
|
||||
connectionManager.readRemoteRssi(result.device)
|
||||
connectionManager.discoverServices(result.device)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -200,7 +249,9 @@ class MainActivity : AppCompatActivity() {
|
||||
return mngr
|
||||
}
|
||||
|
||||
private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice {
|
||||
private fun newKirbyDevice(
|
||||
bleDevice: BluetoothDevice
|
||||
): KirbyDevice {
|
||||
|
||||
val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb) {
|
||||
val i = kirbyDevices.indexOfFirst { d -> d === it }
|
||||
@@ -208,9 +259,9 @@ class MainActivity : AppCompatActivity() {
|
||||
deviceListAdapter.notifyItemChanged(i)
|
||||
}
|
||||
}
|
||||
Log.i("MainActivity", bleDevice.address)
|
||||
connectionManager.register(device)
|
||||
connectionManager.connect(bleDevice)
|
||||
connectionManager.discoverServices(bleDevice)
|
||||
|
||||
kirbyDevices.add(device)
|
||||
deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1)
|
||||
return device
|
||||
@@ -236,6 +287,22 @@ class MainActivity : AppCompatActivity() {
|
||||
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
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -407,7 +474,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
|
||||
return "Test action 1"
|
||||
}
|
||||
|
||||
override fun getIcon(): Int? {
|
||||
override fun getIcon(): Int {
|
||||
return R.drawable.action_icon_disconnect
|
||||
}
|
||||
|
||||
@@ -419,7 +486,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
|
||||
return "Test action 2"
|
||||
}
|
||||
|
||||
override fun getIcon(): Int? {
|
||||
override fun getIcon(): Int {
|
||||
return R.drawable.action_icon_connect
|
||||
}
|
||||
|
||||
@@ -439,7 +506,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
|
||||
return "21.2 °C"
|
||||
}
|
||||
|
||||
override fun getIcon(): Int? {
|
||||
override fun getIcon(): Int {
|
||||
return R.drawable.baseline_device_thermostat_24
|
||||
}
|
||||
}, object : Measurement {
|
||||
@@ -451,7 +518,7 @@ class DummyListEntry(override val address: String) : DeviceListEntry {
|
||||
return "232 bar"
|
||||
}
|
||||
|
||||
override fun getIcon(): Int? {
|
||||
override fun getIcon(): Int {
|
||||
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.util.AttributeSet;
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.example.sensortestingapp.NonScrollListView
|
||||
<com.logitech.vc.kirbytest.NonScrollListView
|
||||
android:id="@+id/measurement_fields"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="com.example.sensortestingapp.MainActivity" />
|
||||
tools:context="com.logitech.vc.kirbytest.MainActivity" />
|
||||
@@ -1,10 +1,15 @@
|
||||
<menu 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"
|
||||
tools:context="com.example.sensortestingapp.MainActivity">
|
||||
tools:context="com.logitech.vc.kirbytest.MainActivity">
|
||||
<item
|
||||
android:id="@+id/action_export"
|
||||
android:orderInCategory="100"
|
||||
android:title="@string/action_export"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_reset_log"
|
||||
android:orderInCategory="200"
|
||||
android:title="@string/action_reset_log"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
@@ -1,46 +1,11 @@
|
||||
<resources>
|
||||
<string name="app_name">Kirby Test App</string>
|
||||
<string name="action_export">Export to csv</string>
|
||||
<string name="action_reset_log">Reset log</string>
|
||||
<!-- Strings used for fragments for navigation -->
|
||||
<string name="first_fragment_label">First Fragment</string>
|
||||
<string name="second_fragment_label">Second Fragment</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="previous">Previous</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>
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.sensortestingapp
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
@@ -12,7 +12,7 @@ import org.junit.Test
|
||||
class DecoderTest {
|
||||
@Test
|
||||
fun message_type_0_decodes_correctly() {
|
||||
val res2 = DecoderIaq.parseMeasurement("006b04ab74a1ed0d101404");
|
||||
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)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class DecoderTest {
|
||||
|
||||
@Test
|
||||
fun message_type_1_decodes_correctly() {
|
||||
val res2 = DecoderIaq.parseMeasurement("106b04ab74a1ed0d10");
|
||||
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)
|
||||
}
|
||||
@@ -12,5 +12,5 @@ dependencyResolutionManagement {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
rootProject.name = "Sensor Testing App"
|
||||
rootProject.name = "Kirby Test App"
|
||||
include ':app'
|
||||
|
||||
Reference in New Issue
Block a user