04bd80b25b
Major milestone: GPS coordinates now auto-extract from filenames and uploads appear on map with full end-to-end workflow functional! ✨ GPS Auto-Extraction Features: - JavaScript GPS extractor class matching Python patterns - Supports 3 filename formats: * N/S/E/W: 34.0478N_118.2349W_filename.sub * lat/lon prefix: lat34.0478lon-118.2348_filename.sub * Signed decimal: -34.0478_118.2348_filename.sub - Auto-populates latitude/longitude form fields on file drop - Green notification toast shows detected coordinates - File list shows GPS badge for files with coordinates 🗺️ Web Interface Improvements: - Upload endpoint now stores captures in-memory - Query endpoint returns uploaded captures for map display - Stats endpoint shows real-time upload counts - Map displays uploaded captures as markers - Color-coded by frequency band 📁 Updated Files: - static/js/upload.js: GPS extraction + auto-population - src/api/main_simple.py: In-memory storage + endpoints - src/parser/gps_extractor.py: Backend GPS extraction (Python) - scripts/test_gps_extraction.py: Python test suite - test_gps_extraction.html: Browser test suite 📊 T-Embed Files Updated: - 34.0478N_118.2348W_1637_raw_8.sub: 315 MHz Princeton - 34.0478N_118.2349W_1351_raw_10.sub: 433.92 MHz Princeton - 34.0478N_118.2349W_1650_test_raw.sub: 433.92 MHz RAW - All now have proper Flipper SubGhz headers ✅ Tested Features: - GPS extraction from filename: 34.0478N_118.2349W → 34.0478, -118.2349 - Auto-population of GPS fields in upload form - File upload with GPS validation - Capture appears on map after upload - Statistics update in real-time - Frequency distribution calculated correctly 🎯 End-to-End Flow Working: 1. User drops .sub file with GPS in filename 2. GPS auto-detected and form fields populate 3. User clicks Upload 4. Server parses RF data + GPS coordinates 5. Capture stored in memory 6. Map refreshes and displays new marker 7. Stats update with new counts 🚀 Demo: http://localhost:8000 Upload 34.0478N_118.2349W_1351_raw_10.sub and watch it appear on map! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
179 lines
5.7 KiB
Python
Executable File
179 lines
5.7 KiB
Python
Executable File
#!/data/data/com.termux/files/usr/bin/python3
|
|
"""
|
|
Quick GPS+RF Capture - Uses existing wardriving infrastructure
|
|
Captures current GPS and associates it with a Bruce .sub file
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import subprocess
|
|
import hashlib
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
# Import existing infrastructure
|
|
from database import SubGHzWardrivingDB
|
|
|
|
|
|
def get_current_gps(timeout=15):
|
|
"""Get current GPS using termux-location."""
|
|
try:
|
|
cmd = ["termux-location", "-p", "gps"]
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout
|
|
)
|
|
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
gps_data = json.loads(result.stdout.strip())
|
|
if 'latitude' in gps_data and 'longitude' in gps_data:
|
|
print(f"✓ GPS Location: {gps_data['latitude']:.6f}, {gps_data['longitude']:.6f}")
|
|
print(f" Accuracy: {gps_data.get('accuracy', 'N/A')}m")
|
|
return gps_data
|
|
|
|
except subprocess.TimeoutExpired:
|
|
print(f"✗ GPS timeout after {timeout}s")
|
|
except Exception as e:
|
|
print(f"✗ GPS error: {e}")
|
|
|
|
return None
|
|
|
|
|
|
def parse_sub_file(sub_file):
|
|
"""Parse Bruce .sub file to extract RF metadata."""
|
|
metadata = {}
|
|
|
|
try:
|
|
with open(sub_file, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or not ':' in line:
|
|
continue
|
|
|
|
key, value = line.split(':', 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
|
|
if key == 'Frequency':
|
|
metadata['frequency_hz'] = int(value)
|
|
elif key == 'Protocol':
|
|
metadata['protocol'] = value
|
|
elif key == 'Preset':
|
|
metadata['preset'] = value
|
|
|
|
return metadata if metadata else None
|
|
|
|
except Exception as e:
|
|
print(f"✗ Error parsing {sub_file}: {e}")
|
|
return None
|
|
|
|
|
|
def calculate_file_hash(file_path):
|
|
"""Calculate MD5 hash of file."""
|
|
md5 = hashlib.md5()
|
|
with open(file_path, 'rb') as f:
|
|
for chunk in iter(lambda: f.read(4096), b''):
|
|
md5.update(chunk)
|
|
return md5.hexdigest()
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: quick_capture.py <bruce_file.sub> [session_id]")
|
|
print("\nExample:")
|
|
print(" python3 quick_capture.py raw10.sub")
|
|
print(" python3 quick_capture.py ~/projects/device-integration/bruce/data/captures/raw10.sub mysession")
|
|
return 1
|
|
|
|
sub_file = Path(sys.argv[1])
|
|
session_id = sys.argv[2] if len(sys.argv) > 2 else f"quick_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
|
|
if not sub_file.exists():
|
|
print(f"✗ File not found: {sub_file}")
|
|
return 1
|
|
|
|
print(f"\n📡 SubGHz GPS Capture")
|
|
print(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
print(f"Bruce File: {sub_file.name}")
|
|
print(f"Session: {session_id}")
|
|
print()
|
|
|
|
# Get GPS
|
|
print("🛰️ Getting GPS location...")
|
|
gps_data = get_current_gps()
|
|
if not gps_data:
|
|
print("\n✗ Failed to get GPS - trying to use latest from log...")
|
|
gps_log = Path.home() / "logs" / "gps_quick.jsonl"
|
|
if gps_log.exists():
|
|
with open(gps_log, 'r') as f:
|
|
last_line = f.readlines()[-1]
|
|
gps_data = json.loads(last_line.strip())
|
|
print(f"✓ Using latest GPS: {gps_data['latitude']:.6f}, {gps_data['longitude']:.6f}")
|
|
else:
|
|
print("✗ No GPS data available")
|
|
return 1
|
|
|
|
# Parse RF file
|
|
print("\n📻 Parsing RF file...")
|
|
rf_metadata = parse_sub_file(sub_file)
|
|
if not rf_metadata:
|
|
print("✗ Failed to parse RF file")
|
|
return 1
|
|
|
|
print(f"✓ Frequency: {rf_metadata.get('frequency_hz', 'N/A')} Hz ({rf_metadata.get('frequency_hz', 0) / 1e6:.3f} MHz)")
|
|
print(f" Protocol: {rf_metadata.get('protocol', 'N/A')}")
|
|
print(f" Preset: {rf_metadata.get('preset', 'N/A')}")
|
|
|
|
# Initialize database
|
|
print("\n💾 Storing to database...")
|
|
db = SubGHzWardrivingDB()
|
|
|
|
# Create session if it doesn't exist
|
|
db.create_session(session_id, device_info="Bruce T-Embed CC1101", notes="Quick GPS capture")
|
|
|
|
# Add GPS track point
|
|
db.add_gps_point(
|
|
session_id=session_id,
|
|
latitude=gps_data['latitude'],
|
|
longitude=gps_data['longitude'],
|
|
altitude=gps_data.get('altitude'),
|
|
accuracy=gps_data.get('accuracy'),
|
|
speed=gps_data.get('speed'),
|
|
provider=gps_data.get('provider', 'gps')
|
|
)
|
|
|
|
# Add signal capture
|
|
timestamp = datetime.now(timezone.utc).isoformat()
|
|
capture_id = f"{session_id}_{sub_file.stem}_{int(datetime.now().timestamp())}"
|
|
|
|
db.add_signal(
|
|
session_id=session_id,
|
|
capture_id=capture_id,
|
|
timestamp=timestamp,
|
|
bruce_filename=sub_file.name,
|
|
bruce_file_path=str(sub_file),
|
|
file_size_bytes=sub_file.stat().st_size,
|
|
frequency_hz=rf_metadata.get('frequency_hz'),
|
|
protocol=rf_metadata.get('protocol'),
|
|
gps_latitude=gps_data['latitude'],
|
|
gps_longitude=gps_data['longitude'],
|
|
gps_accuracy_meters=gps_data.get('accuracy'),
|
|
interpolated=False,
|
|
interpolation_confidence=1.0,
|
|
file_hash_md5=calculate_file_hash(sub_file)
|
|
)
|
|
|
|
print(f"✓ Stored signal: {capture_id}")
|
|
print(f"\n✅ Capture paired successfully!")
|
|
print(f" Database: ~/logs/subghz_wardriving.db")
|
|
print(f" Export with: python3 -m subghz_wardriving.exporter")
|
|
print()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|