From 25c067c14dcd79a6948cdc1b9089a9cb2979fea7 Mon Sep 17 00:00:00 2001 From: nbschultz97 <126931519+nbschultz97@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:36:45 -0600 Subject: [PATCH 1/3] Add offline drone OUI database and loader --- .gitignore | 7 ++- README.md | 10 ++++ .../com/vantagescanner/DroneSignalDetector.kt | 37 +++++++++++++ assets/drone_ouis.json | 17 ++++++ scripts/bootstrap_assets.py | 9 +++- scripts/update_drone_ouis.py | 54 +++++++++++++++++++ 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/vantagescanner/DroneSignalDetector.kt create mode 100644 assets/drone_ouis.json create mode 100755 scripts/update_drone_ouis.py 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..aceeb11 --- /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" +] \ No newline at end of file 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..866dcad --- /dev/null +++ b/scripts/update_drone_ouis.py @@ -0,0 +1,54 @@ +#!/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 +from pathlib import Path + +# 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: + ouis.add(row["Assignment"].upper()) + 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)) + print(f"wrote {len(ouis)} OUIs -> {out_path}") + + +if __name__ == "__main__": + main() From d32026c2e158483242b1112f3681c26e092209fc Mon Sep 17 00:00:00 2001 From: nbschultz97 <126931519+nbschultz97@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:40:33 -0600 Subject: [PATCH 2/3] Ensure newline in OUI asset and update script --- assets/drone_ouis.json | 2 +- scripts/update_drone_ouis.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/assets/drone_ouis.json b/assets/drone_ouis.json index aceeb11..77adedf 100644 --- a/assets/drone_ouis.json +++ b/assets/drone_ouis.json @@ -14,4 +14,4 @@ "9C5A8A", "A0143D", "E47A2C" -] \ No newline at end of file +] diff --git a/scripts/update_drone_ouis.py b/scripts/update_drone_ouis.py index 866dcad..3d40fa6 100755 --- a/scripts/update_drone_ouis.py +++ b/scripts/update_drone_ouis.py @@ -9,6 +9,7 @@ import csv import json from pathlib import Path +from typing import List # Keywords identifying drone manufacturers in the IEEE registry DRONE_KEYWORDS = ["DJI", "PARROT", "SKYDIO"] @@ -17,7 +18,7 @@ ASSET_PATH = ROOT / "assets" / "drone_ouis.json" -def extract_ouis(csv_path: Path) -> list[str]: +def extract_ouis(csv_path: Path) -> List[str]: ouis: set[str] = set() with csv_path.open(newline="") as fh: reader = csv.DictReader(fh) @@ -46,7 +47,7 @@ def main() -> None: 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)) + out_path.write_text(json.dumps(ouis, indent=2) + "\n") print(f"wrote {len(ouis)} OUIs -> {out_path}") From 9256d9c1fe22f665c012343cef9ca18dab2d4419 Mon Sep 17 00:00:00 2001 From: nbschultz97 <126931519+nbschultz97@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:44:19 -0600 Subject: [PATCH 3/3] Normalize OUIs to match detector format --- scripts/update_drone_ouis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/update_drone_ouis.py b/scripts/update_drone_ouis.py index 3d40fa6..7280259 100755 --- a/scripts/update_drone_ouis.py +++ b/scripts/update_drone_ouis.py @@ -8,6 +8,7 @@ import argparse import csv import json +import re from pathlib import Path from typing import List @@ -26,7 +27,8 @@ def extract_ouis(csv_path: Path) -> List[str]: name = row.get("Organization Name", "").upper() for key in DRONE_KEYWORDS: if key in name: - ouis.add(row["Assignment"].upper()) + assignment = re.sub("[^0-9A-F]", "", row["Assignment"].upper()) + ouis.add(assignment) break return sorted(ouis)