#!/usr/bin/env python3 """ Import Flipper Zero Captures with Random African GPS Coordinates Imports all downloaded .sub files with random GPS coordinates across Africa to visualize device distribution on the map. Africa Coverage: - Latitude: -35° to 37° (South Africa to Mediterranean) - Longitude: -17° to 51° (West coast to East coast) Cities included as hotspots: - Cairo, Egypt - Lagos, Nigeria - Nairobi, Kenya - Johannesburg, South Africa - Casablanca, Morocco - Addis Ababa, Ethiopia - Dar es Salaam, Tanzania - Khartoum, Sudan - Accra, Ghana - Kampala, Uganda """ import sys import os import random import hashlib from pathlib import Path from typing import Tuple, List from datetime import datetime, timedelta sys.path.insert(0, str(Path(__file__).parent.parent)) from sqlalchemy.orm import Session from loguru import logger from src.database.connection import get_session from src.database.models import Capture, Device, CaptureMatch from src.parser.sub_parser import parse_sub_file from src.matcher.engine import SignatureMatcher # ============================================================================= # CONFIGURATION # ============================================================================= DATA_DIR = Path(__file__).parent.parent / "data" / "rf_test_datasets" FLIPPER_DATASETS = [ DATA_DIR / "FlipperZero-Subghz-DB", DATA_DIR / "UberGuidoZ_Flipper", DATA_DIR / "Full_Flipper_Database", ] # Africa bounding box AFRICA_LAT_MIN = -35.0 # South Africa AFRICA_LAT_MAX = 37.0 # Mediterranean coast AFRICA_LON_MIN = -17.0 # West coast (Senegal) AFRICA_LON_MAX = 51.0 # East coast (Somalia) # Major African cities (hotspots with higher concentration) AFRICAN_CITIES = [ {"name": "Cairo", "lat": 30.0444, "lon": 31.2357, "weight": 10}, {"name": "Lagos", "lat": 6.5244, "lon": 3.3792, "weight": 8}, {"name": "Nairobi", "lat": -1.2921, "lon": 36.8219, "weight": 7}, {"name": "Johannesburg", "lat": -26.2041, "lon": 28.0473, "weight": 8}, {"name": "Casablanca", "lat": 33.5731, "lon": -7.5898, "weight": 6}, {"name": "Addis Ababa", "lat": 9.0320, "lon": 38.7469, "weight": 6}, {"name": "Dar es Salaam", "lat": -6.7924, "lon": 39.2083, "weight": 5}, {"name": "Khartoum", "lat": 15.5007, "lon": 32.5599, "weight": 5}, {"name": "Accra", "lat": 5.6037, "lon": -0.1870, "weight": 5}, {"name": "Kampala", "lat": 0.3476, "lon": 32.5825, "weight": 5}, {"name": "Kinshasa", "lat": -4.4419, "lon": 15.2663, "weight": 6}, {"name": "Luanda", "lat": -8.8368, "lon": 13.2343, "weight": 5}, {"name": "Dakar", "lat": 14.7167, "lon": -17.4677, "weight": 4}, {"name": "Cape Town", "lat": -33.9249, "lon": 18.4241, "weight": 6}, {"name": "Abidjan", "lat": 5.3600, "lon": -4.0083, "weight": 5}, ] # ============================================================================= # GPS GENERATION # ============================================================================= def generate_random_african_gps(use_hotspots: bool = True) -> Tuple[float, float, float]: """ Generate random GPS coordinates in Africa Returns: (latitude, longitude, accuracy_meters) """ if use_hotspots and random.random() < 0.6: # 60% chance of city hotspot # Weight by city population/importance weights = [city["weight"] for city in AFRICAN_CITIES] city = random.choices(AFRICAN_CITIES, weights=weights, k=1)[0] # Add random offset within ~10km of city center lat_offset = random.gauss(0, 0.05) # ~5.5 km std dev lon_offset = random.gauss(0, 0.05) latitude = city["lat"] + lat_offset longitude = city["lon"] + lon_offset accuracy = random.uniform(5.0, 15.0) # Urban GPS accuracy else: # Random location anywhere in Africa latitude = random.uniform(AFRICA_LAT_MIN, AFRICA_LAT_MAX) longitude = random.uniform(AFRICA_LON_MIN, AFRICA_LON_MAX) accuracy = random.uniform(10.0, 50.0) # Rural GPS accuracy return latitude, longitude, accuracy def generate_random_timestamp(days_back: int = 90) -> datetime: """Generate random timestamp within last N days""" now = datetime.utcnow() delta = timedelta(days=random.randint(0, days_back)) return now - delta # ============================================================================= # IMPORT WITH GPS # ============================================================================= def import_captures_with_african_gps( db: Session, limit: int = None, skip_existing: bool = True, run_matching: bool = True ) -> dict: """ Import all .sub files with random African GPS coordinates Returns: Statistics dict """ stats = { "files_processed": 0, "captures_created": 0, "duplicates_skipped": 0, "parse_errors": 0, "matches_created": 0, "cities_covered": set() } # Collect all .sub files sub_files: List[Path] = [] for dataset_dir in FLIPPER_DATASETS: if not dataset_dir.exists(): logger.warning(f"Dataset not found: {dataset_dir}") continue logger.info(f"Scanning: {dataset_dir}") sub_files.extend(dataset_dir.rglob("*.sub")) logger.info(f"Found {len(sub_files)} .sub files total") if limit: sub_files = sub_files[:limit] logger.info(f"Limited to {limit} files") # Shuffle for geographic randomness random.shuffle(sub_files) # Initialize matcher if enabled matcher = SignatureMatcher(db) if run_matching else None # Process files for idx, sub_file in enumerate(sub_files, 1): try: if idx % 100 == 0: logger.info(f"Progress: {idx}/{len(sub_files)} ({idx/len(sub_files)*100:.1f}%)") db.commit() # Commit in batches stats["files_processed"] += 1 # Read file content = sub_file.read_bytes() file_hash = hashlib.sha256(content).hexdigest() # Check for duplicates if skip_existing: existing = db.query(Capture).filter_by(file_hash=file_hash).first() if existing: stats["duplicates_skipped"] += 1 continue # Parse .sub file try: metadata = parse_sub_file(str(sub_file)) except Exception as e: logger.debug(f"Parse failed: {sub_file.name} - {e}") stats["parse_errors"] += 1 continue # Generate random African GPS latitude, longitude, accuracy = generate_random_african_gps() # Track which city region this is near for city in AFRICAN_CITIES: dist_lat = abs(latitude - city["lat"]) dist_lon = abs(longitude - city["lon"]) if dist_lat < 1.0 and dist_lon < 1.0: # Within ~100km stats["cities_covered"].add(city["name"]) break # Generate random timestamp (last 90 days) captured_at = generate_random_timestamp(days_back=90) # Store file (simplified - just use relative path) storage_path = f"data/uploads/{file_hash[:2]}/{file_hash}.sub" # Ensure directory exists os.makedirs(os.path.dirname(storage_path), exist_ok=True) # Write file with open(storage_path, 'wb') as f: f.write(content) # Create Capture record capture = Capture( file_hash=file_hash, session_id=None, user_id=None, latitude=latitude, longitude=longitude, altitude=random.uniform(0, 1500), # 0-1500m elevation gps_accuracy=accuracy, captured_at=captured_at, frequency=metadata.frequency, modulation=metadata.modulation, preset=metadata.preset, protocol=metadata.protocol, bit_length=metadata.bit_length, key_data=metadata.key_data, timing_element=metadata.timing_element, raw_data=str(metadata.raw_data) if metadata.raw_data else None, raw_format=metadata.file_format, file_path=storage_path, file_size=len(content) ) db.add(capture) db.flush() # Get capture ID stats["captures_created"] += 1 # Run matching engine if matcher and run_matching: try: match_results = matcher.match(metadata) if match_results: # Store top 5 matches for match_result in match_results[:5]: device = db.query(Device).filter_by(id=match_result.device_id).first() if not device: continue capture_match = CaptureMatch( capture_id=capture.id, device_id=device.id, confidence=match_result.confidence, match_method=match_result.method, match_details=match_result.details or {} ) db.add(capture_match) stats["matches_created"] += 1 # Set best match best_match = match_results[0] capture.device_id = best_match.device_id capture.match_confidence = best_match.confidence capture.match_method = best_match.method except Exception as e: logger.debug(f"Matching failed for {sub_file.name}: {e}") except Exception as e: logger.error(f"Failed to process {sub_file}: {e}") continue # Final commit db.commit() return stats # ============================================================================= # MAIN # ============================================================================= def main(): import argparse parser = argparse.ArgumentParser( description="Import .sub files with random African GPS coordinates" ) parser.add_argument("--limit", type=int, default=None, help="Limit number of files") parser.add_argument("--no-matching", action="store_true", help="Skip device matching") parser.add_argument("--skip-existing", action="store_true", default=True) args = parser.parse_args() # Setup logging logger.remove() logger.add(sys.stderr, level="INFO") logger.add("logs/import_african_gps.log", rotation="10 MB", level="DEBUG") logger.info("=" * 80) logger.info("Importing Captures with African GPS Coordinates") logger.info("=" * 80) # Setup db = get_session() # Set random seed for reproducibility (optional) random.seed(42) try: stats = import_captures_with_african_gps( db=db, limit=args.limit, skip_existing=args.skip_existing, run_matching=not args.no_matching ) logger.info("\n" + "=" * 80) logger.info("Import Complete!") logger.info("=" * 80) logger.info(f"Files processed: {stats['files_processed']}") logger.info(f"Captures created: {stats['captures_created']}") logger.info(f"Duplicates skipped: {stats['duplicates_skipped']}") logger.info(f"Parse errors: {stats['parse_errors']}") logger.info(f"Matches created: {stats['matches_created']}") logger.info(f"Cities covered: {len(stats['cities_covered'])}") logger.info(f" → {', '.join(sorted(stats['cities_covered']))}") # Database totals try: total_captures = db.query(Capture).count() total_matches = db.query(CaptureMatch).count() logger.info(f"\nDatabase totals:") logger.info(f" Total captures: {total_captures}") logger.info(f" Total matches: {total_matches}") except Exception as e: logger.warning(f"Could not query database totals: {e}") # Geographic distribution logger.info(f"\nGeographic distribution:") result = db.execute(""" SELECT COUNT(*) as count, AVG(latitude) as avg_lat, AVG(longitude) as avg_lon FROM captures WHERE latitude BETWEEN -35 AND 37 AND longitude BETWEEN -17 AND 51 """).fetchone() if result: logger.info(f" Captures in Africa: {result[0]}") logger.info(f" Center point: {result[1]:.2f}°, {result[2]:.2f}°") finally: db.close() if __name__ == "__main__": main()