feat: api integration
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@
|
|||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
deploymentTargetDropDown.xml
|
deploymentTargetDropDown.xml
|
||||||
|
env.properties
|
||||||
|
|||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -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
2
.idea/misc.xml
generated
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user