From 04d76e127474e2621dcaafb06f7abae246182948 Mon Sep 17 00:00:00 2001 From: Colonel Panic <90460753+colonelpanichacks@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:23:04 -0400 Subject: [PATCH] Add files via upload --- api/flockyou.py | 213 ++++++++-- api/templates/index.html | 898 +++++++++++++++++++++++++++++++++++---- 2 files changed, 1001 insertions(+), 110 deletions(-) diff --git a/api/flockyou.py b/api/flockyou.py index ea3d49e..7014c8f 100644 --- a/api/flockyou.py +++ b/api/flockyou.py @@ -10,6 +10,8 @@ import serial import serial.tools.list_ports import queue import uuid +import pickle +from pathlib import Path app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'flockyou_dev_key_2024') @@ -17,6 +19,8 @@ socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading', logge # Global variables detections = [] +cumulative_detections = [] +session_start_time = datetime.now() gps_data = None serial_connection = None gps_enabled = False @@ -31,6 +35,59 @@ reconnect_delay = 3 # seconds connection_lock = threading.Lock() serial_queue = queue.Queue() next_detection_id = 1 # Unique ID counter +settings = {'gps_port': '', 'flock_port': '', 'filter': 'all'} + +# Data storage paths +DATA_DIR = Path('data') +CUMULATIVE_DATA_FILE = DATA_DIR / 'cumulative_detections.pkl' +SETTINGS_FILE = DATA_DIR / 'settings.json' + +# Ensure data directory exists +DATA_DIR.mkdir(exist_ok=True) + +# Persistent storage functions +def load_cumulative_detections(): + """Load cumulative detections from disk""" + global cumulative_detections + try: + if CUMULATIVE_DATA_FILE.exists(): + with open(CUMULATIVE_DATA_FILE, 'rb') as f: + cumulative_detections = pickle.load(f) + print(f"Loaded {len(cumulative_detections)} cumulative detections") + else: + cumulative_detections = [] + except Exception as e: + print(f"Error loading cumulative detections: {e}") + cumulative_detections = [] + +def save_cumulative_detections(): + """Save cumulative detections to disk""" + try: + with open(CUMULATIVE_DATA_FILE, 'wb') as f: + pickle.dump(cumulative_detections, f) + print(f"Saved {len(cumulative_detections)} cumulative detections") + except Exception as e: + print(f"Error saving cumulative detections: {e}") + +def load_settings(): + """Load settings from disk""" + global settings + try: + if SETTINGS_FILE.exists(): + with open(SETTINGS_FILE, 'r') as f: + settings.update(json.load(f)) + print(f"Loaded settings: {settings}") + except Exception as e: + print(f"Error loading settings: {e}") + +def save_settings(): + """Save settings to disk""" + try: + with open(SETTINGS_FILE, 'w') as f: + json.dump(settings, f, indent=2) + print(f"Saved settings: {settings}") + except Exception as e: + print(f"Error saving settings: {e}") # Load OUI database def load_oui_database(): @@ -78,7 +135,7 @@ class GPSData: self.satellites = 0 def parse_nmea_sentence(sentence): - """Parse NMEA GPS sentence""" + """Parse NMEA GPS sentence with improved accuracy""" if not sentence.startswith('$'): return None @@ -88,33 +145,49 @@ def parse_nmea_sentence(sentence): sentence_type = parts[0] - if sentence_type == '$GPGGA': # Global Positioning System Fix Data + if sentence_type in ['$GPGGA', '$GNGGA']: # Global Positioning System Fix Data (GPS + GLONASS) if len(parts) >= 15: try: time_str = parts[1] - lat = float(parts[2]) / 100 + lat_raw = parts[2] lat_dir = parts[3] - lon = float(parts[4]) / 100 + lon_raw = parts[4] lon_dir = parts[5] - fix_quality = int(parts[6]) - satellites = int(parts[7]) + fix_quality = int(parts[6]) if parts[6] else 0 + satellites = int(parts[7]) if parts[7] else 0 + hdop = float(parts[8]) if parts[8] else 0 # Horizontal Dilution of Precision altitude = float(parts[9]) if parts[9] else 0 - # Convert to decimal degrees + # Skip if no fix + if fix_quality == 0 or not lat_raw or not lon_raw: + return None + + # Convert NMEA format (DDMM.MMMM) to decimal degrees with high precision + lat_degrees = int(lat_raw[:2]) + lat_minutes = float(lat_raw[2:]) + lat = lat_degrees + (lat_minutes / 60.0) + + lon_degrees = int(lon_raw[:3]) + lon_minutes = float(lon_raw[3:]) + lon = lon_degrees + (lon_minutes / 60.0) + + # Apply direction if lat_dir == 'S': lat = -lat if lon_dir == 'W': lon = -lon return { - 'latitude': lat, - 'longitude': lon, - 'altitude': altitude, + 'latitude': round(lat, 8), # 8 decimal places for ~1.1mm accuracy + 'longitude': round(lon, 8), + 'altitude': round(altitude, 3), 'fix_quality': fix_quality, 'satellites': satellites, + 'hdop': hdop, 'timestamp': time_str } - except (ValueError, IndexError): + except (ValueError, IndexError) as e: + print(f"GPS parsing error: {e}") return None return None @@ -202,7 +275,7 @@ def flock_reader(): def add_detection_from_serial(data): """Add detection from serial data - counts detections per MAC address""" - global detections, gps_data, next_detection_id + global detections, cumulative_detections, gps_data, next_detection_id # Add GPS data if available if gps_data and gps_data.get('fix_quality') > 0: @@ -250,6 +323,13 @@ def add_detection_from_serial(data): if data.get('gps'): existing_detection['gps'] = data['gps'] + # Update cumulative detections + for cum_detection in cumulative_detections: + if cum_detection.get('mac_address') == mac_address: + cum_detection.update(existing_detection) + break + save_cumulative_detections() + # Emit updated detection safe_socket_emit('detection_updated', existing_detection) print(f"Updated detection: MAC {mac_address}, Count: {existing_detection['detection_count']}, Method: {existing_detection.get('detection_method')}") @@ -264,6 +344,10 @@ def add_detection_from_serial(data): detections.append(data) + # Add to cumulative detections + cumulative_detections.append(data.copy()) + save_cumulative_detections() + # Emit to connected clients safe_socket_emit('new_detection', data) print(f"New detection added: ID {data['id']}, Method: {data.get('detection_method')}, MAC: {mac_address}") @@ -416,11 +500,19 @@ def index(): def get_detections(): """Get all detections with optional filtering""" filter_type = request.args.get('filter', 'all') + data_type = request.args.get('type', 'session') - if filter_type == 'all': - return jsonify(detections) + # Choose data source + if data_type == 'cumulative': + source_data = cumulative_detections else: - filtered = [d for d in detections if d.get('detection_method') == filter_type] + source_data = detections + + # Apply filter + if filter_type == 'all': + return jsonify(source_data) + else: + filtered = [d for d in source_data if d.get('detection_method') == filter_type] return jsonify(filtered) @app.route('/api/detections', methods=['POST']) @@ -573,11 +665,20 @@ def get_flock_ports(): @app.route('/api/export/csv', methods=['GET']) def export_csv(): - """Export detections as CSV""" - if not detections: + """Export session detections as CSV""" + export_type = request.args.get('type', 'session') + + if export_type == 'cumulative': + data_to_export = cumulative_detections + filename_prefix = "flockyou_cumulative" + else: + data_to_export = detections + filename_prefix = f"flockyou_session_{session_start_time.strftime('%Y%m%d_%H%M%S')}" + + if not data_to_export: return jsonify({'status': 'error', 'message': 'No detections to export'}), 400 - filename = f"flockyou_detections_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + filename = f"{filename_prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" filepath = os.path.join('exports', filename) os.makedirs('exports', exist_ok=True) @@ -586,12 +687,12 @@ def export_csv(): fieldnames = [ 'timestamp', 'detection_time', 'protocol', 'detection_method', 'ssid', 'mac_address', 'manufacturer', 'alias', 'rssi', 'signal_strength', 'channel', - 'latitude', 'longitude', 'altitude', 'gps_timestamp', 'satellites' + 'latitude', 'longitude', 'altitude', 'gps_timestamp', 'satellites', 'detection_count' ] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() - for detection in detections: + for detection in data_to_export: row = { 'timestamp': detection.get('timestamp'), 'detection_time': detection.get('detection_time'), @@ -601,14 +702,15 @@ def export_csv(): 'mac_address': detection.get('mac_address'), 'manufacturer': detection.get('manufacturer', 'Unknown'), 'alias': detection.get('alias', ''), - 'rssi': detection.get('rssi'), + 'rssi': detection.get('last_rssi') or detection.get('rssi'), 'signal_strength': detection.get('signal_strength'), - 'channel': detection.get('channel'), + 'channel': detection.get('last_channel') or detection.get('channel'), 'latitude': detection.get('gps', {}).get('latitude'), 'longitude': detection.get('gps', {}).get('longitude'), 'altitude': detection.get('gps', {}).get('altitude'), 'gps_timestamp': detection.get('gps', {}).get('timestamp'), - 'satellites': detection.get('gps', {}).get('satellites') + 'satellites': detection.get('gps', {}).get('satellites'), + 'detection_count': detection.get('detection_count', 1) } writer.writerow(row) @@ -617,10 +719,21 @@ def export_csv(): @app.route('/api/export/kml', methods=['GET']) def export_kml(): """Export detections as KML""" - if not detections: + export_type = request.args.get('type', 'session') + + if export_type == 'cumulative': + data_to_export = cumulative_detections + filename_prefix = "flockyou_cumulative" + document_name = "Flock You Cumulative Detections" + else: + data_to_export = detections + filename_prefix = f"flockyou_session_{session_start_time.strftime('%Y%m%d_%H%M%S')}" + document_name = f"Flock You Session Detections - {session_start_time.strftime('%Y-%m-%d %H:%M:%S')}" + + if not data_to_export: return jsonify({'status': 'error', 'message': 'No detections to export'}), 400 - filename = f"flockyou_detections_{datetime.now().strftime('%Y%m%d_%H%M%S')}.kml" + filename = f"{filename_prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.kml" filepath = os.path.join('exports', filename) os.makedirs('exports', exist_ok=True) @@ -628,11 +741,11 @@ def export_kml(): kml_content = f""" - Flock You Detections - Surveillance device detections with GPS coordinates + {document_name} + Surveillance device detections with GPS coordinates ({len(data_to_export)} detections) """ - for i, detection in enumerate(detections): + for i, detection in enumerate(data_to_export): gps = detection.get('gps', {}) if gps.get('latitude') and gps.get('longitude'): kml_content += f""" @@ -670,12 +783,13 @@ def export_kml(): @app.route('/api/clear', methods=['POST']) def clear_detections(): - """Clear all detections""" - global detections, next_detection_id + """Clear session detections""" + global detections, next_detection_id, session_start_time detections.clear() next_detection_id = 1 # Reset ID counter + session_start_time = datetime.now() # Reset session start time safe_socket_emit('detections_cleared', {}) - return jsonify({'status': 'success', 'message': 'All detections cleared'}) + return jsonify({'status': 'success', 'message': 'Session detections cleared'}) @app.route('/api/test/detection', methods=['POST']) def test_detection(): @@ -733,6 +847,39 @@ def update_detection_alias(): return jsonify({'status': 'error', 'message': 'Detection not found'}), 404 +@app.route('/api/settings', methods=['GET']) +def get_settings(): + """Get current settings""" + return jsonify(settings) + +@app.route('/api/settings', methods=['POST']) +def update_settings(): + """Update settings""" + global settings + data = request.json + settings.update(data) + save_settings() + return jsonify({'status': 'success', 'settings': settings}) + +@app.route('/api/stats', methods=['GET']) +def get_stats(): + """Get detection statistics""" + return jsonify({ + 'session': { + 'total': len(detections), + 'wifi': len([d for d in detections if d.get('protocol') == 'wifi']), + 'ble': len([d for d in detections if d.get('protocol') in ['bluetooth_le', 'bluetooth_classic']]), + 'gps': len([d for d in detections if d.get('gps')]), + 'start_time': session_start_time.isoformat() + }, + 'cumulative': { + 'total': len(cumulative_detections), + 'wifi': len([d for d in cumulative_detections if d.get('protocol') == 'wifi']), + 'ble': len([d for d in cumulative_detections if d.get('protocol') in ['bluetooth_le', 'bluetooth_classic']]), + 'gps': len([d for d in cumulative_detections if d.get('gps')]) + } + }) + @app.route('/api/oui/search', methods=['POST']) def search_oui(): """Search OUI database""" @@ -924,8 +1071,10 @@ def handle_serial_terminal_request(data): emit('serial_error', {'message': f'Failed to start terminal: {str(e)}'}) if __name__ == '__main__': - # Load OUI database on startup + # Load data on startup load_oui_database() + load_cumulative_detections() + load_settings() # Start connection monitor thread monitor_thread = threading.Thread(target=connection_monitor, daemon=True) diff --git a/api/templates/index.html b/api/templates/index.html index 7a20994..76d746e 100644 --- a/api/templates/index.html +++ b/api/templates/index.html @@ -24,28 +24,40 @@ .header { background: rgba(26, 16, 51, 0.95); color: #e0e0e0; - padding: 1rem 2rem; + padding: 1rem; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); border-bottom: 2px solid #8b5cf6; } .header-content { - display: flex; - justify-content: space-between; + display: grid; + grid-template-areas: + "title controls buttons"; + grid-template-columns: auto 1fr auto; + gap: 1rem; align-items: center; width: 100%; - gap: 1rem; } - .header-left { - display: flex; - align-items: center; + .header-title { + grid-area: title; + text-align: left; } - .header-right { + .header-controls { + grid-area: controls; display: flex; - align-items: center; + flex-wrap: wrap; gap: 1rem; + align-items: center; + justify-content: center; + } + + .header-buttons { + grid-area: buttons; + display: flex; + gap: 0.5rem; + align-items: center; } .header h1 { @@ -66,9 +78,15 @@ flex-shrink: 0; } + @media (max-width: 1200px) { + .flock-you-title { + font-size: 1.3rem; + } + } + .controls { display: flex; - gap: 1rem; + gap: 0.75rem; align-items: center; flex-wrap: wrap; } @@ -76,14 +94,16 @@ .control-group { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.25rem; position: relative; + flex-shrink: 0; } .device-control-group { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.25rem; + flex-shrink: 0; } .port-controls { @@ -117,10 +137,11 @@ transform: translateY(0); } - .header-buttons { + .action-buttons { display: flex; align-items: center; - gap: 1rem; + gap: 0.5rem; + flex-shrink: 0; } @@ -128,8 +149,9 @@ .control-group label { font-weight: 600; color: #e0e0e0; - font-size: 0.8rem; + font-size: 0.75rem; white-space: nowrap; + min-width: fit-content; } select, button { @@ -145,8 +167,9 @@ background: #2d1b69; color: #e0e0e0; border: 1px solid #8b5cf6; - min-width: 120px; - max-width: 150px; + min-width: 100px; + max-width: 140px; + font-size: 0.75rem; } select:focus { @@ -193,9 +216,8 @@ color: white; font-weight: 600; border: 1px solid #047857; - padding: 0.4rem 0.8rem; - font-size: 0.8rem; - margin-left: 1rem; + padding: 0.3rem 0.6rem; + font-size: 0.75rem; } .serial-terminal-btn:hover { @@ -209,9 +231,8 @@ color: white; font-weight: 600; border: 1px solid #6d28d9; - padding: 0.4rem 0.8rem; - font-size: 0.8rem; - margin-left: 0.5rem; + padding: 0.3rem 0.6rem; + font-size: 0.75rem; } .oui-search-btn:hover { @@ -220,6 +241,21 @@ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4); } + .map-btn { + background: linear-gradient(135deg, #0891b2 0%, #06b6d4 100%); + color: white; + font-weight: 600; + border: 1px solid #0e7490; + padding: 0.3rem 0.6rem; + font-size: 0.75rem; + } + + .map-btn:hover { + background: linear-gradient(135deg, #06b6d4 0%, #22d3ee 100%); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(8, 145, 178, 0.4); + } + .serial-terminal-container { background: rgba(45, 27, 105, 0.8); border: 1px solid #8b5cf6; @@ -236,6 +272,8 @@ display: flex; justify-content: space-between; align-items: center; + flex-wrap: wrap; + gap: 1rem; } .serial-terminal-header h2 { @@ -249,6 +287,35 @@ display: flex; align-items: center; gap: 1rem; + flex-wrap: wrap; + } + + .terminal-filter-group { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .terminal-filter-group label { + color: #c084fc; + font-size: 0.85rem; + font-weight: 600; + } + + .terminal-filter-select { + background: rgba(15, 10, 26, 0.95); + border: 1px solid #8b5cf6; + border-radius: 4px; + color: #e0e0e0; + padding: 0.3rem 0.6rem; + font-size: 0.8rem; + min-width: 100px; + } + + .terminal-filter-select:focus { + outline: none; + border-color: #c084fc; + box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3); } .serial-status { @@ -674,6 +741,13 @@ margin-top: 0.5rem; } + .stat-hint { + color: #9ca3af; + font-size: 0.7rem; + margin-top: 0.25rem; + font-style: italic; + } + .detections-container { background: rgba(45, 27, 105, 0.8); border-radius: 12px; @@ -903,23 +977,250 @@ box-shadow: 0 4px 12px rgba(236, 72, 153, 0.4); } - @media (max-width: 768px) { + .export-dropdown { + position: relative; + display: inline-block; + } + + .export-dropdown-content { + display: none; + position: absolute; + right: 0; + background: rgba(45, 27, 105, 0.95); + min-width: 160px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + border: 1px solid #8b5cf6; + border-radius: 6px; + z-index: 1000; + } + + .export-dropdown-content a { + color: #e0e0e0; + padding: 8px 12px; + text-decoration: none; + display: block; + font-size: 0.8rem; + transition: background-color 0.3s; + } + + .export-dropdown-content a:hover { + background: rgba(139, 92, 246, 0.3); + } + + .dropdown-toggle { + cursor: pointer; + } + + .map-container { + background: rgba(45, 27, 105, 0.8); + border: 1px solid #8b5cf6; + border-radius: 12px; + margin-top: 1rem; + overflow: hidden; + box-shadow: 0 8px 32px rgba(139, 92, 246, 0.3); + } + + .map-header { + background: rgba(45, 27, 105, 0.9); + padding: 1rem 1.5rem; + border-bottom: 1px solid #8b5cf6; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; + } + + .map-header h2 { + color: #c084fc; + font-size: 1.2rem; + font-weight: 600; + margin: 0; + } + + .map-controls { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + } + + .map-layer-group, + .map-filter-group { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .map-layer-group label, + .map-filter-group label { + color: #c084fc; + font-size: 0.85rem; + font-weight: 600; + } + + .map-layer-select, + .map-filter-select { + background: rgba(15, 10, 26, 0.95); + border: 1px solid #8b5cf6; + border-radius: 4px; + color: #e0e0e0; + padding: 0.3rem 0.6rem; + font-size: 0.8rem; + min-width: 120px; + } + + .map-layer-select:focus, + .map-filter-select:focus { + outline: none; + border-color: #c084fc; + box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3); + } + + .map-status { + color: #a855f7; + font-size: 0.9rem; + font-weight: 500; + } + + .clear-map-btn { + background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%); + color: white; + font-size: 0.8rem; + padding: 0.3rem 0.8rem; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.3s ease; + } + + .clear-map-btn:hover { + background: linear-gradient(135deg, #ef4444 0%, #f87171 100%); + transform: translateY(-1px); + } + + .map-content { + padding: 0; + } + + .leaflet-popup-content { + color: #333; + font-size: 0.9rem; + } + + .leaflet-popup-content h3 { + margin: 0 0 0.5rem 0; + color: #ec4899; + } + + .map-legend { + background: rgba(15, 10, 26, 0.9); + border: 1px solid #8b5cf6; + border-radius: 6px; + padding: 0.5rem; + margin-top: 0.5rem; + font-size: 0.8rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 1rem; + } + + .legend-title { + color: #c084fc; + margin: 0; + font-size: 0.85rem; + font-weight: 600; + } + + .legend-items { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: center; + } + + .legend-item { + display: flex; + align-items: center; + gap: 0.3rem; + font-size: 0.75rem; + white-space: nowrap; + } + + .legend-marker { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid; + } + + .wifi-session { background: #ef4444; border-color: #22c55e; } + .wifi-cumulative { background: #ef4444; border-color: #f59e0b; } + .ble-session { background: #3b82f6; border-color: #22c55e; } + .ble-cumulative { background: #3b82f6; border-color: #f59e0b; } + + @media (max-width: 1200px) { .header-content { - flex-direction: column; - gap: 1rem; - align-items: stretch; + grid-template-areas: + "title title" + "controls buttons"; + grid-template-columns: 1fr auto; + } + + .header-title { + text-align: center; + } + + .header-controls { + justify-content: center; + } + + .header-buttons { + justify-content: center; + } + } + + @media (max-width: 900px) { + .header-content { + grid-template-areas: + "title title" + "controls controls" + "buttons buttons"; + grid-template-columns: 1fr; + } + + .header-title { + text-align: center; + } + + .header-controls { + justify-content: center; + } + + .header-buttons { + justify-content: center; + } + } + + @media (max-width: 768px) { + .header { + padding: 0.75rem; } .flock-you-title { - font-size: 1.2rem; - text-align: center; - order: -1; + font-size: 1.1rem; + letter-spacing: 1px; + } + + .header-controls { + margin-top: 0.5rem; } .controls { flex-direction: column; align-items: stretch; - gap: 1rem; + gap: 0.75rem; width: 100%; } @@ -929,14 +1230,39 @@ justify-content: space-between; } - .header-buttons { - justify-content: center; + .action-buttons { + flex-direction: column; + gap: 0.5rem; width: 100%; } + .serial-terminal-btn, + .oui-search-btn, + .map-btn { + width: 100%; + text-align: center; + } + .detection-details { grid-template-columns: 1fr; } + + select { + min-width: 80px; + max-width: none; + flex: 1; + } + + .port-controls { + flex: 1; + gap: 0.25rem; + } + + .refresh-btn { + width: 28px; + height: 28px; + font-size: 0.7rem; + } } @@ -944,50 +1270,57 @@
-

FLOCK-YOU

+
+

FLOCK-YOU

+
-
-
-
- -
- - +
+
+
+
+ +
+ + +
+ +
- - -
- -
-
- -
- - + +
+
+ +
+ + +
+ + +
+ +
+ +
- - -
- -
- -
- - +
+ + + +
@@ -996,19 +1329,23 @@
0
-
Total Detections
+
Session Detections
+
(Hover for cumulative)
0
WiFi Detections
+
(Hover for cumulative)
0
BLE Detections
+
(Hover for cumulative)
0
GPS Tagged
+
(Hover for cumulative)
@@ -1019,8 +1356,15 @@
- - +
@@ -1036,6 +1380,14 @@

Serial Terminal

+
+ + +
Status: Disconnected
@@ -1065,8 +1417,60 @@
+ + + +