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:
+126
-10
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user