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

View File

@@ -15,6 +15,13 @@ android {
versionName "1.0"
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 {
@@ -32,6 +39,7 @@ android {
}
buildFeatures {
viewBinding true
buildConfig true
}
}
@@ -47,4 +55,6 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.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"
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"
@@ -22,6 +23,9 @@
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" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@@ -7,91 +7,27 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.le.ScanResult
import android.content.Context
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.ByteOrder
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Base64
import java.util.EnumSet
import java.util.UUID
import java.util.stream.Collectors
private val DEMO_SERVICE_UUID = UUID.fromString("00000000-0001-11E1-9AB4-0002A5D5C51B")
private val DEMO_CHAR_UUID = UUID.fromString("00140000-0001-11E1-AC36-0002A5D5C51B")
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
}
})
}
// Kirby service uuid: 6e400001-b5a3-f393-e1a9-e50e24dcac9e
private val DEMO_SERVICE_UUID = UUID.fromString("6e400001-b5a3-f393-e1a9-e50e24dcac9e")
private val DEMO_CHAR_UUID = UUID.fromString("6e400005-b5a3-f393-e1a9-e50e24dcac9e")
enum class DeviceStatus {
CONNECTED, BONDED, SUBSCRIBED, MISSING
@@ -99,11 +35,13 @@ enum class DeviceStatus {
@SuppressLint("MissingPermission")
class KirbyDevice(
private val context: Context,
private val connectionManager: ConnectionManager,
private val bleDevice: BluetoothDevice,
private val onStateChange: (device: KirbyDevice) -> Unit
) : BleListener(bleDevice.address), DeviceListEntry {
private val onStateChange: (device: KirbyDevice) -> Unit,
) : BleListener(bleDevice.address), DeviceListEntry {
private val queue : RequestQueue = Volley.newRequestQueue(context)
override fun onScanResult(callbackType: Int, result: ScanResult) {
rssi = result.rssi
@@ -179,13 +117,42 @@ class KirbyDevice(
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
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")
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)
@@ -223,7 +190,7 @@ class KirbyDevice(
return measurements.size.toString()
}
})
result.addAll(demoPayloadToMeasurements(latest))
result.addAll(payloadToMeasurements(latest))
return result
}
@@ -365,4 +332,60 @@ class KirbyDevice(
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 {
Log.i("kby", "found device ${device.name}")
return (device.name ?: "").lowercase().contains("kirby")
}
@@ -177,7 +178,8 @@ class MainActivity : AppCompatActivity() {
}
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 }
runOnUiThread {
deviceListAdapter.notifyItemChanged(i)