From f2a89badf10e1fdbdfc223c05c80b7b7c5f1cf18 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 14 Mar 2026 15:49:32 -0700 Subject: [PATCH] feat: wigle config and status endpoints --- adacam_api/app.py | 3 +- adacam_api/routes/wigle.py | 148 +++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 adacam_api/routes/wigle.py diff --git a/adacam_api/app.py b/adacam_api/app.py index 9b623cd..57c3a3a 100644 --- a/adacam_api/app.py +++ b/adacam_api/app.py @@ -3,7 +3,7 @@ import subprocess from flask import Flask, request, jsonify from . import config, db, forwarder from .auth import get_device_serial, get_api_token, require_auth -from .routes import landmarks, gnss, status, frames +from .routes import landmarks, gnss, status, frames, wigle def create_app(): @@ -19,6 +19,7 @@ def create_app(): app.register_blueprint(gnss.bp) app.register_blueprint(status.bp) app.register_blueprint(frames.bp) + app.register_blueprint(wigle.bp) # ── PAIRING ENDPOINT ───────────────────────────────────────────────────── @app.route('/pair') diff --git a/adacam_api/routes/wigle.py b/adacam_api/routes/wigle.py new file mode 100644 index 0000000..d9220ed --- /dev/null +++ b/adacam_api/routes/wigle.py @@ -0,0 +1,148 @@ +"""WiGLE wardriving status and config endpoints.""" +import os +import sqlite3 +import subprocess +import time +from flask import Blueprint, jsonify, request +from ..auth import require_auth + +bp = Blueprint('wigle', __name__, url_prefix='/api/1/wigle') + +WIGLE_DB = '/data/adacam/wigle.db' + + +def _get_db(): + """Get SQLite connection to wigle.db, create tables if needed.""" + os.makedirs(os.path.dirname(WIGLE_DB), exist_ok=True) + conn = sqlite3.connect(WIGLE_DB) + conn.execute('''CREATE TABLE IF NOT EXISTS wigle_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, bssid TEXT NOT NULL, ssid TEXT, + auth_mode TEXT, first_seen TEXT, channel INTEGER, rssi INTEGER, + lat REAL, lon REAL, alt REAL, accuracy REAL, uploaded INTEGER DEFAULT 0, created_at INTEGER)''') + conn.execute('''CREATE TABLE IF NOT EXISTS wigle_config (key TEXT PRIMARY KEY, value TEXT)''') + return conn + + +def _get_config(conn): + cur = conn.execute('SELECT key, value FROM wigle_config') + return dict(cur.fetchall()) + + +def _mask_api_name(api_name: str) -> str: + """Mask API name showing only first 4 chars.""" + if not api_name or len(api_name) < 4: + return '●●●●●●●●' if api_name else '' + return api_name[:4] + '●●●●●●●●' + + +@bp.route('/status') +def wigle_status(): + """Get WiGLE service status (unauthenticated).""" + try: + conn = _get_db() + cfg = _get_config(conn) + + total = conn.execute('SELECT COUNT(*) FROM wigle_records').fetchone()[0] + pending = conn.execute('SELECT COUNT(*) FROM wigle_records WHERE uploaded=0').fetchone()[0] + + last_scan = cfg.get('last_scan') + last_upload = cfg.get('last_upload') + + conn.close() + + return jsonify({ + 'enabled': cfg.get('enabled', '0') == '1', + 'api_name': _mask_api_name(cfg.get('api_name', '')), + 'total_networks': total, + 'pending_upload': pending, + 'last_scan': int(last_scan) if last_scan else None, + 'last_upload': int(last_upload) if last_upload else None + }) + except Exception as e: + return jsonify({'error': str(e), 'enabled': False, 'total_networks': 0, 'pending_upload': 0}) + + +@bp.route('/stats') +def wigle_stats(): + """Get WiGLE statistics (unauthenticated).""" + try: + conn = _get_db() + + total = conn.execute('SELECT COUNT(*) FROM wigle_records').fetchone()[0] + uploaded = conn.execute('SELECT COUNT(*) FROM wigle_records WHERE uploaded=1').fetchone()[0] + pending = conn.execute('SELECT COUNT(*) FROM wigle_records WHERE uploaded=0').fetchone()[0] + + # Scans today (records created in last 24h) + day_ago = int(time.time()) - 86400 + scans_today = conn.execute( + 'SELECT COUNT(DISTINCT first_seen) FROM wigle_records WHERE created_at > ?', + (day_ago,) + ).fetchone()[0] + + conn.close() + + return jsonify({ + 'total_networks': total, + 'uploaded_networks': uploaded, + 'pending_upload': pending, + 'scans_today': scans_today + }) + except Exception as e: + return jsonify({'error': str(e), 'total_networks': 0, 'uploaded_networks': 0, 'pending_upload': 0, 'scans_today': 0}) + + +@bp.route('/config', methods=['POST']) +@require_auth +def wigle_config(): + """Set WiGLE configuration (authenticated).""" + data = request.get_json() or {} + + try: + conn = _get_db() + + # Update enabled status + if 'enabled' in data: + conn.execute( + 'INSERT OR REPLACE INTO wigle_config (key, value) VALUES (?, ?)', + ('enabled', '1' if data['enabled'] else '0') + ) + + # Update API credentials + if 'api_name' in data: + conn.execute( + 'INSERT OR REPLACE INTO wigle_config (key, value) VALUES (?, ?)', + ('api_name', data['api_name']) + ) + + if 'api_token' in data: + conn.execute( + 'INSERT OR REPLACE INTO wigle_config (key, value) VALUES (?, ?)', + ('api_token', data['api_token']) + ) + + # Optional: scan/upload intervals + if 'scan_interval_seconds' in data: + conn.execute( + 'INSERT OR REPLACE INTO wigle_config (key, value) VALUES (?, ?)', + ('scan_interval_seconds', str(data['scan_interval_seconds'])) + ) + + if 'upload_interval_seconds' in data: + conn.execute( + 'INSERT OR REPLACE INTO wigle_config (key, value) VALUES (?, ?)', + ('upload_interval_seconds', str(data['upload_interval_seconds'])) + ) + + conn.commit() + conn.close() + + # Restart the wigle service to pick up config changes + try: + subprocess.run(['systemctl', 'restart', 'adacam-wigle'], timeout=10) + except Exception: + pass # Service may not exist yet + + return jsonify({'ok': True, 'message': 'WiGLE configuration updated'}) + + except Exception as e: + return jsonify({'error': str(e)}), 500