Skip to content
Open
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
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Generated binary assets
app/
# Generated binary assets and build outputs
app/build/
app/src/main/assets/
app/src/main/res/

__pycache__/
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ python3 scripts/bootstrap_assets.py

This will recreate the `rotor_v1.tflite` model and the required PNG
drawables under `app/src/main/...`.

## Refresh drone OUI prefixes

`assets/drone_ouis.json` tracks MAC address prefixes for common drone
vendors. Regenerate it from a locally downloaded IEEE `oui.csv` file:

```bash
python3 scripts/update_drone_ouis.py /path/to/oui.csv
python3 scripts/bootstrap_assets.py # copy JSON into app assets
```
37 changes: 37 additions & 0 deletions app/src/main/java/com/vantagescanner/DroneSignalDetector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.vantagescanner

import android.content.Context
import org.json.JSONArray
import java.io.BufferedReader
import java.util.Locale

/**
* Detects drone Wi-Fi signals by comparing MAC prefixes against a
* locally maintained list of Organizationally Unique Identifiers.
*
* OUI prefixes are loaded from the `drone_ouis.json` asset at runtime
* to avoid hard-coding vendor data.
*/
class DroneSignalDetector(private val context: Context) {
private val droneOuis: Set<String> by lazy { loadOuis() }

private fun loadOuis(): Set<String> {
context.assets.open("drone_ouis.json").use { input ->
val text = input.bufferedReader().use(BufferedReader::readText)
val arr = JSONArray(text)
val set = mutableSetOf<String>()
for (i in 0 until arr.length()) {
set += arr.getString(i).uppercase(Locale.US)
}
return set
}
}

/**
* Returns true if the MAC address belongs to a known drone vendor.
*/
fun isDrone(macAddress: String): Boolean {
val prefix = macAddress.uppercase(Locale.US).replace(":", "").take(6)
return prefix in droneOuis
}
}
17 changes: 17 additions & 0 deletions assets/drone_ouis.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
"00121C",
"00267E",
"04A85A",
"0C9AE6",
"34D262",
"381D14",
"481CB9",
"58B858",
"60601F",
"8C5823",
"9003B7",
"903AE6",
"9C5A8A",
"A0143D",
"E47A2C"
]
9 changes: 8 additions & 1 deletion scripts/bootstrap_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"""Decode base64-encoded assets into their binary forms.

Designed for air-gapped setups: keeps repository text-only and restores
binary assets locally. Currently handles rotor model and PNG icons.
binary assets locally. Handles the rotor model, PNG icons, and JSON
lookup tables.
"""
import base64
from pathlib import Path
Expand All @@ -29,6 +30,12 @@ def main() -> None:
for b64_file in ASSET_SRC.glob('*.png.b64'):
out_name = b64_file.name[:-4] # strip .b64
decode_file(b64_file, DRAWABLE_DST / out_name)
# JSON assets (copied as-is)
for json_file in ASSET_SRC.glob('*.json'):
dst = MODEL_DST / json_file.name
dst.parent.mkdir(parents=True, exist_ok=True)
dst.write_text(json_file.read_text())
print(f"copied {json_file} -> {dst}")


if __name__ == '__main__':
Expand Down
57 changes: 57 additions & 0 deletions scripts/update_drone_ouis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Generate assets/drone_ouis.json from an IEEE OUI CSV.

The IEEE registry file `oui.csv` can be downloaded separately and stored
locally for offline use. This script extracts OUI prefixes for known drone
vendors and writes them to a JSON asset.
"""
import argparse
import csv
import json
import re
from pathlib import Path
from typing import List

# Keywords identifying drone manufacturers in the IEEE registry
DRONE_KEYWORDS = ["DJI", "PARROT", "SKYDIO"]

ROOT = Path(__file__).resolve().parent.parent
ASSET_PATH = ROOT / "assets" / "drone_ouis.json"


def extract_ouis(csv_path: Path) -> List[str]:
ouis: set[str] = set()
with csv_path.open(newline="") as fh:
reader = csv.DictReader(fh)
for row in reader:
name = row.get("Organization Name", "").upper()
for key in DRONE_KEYWORDS:
if key in name:
assignment = re.sub("[^0-9A-F]", "", row["Assignment"].upper())
ouis.add(assignment)
break
return sorted(ouis)


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"csv", nargs="?", default="oui.csv", help="Path to downloaded oui.csv"
)
parser.add_argument(
"-o",
"--output",
default=str(ASSET_PATH),
help="Destination JSON asset (default: assets/drone_ouis.json)",
)
args = parser.parse_args()
csv_path = Path(args.csv)
ouis = extract_ouis(csv_path)
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(ouis, indent=2) + "\n")
print(f"wrote {len(ouis)} OUIs -> {out_path}")


if __name__ == "__main__":
main()