GPS Auto-Extraction + First Successful Upload Complete

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>
This commit is contained in:
2026-01-13 18:37:13 -08:00
parent 48fcb00241
commit 04bd80b25b
33 changed files with 1903 additions and 22 deletions
+126 -10
View File
@@ -4,13 +4,22 @@ GigLez FastAPI Application - Simplified Version
Runs without database requirement for testing web interface
"""
from fastapi import FastAPI, Request
import json
import sys
from pathlib import Path
from typing import List
from fastapi import FastAPI, Request, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pathlib import Path
# Add project root to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from src.parser.sub_parser import SubFileParser
from src.parser.gps_extractor import GPSFilenameExtractor
# =============================================================================
# APPLICATION INSTANCE
@@ -29,6 +38,10 @@ BASE_DIR = Path(__file__).parent.parent.parent
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
# In-memory storage for uploaded captures (simplified mode)
captures_storage = []
upload_counter = 0
# =============================================================================
# MIDDLEWARE
@@ -85,10 +98,10 @@ async def api_root():
@app.get("/api/v1/query/captures")
async def get_captures():
"""Mock endpoint - return empty captures for now"""
"""Return stored captures from in-memory storage"""
return {
"captures": [],
"total": 0,
"captures": captures_storage,
"total": len(captures_storage),
"page": 1,
"page_size": 100
}
@@ -96,17 +109,120 @@ async def get_captures():
@app.get("/api/v1/stats/summary")
async def get_stats():
"""Mock endpoint - return zero stats for now"""
"""Return stats from in-memory storage"""
# Calculate frequency distribution
freq_dist = {}
for capture in captures_storage:
freq = capture.get("frequency", 0)
if freq > 0:
freq_dist[freq] = freq_dist.get(freq, 0) + 1
# Count unique protocols as proxy for unique devices
unique_protocols = len(set(c.get("protocol", "Unknown") for c in captures_storage))
return {
"total_captures": 0,
"unique_devices": 0,
"total_captures": len(captures_storage),
"unique_devices": unique_protocols,
"coverage_area_km2": 0,
"total_contributors": 0,
"frequency_distribution": {},
"total_contributors": 1 if len(captures_storage) > 0 else 0,
"frequency_distribution": freq_dist,
"captures_timeline": []
}
@app.post("/api/v1/captures/upload")
async def upload_captures(
files: List[UploadFile] = File(...),
manifest: str = Form(...)
):
"""
Mock upload endpoint - parses files and returns success
Does not save to database (simplified version)
"""
import tempfile
import os
try:
# Parse manifest
manifest_data = json.loads(manifest)
# Initialize parsers
sub_parser = SubFileParser()
gps_extractor = GPSFilenameExtractor()
successful = []
failed = []
# Process each file
for file in files:
temp_path = None
try:
# Read file content
content = await file.read()
# Save to temporary file for parsing
with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.sub') as temp_file:
temp_path = temp_file.name
temp_file.write(content)
# Parse .sub file
metadata = sub_parser.parse(temp_path)
# Try to extract GPS from filename
gps_coords = gps_extractor.extract(file.filename)
# Use GPS from manifest or filename
global upload_counter
upload_counter += 1
capture_info = {
"id": upload_counter,
"filename": file.filename,
"frequency": metadata.frequency if hasattr(metadata, 'frequency') else 0,
"protocol": metadata.protocol if hasattr(metadata, 'protocol') else "RAW",
"preset": metadata.preset if hasattr(metadata, 'preset') else "Unknown",
"latitude": gps_coords.latitude if gps_coords else manifest_data.get("captures", [{}])[0].get("latitude"),
"longitude": gps_coords.longitude if gps_coords else manifest_data.get("captures", [{}])[0].get("longitude"),
"gps_source": gps_coords.source if gps_coords else "manual",
"timestamp": manifest_data.get("captures", [{}])[0].get("timestamp", ""),
}
# Store in memory
captures_storage.append(capture_info)
successful.append(capture_info)
except Exception as e:
failed.append({
"filename": file.filename,
"error": str(e)
})
finally:
# Clean up temp file
if temp_path and os.path.exists(temp_path):
os.unlink(temp_path)
return {
"success": True,
"message": f"Processed {len(successful)} files successfully",
"successful": successful,
"failed": failed,
"total_uploaded": len(files),
"total_successful": len(successful),
"total_failed": len(failed)
}
except Exception as e:
return {
"success": False,
"message": f"Upload failed: {str(e)}",
"successful": [],
"failed": [],
"total_uploaded": 0,
"total_successful": 0,
"total_failed": 0
}
# =============================================================================
# RUN WITH UVICORN (for development)
# =============================================================================