Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# Generated binary assets
__pycache__/
gradle/wrapper/gradle-wrapper.jar
# Gradle and build outputs
.gradle/
build/
app/build/

# Local Android Studio config
.idea/
*.iml
local.properties

# Generated assets from bootstrap
app/src/main/assets/
app/src/main/res/drawable/*.png

# OS cruft
.DS_Store
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
# DroneDetectAndroid

This repo hosts Android-side utilities for drone rotor detection.
Binary assets (TensorFlow Lite model and icons) are stored in base64 to
keep the repo lean for air-gapped deployments.
Android-side utilities for rotor detection and Wi‑Fi reconnaissance. Assets ship as base64 to keep the repo lean for air-gapped deployments and are restored automatically during Gradle builds.

## Bootstrap

After cloning, restore the binary assets:
After cloning, restore the binary assets (TensorFlow Lite model and PNG drawables):

```bash
python3 scripts/bootstrap_assets.py
```

This will recreate the `rotor_v1.tflite` model and the required PNG
drawables under `app/src/main/...`.
Gradle also depends on this script via the `preBuild` hook, so IDE and CI builds will automatically decode assets into `app/src/main/...` if Python is available.

## Pipeline

1. **Asset bootstrap** – Decode the model and icons from `assets/*.b64` using the script above. Generated files land under `app/src/main/...`.
2. **Runtime wiring** – `MainActivity` creates a `DroneSignalDetector`, which launches a coroutine to fetch flight data and schedule Wi‑Fi scans.
3. **Scan handling** – `WifiScanReceiver` receives scan results. Future work will feed the TFLite model for rotor classification.
1. **Asset bootstrap** – Decode the model and icons from `assets/*.b64` using the script above (or let Gradle call it).
2. **Runtime wiring** – `MainActivity` requests Wi‑Fi/location permissions, wires a simple UI, and calls `DroneSignalDetector.startScan()`.
3. **Scan handling** – `DroneSignalDetector` schedules repeated `WifiManager.startScan()` calls, ingests stub flight data, and surfaces scan results plus the last telemetry snapshot to the UI via callbacks.

## Building

This repo ships a minimal Gradle wrapper script for offline/air-gapped work. Supply a compatible `gradle/wrapper/gradle-wrapper.jar` (from Gradle 8.2.1) in your environment, then run:

```bash
./gradlew assembleDebug
```

If you prefer a hosted toolchain, open the project in Android Studio (Giraffe+), accept SDK prompts, and build the `app` module. Remember to run the asset bootstrap script if Python is missing from your build host.

## Testing

Verify the asset bootstrap step restores the binaries:
To validate the asset bootstrap step:

```bash
python3 scripts/bootstrap_assets.py
```

Each asset should report a `decoded` message. Remove the generated `app/src/main/assets` and `app/src/main/res` directories after testing to keep the repo text‑only.
Each asset should report a `decoded` message. Remove the generated `app/src/main/assets` and `app/src/main/res/drawable` contents after testing to keep the repo text‑only.
68 changes: 68 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
plugins {
id "com.android.application"
id "org.jetbrains.kotlin.android"
}

android {
namespace "com.example.dronedetect"
compileSdk = 34

defaultConfig {
applicationId = "com.example.dronedetect"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
isMinifyEnabled = false
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}

buildFeatures {
viewBinding = true
}

tasks.named("preBuild") {
dependsOn("bootstrapAssets")
}
}

val bootstrapAssets by tasks.registering { task ->
task.group = "assets"
task.description = "Decode bundled base64 assets into their binary forms"
task.doLast {
exec {
commandLine("python3", rootProject.file("scripts/bootstrap_assets.py").absolutePath)
}
}
}

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.constraintlayout:constraintlayout:2.1.4")
}
1 change: 1 addition & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Keep file intentionally minimal; add rules when obfuscation is enabled.
34 changes: 34 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.dronedetect">

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission
android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"
android:required="false" />

<uses-feature android:name="android.hardware.wifi" android:required="true" />

<application
android:allowBackup="true"
android:icon="@drawable/drone_icon"
android:label="@string/app_name"
android:roundIcon="@drawable/drone_icon"
android:supportsRtl="true"
android:theme="@style/Theme.DroneDetect">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
82 changes: 70 additions & 12 deletions app/src/main/java/com/example/dronedetect/DroneSignalDetector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,100 @@ package com.example.dronedetect
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.wifi.ScanResult
import android.net.wifi.WifiManager
import android.os.Handler
import android.os.Looper
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.Instant

class DroneSignalDetector(private val context: Context) {
class DroneSignalDetector(
private val context: Context,
private val onResults: (List<ScanResult>, FlightSnapshot) -> Unit = { _, _ -> }
) {
private val handler = Handler(Looper.getMainLooper())
private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
private val receiver = WifiScanReceiver()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val receiver = WifiScanReceiver(wifiManager, ::handleScanResults)
private var scanScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var isScanning = false
private var latestFlightData: FlightSnapshot = FlightSnapshot(timestamp = Instant.now(), source = "bootstrap", payload = emptyMap())

fun startScan() {
scope.launch {
fetchFlightData()
// TODO: implement scanning logic
}
if (isScanning) return
isScanning = true
context.registerReceiver(receiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
scanScope.launch { fetchFlightData() }
requestScan()
}

suspend fun fetchFlightData() = withContext(Dispatchers.IO) {
// TODO: implement fetching flight data
// Placeholder for real RF/telemetry ingestion.
// Here we stamp a deterministic payload so downstream processing can
// align Wi-Fi RSSI snapshots with the last known telemetry sample.
latestFlightData = FlightSnapshot(
timestamp = Instant.now(),
source = "local_stub",
payload = mapOf(
"note" to "Replace with CSI/telemetry feed",
"status" to "idle"
)
)
Log.d(TAG, "Flight data refreshed at ${latestFlightData.timestamp}")
}

fun stopScan() {
if (!isScanning) return
isScanning = false
handler.removeCallbacksAndMessages(null)
context.unregisterReceiver(receiver)
scope.cancel()
runCatching { context.unregisterReceiver(receiver) }
scanScope.cancel()
scanScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
}

private fun requestScan() {
val started = wifiManager.startScan()
if (!started) {
Log.w(TAG, "Wi-Fi scan request rejected by platform")
}
handler.postDelayed({
if (isScanning) {
requestScan()
}
}, SCAN_INTERVAL_MS)
}

private fun handleScanResults(results: List<ScanResult>) {
if (!isScanning) return
onResults(results, latestFlightData)
}

companion object {
private const val TAG = "DroneSignalDetector"
private const val SCAN_INTERVAL_MS = 10_000L
}
}

class WifiScanReceiver : BroadcastReceiver() {
data class FlightSnapshot(
val timestamp: Instant,
val source: String,
val payload: Map<String, String>
)

private class WifiScanReceiver(
private val wifiManager: WifiManager,
private val onResults: (List<ScanResult>) -> Unit
) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// TODO: handle scan results
val action = intent?.action ?: return
if (action != WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) return
val results = wifiManager.scanResults.orEmpty()
onResults(results)
}
}
Loading
Loading