diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..8ced4d2 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdk 34 + namespace 'com.saltylab.uwbtag' + + defaultConfig { + applicationId "com.saltylab.uwbtag" + minSdk 26 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + } + } + + buildFeatures { + viewBinding true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation 'com.google.code.gson:gson:2.10.1' +} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7c9f530 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/src/main/kotlin/com/saltylab/uwbtag/UwbTagBleActivity.kt b/android/src/main/kotlin/com/saltylab/uwbtag/UwbTagBleActivity.kt new file mode 100644 index 0000000..e7dfdf6 --- /dev/null +++ b/android/src/main/kotlin/com/saltylab/uwbtag/UwbTagBleActivity.kt @@ -0,0 +1,444 @@ +package com.saltylab.uwbtag + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.* +import android.bluetooth.le.* +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.card.MaterialCardView +import com.google.android.material.switchmaterial.SwitchMaterial +import com.google.android.material.textfield.TextInputEditText +import com.google.gson.Gson +import com.saltylab.uwbtag.databinding.ActivityUwbTagBleBinding +import java.util.UUID + +// --------------------------------------------------------------------------- +// GATT service / characteristic UUIDs +// --------------------------------------------------------------------------- +private val SERVICE_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef0") +private val CHAR_CONFIG_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef1") // read/write JSON config +private val CHAR_STATUS_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef2") // notify: tag status string +private val CHAR_BATT_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef3") // notify: battery % +private val CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") + +// BLE scan timeout +private const val SCAN_TIMEOUT_MS = 15_000L + +// Permissions request code +private const val REQ_PERMISSIONS = 1001 + +// --------------------------------------------------------------------------- +// Data model +// --------------------------------------------------------------------------- +data class TagConfig( + val tag_name: String = "UWB_TAG_0001", + val sleep_timeout_s: Int = 300, + val display_brightness: Int = 50, + val uwb_channel: Int = 9, + val ranging_interval_ms: Int = 100, + val battery_report: Boolean = true +) + +data class ScannedDevice( + val name: String, + val address: String, + var rssi: Int, + val device: BluetoothDevice +) + +// --------------------------------------------------------------------------- +// RecyclerView adapter for scanned devices +// --------------------------------------------------------------------------- +class DeviceAdapter( + private val onConnect: (ScannedDevice) -> Unit +) : RecyclerView.Adapter() { + + private val items = mutableListOf() + + fun update(device: ScannedDevice) { + val idx = items.indexOfFirst { it.address == device.address } + if (idx >= 0) { + items[idx] = device + notifyItemChanged(idx) + } else { + items.add(device) + notifyItemInserted(items.size - 1) + } + } + + fun clear() { + items.clear() + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_ble_device, parent, false) + return VH(view) + } + + override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(items[position]) + override fun getItemCount() = items.size + + inner class VH(view: View) : RecyclerView.ViewHolder(view) { + private val tvName = view.findViewById(R.id.tvDeviceName) + private val tvAddress = view.findViewById(R.id.tvDeviceAddress) + private val tvRssi = view.findViewById(R.id.tvRssi) + private val btnConn = view.findViewById