Files
giglez/scripts/import_with_african_gps.py
T
leetcrypt 9f73595b20 feat: RTL_433 protocol database import - iteration 1/5
- Expanded protocol database from 18 → 299 signatures (16.6x increase)
- Imported 281 protocols from RTL_433 open-source database (286 total devices)
- Created automated import script: scripts/import_rtl433_protocols.py
- Generated rtl433_protocols_imported.py with timing/frequency/modulation data
- Updated protocol_database.py to include RTL433_PROTOCOLS
- All 26 tests passing

Breakdown by category:
  - Weather: 116 protocols
  - Sensors: 36 protocols
  - TPMS: 25 protocols
  - Security: 23 protocols
  - Home Automation: 18 protocols
  - Other: 50+ protocols

Frequency coverage:
  - 433.92 MHz: 248 protocols
  - 315.00 MHz: 32 protocols
  - 915.00 MHz: 1 protocol

This provides comprehensive coverage of Sub-GHz IoT devices for accurate
identification from raw RF captures.
2026-02-14 18:55:55 -08:00

371 lines
13 KiB
Python
Executable File

#!/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()