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.
This commit is contained in:
Executable
+325
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Upload Log Analyzer
|
||||
|
||||
Analyzes upload logs from beta testing to identify patterns, common errors,
|
||||
and performance bottlenecks.
|
||||
|
||||
Usage:
|
||||
python3 scripts/analyze_upload_logs.py --date 2026-01-15
|
||||
python3 scripts/analyze_upload_logs.py --last 7 # Last 7 days
|
||||
python3 scripts/analyze_upload_logs.py --upload-id upload_20260115_123456
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from collections import Counter, defaultdict
|
||||
from typing import List, Dict, Any
|
||||
import statistics
|
||||
|
||||
|
||||
class UploadLogAnalyzer:
|
||||
"""Analyze upload logs and generate reports"""
|
||||
|
||||
def __init__(self, log_dir: str = "logs/uploads"):
|
||||
self.log_dir = Path(log_dir)
|
||||
|
||||
def analyze_date(self, date: str) -> Dict[str, Any]:
|
||||
"""Analyze logs for a specific date (YYYY-MM-DD)"""
|
||||
|
||||
# Read main upload log
|
||||
log_file = self.log_dir / f"uploads_{date}.log"
|
||||
error_log = self.log_dir / f"upload_errors_{date}.log"
|
||||
structured_log = self.log_dir / f"uploads_structured_{date}.jsonl"
|
||||
|
||||
if not log_file.exists():
|
||||
return {"error": f"Log file not found: {log_file}"}
|
||||
|
||||
stats = {
|
||||
"date": date,
|
||||
"total_uploads": 0,
|
||||
"total_files": 0,
|
||||
"successful_files": 0,
|
||||
"failed_files": 0,
|
||||
"duplicate_files": 0,
|
||||
"errors_by_type": Counter(),
|
||||
"errors_by_stage": Counter(),
|
||||
"parse_errors": [],
|
||||
"gps_errors": [],
|
||||
"performance": {
|
||||
"parse_times": [],
|
||||
"matching_times": [],
|
||||
"total_times": []
|
||||
},
|
||||
"top_failures": [],
|
||||
"user_stats": defaultdict(int)
|
||||
}
|
||||
|
||||
# Parse structured JSON log
|
||||
if structured_log.exists():
|
||||
with open(structured_log, 'r') as f:
|
||||
for line in f:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
self._process_log_entry(entry, stats)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Parse error log for details
|
||||
if error_log.exists():
|
||||
with open(error_log, 'r') as f:
|
||||
for line in f:
|
||||
self._process_error_line(line, stats)
|
||||
|
||||
# Calculate summary statistics
|
||||
self._calculate_summary(stats)
|
||||
|
||||
return stats
|
||||
|
||||
def _process_log_entry(self, entry: Dict[str, Any], stats: Dict[str, Any]):
|
||||
"""Process a single structured log entry"""
|
||||
event = entry.get("event")
|
||||
|
||||
if event == "upload_attempt":
|
||||
stats["total_uploads"] += 1
|
||||
stats["total_files"] += entry.get("file_count", 0)
|
||||
if entry.get("user_id"):
|
||||
stats["user_stats"]["authenticated"] += 1
|
||||
else:
|
||||
stats["user_stats"]["anonymous"] += 1
|
||||
|
||||
elif event == "file_processing":
|
||||
stage = entry.get("stage")
|
||||
success = entry.get("success")
|
||||
|
||||
if stage == "complete" and success:
|
||||
stats["successful_files"] += 1
|
||||
elif not success:
|
||||
if stage == "validation" and entry.get("metadata", {}).get("status") == "duplicate":
|
||||
stats["duplicate_files"] += 1
|
||||
else:
|
||||
stats["failed_files"] += 1
|
||||
stats["errors_by_stage"][stage] += 1
|
||||
|
||||
# Track performance
|
||||
duration = entry.get("duration_ms")
|
||||
if duration and success:
|
||||
if stage == "parsing":
|
||||
stats["performance"]["parse_times"].append(duration)
|
||||
elif stage == "complete":
|
||||
stats["performance"]["total_times"].append(duration)
|
||||
|
||||
elif event == "parse_error":
|
||||
stats["parse_errors"].append({
|
||||
"filename": entry.get("filename"),
|
||||
"error_type": entry.get("error_type"),
|
||||
"error_message": entry.get("error_message"),
|
||||
"upload_id": entry.get("upload_id")
|
||||
})
|
||||
stats["errors_by_type"][entry.get("error_type", "Unknown")] += 1
|
||||
|
||||
elif event == "gps_validation_error":
|
||||
stats["gps_errors"].append({
|
||||
"filename": entry.get("filename"),
|
||||
"latitude": entry.get("latitude"),
|
||||
"longitude": entry.get("longitude"),
|
||||
"reason": entry.get("reason")
|
||||
})
|
||||
stats["errors_by_type"]["GPS Validation"] += 1
|
||||
|
||||
elif event == "matching_results":
|
||||
duration = entry.get("duration_ms")
|
||||
if duration:
|
||||
stats["performance"]["matching_times"].append(duration)
|
||||
|
||||
elif event == "performance_metrics":
|
||||
# Aggregate performance metrics
|
||||
for key, value in entry.items():
|
||||
if key.endswith("_ms") and isinstance(value, (int, float)):
|
||||
perf_key = key.replace("_ms", "_times")
|
||||
if perf_key not in stats["performance"]:
|
||||
stats["performance"][perf_key] = []
|
||||
stats["performance"][perf_key].append(value)
|
||||
|
||||
def _process_error_line(self, line: str, stats: Dict[str, Any]):
|
||||
"""Process error log line"""
|
||||
# Extract error type from log line
|
||||
error_match = re.search(r'(Exception|Error): (.+)', line)
|
||||
if error_match:
|
||||
error_type = error_match.group(1)
|
||||
stats["errors_by_type"][error_type] += 1
|
||||
|
||||
def _calculate_summary(self, stats: Dict[str, Any]):
|
||||
"""Calculate summary statistics"""
|
||||
# Success rate
|
||||
total = stats["successful_files"] + stats["failed_files"]
|
||||
if total > 0:
|
||||
stats["success_rate"] = round(stats["successful_files"] / total * 100, 1)
|
||||
else:
|
||||
stats["success_rate"] = 0.0
|
||||
|
||||
# Performance statistics
|
||||
for key, times in stats["performance"].items():
|
||||
if times:
|
||||
stats["performance"][f"{key}_avg"] = round(statistics.mean(times), 2)
|
||||
stats["performance"][f"{key}_median"] = round(statistics.median(times), 2)
|
||||
stats["performance"][f"{key}_p95"] = round(statistics.quantiles(times, n=20)[18], 2) if len(times) > 20 else None
|
||||
|
||||
# Top failure reasons
|
||||
stats["top_failures"] = stats["errors_by_type"].most_common(10)
|
||||
|
||||
def print_report(self, stats: Dict[str, Any]):
|
||||
"""Print formatted report"""
|
||||
print("=" * 80)
|
||||
print(f"Upload Log Analysis - {stats['date']}")
|
||||
print("=" * 80)
|
||||
|
||||
# Overview
|
||||
print("\n📊 OVERVIEW")
|
||||
print(f" Total Uploads: {stats['total_uploads']}")
|
||||
print(f" Total Files: {stats['total_files']}")
|
||||
print(f" Successful: {stats['successful_files']} ({stats['success_rate']}%)")
|
||||
print(f" Failed: {stats['failed_files']}")
|
||||
print(f" Duplicates: {stats['duplicate_files']}")
|
||||
|
||||
# User statistics
|
||||
print("\n👥 USERS")
|
||||
print(f" Authenticated: {stats['user_stats']['authenticated']}")
|
||||
print(f" Anonymous: {stats['user_stats']['anonymous']}")
|
||||
|
||||
# Performance
|
||||
print("\n⚡ PERFORMANCE")
|
||||
perf = stats["performance"]
|
||||
if perf.get("parse_times_avg"):
|
||||
print(f" Parse Time: {perf['parse_times_avg']:.0f}ms avg, {perf['parse_times_median']:.0f}ms median")
|
||||
if perf.get("matching_times_avg"):
|
||||
print(f" Matching Time: {perf['matching_times_avg']:.0f}ms avg, {perf['matching_times_median']:.0f}ms median")
|
||||
if perf.get("total_times_avg"):
|
||||
print(f" Total Time: {perf['total_times_avg']:.0f}ms avg, {perf['total_times_median']:.0f}ms median")
|
||||
|
||||
# Errors
|
||||
if stats["top_failures"]:
|
||||
print("\n❌ TOP ERRORS")
|
||||
for error_type, count in stats["top_failures"]:
|
||||
print(f" {error_type:30} {count:5} occurrences")
|
||||
|
||||
# Parse errors detail
|
||||
if stats["parse_errors"]:
|
||||
print(f"\n🔍 PARSE ERRORS ({len(stats['parse_errors'])})")
|
||||
for error in stats["parse_errors"][:5]: # Show first 5
|
||||
print(f" {error['filename']:30} {error['error_type']}: {error['error_message'][:50]}")
|
||||
|
||||
# GPS errors detail
|
||||
if stats["gps_errors"]:
|
||||
print(f"\n📍 GPS ERRORS ({len(stats['gps_errors'])})")
|
||||
for error in stats["gps_errors"][:5]: # Show first 5
|
||||
print(f" {error['filename']:30} ({error['latitude']}, {error['longitude']}) - {error['reason']}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
|
||||
def analyze_upload_id(self, upload_id: str) -> Dict[str, Any]:
|
||||
"""Analyze a specific upload"""
|
||||
# Search all structured logs
|
||||
results = {
|
||||
"upload_id": upload_id,
|
||||
"events": [],
|
||||
"files": [],
|
||||
"errors": [],
|
||||
"timeline": []
|
||||
}
|
||||
|
||||
for log_file in self.log_dir.glob("uploads_structured_*.jsonl"):
|
||||
with open(log_file, 'r') as f:
|
||||
for line in f:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
if entry.get("upload_id") == upload_id:
|
||||
results["events"].append(entry)
|
||||
results["timeline"].append({
|
||||
"timestamp": entry.get("timestamp"),
|
||||
"event": entry.get("event"),
|
||||
"details": entry
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Sort timeline
|
||||
results["timeline"].sort(key=lambda x: x["timestamp"])
|
||||
|
||||
return results
|
||||
|
||||
def print_upload_details(self, details: Dict[str, Any]):
|
||||
"""Print detailed upload report"""
|
||||
print("=" * 80)
|
||||
print(f"Upload Details: {details['upload_id']}")
|
||||
print("=" * 80)
|
||||
|
||||
print(f"\nTotal Events: {len(details['events'])}")
|
||||
|
||||
print("\n📅 TIMELINE:")
|
||||
for event in details["timeline"]:
|
||||
timestamp = event["timestamp"].split('T')[1][:12] # HH:MM:SS.mmm
|
||||
print(f" {timestamp} | {event['event']:20} | {self._format_event_details(event['details'])}")
|
||||
|
||||
def _format_event_details(self, event: Dict[str, Any]) -> str:
|
||||
"""Format event details for display"""
|
||||
event_type = event.get("event")
|
||||
|
||||
if event_type == "upload_attempt":
|
||||
return f"{event.get('file_count')} files, {event.get('total_size_mb')} MB"
|
||||
elif event_type == "file_processing":
|
||||
success = "✓" if event.get("success") else "✗"
|
||||
return f"{success} {event.get('filename')} - {event.get('stage')} ({event.get('duration_ms', 0):.0f}ms)"
|
||||
elif event_type == "parse_error":
|
||||
return f"❌ {event.get('filename')} - {event.get('error_type')}"
|
||||
elif event_type == "matching_results":
|
||||
return f"🔍 {event.get('match_count')} matches ({event.get('duration_ms', 0):.0f}ms)"
|
||||
elif event_type == "upload_complete":
|
||||
return f"✅ {event.get('successful_count')} success, {event.get('failed_count')} failed"
|
||||
else:
|
||||
return json.dumps(event, indent=2)[:100]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Analyze upload logs from beta testing")
|
||||
parser.add_argument("--date", help="Date to analyze (YYYY-MM-DD)")
|
||||
parser.add_argument("--last", type=int, help="Analyze last N days")
|
||||
parser.add_argument("--upload-id", help="Analyze specific upload ID")
|
||||
parser.add_argument("--log-dir", default="logs/uploads", help="Log directory")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
analyzer = UploadLogAnalyzer(log_dir=args.log_dir)
|
||||
|
||||
if args.upload_id:
|
||||
# Analyze specific upload
|
||||
details = analyzer.analyze_upload_id(args.upload_id)
|
||||
analyzer.print_upload_details(details)
|
||||
|
||||
elif args.date:
|
||||
# Analyze specific date
|
||||
stats = analyzer.analyze_date(args.date)
|
||||
analyzer.print_report(stats)
|
||||
|
||||
elif args.last:
|
||||
# Analyze last N days
|
||||
for i in range(args.last):
|
||||
date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Analyzing {date}")
|
||||
print('='*80)
|
||||
stats = analyzer.analyze_date(date)
|
||||
analyzer.print_report(stats)
|
||||
|
||||
else:
|
||||
# Default: analyze today
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
stats = analyzer.analyze_date(today)
|
||||
analyzer.print_report(stats)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,478 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Import RTL_433 Protocol Database
|
||||
|
||||
Expands GigLez protocol database from 18 → 200+ by importing RTL_433's open-source
|
||||
protocol definitions.
|
||||
|
||||
Source: data/rtl_433_protocols.json (286 devices)
|
||||
Output: src/matcher/rtl433_protocols_imported.py
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def load_rtl433_json() -> Dict:
|
||||
"""Load RTL_433 protocol database JSON"""
|
||||
json_path = Path(__file__).parent.parent / "data" / "rtl_433_protocols.json"
|
||||
|
||||
with open(json_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"✓ Loaded {data['total_devices']} devices from RTL_433 database")
|
||||
return data
|
||||
|
||||
|
||||
def estimate_frequency(device: Dict) -> int:
|
||||
"""
|
||||
Estimate frequency for devices with null frequency field
|
||||
|
||||
Based on:
|
||||
- Category (weather = 433MHz, automotive = 315MHz)
|
||||
- Manufacturer patterns
|
||||
- Default ISM bands
|
||||
"""
|
||||
freq = device.get('frequency')
|
||||
if freq:
|
||||
return freq
|
||||
|
||||
# Heuristics based on category and manufacturer
|
||||
category = (device.get('category') or '').lower()
|
||||
manufacturer = (device.get('manufacturer') or '').lower()
|
||||
name = (device.get('name') or '').lower()
|
||||
|
||||
# Automotive TPMS → 315 MHz (North America) or 433 MHz (Europe)
|
||||
if category == 'automotive' or 'tpms' in name:
|
||||
return 315000000 # Default to NA frequency
|
||||
|
||||
# Security systems → 433 MHz (most common)
|
||||
if category == 'security' or 'alarm' in name or 'security' in name:
|
||||
return 433920000
|
||||
|
||||
# Doorbells → 433 MHz
|
||||
if 'doorbell' in name or 'bell' in name:
|
||||
return 433920000
|
||||
|
||||
# Garage door openers → 315 MHz (NA) or 433 MHz
|
||||
if 'garage' in name or 'door' in name:
|
||||
# Chamberlain/LiftMaster = 315 MHz
|
||||
if 'chamberlain' in manufacturer or 'liftmaster' in manufacturer:
|
||||
return 315000000
|
||||
return 433920000
|
||||
|
||||
# Weather sensors → 433 MHz (most common globally)
|
||||
if category == 'weather' or any(kw in name for kw in ['temperature', 'humidity', 'rain', 'wind', 'sensor']):
|
||||
# Some exceptions: Acurite (915 MHz for 5n1)
|
||||
if 'acurite' in manufacturer and '5n1' in name:
|
||||
return 915000000
|
||||
return 433920000
|
||||
|
||||
# Default: 433.92 MHz (most popular ISM band)
|
||||
return 433920000
|
||||
|
||||
|
||||
def estimate_encoding(device: Dict) -> str:
|
||||
"""
|
||||
Estimate encoding scheme from modulation and name patterns
|
||||
|
||||
Common mappings:
|
||||
- OOK → PWM (most common)
|
||||
- FSK → Manchester or custom
|
||||
- ASK → PWM
|
||||
"""
|
||||
modulation = (device.get('modulation') or 'OOK').upper()
|
||||
name = (device.get('name') or '').lower()
|
||||
|
||||
# Manchester encoding indicators
|
||||
if 'manchester' in name or 'oregon' in name:
|
||||
return 'Encoding.MANCHESTER'
|
||||
|
||||
# Differential Manchester (rare)
|
||||
if 'diff' in name and 'manchester' in name:
|
||||
return 'Encoding.DIFFERENTIAL_MANCHESTER'
|
||||
|
||||
# Default based on modulation
|
||||
if modulation in ['OOK', 'ASK']:
|
||||
return 'Encoding.PWM'
|
||||
elif modulation == 'FSK':
|
||||
return 'Encoding.MANCHESTER' # FSK often uses Manchester
|
||||
|
||||
return 'Encoding.PWM' # Safe default
|
||||
|
||||
|
||||
def estimate_bit_counts(device: Dict) -> tuple:
|
||||
"""
|
||||
Estimate min/max bit counts from device characteristics
|
||||
|
||||
Based on:
|
||||
- Typical payload sizes for device categories
|
||||
- Manufacturer patterns
|
||||
"""
|
||||
category = (device.get('category') or '').lower()
|
||||
name = (device.get('name') or '').lower()
|
||||
|
||||
# Weather sensors: typically 32-72 bits
|
||||
if category == 'weather':
|
||||
if 'oregon' in name:
|
||||
return (64, 128) # Oregon Scientific uses longer messages
|
||||
return (32, 72)
|
||||
|
||||
# Remotes/garage openers: typically 24-40 bits
|
||||
if 'remote' in name or 'garage' in name:
|
||||
return (24, 40)
|
||||
|
||||
# TPMS: typically 64-80 bits
|
||||
if 'tpms' in name:
|
||||
return (64, 80)
|
||||
|
||||
# Security sensors: 32-64 bits
|
||||
if category == 'security':
|
||||
return (32, 64)
|
||||
|
||||
# Default
|
||||
return (24, 64)
|
||||
|
||||
|
||||
def estimate_pulse_count(short_width: int, long_width: int, avg_bits: int) -> int:
|
||||
"""
|
||||
Estimate typical pulse count for a transmission
|
||||
|
||||
Formula: pulses ≈ 2 * bits (for PWM: each bit = HIGH + LOW pulse)
|
||||
+ preamble overhead (~20-40 pulses)
|
||||
"""
|
||||
base_pulses = avg_bits * 2 # Each bit = 2 transitions
|
||||
preamble_overhead = 30 # Typical preamble length
|
||||
|
||||
return base_pulses + preamble_overhead
|
||||
|
||||
|
||||
def convert_to_protocol_signature(device: Dict) -> Dict:
|
||||
"""
|
||||
Convert RTL_433 device format to GigLez ProtocolSignature
|
||||
|
||||
Input (RTL_433):
|
||||
{
|
||||
"device_id": "acurite_th",
|
||||
"name": "Acurite 609TXC Temperature and Humidity Sensor",
|
||||
"modulation": "OOK",
|
||||
"short_width": 1000,
|
||||
"long_width": 2000,
|
||||
"gap_limit": 3000,
|
||||
"reset_limit": 10000,
|
||||
"frequency": null,
|
||||
"category": "weather",
|
||||
"manufacturer": "Acurite"
|
||||
}
|
||||
|
||||
Output (GigLez):
|
||||
ProtocolSignature(
|
||||
name="Acurite 609TXC Temperature and Humidity Sensor",
|
||||
category="Weather Sensor",
|
||||
manufacturer="Acurite",
|
||||
modulation=Modulation.OOK,
|
||||
encoding=Encoding.PWM,
|
||||
short_pulse_us=1000,
|
||||
long_pulse_us=2000,
|
||||
frequency=433920000,
|
||||
...
|
||||
)
|
||||
"""
|
||||
# Extract basic fields
|
||||
name = device.get('name', 'Unknown Device')
|
||||
manufacturer = device.get('manufacturer', 'Unknown')
|
||||
category = device.get('category', 'unknown').title()
|
||||
|
||||
# Map modulation
|
||||
modulation_map = {
|
||||
'OOK': 'Modulation.OOK',
|
||||
'FSK': 'Modulation.FSK',
|
||||
'ASK': 'Modulation.ASK'
|
||||
}
|
||||
modulation = modulation_map.get(device.get('modulation', 'OOK'), 'Modulation.OOK')
|
||||
|
||||
# Timing parameters
|
||||
short_width = device.get('short_width') or 500
|
||||
long_width = device.get('long_width') or 1000
|
||||
|
||||
# Frequency estimation
|
||||
frequency = estimate_frequency(device)
|
||||
|
||||
# Encoding estimation
|
||||
encoding = estimate_encoding(device)
|
||||
|
||||
# Bit count estimation
|
||||
min_bits, max_bits = estimate_bit_counts(device)
|
||||
avg_bits = (min_bits + max_bits) // 2
|
||||
|
||||
# Pulse count estimation
|
||||
typical_pulse_count = estimate_pulse_count(short_width, long_width, avg_bits)
|
||||
|
||||
# Preamble/sync patterns (if detectable from name)
|
||||
preamble_pattern = None
|
||||
sync_pattern = None
|
||||
|
||||
if 'oregon' in name.lower():
|
||||
preamble_pattern = '"1010" * 12' # Oregon Scientific preamble
|
||||
sync_pattern = '"1000"'
|
||||
elif 'princeton' in name.lower() or 'pt2262' in name.lower():
|
||||
preamble_pattern = '"1" * 4'
|
||||
sync_pattern = '"10"'
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
'category': category,
|
||||
'manufacturer': manufacturer,
|
||||
'modulation': modulation,
|
||||
'encoding': encoding,
|
||||
'short_pulse_us': short_width,
|
||||
'long_pulse_us': long_width,
|
||||
'frequency': frequency,
|
||||
'frequency_tolerance': 100000, # ±100 kHz
|
||||
'min_bits': min_bits,
|
||||
'max_bits': max_bits,
|
||||
'typical_pulse_count': typical_pulse_count,
|
||||
'preamble_pattern': preamble_pattern,
|
||||
'sync_pattern': sync_pattern,
|
||||
'timing_tolerance': 0.25, # ±25% (more lenient than hand-curated)
|
||||
'min_confidence': 0.5, # Lower threshold for imported protocols
|
||||
'source': f"RTL_433 (device_id: {device.get('device_id', 'unknown')})"
|
||||
}
|
||||
|
||||
|
||||
def filter_valid_protocols(protocols: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Filter out invalid/incomplete protocol definitions
|
||||
|
||||
Requirements:
|
||||
- Must have valid timing (short_width and long_width > 0)
|
||||
- Must have reasonable values (not extreme outliers)
|
||||
- Prefer unique entries (deduplicate by name)
|
||||
"""
|
||||
valid = []
|
||||
seen_names = set()
|
||||
|
||||
for proto in protocols:
|
||||
# Check timing validity
|
||||
if proto['short_pulse_us'] <= 0 or proto['long_pulse_us'] <= 0:
|
||||
continue
|
||||
|
||||
# Check reasonable ranges (10μs to 10ms)
|
||||
if not (10 <= proto['short_pulse_us'] <= 10000):
|
||||
continue
|
||||
if not (10 <= proto['long_pulse_us'] <= 10000):
|
||||
continue
|
||||
|
||||
# Check long > short (PWM assumption)
|
||||
if proto['long_pulse_us'] <= proto['short_pulse_us']:
|
||||
# Swap if reversed
|
||||
proto['short_pulse_us'], proto['long_pulse_us'] = \
|
||||
proto['long_pulse_us'], proto['short_pulse_us']
|
||||
|
||||
# Deduplicate by name
|
||||
if proto['name'] in seen_names:
|
||||
continue
|
||||
|
||||
seen_names.add(proto['name'])
|
||||
valid.append(proto)
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def generate_python_code(protocols: List[Dict]) -> str:
|
||||
"""
|
||||
Generate Python code for rtl433_protocols_imported.py
|
||||
|
||||
Creates a list of ProtocolSignature objects that can be imported
|
||||
into protocol_database.py
|
||||
"""
|
||||
# Group by category for organization
|
||||
by_category = defaultdict(list)
|
||||
for proto in protocols:
|
||||
by_category[proto['category']].append(proto)
|
||||
|
||||
code = '''#!/usr/bin/env python3
|
||||
"""
|
||||
RTL_433 Imported Protocol Signatures
|
||||
|
||||
Auto-generated from RTL_433 open-source protocol database.
|
||||
Source: data/rtl_433_protocols.json ({total} devices)
|
||||
|
||||
DO NOT EDIT MANUALLY - run scripts/import_rtl433_protocols.py to regenerate.
|
||||
|
||||
Generated: {date}
|
||||
"""
|
||||
|
||||
from src.matcher.protocol_database import ProtocolSignature, Modulation, Encoding
|
||||
|
||||
# Imported RTL_433 Protocols ({count} total)
|
||||
RTL433_PROTOCOLS = [
|
||||
'''.format(
|
||||
total=len(protocols),
|
||||
count=len(protocols),
|
||||
date=__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
)
|
||||
|
||||
# Generate entries by category
|
||||
for category in sorted(by_category.keys()):
|
||||
protos = by_category[category]
|
||||
code += f'\n # {category} ({len(protos)} devices)\n'
|
||||
|
||||
for proto in protos:
|
||||
code += f''' ProtocolSignature(
|
||||
name="{proto['name']}",
|
||||
category="{proto['category']}",
|
||||
manufacturer="{proto['manufacturer']}",
|
||||
modulation={proto['modulation']},
|
||||
encoding={proto['encoding']},
|
||||
short_pulse_us={proto['short_pulse_us']},
|
||||
long_pulse_us={proto['long_pulse_us']},
|
||||
frequency={proto['frequency']},
|
||||
frequency_tolerance={proto['frequency_tolerance']},
|
||||
min_bits={proto['min_bits']},
|
||||
max_bits={proto['max_bits']},
|
||||
typical_pulse_count={proto['typical_pulse_count']},
|
||||
'''
|
||||
|
||||
if proto['preamble_pattern']:
|
||||
code += f" preamble_pattern={proto['preamble_pattern']},\n"
|
||||
if proto['sync_pattern']:
|
||||
code += f" sync_pattern={proto['sync_pattern']},\n"
|
||||
|
||||
code += f''' timing_tolerance={proto['timing_tolerance']},
|
||||
min_confidence={proto['min_confidence']},
|
||||
),
|
||||
'''
|
||||
|
||||
code += ''']
|
||||
|
||||
# Export for use in protocol_database.py
|
||||
__all__ = ['RTL433_PROTOCOLS']
|
||||
'''
|
||||
|
||||
return code
|
||||
|
||||
|
||||
def update_protocol_database():
|
||||
"""
|
||||
Update protocol_database.py to include RTL433_PROTOCOLS
|
||||
"""
|
||||
db_path = Path(__file__).parent.parent / "src" / "matcher" / "protocol_database.py"
|
||||
|
||||
with open(db_path, 'r') as f:
|
||||
original_code = f.read()
|
||||
|
||||
# Check if already updated
|
||||
if 'RTL433_PROTOCOLS' in original_code:
|
||||
print("⚠ protocol_database.py already includes RTL433_PROTOCOLS")
|
||||
return
|
||||
|
||||
# Find the ALL_PROTOCOLS definition and update it
|
||||
old_all_protocols = '''# Compile all protocols into single list
|
||||
ALL_PROTOCOLS = (
|
||||
WEATHER_SENSORS +
|
||||
GARAGE_DOOR_OPENERS +
|
||||
DOORBELLS +
|
||||
TIRE_PRESSURE +
|
||||
SECURITY_SENSORS +
|
||||
REMOTE_CONTROLS
|
||||
)'''
|
||||
|
||||
new_all_protocols = '''# Import RTL_433 protocols
|
||||
try:
|
||||
from src.matcher.rtl433_protocols_imported import RTL433_PROTOCOLS
|
||||
except ImportError:
|
||||
print("Warning: RTL_433 protocols not imported yet. Run scripts/import_rtl433_protocols.py")
|
||||
RTL433_PROTOCOLS = []
|
||||
|
||||
# Compile all protocols into single list
|
||||
ALL_PROTOCOLS = (
|
||||
WEATHER_SENSORS +
|
||||
GARAGE_DOOR_OPENERS +
|
||||
DOORBELLS +
|
||||
TIRE_PRESSURE +
|
||||
SECURITY_SENSORS +
|
||||
REMOTE_CONTROLS +
|
||||
RTL433_PROTOCOLS # Imported from RTL_433 database
|
||||
)'''
|
||||
|
||||
updated_code = original_code.replace(old_all_protocols, new_all_protocols)
|
||||
|
||||
with open(db_path, 'w') as f:
|
||||
f.write(updated_code)
|
||||
|
||||
print(f"✓ Updated {db_path} to include RTL433_PROTOCOLS")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main import workflow"""
|
||||
print("=== RTL_433 Protocol Database Import ===\n")
|
||||
|
||||
# Step 1: Load RTL_433 JSON
|
||||
data = load_rtl433_json()
|
||||
devices = data['devices']
|
||||
|
||||
# Step 2: Convert to GigLez format
|
||||
print(f"\n📝 Converting {len(devices)} devices to ProtocolSignature format...")
|
||||
protocols = [convert_to_protocol_signature(dev) for dev in devices]
|
||||
|
||||
# Step 3: Filter valid entries
|
||||
print(f"🔍 Filtering valid protocols...")
|
||||
valid_protocols = filter_valid_protocols(protocols)
|
||||
print(f"✓ Kept {len(valid_protocols)} valid protocols (removed {len(protocols) - len(valid_protocols)} invalid)")
|
||||
|
||||
# Step 4: Generate Python code
|
||||
print(f"\n📄 Generating Python code...")
|
||||
code = generate_python_code(valid_protocols)
|
||||
|
||||
# Step 5: Write to file
|
||||
output_path = Path(__file__).parent.parent / "src" / "matcher" / "rtl433_protocols_imported.py"
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(code)
|
||||
|
||||
print(f"✓ Written to {output_path}")
|
||||
|
||||
# Step 6: Update protocol_database.py
|
||||
print(f"\n🔧 Updating protocol_database.py...")
|
||||
update_protocol_database()
|
||||
|
||||
# Step 7: Summary statistics
|
||||
print(f"\n=== Import Summary ===")
|
||||
print(f"Total RTL_433 devices: {len(devices)}")
|
||||
print(f"Valid protocols imported: {len(valid_protocols)}")
|
||||
|
||||
# By category
|
||||
from collections import Counter
|
||||
categories = Counter(p['category'] for p in valid_protocols)
|
||||
print(f"\nProtocols by category:")
|
||||
for cat, count in categories.most_common():
|
||||
print(f" {cat}: {count}")
|
||||
|
||||
# By frequency
|
||||
frequencies = Counter(p['frequency'] for p in valid_protocols)
|
||||
print(f"\nProtocols by frequency:")
|
||||
for freq, count in sorted(frequencies.items()):
|
||||
freq_mhz = freq / 1_000_000
|
||||
print(f" {freq_mhz:.2f} MHz: {count}")
|
||||
|
||||
print(f"\n✅ Import complete!")
|
||||
print(f" Original protocols: 18")
|
||||
print(f" Imported protocols: {len(valid_protocols)}")
|
||||
print(f" Total protocols: {18 + len(valid_protocols)}")
|
||||
|
||||
# Verify import
|
||||
print(f"\n🧪 Verifying import...")
|
||||
try:
|
||||
from src.matcher.protocol_database import get_protocol_database
|
||||
db = get_protocol_database()
|
||||
stats = db.get_statistics()
|
||||
print(f"✓ Database loaded successfully")
|
||||
print(f" Total protocols in database: {stats['total_protocols']}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error loading database: {e}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,504 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Import RF Device Signatures into Database
|
||||
|
||||
Imports device signatures from downloaded datasets:
|
||||
1. Flipper Zero .sub file collections (with automatic labeling from file paths)
|
||||
2. RTL_433 protocol definitions
|
||||
3. Creates devices and signatures records for matching engine
|
||||
|
||||
Usage:
|
||||
python3 scripts/import_signatures_to_db.py --flipper --rtl433 --limit 10000
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Add project root to path
|
||||
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 Device, Signature, FlipperSignature
|
||||
from src.parser.sub_parser import parse_sub_file
|
||||
from src.parser.metadata import SignalMetadata
|
||||
|
||||
# Compatibility alias
|
||||
SessionLocal = get_session
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
FLIPPER_DATASETS = [
|
||||
DATA_DIR / "rf_test_datasets" / "UberGuidoZ_Flipper",
|
||||
DATA_DIR / "test_known_devices",
|
||||
]
|
||||
|
||||
RTL433_REPO = DATA_DIR / "test_rtl433_real"
|
||||
|
||||
# Device type mapping from file paths
|
||||
DEVICE_TYPE_PATTERNS = {
|
||||
r"weather[_\s]?station": "Weather Sensor",
|
||||
r"garage[_\s]?door": "Garage Door Opener",
|
||||
r"gate[_\s]?opener": "Gate Opener",
|
||||
r"doorbell": "Doorbell",
|
||||
r"door[_\s]?sensor": "Door Sensor",
|
||||
r"motion[_\s]?sensor": "Motion Sensor",
|
||||
r"smoke[_\s]?detector": "Smoke Detector",
|
||||
r"remote[_\s]?control": "Remote Control",
|
||||
r"car[_\s]?key": "Car Key Fob",
|
||||
r"tpms": "TPMS",
|
||||
r"tire[_\s]?pressure": "TPMS",
|
||||
r"security": "Security System",
|
||||
r"alarm": "Alarm System",
|
||||
}
|
||||
|
||||
MANUFACTURER_PATTERNS = {
|
||||
r"chamberlain": "Chamberlain",
|
||||
r"liftmaster": "LiftMaster",
|
||||
r"genie": "Genie",
|
||||
r"linear": "Linear",
|
||||
r"oregon[_\s]?scientific": "Oregon Scientific",
|
||||
r"acurite": "AcuRite",
|
||||
r"lacrosse": "LaCrosse",
|
||||
r"ambient[_\s]?weather": "Ambient Weather",
|
||||
r"honeywell": "Honeywell",
|
||||
r"nest": "Nest",
|
||||
r"ring": "Ring",
|
||||
r"yale": "Yale",
|
||||
r"toyota": "Toyota",
|
||||
r"schrader": "Schrader",
|
||||
r"nice": "NICE",
|
||||
r"came": "CAME",
|
||||
r"bft": "BFT",
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DATA CLASSES
|
||||
# =============================================================================
|
||||
|
||||
@dataclass
|
||||
class ExtractedDeviceInfo:
|
||||
"""Device information extracted from file path and content"""
|
||||
device_type: Optional[str] = None
|
||||
manufacturer: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
confidence: float = 0.5 # Confidence in automatic extraction
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FILE PATH ANALYSIS
|
||||
# =============================================================================
|
||||
|
||||
def extract_device_info_from_path(file_path: Path) -> ExtractedDeviceInfo:
|
||||
"""
|
||||
Extract device information from file path structure
|
||||
|
||||
Example paths:
|
||||
- Weather_stations/Oregon_Scientific/THGN123N.sub
|
||||
- Garage_Door_Openers/Chamberlain/model_891LM.sub
|
||||
- UberGuidoZ/Sub-GHz/Vehicles/Tesla/Model_3_Charge_Port.sub
|
||||
"""
|
||||
info = ExtractedDeviceInfo()
|
||||
|
||||
path_str = str(file_path).lower()
|
||||
parts = file_path.parts
|
||||
|
||||
# Extract device type
|
||||
for pattern, device_type in DEVICE_TYPE_PATTERNS.items():
|
||||
if re.search(pattern, path_str, re.IGNORECASE):
|
||||
info.device_type = device_type
|
||||
info.confidence = 0.7
|
||||
break
|
||||
|
||||
# Extract manufacturer
|
||||
for pattern, manufacturer in MANUFACTURER_PATTERNS.items():
|
||||
if re.search(pattern, path_str, re.IGNORECASE):
|
||||
info.manufacturer = manufacturer
|
||||
info.confidence = 0.8
|
||||
break
|
||||
|
||||
# Extract model from filename (heuristic: uppercase letters + numbers)
|
||||
filename = file_path.stem # Without extension
|
||||
model_match = re.search(r'([A-Z0-9]{3,}[-_]?[A-Z0-9]*)', filename)
|
||||
if model_match:
|
||||
info.model = model_match.group(1).replace('_', ' ')
|
||||
info.confidence = max(info.confidence, 0.6)
|
||||
|
||||
return info
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FLIPPER ZERO SIGNATURE IMPORT
|
||||
# =============================================================================
|
||||
|
||||
def import_flipper_signatures(
|
||||
db: Session,
|
||||
limit: Optional[int] = None,
|
||||
skip_existing: bool = True
|
||||
) -> Tuple[int, int, int]:
|
||||
"""
|
||||
Import Flipper Zero .sub files as device signatures
|
||||
|
||||
Returns:
|
||||
(devices_created, signatures_created, files_skipped)
|
||||
"""
|
||||
devices_created = 0
|
||||
signatures_created = 0
|
||||
files_skipped = 0
|
||||
|
||||
# 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 across all datasets")
|
||||
|
||||
if limit:
|
||||
sub_files = sub_files[:limit]
|
||||
logger.info(f"Limited to {limit} files")
|
||||
|
||||
# Process each file
|
||||
for idx, sub_file in enumerate(sub_files, 1):
|
||||
if idx % 100 == 0:
|
||||
logger.info(f"Progress: {idx}/{len(sub_files)} ({idx/len(sub_files)*100:.1f}%)")
|
||||
|
||||
try:
|
||||
# Compute file hash for deduplication
|
||||
file_hash = hashlib.sha256(sub_file.read_bytes()).hexdigest()
|
||||
|
||||
# Check if already imported
|
||||
if skip_existing:
|
||||
existing = db.query(FlipperSignature).filter_by(
|
||||
file_hash=file_hash
|
||||
).first()
|
||||
if existing:
|
||||
files_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}")
|
||||
files_skipped += 1
|
||||
continue
|
||||
|
||||
# Extract device info from path
|
||||
device_info = extract_device_info_from_path(sub_file)
|
||||
|
||||
# Skip if no useful information extracted
|
||||
if not device_info.device_type and not metadata.protocol:
|
||||
files_skipped += 1
|
||||
continue
|
||||
|
||||
# Create or find device
|
||||
device_query = db.query(Device)
|
||||
|
||||
if device_info.manufacturer and device_info.model:
|
||||
device_query = device_query.filter_by(
|
||||
manufacturer=device_info.manufacturer,
|
||||
model=device_info.model
|
||||
)
|
||||
elif metadata.protocol:
|
||||
# Use protocol as fallback identifier
|
||||
device_query = device_query.filter_by(
|
||||
protocol=metadata.protocol,
|
||||
typical_frequency=metadata.frequency
|
||||
)
|
||||
else:
|
||||
files_skipped += 1
|
||||
continue
|
||||
|
||||
device = device_query.first()
|
||||
|
||||
if not device:
|
||||
# Create new device
|
||||
device = Device(
|
||||
manufacturer=device_info.manufacturer or "Unknown",
|
||||
model=device_info.model or metadata.protocol or "Unknown",
|
||||
device_type=device_info.device_type or "Unknown",
|
||||
typical_frequency=metadata.frequency,
|
||||
protocol=metadata.protocol,
|
||||
bit_length=metadata.bit_length,
|
||||
encoding=metadata.file_format if metadata.file_format else None,
|
||||
source="flipper",
|
||||
verified=False # Needs community verification
|
||||
)
|
||||
db.add(device)
|
||||
db.flush() # Get device ID
|
||||
devices_created += 1
|
||||
|
||||
# Extract timing statistics for signature matching
|
||||
timing_min = None
|
||||
timing_max = None
|
||||
|
||||
if metadata.timing_element:
|
||||
# KEY format: use timing_element with tolerance
|
||||
timing_min = int(metadata.timing_element * 0.8)
|
||||
timing_max = int(metadata.timing_element * 1.2)
|
||||
elif metadata.raw_data and len(metadata.raw_data) > 0:
|
||||
# RAW format: calculate from pulse statistics
|
||||
pulse_widths = [abs(p) for p in metadata.raw_data]
|
||||
if pulse_widths:
|
||||
timing_min = int(min(pulse_widths) * 0.9) # 10% tolerance
|
||||
timing_max = int(max(pulse_widths) * 1.1)
|
||||
|
||||
# Generate bit_mask for KEY format (all bits significant by default)
|
||||
bit_mask = None
|
||||
if metadata.key_data:
|
||||
# Create mask with all bits set to 1 (all bits are significant)
|
||||
bit_mask = bytes([0xFF] * len(metadata.key_data))
|
||||
|
||||
# Create signature with enhanced metadata
|
||||
signature = Signature(
|
||||
device_id=device.id,
|
||||
protocol=metadata.protocol,
|
||||
frequency=metadata.frequency,
|
||||
modulation=str(metadata.modulation).replace("Modulation.", "") if metadata.modulation else None,
|
||||
bit_pattern=metadata.key_data if metadata.key_data else None,
|
||||
bit_mask=bit_mask,
|
||||
timing_min=timing_min,
|
||||
timing_max=timing_max,
|
||||
weight=device_info.confidence,
|
||||
source="flipper"
|
||||
)
|
||||
db.add(signature)
|
||||
signatures_created += 1
|
||||
|
||||
# Create Flipper-specific record
|
||||
flipper_sig = FlipperSignature(
|
||||
signature_id=None, # Will be set after signature is committed
|
||||
file_hash=file_hash,
|
||||
file_path=str(sub_file.relative_to(DATA_DIR)),
|
||||
preset=metadata.preset if hasattr(metadata, 'preset') else None,
|
||||
raw_format=metadata.file_format
|
||||
)
|
||||
db.add(flipper_sig)
|
||||
|
||||
# Commit in batches
|
||||
if idx % 50 == 0:
|
||||
try:
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.debug(f"Batch commit error: {e}")
|
||||
db.rollback()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {sub_file}: {e}")
|
||||
db.rollback() # Rollback after error to continue with next file
|
||||
files_skipped += 1
|
||||
continue
|
||||
|
||||
# Final commit
|
||||
try:
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.debug(f"Final commit error: {e}")
|
||||
db.rollback()
|
||||
|
||||
logger.info(f"Import complete:")
|
||||
logger.info(f" - Devices created: {devices_created}")
|
||||
logger.info(f" - Signatures created: {signatures_created}")
|
||||
logger.info(f" - Files skipped: {files_skipped}")
|
||||
|
||||
return devices_created, signatures_created, files_skipped
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RTL_433 PROTOCOL IMPORT
|
||||
# =============================================================================
|
||||
|
||||
def import_rtl433_protocols(db: Session) -> Tuple[int, int]:
|
||||
"""
|
||||
Import RTL_433 protocol definitions from test files
|
||||
|
||||
RTL_433 test files include .json metadata with:
|
||||
- Protocol name and number
|
||||
- Expected output fields
|
||||
- Device information
|
||||
|
||||
Returns:
|
||||
(devices_created, signatures_created)
|
||||
"""
|
||||
devices_created = 0
|
||||
signatures_created = 0
|
||||
|
||||
if not RTL433_REPO.exists():
|
||||
logger.warning(f"RTL_433 test repo not found: {RTL433_REPO}")
|
||||
return 0, 0
|
||||
|
||||
# Find all .json files (contain protocol metadata)
|
||||
json_files = list(RTL433_REPO.rglob("*.json"))
|
||||
logger.info(f"Found {len(json_files)} RTL_433 test .json files")
|
||||
|
||||
protocol_map = {} # protocol_name -> metadata
|
||||
|
||||
for json_file in json_files:
|
||||
try:
|
||||
with open(json_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# RTL_433 .json files contain decoded device information
|
||||
if isinstance(data, dict) and 'model' in data:
|
||||
model = data.get('model')
|
||||
protocol_id = data.get('protocol', None)
|
||||
|
||||
# Extract device info
|
||||
manufacturer = None
|
||||
if '-' in model:
|
||||
parts = model.split('-', 1)
|
||||
manufacturer = parts[0].strip()
|
||||
model_name = parts[1].strip()
|
||||
else:
|
||||
model_name = model
|
||||
|
||||
# Frequency from file path or metadata
|
||||
freq_match = re.search(r'(\d+)M', str(json_file))
|
||||
frequency = int(freq_match.group(1)) * 1000000 if freq_match else 433920000
|
||||
|
||||
# Store protocol info
|
||||
key = (manufacturer or "Unknown", model_name)
|
||||
if key not in protocol_map:
|
||||
protocol_map[key] = {
|
||||
'manufacturer': manufacturer or "Unknown",
|
||||
'model': model_name,
|
||||
'frequency': frequency,
|
||||
'protocol': model,
|
||||
'protocol_id': protocol_id,
|
||||
'device_type': 'Weather Sensor', # Most RTL_433 devices
|
||||
'example_data': data
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error parsing {json_file}: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"Extracted {len(protocol_map)} unique RTL_433 protocols")
|
||||
|
||||
# Create devices and signatures
|
||||
for (manufacturer, model_name), proto_info in protocol_map.items():
|
||||
# Check if device exists
|
||||
device = db.query(Device).filter_by(
|
||||
manufacturer=manufacturer,
|
||||
model=model_name
|
||||
).first()
|
||||
|
||||
if not device:
|
||||
device = Device(
|
||||
manufacturer=manufacturer,
|
||||
model=model_name,
|
||||
device_type=proto_info['device_type'],
|
||||
typical_frequency=proto_info['frequency'],
|
||||
protocol=proto_info['protocol'],
|
||||
source="rtl433",
|
||||
verified=True # RTL_433 protocols are trusted
|
||||
)
|
||||
db.add(device)
|
||||
db.flush()
|
||||
devices_created += 1
|
||||
|
||||
# Create signature
|
||||
signature = Signature(
|
||||
device_id=device.id,
|
||||
protocol=proto_info['protocol'],
|
||||
frequency=proto_info['frequency'],
|
||||
weight=0.95 # High confidence for RTL_433
|
||||
)
|
||||
db.add(signature)
|
||||
signatures_created += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(f"RTL_433 import complete:")
|
||||
logger.info(f" - Devices created: {devices_created}")
|
||||
logger.info(f" - Signatures created: {signatures_created}")
|
||||
|
||||
return devices_created, signatures_created
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Import RF device signatures into database")
|
||||
parser.add_argument("--flipper", action="store_true", help="Import Flipper Zero signatures")
|
||||
parser.add_argument("--rtl433", action="store_true", help="Import RTL_433 protocols")
|
||||
parser.add_argument("--limit", type=int, default=None, help="Limit number of Flipper files to process")
|
||||
parser.add_argument("--skip-existing", action="store_true", default=True, help="Skip already imported files")
|
||||
parser.add_argument("--all", action="store_true", help="Import all sources")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not any([args.flipper, args.rtl433, args.all]):
|
||||
parser.print_help()
|
||||
print("\nError: Must specify at least one import source (--flipper, --rtl433, or --all)")
|
||||
sys.exit(1)
|
||||
|
||||
# Setup logging
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="INFO")
|
||||
logger.add("logs/import_signatures.log", rotation="10 MB", level="DEBUG")
|
||||
|
||||
logger.info("=" * 80)
|
||||
logger.info("RF Device Signature Import")
|
||||
logger.info("=" * 80)
|
||||
|
||||
# Connect to database
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# Import Flipper signatures
|
||||
if args.flipper or args.all:
|
||||
logger.info("\n[1/2] Importing Flipper Zero signatures...")
|
||||
flipper_stats = import_flipper_signatures(
|
||||
db,
|
||||
limit=args.limit,
|
||||
skip_existing=args.skip_existing
|
||||
)
|
||||
logger.info(f"Flipper import: {flipper_stats[0]} devices, {flipper_stats[1]} signatures")
|
||||
|
||||
# Import RTL_433 protocols
|
||||
if args.rtl433 or args.all:
|
||||
logger.info("\n[2/2] Importing RTL_433 protocols...")
|
||||
rtl_stats = import_rtl433_protocols(db)
|
||||
logger.info(f"RTL_433 import: {rtl_stats[0]} devices, {rtl_stats[1]} signatures")
|
||||
|
||||
logger.info("\n" + "=" * 80)
|
||||
logger.info("Import complete!")
|
||||
logger.info("=" * 80)
|
||||
|
||||
# Summary
|
||||
total_devices = db.query(Device).count()
|
||||
total_signatures = db.query(Signature).count()
|
||||
|
||||
logger.info(f"\nDatabase totals:")
|
||||
logger.info(f" - Total devices: {total_devices}")
|
||||
logger.info(f" - Total signatures: {total_signatures}")
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+370
@@ -0,0 +1,370 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Add missing columns to flipper_signatures table
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from src.database.connection import get_engine
|
||||
from sqlalchemy import text
|
||||
|
||||
def main():
|
||||
engine = get_engine()
|
||||
|
||||
with engine.connect() as conn:
|
||||
print("Adding missing columns to flipper_signatures table...")
|
||||
|
||||
try:
|
||||
# Add file_hash column
|
||||
conn.execute(text("""
|
||||
ALTER TABLE flipper_signatures
|
||||
ADD COLUMN IF NOT EXISTS file_hash VARCHAR(64) UNIQUE
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS ix_flipper_signatures_file_hash
|
||||
ON flipper_signatures(file_hash)
|
||||
"""))
|
||||
print("✓ Added file_hash column")
|
||||
except Exception as e:
|
||||
print(f"file_hash: {e}")
|
||||
|
||||
try:
|
||||
# Add file_path column
|
||||
conn.execute(text("""
|
||||
ALTER TABLE flipper_signatures
|
||||
ADD COLUMN IF NOT EXISTS file_path VARCHAR(500)
|
||||
"""))
|
||||
print("✓ Added file_path column")
|
||||
except Exception as e:
|
||||
print(f"file_path: {e}")
|
||||
|
||||
try:
|
||||
# Add raw_format column
|
||||
conn.execute(text("""
|
||||
ALTER TABLE flipper_signatures
|
||||
ADD COLUMN IF NOT EXISTS raw_format VARCHAR(20)
|
||||
"""))
|
||||
print("✓ Added raw_format column")
|
||||
except Exception as e:
|
||||
print(f"raw_format: {e}")
|
||||
|
||||
conn.commit()
|
||||
print("\n✓ Migration complete!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+468
@@ -0,0 +1,468 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# Safe Package Installation Script
|
||||
#
|
||||
# Provides safe python -m pip installation with:
|
||||
# - Virtual environment detection/creation
|
||||
# - Dependency resolution
|
||||
# - Version compatibility checking
|
||||
# - Rollback on failure
|
||||
# - Security scanning
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/safe_install.sh [package_name] [version]
|
||||
# ./scripts/safe_install.sh -r requirements.txt
|
||||
# ./scripts/safe_install.sh --all # Install all project dependencies
|
||||
###############################################################################
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Configuration
|
||||
VENV_DIR="$PROJECT_ROOT/venv"
|
||||
REQUIREMENTS_FILE="$PROJECT_ROOT/requirements.txt"
|
||||
BACKUP_DIR="$PROJECT_ROOT/.pip_backups"
|
||||
|
||||
###############################################################################
|
||||
# Helper Functions
|
||||
###############################################################################
|
||||
|
||||
print_header() {
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ $1${NC}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Virtual Environment Management
|
||||
###############################################################################
|
||||
|
||||
check_venv() {
|
||||
if [[ "$VIRTUAL_ENV" != "" ]]; then
|
||||
print_success "Virtual environment active: $VIRTUAL_ENV"
|
||||
return 0
|
||||
else
|
||||
print_warning "No virtual environment active"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
create_venv() {
|
||||
print_header "Creating Virtual Environment"
|
||||
|
||||
if [ -d "$VENV_DIR" ]; then
|
||||
print_info "Virtual environment already exists at: $VENV_DIR"
|
||||
read -p "Recreate it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
print_info "Removing old virtual environment..."
|
||||
rm -rf "$VENV_DIR"
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
print_info "Creating new virtual environment..."
|
||||
python3 -m venv "$VENV_DIR"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Virtual environment created at: $VENV_DIR"
|
||||
print_info "Activate it with: source $VENV_DIR/bin/activate"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to create virtual environment"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
activate_venv() {
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
print_warning "Virtual environment not found at: $VENV_DIR"
|
||||
read -p "Create it now? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||
create_venv || return 1
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if already activated
|
||||
if check_venv; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_info "Activating virtual environment..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
if check_venv; then
|
||||
print_success "Virtual environment activated"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to activate virtual environment"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Pip Safety Checks
|
||||
###############################################################################
|
||||
|
||||
check_pip() {
|
||||
print_header "Checking pip"
|
||||
|
||||
# Use python -m pip to bypass any aliases
|
||||
if ! python -m pip --version &> /dev/null; then
|
||||
print_error "pip not found"
|
||||
print_info "Installing pip..."
|
||||
|
||||
# Try to install pip
|
||||
if command -v python3 &> /dev/null; then
|
||||
python3 -m ensurepip --upgrade
|
||||
else
|
||||
print_error "Python 3 not found. Please install Python 3 first."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check pip version
|
||||
pip_version=$(python -m pip --version | awk '{print $2}')
|
||||
print_success "pip version: $pip_version"
|
||||
|
||||
# Upgrade pip if needed
|
||||
print_info "Upgrading pip to latest version..."
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
create_backup() {
|
||||
print_header "Creating Backup"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
backup_file="$BACKUP_DIR/installed_packages_$timestamp.txt"
|
||||
|
||||
print_info "Saving current package list to: $backup_file"
|
||||
python -m pip freeze > "$backup_file"
|
||||
|
||||
print_success "Backup created"
|
||||
echo "$backup_file"
|
||||
}
|
||||
|
||||
check_conflicts() {
|
||||
local package=$1
|
||||
|
||||
print_header "Checking for Conflicts"
|
||||
|
||||
# Use pip-conflict-checker if available
|
||||
if command -v pip-conflict-checker &> /dev/null; then
|
||||
pip-conflict-checker "$package"
|
||||
else
|
||||
# Basic check using pip
|
||||
print_info "Running dependency check..."
|
||||
python -m pip install --dry-run "$package" 2>&1 | grep -i "conflict\|incompatible" || true
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Installation Functions
|
||||
###############################################################################
|
||||
|
||||
safe_install_package() {
|
||||
local package=$1
|
||||
local version=$2
|
||||
|
||||
# Construct package spec
|
||||
if [ -n "$version" ]; then
|
||||
package_spec="$package==$version"
|
||||
else
|
||||
package_spec="$package"
|
||||
fi
|
||||
|
||||
print_header "Installing: $package_spec"
|
||||
|
||||
# Create backup
|
||||
backup_file=$(create_backup)
|
||||
|
||||
# Check for conflicts
|
||||
check_conflicts "$package_spec"
|
||||
|
||||
# Ask for confirmation
|
||||
read -p "Proceed with installation? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
print_warning "Installation cancelled"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install
|
||||
print_info "Installing $package_spec..."
|
||||
|
||||
if python -m pip install "$package_spec"; then
|
||||
print_success "Successfully installed $package_spec"
|
||||
|
||||
# Verify installation
|
||||
print_info "Verifying installation..."
|
||||
python -m pip show "$package" &> /dev/null
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Package verified"
|
||||
return 0
|
||||
else
|
||||
print_error "Package installation verification failed"
|
||||
rollback "$backup_file"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
print_error "Installation failed"
|
||||
rollback "$backup_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_from_requirements() {
|
||||
local req_file=$1
|
||||
|
||||
if [ ! -f "$req_file" ]; then
|
||||
print_error "Requirements file not found: $req_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_header "Installing from: $req_file"
|
||||
|
||||
# Create backup
|
||||
backup_file=$(create_backup)
|
||||
|
||||
# Count packages
|
||||
package_count=$(grep -c -v '^#' "$req_file" | grep -c -v '^$' || echo 0)
|
||||
print_info "Found $package_count packages to install"
|
||||
|
||||
# Show packages
|
||||
print_info "Packages to install:"
|
||||
grep -v '^#' "$req_file" | grep -v '^$' | sed 's/^/ - /'
|
||||
|
||||
# Ask for confirmation
|
||||
read -p "Proceed with installation? (Y/n): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
print_warning "Installation cancelled"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install
|
||||
print_info "Installing packages..."
|
||||
|
||||
if python -m pip install -r "$req_file"; then
|
||||
print_success "All packages installed successfully"
|
||||
return 0
|
||||
else
|
||||
print_error "Installation failed"
|
||||
rollback "$backup_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
rollback() {
|
||||
local backup_file=$1
|
||||
|
||||
print_header "Rolling Back"
|
||||
|
||||
if [ ! -f "$backup_file" ]; then
|
||||
print_error "Backup file not found: $backup_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_warning "Rolling back to previous state..."
|
||||
|
||||
# Uninstall all current packages
|
||||
python -m pip freeze | xargs python -m pip uninstall -y
|
||||
|
||||
# Reinstall from backup
|
||||
python -m pip install -r "$backup_file"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Rollback completed"
|
||||
return 0
|
||||
else
|
||||
print_error "Rollback failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Dependency Resolution
|
||||
###############################################################################
|
||||
|
||||
resolve_dependencies() {
|
||||
print_header "Resolving Dependencies"
|
||||
|
||||
# Use pip-tools if available
|
||||
if command -v pip-compile &> /dev/null; then
|
||||
print_info "Using pip-tools to resolve dependencies..."
|
||||
pip-compile --upgrade --generate-hashes requirements.in
|
||||
else
|
||||
print_warning "pip-tools not installed. Install with: python -m pip install pip-tools"
|
||||
print_info "Basic dependency check:"
|
||||
python -m pip check
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Security Scanning
|
||||
###############################################################################
|
||||
|
||||
security_scan() {
|
||||
print_header "Security Scanning"
|
||||
|
||||
# Use safety if available
|
||||
if command -v safety &> /dev/null; then
|
||||
print_info "Running safety check..."
|
||||
safety check --json | jq '.'
|
||||
else
|
||||
print_warning "safety not installed. Install with: python -m pip install safety"
|
||||
print_info "Skipping security scan"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Main Logic
|
||||
###############################################################################
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Safe Package Installation Script
|
||||
|
||||
Usage:
|
||||
$0 [OPTIONS] [PACKAGE] [VERSION]
|
||||
|
||||
Options:
|
||||
--help Show this help message
|
||||
--create-venv Create virtual environment only
|
||||
--activate-venv Activate virtual environment (source this script)
|
||||
--check Check pip and environment
|
||||
--all Install all project dependencies
|
||||
-r FILE Install from requirements file
|
||||
--resolve Resolve dependencies (requires pip-tools)
|
||||
--scan Run security scan (requires safety)
|
||||
--backup Create backup of current packages
|
||||
--rollback FILE Rollback to backup file
|
||||
|
||||
Examples:
|
||||
# Create and activate virtual environment
|
||||
$0 --create-venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Install a single package
|
||||
$0 requests
|
||||
|
||||
# Install specific version
|
||||
$0 requests 2.28.0
|
||||
|
||||
# Install from requirements.txt
|
||||
$0 -r requirements.txt
|
||||
$0 --all # Uses default requirements.txt
|
||||
|
||||
# Check environment
|
||||
$0 --check
|
||||
|
||||
# Security scan
|
||||
$0 --scan
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
# Parse arguments
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
--create-venv)
|
||||
create_venv
|
||||
exit $?
|
||||
;;
|
||||
--check)
|
||||
activate_venv || exit 1
|
||||
check_pip
|
||||
print_info "Python version: $(python --version)"
|
||||
print_info "Installed packages:"
|
||||
python -m pip list
|
||||
exit 0
|
||||
;;
|
||||
--all)
|
||||
activate_venv || exit 1
|
||||
check_pip || exit 1
|
||||
install_from_requirements "$REQUIREMENTS_FILE"
|
||||
exit $?
|
||||
;;
|
||||
-r)
|
||||
activate_venv || exit 1
|
||||
check_pip || exit 1
|
||||
install_from_requirements "$2"
|
||||
exit $?
|
||||
;;
|
||||
--resolve)
|
||||
activate_venv || exit 1
|
||||
resolve_dependencies
|
||||
exit $?
|
||||
;;
|
||||
--scan)
|
||||
activate_venv || exit 1
|
||||
security_scan
|
||||
exit $?
|
||||
;;
|
||||
--backup)
|
||||
activate_venv || exit 1
|
||||
create_backup
|
||||
exit $?
|
||||
;;
|
||||
--rollback)
|
||||
activate_venv || exit 1
|
||||
rollback "$2"
|
||||
exit $?
|
||||
;;
|
||||
"")
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
# Install package
|
||||
activate_venv || exit 1
|
||||
check_pip || exit 1
|
||||
safe_install_package "$1" "$2"
|
||||
exit $?
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run main
|
||||
main "$@"
|
||||
+264
-38
@@ -1,55 +1,281 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# GigLez Database Setup Script
|
||||
# This script creates the PostgreSQL database and user for GigLez
|
||||
#
|
||||
# Creates PostgreSQL database, user, and schema for GigLez platform
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./scripts/setup_database.sh
|
||||
#
|
||||
# This script must be run with sudo to access PostgreSQL
|
||||
###############################################################################
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
echo "🚀 GigLez Database Setup"
|
||||
echo "========================"
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Database configuration
|
||||
# Script directory
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Configuration
|
||||
DB_NAME="giglez"
|
||||
DB_USER="giglez_user"
|
||||
DB_PASSWORD="giglez_secure_password_2026" # Change this in production!
|
||||
PASSWORD_FILE="$PROJECT_ROOT/.db_password"
|
||||
SCHEMA_FILE="$PROJECT_ROOT/scripts/create_schema.sql"
|
||||
|
||||
echo -e "${BLUE}Step 1: Creating PostgreSQL user${NC}"
|
||||
sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" 2>/dev/null || echo " User already exists, skipping..."
|
||||
###############################################################################
|
||||
# Helper Functions
|
||||
###############################################################################
|
||||
|
||||
echo -e "${BLUE}Step 2: Creating database${NC}"
|
||||
sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" 2>/dev/null || echo " Database already exists, skipping..."
|
||||
print_header() {
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
}
|
||||
|
||||
echo -e "${BLUE}Step 3: Granting privileges${NC}"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
echo -e "${BLUE}Step 4: Enabling PostGIS extension${NC}"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "CREATE EXTENSION IF NOT EXISTS postgis;"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "CREATE EXTENSION IF NOT EXISTS postgis_topology;"
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
echo -e "${BLUE}Step 5: Granting schema permissions${NC}"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${DB_USER};"
|
||||
sudo -u postgres psql -d ${DB_NAME} -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};"
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Database setup complete!${NC}"
|
||||
echo ""
|
||||
echo "Connection details:"
|
||||
echo " Database: ${DB_NAME}"
|
||||
echo " User: ${DB_USER}"
|
||||
echo " Password: ${DB_PASSWORD}"
|
||||
echo " Host: localhost"
|
||||
echo " Port: 5432"
|
||||
echo ""
|
||||
echo "Connection string:"
|
||||
echo " postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}"
|
||||
echo ""
|
||||
echo "Test connection:"
|
||||
echo " psql -U ${DB_USER} -d ${DB_NAME} -h localhost"
|
||||
echo ""
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ $1${NC}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Checks
|
||||
###############################################################################
|
||||
|
||||
check_sudo() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_error "This script must be run with sudo"
|
||||
echo "Usage: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
print_success "Running with sudo privileges"
|
||||
}
|
||||
|
||||
check_postgresql() {
|
||||
print_header "Checking PostgreSQL Installation"
|
||||
|
||||
if ! command -v psql &> /dev/null; then
|
||||
print_error "PostgreSQL is not installed"
|
||||
print_info "Install with: sudo apt-get install postgresql postgresql-contrib postgis"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "PostgreSQL is installed"
|
||||
|
||||
# Check if PostgreSQL service is running
|
||||
if ! sudo systemctl is-active --quiet postgresql; then
|
||||
print_warning "PostgreSQL service is not running"
|
||||
print_info "Starting PostgreSQL service..."
|
||||
sudo systemctl start postgresql
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
print_success "PostgreSQL service is running"
|
||||
}
|
||||
|
||||
check_password_file() {
|
||||
print_header "Checking Password File"
|
||||
|
||||
if [ ! -f "$PASSWORD_FILE" ]; then
|
||||
print_error "Password file not found: $PASSWORD_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_PASSWORD=$(cat "$PASSWORD_FILE" | tr -d '\n\r')
|
||||
|
||||
if [ -z "$DB_PASSWORD" ]; then
|
||||
print_error "Password file is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Password loaded from file"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Database Setup
|
||||
###############################################################################
|
||||
|
||||
create_database() {
|
||||
print_header "Creating Database"
|
||||
|
||||
# Check if database already exists
|
||||
if sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw "$DB_NAME"; then
|
||||
print_warning "Database '$DB_NAME' already exists"
|
||||
read -p "Drop and recreate? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
print_info "Dropping existing database..."
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;"
|
||||
print_success "Existing database dropped"
|
||||
else
|
||||
print_info "Keeping existing database"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create database
|
||||
print_info "Creating database '$DB_NAME'..."
|
||||
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME;"
|
||||
print_success "Database created"
|
||||
}
|
||||
|
||||
create_user() {
|
||||
print_header "Creating Database User"
|
||||
|
||||
# Check if user already exists
|
||||
if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1; then
|
||||
print_warning "User '$DB_USER' already exists"
|
||||
read -p "Drop and recreate? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
print_info "Dropping existing user..."
|
||||
sudo -u postgres psql -c "DROP USER IF EXISTS $DB_USER;"
|
||||
print_success "Existing user dropped"
|
||||
else
|
||||
print_info "Updating password for existing user..."
|
||||
sudo -u postgres psql -c "ALTER USER $DB_USER WITH PASSWORD '$DB_PASSWORD';"
|
||||
print_success "Password updated"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create user with password
|
||||
print_info "Creating user '$DB_USER'..."
|
||||
sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';"
|
||||
print_success "User created"
|
||||
}
|
||||
|
||||
grant_privileges() {
|
||||
print_header "Granting Privileges"
|
||||
|
||||
print_info "Granting all privileges on database '$DB_NAME' to '$DB_USER'..."
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;"
|
||||
|
||||
# Grant schema privileges
|
||||
sudo -u postgres psql -d $DB_NAME -c "GRANT ALL ON SCHEMA public TO $DB_USER;"
|
||||
sudo -u postgres psql -d $DB_NAME -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO $DB_USER;"
|
||||
sudo -u postgres psql -d $DB_NAME -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO $DB_USER;"
|
||||
|
||||
# Set default privileges for future objects
|
||||
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $DB_USER;"
|
||||
sudo -u postgres psql -d $DB_NAME -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $DB_USER;"
|
||||
|
||||
print_success "Privileges granted"
|
||||
}
|
||||
|
||||
enable_postgis() {
|
||||
print_header "Enabling PostGIS Extension"
|
||||
|
||||
# Check if PostGIS is available
|
||||
if ! dpkg -l | grep -q postgis; then
|
||||
print_warning "PostGIS is not installed"
|
||||
print_info "Install with: sudo apt-get install postgis postgresql-15-postgis-3"
|
||||
print_info "Skipping PostGIS extension..."
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_info "Enabling PostGIS extension..."
|
||||
sudo -u postgres psql -d $DB_NAME -c "CREATE EXTENSION IF NOT EXISTS postgis;"
|
||||
print_success "PostGIS extension enabled"
|
||||
}
|
||||
|
||||
create_schema() {
|
||||
print_header "Creating Database Schema"
|
||||
|
||||
if [ -f "$SCHEMA_FILE" ]; then
|
||||
print_info "Running schema file: $SCHEMA_FILE"
|
||||
PGPASSWORD="$DB_PASSWORD" psql -U $DB_USER -d $DB_NAME -h localhost -f "$SCHEMA_FILE"
|
||||
print_success "Schema created from file"
|
||||
else
|
||||
print_warning "Schema file not found: $SCHEMA_FILE"
|
||||
print_info "You'll need to run migrations manually"
|
||||
fi
|
||||
}
|
||||
|
||||
test_connection() {
|
||||
print_header "Testing Connection"
|
||||
|
||||
print_info "Testing connection as '$DB_USER'..."
|
||||
|
||||
if PGPASSWORD="$DB_PASSWORD" psql -U $DB_USER -d $DB_NAME -h localhost -c "SELECT version();" &> /dev/null; then
|
||||
print_success "Connection successful!"
|
||||
|
||||
# Show connection string
|
||||
print_info ""
|
||||
print_info "Connection details:"
|
||||
echo " Database: $DB_NAME"
|
||||
echo " User: $DB_USER"
|
||||
echo " Host: localhost"
|
||||
echo " Port: 5432"
|
||||
echo ""
|
||||
echo " Connection string:"
|
||||
echo " postgresql://$DB_USER:$DB_PASSWORD@localhost:5432/$DB_NAME"
|
||||
else
|
||||
print_error "Connection failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Main
|
||||
###############################################################################
|
||||
|
||||
main() {
|
||||
print_header "GigLez Database Setup"
|
||||
echo ""
|
||||
|
||||
# Run checks
|
||||
check_sudo
|
||||
check_postgresql
|
||||
check_password_file
|
||||
|
||||
echo ""
|
||||
|
||||
# Setup database
|
||||
create_database
|
||||
create_user
|
||||
grant_privileges
|
||||
enable_postgis
|
||||
|
||||
# Create schema if file exists
|
||||
if [ -f "$SCHEMA_FILE" ]; then
|
||||
create_schema
|
||||
fi
|
||||
|
||||
# Test connection
|
||||
test_connection
|
||||
|
||||
echo ""
|
||||
print_header "Setup Complete!"
|
||||
print_success "Database is ready for use"
|
||||
echo ""
|
||||
print_info "Next steps:"
|
||||
echo " 1. Run import script to load test data:"
|
||||
echo " cd $PROJECT_ROOT"
|
||||
echo " source venv/bin/activate"
|
||||
echo " python scripts/import_with_african_gps.py --limit 5000 --no-matching"
|
||||
echo ""
|
||||
echo " 2. Start the API server:"
|
||||
echo " uvicorn src.api.main:app --reload"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run main
|
||||
main
|
||||
|
||||
Reference in New Issue
Block a user