diff --git a/.gitignore b/.gitignore index 6a809ec..484cc59 100644 --- a/.gitignore +++ b/.gitignore @@ -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__/ diff --git a/README.md b/README.md index dfaabd6..c755dd2 100644 --- a/README.md +++ b/README.md @@ -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 +``` diff --git a/app/src/main/java/com/vantagescanner/DroneSignalDetector.kt b/app/src/main/java/com/vantagescanner/DroneSignalDetector.kt new file mode 100644 index 0000000..a5e7136 --- /dev/null +++ b/app/src/main/java/com/vantagescanner/DroneSignalDetector.kt @@ -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 by lazy { loadOuis() } + + private fun loadOuis(): Set { + context.assets.open("drone_ouis.json").use { input -> + val text = input.bufferedReader().use(BufferedReader::readText) + val arr = JSONArray(text) + val set = mutableSetOf() + 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 + } +} diff --git a/assets/drone_ouis.json b/assets/drone_ouis.json new file mode 100644 index 0000000..77adedf --- /dev/null +++ b/assets/drone_ouis.json @@ -0,0 +1,17 @@ +[ + "00121C", + "00267E", + "04A85A", + "0C9AE6", + "34D262", + "381D14", + "481CB9", + "58B858", + "60601F", + "8C5823", + "9003B7", + "903AE6", + "9C5A8A", + "A0143D", + "E47A2C" +] diff --git a/scripts/bootstrap_assets.py b/scripts/bootstrap_assets.py index 061ffd1..311834b 100755 --- a/scripts/bootstrap_assets.py +++ b/scripts/bootstrap_assets.py @@ -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 @@ -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__': diff --git a/scripts/update_drone_ouis.py b/scripts/update_drone_ouis.py new file mode 100755 index 0000000..7280259 --- /dev/null +++ b/scripts/update_drone_ouis.py @@ -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()