feat: api integration

This commit is contained in:
Stefan Zollinger
2023-10-24 12:03:00 +02:00
parent f13fccfe1a
commit 8a49edd6fb
8 changed files with 125 additions and 84 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@
.cxx .cxx
local.properties local.properties
deploymentTargetDropDown.xml deploymentTargetDropDown.xml
env.properties

1
.idea/gradle.xml generated
View File

@@ -7,6 +7,7 @@
<option name="testRunner" value="GRADLE" /> <option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <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="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@@ -15,6 +15,13 @@ android {
versionName "1.0" versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
def propsFile = rootProject.file('env.properties')
def props = new Properties()
props.load(new FileInputStream(propsFile))
buildConfigField "String", "API_BASE_URL", "\"${props['apiBaseUrl']}\""
buildConfigField "String", "API_KEY", "\"${props['apiKey']}\""
} }
buildTypes { buildTypes {
@@ -32,6 +39,7 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
} }
@@ -47,4 +55,6 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.2.0'
implementation "com.android.volley:volley:1.2.1"
} }

View File

@@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- Request legacy Bluetooth permissions on versions older than API 31 (Android 12). --> <!-- Request legacy Bluetooth permissions on versions older than API 31 (Android 12). -->
<uses-permission <uses-permission
android:name="android.permission.BLUETOOTH" android:name="android.permission.BLUETOOTH"
@@ -22,6 +23,9 @@
tools:targetApi="s" /> tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"

View File

@@ -7,91 +7,27 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanResult
import android.content.Context
import android.util.Log import android.util.Log
import com.android.volley.Request
import com.android.volley.RequestQueue
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.ByteBuffer
import java.nio.ByteOrder 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.EnumSet import java.util.EnumSet
import java.util.UUID import java.util.UUID
import java.util.stream.Collectors import java.util.stream.Collectors
private val DEMO_SERVICE_UUID = UUID.fromString("00000000-0001-11E1-9AB4-0002A5D5C51B") // Kirby service uuid: 6e400001-b5a3-f393-e1a9-e50e24dcac9e
private val DEMO_CHAR_UUID = UUID.fromString("00140000-0001-11E1-AC36-0002A5D5C51B") private val DEMO_SERVICE_UUID = UUID.fromString("6e400001-b5a3-f393-e1a9-e50e24dcac9e")
private val DEMO_CHAR_UUID = UUID.fromString("6e400005-b5a3-f393-e1a9-e50e24dcac9e")
data class DemoPayload(
val ts: Int,
val pressure: Float,
val temperature: Float,
val sysTs: String = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("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
}
fun decodeDemoPayload(bytes: ByteArray): DemoPayload {
val ts = bytesToUInt16(bytes, 0)
val pressure = bytesToInt32(bytes, 2) / 100.0f
val temp = bytesToInt16(bytes, 6) / 10.0f;
return DemoPayload(ts, pressure, temp)
}
private fun demoPayloadToMeasurements(payload: DemoPayload): List<Measurement> {
return listOf(object : Measurement {
override fun getLabel(): String {
return "TS"
}
override fun getFormattedValue(): String {
return "${payload.sysTs} (${payload.ts})"
}
override fun getIcon(): Int? {
return R.drawable.baseline_access_time_24
}
}, object : Measurement {
override fun getLabel(): String {
return "Temperature"
}
override fun getFormattedValue(): String {
return "${payload.temperature} °C"
}
override fun getIcon(): Int? {
return R.drawable.baseline_device_thermostat_24
}
}, object : Measurement {
override fun getLabel(): String {
return "Pressure"
}
override fun getFormattedValue(): String {
return "${payload.pressure} hPa"
}
override fun getIcon(): Int? {
return R.drawable.baseline_compress_24
}
})
}
enum class DeviceStatus { enum class DeviceStatus {
CONNECTED, BONDED, SUBSCRIBED, MISSING CONNECTED, BONDED, SUBSCRIBED, MISSING
@@ -99,11 +35,13 @@ enum class DeviceStatus {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
class KirbyDevice( class KirbyDevice(
private val context: Context,
private val connectionManager: ConnectionManager, private val connectionManager: ConnectionManager,
private val bleDevice: BluetoothDevice, private val bleDevice: BluetoothDevice,
private val onStateChange: (device: KirbyDevice) -> Unit private val onStateChange: (device: KirbyDevice) -> Unit,
) : BleListener(bleDevice.address), DeviceListEntry {
) : BleListener(bleDevice.address), DeviceListEntry {
private val queue : RequestQueue = Volley.newRequestQueue(context)
override fun onScanResult(callbackType: Int, result: ScanResult) { override fun onScanResult(callbackType: Int, result: ScanResult) {
rssi = result.rssi rssi = result.rssi
@@ -179,13 +117,42 @@ class KirbyDevice(
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) { private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
if (characteristic.service.uuid == DEMO_SERVICE_UUID && characteristic.uuid == DEMO_CHAR_UUID) { if (characteristic.service.uuid == DEMO_SERVICE_UUID && characteristic.uuid == DEMO_CHAR_UUID) {
val payload = decodeDemoPayload(characteristic.value)
val hexPayload = characteristic.value.toHexString()
val payload = Payload(hexPayload)
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
Log.i("BleListener", "Demo char received: $payload") Log.i("BleListener", "Demo char received: $payload")
measurements.add(payload) measurements.add(payload)
publishMeasurement(base64Payload)
} }
} }
private val measurements = ArrayList<DemoPayload>() private fun publishMeasurement(payload: String) {
val accessKey = BuildConfig.API_BASE_URL
val url = BuildConfig.API_KEY
val eui = "0000${bleDevice.address}"
val postData = JSONObject()
try {
postData.put("accessKey", accessKey)
postData.put("metricPayload", payload)
postData.put("eui", eui)
} 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 statuses = EnumSet.noneOf(DeviceStatus::class.java) private val statuses = EnumSet.noneOf(DeviceStatus::class.java)
@@ -223,7 +190,7 @@ class KirbyDevice(
return measurements.size.toString() return measurements.size.toString()
} }
}) })
result.addAll(demoPayloadToMeasurements(latest)) result.addAll(payloadToMeasurements(latest))
return result return result
} }
@@ -365,4 +332,60 @@ class KirbyDevice(
return actions; return actions;
} }
}
data class Payload(
val payload: String,
val ts: String = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("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 "TS"
}
override fun getFormattedValue(): String {
return "${payload.ts}"
}
override fun getIcon(): Int? {
return R.drawable.baseline_access_time_24
}
},
object : Measurement {
override fun getLabel(): String {
return "Payload"
}
override fun getFormattedValue(): String {
return payload.payload
}
override fun getIcon(): Int? {
return null
}
}
)
} }

View File

@@ -103,6 +103,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun isKirbyDevice(device: BluetoothDevice): Boolean { private fun isKirbyDevice(device: BluetoothDevice): Boolean {
Log.i("kby", "found device ${device.name}")
return (device.name ?: "").lowercase().contains("kirby") return (device.name ?: "").lowercase().contains("kirby")
} }
@@ -177,7 +178,8 @@ class MainActivity : AppCompatActivity() {
} }
private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice { private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice {
val device = KirbyDevice(connectionManager, bleDevice) {
val device = KirbyDevice( this.applicationContext, connectionManager, bleDevice) {
val i = kirbyDevices.indexOfFirst { d -> d === it } val i = kirbyDevices.indexOfFirst { d -> d === it }
runOnUiThread { runOnUiThread {
deviceListAdapter.notifyItemChanged(i) deviceListAdapter.notifyItemChanged(i)

View File

@@ -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.0.2' apply false id 'com.android.application' version '8.1.2' apply false
id 'com.android.library' version '8.0.2' apply false id 'com.android.library' version '8.1.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
} }