Compare commits
7 Commits
main
...
548032a741
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
548032a741 | ||
|
|
49166d1710 | ||
|
|
897109bfa9 | ||
|
|
9a20f429ea | ||
|
|
7905301e58 | ||
|
|
8721af1482 | ||
|
|
dccecd6417 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,5 +14,4 @@
|
||||
.cxx
|
||||
local.properties
|
||||
deploymentTargetDropDown.xml
|
||||
env.properties
|
||||
app/debug
|
||||
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -5,11 +5,11 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace 'com.logitech.vc.kirbytest'
|
||||
compileSdk 32
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.logitech.vc.kirbytest"
|
||||
minSdk 29
|
||||
minSdk 27
|
||||
targetSdk 32
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
@@ -31,6 +31,29 @@ android {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
applicationVariants.all{
|
||||
variant ->
|
||||
variant.outputs.each{
|
||||
output->
|
||||
// on below line we are specifying our app name.
|
||||
project.ext { appName = 'kirbyTestApp' }
|
||||
// on below line we are adding the formatted date to our apk file name.
|
||||
def formattedDate = new Date().format('yyyyMMdd')
|
||||
// on below line we are creating a new name for our apk.
|
||||
def newName = output.outputFile.name
|
||||
// on below line we are replacing our previous name with our app name.
|
||||
newName = newName.replace("app-", "$project.ext.appName-")
|
||||
// 當build type為 debug時觸發
|
||||
// on below line we are replacing -debug with our formatted date.
|
||||
newName = newName.replace("-debug", "-debug-" + formattedDate)
|
||||
// 當build type為 release時觸發
|
||||
// on below line we are replacing -release with our formatted date.
|
||||
newName = newName.replace("-release", "-release-" + formattedDate)
|
||||
// at last we are setting our apk name to it.
|
||||
output.outputFileName = newName
|
||||
}
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@@ -53,6 +76,7 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'
|
||||
implementation 'androidx.activity:activity:1.8.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
@@ -64,5 +88,4 @@ dependencies {
|
||||
implementation group: 'commons-codec', name: 'commons-codec', version: '1.16.0'
|
||||
|
||||
implementation "com.android.volley:volley:1.2.1"
|
||||
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
|
||||
<!-- Request legacy Bluetooth permissions on versions older than API 31 (Android 12). -->
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
@@ -16,16 +14,12 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||
android:maxSdkVersion="30" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH_SCAN"
|
||||
android:usesPermissionFlags="neverForLocation"
|
||||
tools:targetApi="s" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@@ -35,10 +29,15 @@
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SensorTestingApp"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ServerSettingActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -49,8 +48,10 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service android:name=".BLEService" android:foregroundServiceType="connectedDevice" />
|
||||
|
||||
<service
|
||||
android:name=".BLEService"
|
||||
android:foregroundServiceType="connectedDevice" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -18,7 +18,6 @@ import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.bluetooth.le.ScanSettings
|
||||
import android.bluetooth.le.ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -211,9 +210,10 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
|
||||
private val scanSettings = ScanSettings.Builder()
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
|
||||
.build()
|
||||
|
||||
private val scanFilters = listOf( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BuildConfig.SERVICE_UUID)).build())
|
||||
|
||||
private val bleScanner by lazy {
|
||||
bleAdapter.bluetoothLeScanner
|
||||
}
|
||||
@@ -294,6 +294,10 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
@SuppressLint("MissingPermission")
|
||||
val scanCallback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
Log.d(
|
||||
"ScanCallback",
|
||||
"Found BLE device with address ${result.device.address} (name: ${result.device.name}, rssi: ${result.rssi})"
|
||||
)
|
||||
notifyListeners(result.device.address) { it.onScanResult(callbackType, result) }
|
||||
}
|
||||
|
||||
@@ -310,8 +314,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
fun startScan() {
|
||||
if (!isScanning) {
|
||||
isScanning = true
|
||||
val filter = ScanFilter.Builder().setDeviceName("KIRBY").build()
|
||||
bleScanner.startScan(listOf(filter), scanSettings, scanCallback)
|
||||
bleScanner.startScan( null, scanSettings, scanCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,9 +353,6 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
}
|
||||
|
||||
fun discoverServices(device: BluetoothDevice) {
|
||||
if(operationQueue.any { it.device.address === device.address && it is DiscoverServicesRequest }) {
|
||||
return
|
||||
}
|
||||
enqueueOperation(DiscoverServicesRequest(device))
|
||||
}
|
||||
|
||||
@@ -377,9 +377,6 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
}
|
||||
|
||||
fun readRemoteRssi(device: BluetoothDevice) {
|
||||
if(operationQueue.any { it.device.address === device.address && it is ReadRemoteRssi }) {
|
||||
return
|
||||
}
|
||||
enqueueOperation(ReadRemoteRssi(device))
|
||||
}
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ object LoggerContract {
|
||||
|
||||
files[deviceId] = file
|
||||
|
||||
Log.i(tag, file.absolutePath)
|
||||
file.setReadable(true, false)
|
||||
file
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
object DecoderIaq {
|
||||
private const val INVALID_CO2 = 16383
|
||||
private const val INVALID_VOC = 2047
|
||||
@@ -14,70 +17,18 @@ object DecoderIaq {
|
||||
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? {
|
||||
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)) {
|
||||
if(!supportedMessageTypes.contains(msgType)) {
|
||||
Log.i("Decoder", "Invalid message type: $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
|
||||
@@ -115,16 +66,28 @@ object DecoderIaq {
|
||||
return measurement
|
||||
}
|
||||
|
||||
|
||||
interface Uplink {
|
||||
var deviceId: String?
|
||||
var msgType: Number?
|
||||
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
|
||||
}
|
||||
|
||||
data class Measurement(
|
||||
private fun hexStringToByteArray(encoded: String): ByteArray {
|
||||
return try {
|
||||
Hex.decodeHex(encoded)
|
||||
} catch (e: DecoderException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
data class Measurement (
|
||||
@SerializedName("bleAddress")
|
||||
override var deviceId: String? = null,
|
||||
override var msgType: Number? = null,
|
||||
var deviceId: String? = null,
|
||||
var msgType: Number? = null,
|
||||
var co2: Number? = null,
|
||||
var voc: Number? = null,
|
||||
var humidity: Number? = null,
|
||||
@@ -133,7 +96,9 @@ object DecoderIaq {
|
||||
var occupancy: Number? = null,
|
||||
var pm25: Number? = null,
|
||||
var pm10: Number? = null
|
||||
) : Uplink {
|
||||
) {
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "M{" +
|
||||
"type=" + msgType +
|
||||
@@ -147,35 +112,6 @@ object DecoderIaq {
|
||||
", 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 +
|
||||
'}'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ class DeviceListAdapter(
|
||||
}
|
||||
}
|
||||
val inflater = popup.menuInflater
|
||||
popup.setForceShowIcon(true)
|
||||
// popup.setForceShowIcon(true)
|
||||
inflater.inflate(R.menu.device_menu, popup.menu)
|
||||
popup.show()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import SettingsRepository
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothDevice.BOND_BONDED
|
||||
@@ -16,6 +15,8 @@ import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Base64
|
||||
@@ -33,22 +34,15 @@ enum class DeviceStatus {
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class KirbyDevice(
|
||||
|
||||
private val context: Context,
|
||||
private val connectionManager: ConnectionManager,
|
||||
private val bleDevice: BluetoothDevice,
|
||||
private val loggerDb: LoggerContract.LoggerDb,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val onStateChange: (device: KirbyDevice) -> Unit,
|
||||
|
||||
|
||||
|
||||
) : BleListener(bleDevice.address), DeviceListEntry {
|
||||
private val tag = "KirbyDevice"
|
||||
private var lastSeen: Long = 0
|
||||
private val queue: RequestQueue = Volley.newRequestQueue(context)
|
||||
private val reconnectionDelayMs = 10_000
|
||||
private val settings = settingsRepository.getSettings()
|
||||
|
||||
fun subscribe() {
|
||||
if(statuses.contains(DeviceStatus.CONNECTED)) {
|
||||
@@ -59,14 +53,8 @@ class KirbyDevice(
|
||||
}
|
||||
|
||||
fun connect() {
|
||||
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)
|
||||
}
|
||||
connectionManager.connect(bleDevice)
|
||||
connectionManager.discoverServices(bleDevice)
|
||||
}
|
||||
|
||||
fun readIaq() {
|
||||
@@ -78,9 +66,7 @@ class KirbyDevice(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic
|
||||
) {
|
||||
|
||||
addMeasurement(characteristic)
|
||||
|
||||
onStateChange(this)
|
||||
}
|
||||
|
||||
@@ -107,18 +93,10 @@ class KirbyDevice(
|
||||
statuses.remove(DeviceStatus.CONNECTED)
|
||||
statuses.remove(DeviceStatus.SUBSCRIBED)
|
||||
onStateChange(this)
|
||||
Log.i(tag, "Disconnected")
|
||||
}
|
||||
|
||||
override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||
addMeasurement(characteristic)
|
||||
lastSeen = System.currentTimeMillis()
|
||||
|
||||
if(settings.lowPowerMode){
|
||||
Log.i(tag, "Received data, closing connection")
|
||||
connectionManager.teardownConnection(bleDevice)
|
||||
}
|
||||
|
||||
onStateChange(this)
|
||||
}
|
||||
|
||||
@@ -164,7 +142,7 @@ class KirbyDevice(
|
||||
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
|
||||
val hexPayload = characteristic.value.toHexString().substring(2)
|
||||
val measurement = DecoderIaq.parseMeasurement(hexPayload)
|
||||
val payload : Payload
|
||||
var payload : Payload
|
||||
if (measurement == null) {
|
||||
payload = Payload(hexPayload)
|
||||
} else {
|
||||
@@ -172,7 +150,8 @@ class KirbyDevice(
|
||||
payload = Payload(measurement.toString())
|
||||
Log.i("BleListener", "Char received: $payload")
|
||||
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
|
||||
publishMeasurement(base64Payload)
|
||||
// publishMeasurement(base64Payload)
|
||||
publishMeasurementAutoServer(measurement)
|
||||
|
||||
loggerDb.writeLog(measurement)
|
||||
}
|
||||
@@ -186,10 +165,10 @@ class KirbyDevice(
|
||||
}
|
||||
|
||||
private fun publishMeasurement(payload: String) {
|
||||
val accessKey = settings.apiKey
|
||||
val url = settings.apiUrl
|
||||
val accessKey = BuildConfig.API_KEY
|
||||
val url = BuildConfig.API_BASE_URL
|
||||
|
||||
if(url.isEmpty() || accessKey.isEmpty()) {
|
||||
if(url.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -216,6 +195,50 @@ class KirbyDevice(
|
||||
queue.add(request)
|
||||
}
|
||||
|
||||
private fun publishMeasurementAutoServer(measurement: DecoderIaq.Measurement) {
|
||||
// Read url from SharedPreferences
|
||||
val sharedPref = context.getSharedPreferences(context.getString(R.string.app_name), Context.MODE_PRIVATE)
|
||||
val url = sharedPref.getString(
|
||||
"kirby_data_post_url",
|
||||
context.getString(R.string.kirby_data_post_url_default)) ?: context.getString(R.string.kirby_data_post_url_default)
|
||||
val accessKey = BuildConfig.API_KEY
|
||||
|
||||
if(url.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val eui = "0000${bleDevice.address.replace(":", "")}"
|
||||
|
||||
val postData = JSONObject()
|
||||
|
||||
try {
|
||||
// Log.i("POST", "Transmitting for $eui: $payload")
|
||||
postData.put("accessKey", "${accessKey}_fromAndroid")
|
||||
postData.put("eui", eui)
|
||||
postData.put("deviceId", measurement.deviceId)
|
||||
postData.put("msgType", measurement.msgType)
|
||||
postData.put("co2", measurement.co2)
|
||||
postData.put("voc", measurement.voc)
|
||||
postData.put("humidity", measurement.humidity)
|
||||
postData.put("temperature", measurement.temperature)
|
||||
postData.put("pressure", measurement.pressure)
|
||||
postData.put("occupancy", measurement.occupancy)
|
||||
postData.put("pm25", measurement.pm25)
|
||||
postData.put("pm10", measurement.pm10)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val request = JsonObjectRequest(
|
||||
Request.Method.POST, url, postData,
|
||||
{ response ->
|
||||
Log.i("sendDataResponse", "Response is: $response")
|
||||
}
|
||||
) { error -> error.printStackTrace() }
|
||||
|
||||
queue.add(request)
|
||||
}
|
||||
|
||||
private val measurements = ArrayList<Payload>()
|
||||
private val maxMeasurements = 20
|
||||
|
||||
@@ -394,11 +417,26 @@ data class Payload(
|
||||
.format(DateTimeFormatter.ofPattern("dd.MM.yy HH:mm:ss"))
|
||||
)
|
||||
|
||||
fun bytesToUInt16(arr: ByteArray, start: Int): Int {
|
||||
return ByteBuffer.wrap(arr, start, 2)
|
||||
.order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF
|
||||
}
|
||||
|
||||
fun bytesToInt16(arr: ByteArray, start: Int): Short {
|
||||
return ByteBuffer.wrap(arr, start, 2)
|
||||
.order(ByteOrder.LITTLE_ENDIAN).short
|
||||
}
|
||||
|
||||
fun bytesToInt32(arr: ByteArray, start: Int): Int {
|
||||
return ByteBuffer.wrap(arr, start, 4)
|
||||
.order(ByteOrder.LITTLE_ENDIAN).int
|
||||
}
|
||||
|
||||
|
||||
private fun payloadToMeasurements(payload: Payload): List<Measurement> {
|
||||
return listOf(object : Measurement {
|
||||
override fun getLabel(): String {
|
||||
return payload.ts
|
||||
return payload.ts.toString()
|
||||
}
|
||||
|
||||
override fun getFormattedValue(): String {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import SettingsRepository
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
@@ -9,11 +8,9 @@ import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -23,7 +20,6 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
@@ -37,9 +33,9 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
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 settingsDialog
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Timer
|
||||
@@ -71,7 +67,6 @@ class MainActivity : AppCompatActivity() {
|
||||
private lateinit var loggerDb: LoggerContract.LoggerDb
|
||||
private lateinit var createFileLauncher: ActivityResultLauncher<String>
|
||||
private val bondedDevices = HashSet<String>()
|
||||
private lateinit var settings: SettingsRepository
|
||||
|
||||
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
@@ -83,7 +78,7 @@ class MainActivity : AppCompatActivity() {
|
||||
newConnectionManager()
|
||||
}
|
||||
|
||||
private val kirbyDevices = mutableListOf<KirbyDevice>()
|
||||
private val kirbyDevices = mutableListOf<DeviceListEntry>()
|
||||
|
||||
private val deviceListAdapter: DeviceListAdapter by lazy {
|
||||
DeviceListAdapter(kirbyDevices)
|
||||
@@ -96,7 +91,7 @@ class MainActivity : AppCompatActivity() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
settings = SettingsRepository(applicationContext)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
@@ -139,39 +134,23 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRequiredRuntimePermissions()) {
|
||||
connectionManager.startScan()
|
||||
/*
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
delay(5000L)
|
||||
connectionManager.stopScan()
|
||||
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 {
|
||||
@@ -182,21 +161,6 @@ 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 {
|
||||
val deviceName = (device.name ?: "").lowercase()
|
||||
return deviceName.contains("kirby") || deviceName.contains("krby")
|
||||
@@ -228,7 +192,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun addBondedDevices(): Unit {
|
||||
bondedDevices.clear()
|
||||
bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach {
|
||||
bondedDevices.add(it.address)
|
||||
newKirbyDevice(it)
|
||||
@@ -236,6 +199,14 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun addDummyDevices() {
|
||||
for (i in 0..14) {
|
||||
kirbyDevices.add(DummyListEntry("$i"))
|
||||
}
|
||||
deviceListAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun newConnectionManager(): ConnectionManager {
|
||||
val mngr = ConnectionManager(applicationContext, bluetoothAdapter)
|
||||
mngr.register(object : BleListener(null) {
|
||||
@@ -256,13 +227,20 @@ class MainActivity : AppCompatActivity() {
|
||||
"ScanCallback",
|
||||
"Found Kirby device with name ${result.device.name} (address: ${result.device.address}, rssi: ${result.rssi})"
|
||||
)
|
||||
var kirbyDevice = kirbyDevices.find { it.address == result.device.address }
|
||||
val kirbyDevice = kirbyDevices.find { it.address == result.device.address }
|
||||
if (kirbyDevice == null) {
|
||||
kirbyDevice = newKirbyDevice(result.device)
|
||||
kirbyDevice.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)
|
||||
}
|
||||
|
||||
kirbyDevice.connect()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -273,13 +251,12 @@ class MainActivity : AppCompatActivity() {
|
||||
bleDevice: BluetoothDevice
|
||||
): KirbyDevice {
|
||||
|
||||
val device =
|
||||
KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb, settings) {
|
||||
val i = kirbyDevices.indexOfFirst { d -> d === it }
|
||||
runOnUiThread {
|
||||
deviceListAdapter.notifyItemChanged(i)
|
||||
}
|
||||
val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice, loggerDb) {
|
||||
val i = kirbyDevices.indexOfFirst { d -> d === it }
|
||||
runOnUiThread {
|
||||
deviceListAdapter.notifyItemChanged(i)
|
||||
}
|
||||
}
|
||||
Log.i("MainActivity", bleDevice.address)
|
||||
connectionManager.register(device)
|
||||
|
||||
@@ -324,9 +301,10 @@ class MainActivity : AppCompatActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_settings -> {
|
||||
val settingsDialog = settingsDialog(this, settings)
|
||||
settingsDialog.show()
|
||||
R.id.action_server_setting -> {
|
||||
// Goto server setting intent
|
||||
val intent = Intent(this, ServerSettingActivity::class.java)
|
||||
startActivity(intent)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -344,8 +322,6 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onDestroy() {
|
||||
loggerDb.close()
|
||||
super.onDestroy()
|
||||
|
||||
unregisterReceiver(mReceiver);
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
class ServerSettingActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_server_setting)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
// Get current server setting from SharedPreferences
|
||||
val sharedPref = getSharedPreferences(getString(R.string.app_name), MODE_PRIVATE)
|
||||
var currentUrl = sharedPref.getString(
|
||||
"kirby_data_post_url",
|
||||
getString(R.string.kirby_data_post_url_default)) ?: getString(R.string.kirby_data_post_url_default)
|
||||
val editTextServerSetting = findViewById<EditText>(R.id.editTextServerSetting)
|
||||
editTextServerSetting.setText(currentUrl)
|
||||
|
||||
val doneButton = findViewById<Button>(R.id.buttonDone)
|
||||
doneButton.setOnClickListener {
|
||||
// Save editTextServerSetting to SharedPreferences
|
||||
val serverSetting = editTextServerSetting.text.toString()
|
||||
sharedPref.edit().putString("kirby_data_post_url", serverSetting).apply()
|
||||
finish()
|
||||
}
|
||||
|
||||
val cancelButton = findViewById<Button>(R.id.buttonCancel)
|
||||
cancelButton.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
val defaultButton = findViewById<Button>(R.id.buttonDefault)
|
||||
defaultButton.setOnClickListener {
|
||||
editTextServerSetting.setText(getString(R.string.kirby_data_post_url_default))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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:layout_width="150dp"
|
||||
android:layout_height="match_parent"
|
||||
android:text='@string/start_scan'
|
||||
android:text='Start Scan'
|
||||
app:icon="@drawable/action_icon_scan"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
56
app/src/main/res/layout/activity_server_setting.xml
Normal file
56
app/src/main/res/layout/activity_server_setting.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="10dp"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/main">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewServerSetting"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/server_url"
|
||||
android:textSize="24sp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="20dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editTextServerSetting"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="@string/kirby_data_post_url_default"
|
||||
android:hint="@string/kirby_data_post_url_default" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonDone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/done"
|
||||
android:layout_marginEnd="10dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonCancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="5dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonDefault"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/defaultString"
|
||||
android:layout_marginStart="10dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,62 +0,0 @@
|
||||
<?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>
|
||||
@@ -13,8 +13,8 @@
|
||||
android:title="@string/action_reset_log"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="300"
|
||||
android:title="@string/settings"
|
||||
android:id="@+id/action_server_setting"
|
||||
android:orderInCategory="200"
|
||||
android:title="@string/action_server_setting"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
@@ -2,16 +2,16 @@
|
||||
<string name="app_name">Kirby Test App</string>
|
||||
<string name="action_export">Export to csv</string>
|
||||
<string name="action_reset_log">Reset log</string>
|
||||
<string name="action_server_setting">Server Setting</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="settings">Settings</string>
|
||||
<string name="api_url">API url</string>
|
||||
<string name="api_key">API key</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="server_url">Server URL</string>
|
||||
<string name="kirby_data_post_url_default">http://vc-automation.logitech.com/api/kirby/addData</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="start_scan">Start Scan</string>
|
||||
<string name="defaultString">Default</string>
|
||||
|
||||
</resources>
|
||||
7
app/src/main/res/xml/network_security_config.xml
Normal file
7
app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true" />
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">192.168.1.154:5000</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -24,27 +24,4 @@ class DecoderTest {
|
||||
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.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.5.2' apply false
|
||||
id 'com.android.library' version '8.5.2' apply false
|
||||
id 'com.android.application' version '8.1.2' apply false
|
||||
id 'com.android.library' version '8.1.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
|
||||
}
|
||||
2
env.properties
Normal file
2
env.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
apiBaseUrl=http://vc-automation.logitech.com/teststation/kirby/addData
|
||||
apiKey=CRa04Bk97VWFnnHOCDhyBuAz1a9L69Eu
|
||||
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
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user