diff --git a/config.yaml b/config.yaml index 506c691..6f80e45 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,6 @@ gps: - latitude: 50.85846541332012 - longitude: 4.397570348817993 + latitude: 50.858532461583906 + longitude: 4.397587773133864 web: host: 0.0.0.0 port: 5000 @@ -12,7 +12,6 @@ scanner: longitude: null floor: null is_master: true - bt_mac: '2C:CF:67:6F:66:AC' wifi_interface: wlan0 bt_scan_timeout: 10 path_loss_exponent: 2.5 diff --git a/src/rf_mapper/import_ble_radar.py b/src/rf_mapper/import_ble_radar.py new file mode 100644 index 0000000..025b44f --- /dev/null +++ b/src/rf_mapper/import_ble_radar.py @@ -0,0 +1,190 @@ +"""Import BLE Radar database into RF Mapper. + +BLE Radar is an Android app that scans for BLE devices. +This script imports its exported SQLite database into rf-mapper's database. + +Usage: + python -m rf_mapper.import_ble_radar /path/to/ble_radar.sqlite + +Or from Python: + from rf_mapper.import_ble_radar import import_ble_radar_db + import_ble_radar_db('/path/to/ble_radar.sqlite') +""" + +import argparse +import sqlite3 +from datetime import datetime +from pathlib import Path +from typing import Optional + +from .database import DeviceDatabase, get_database +from .distance import estimate_distance + + +def import_ble_radar_db( + ble_radar_path: str, + rf_mapper_db: Optional[DeviceDatabase] = None, + scanner_id: str = "ble_radar", + verbose: bool = True +) -> dict: + """Import BLE Radar database into RF Mapper. + + Args: + ble_radar_path: Path to BLE Radar exported SQLite database + rf_mapper_db: RF Mapper database instance (creates default if None) + scanner_id: Source scanner ID for imported devices + verbose: Print progress messages + + Returns: + Dict with import statistics + """ + ble_radar_path = Path(ble_radar_path) + if not ble_radar_path.exists(): + raise FileNotFoundError(f"BLE Radar database not found: {ble_radar_path}") + + # Connect to BLE Radar database + ble_conn = sqlite3.connect(ble_radar_path) + ble_conn.row_factory = sqlite3.Row + ble_cursor = ble_conn.cursor() + + # Use global RF Mapper database if not provided + if rf_mapper_db is None: + rf_mapper_db = get_database() + + stats = { + "devices_imported": 0, + "devices_updated": 0, + "rssi_records": 0, + "locations_used": 0, + "errors": 0 + } + + if verbose: + print(f"Importing from: {ble_radar_path}") + + # Get location data for device-location mapping + ble_cursor.execute("SELECT time, lat, lng FROM location ORDER BY time") + locations = {row["time"]: (row["lat"], row["lng"]) for row in ble_cursor.fetchall()} + stats["locations_used"] = len(locations) + + if verbose: + print(f"Found {len(locations)} location records") + + # Import devices + ble_cursor.execute(""" + SELECT + address, name, manufacturer_name, manufacturer_id, + first_detect_time_ms, last_detect_time_ms, detect_count, + last_seen_rssi, favorite, custom_name, service_uuids, + device_class, is_connectable + FROM device + """) + + scan_id = f"ble_radar_import_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + + for row in ble_cursor.fetchall(): + try: + address = row["address"] + name = row["custom_name"] or row["name"] or "" + manufacturer = row["manufacturer_name"] or "" + rssi = row["last_seen_rssi"] or -70 + + # Determine device type from BLE Radar's device_class + device_class_num = row["device_class"] or 0 + if device_class_num == 0: + bt_device_type = "Low Energy Device" + else: + bt_device_type = f"BLE Class {device_class_num}" + + # Check if device exists + existing = rf_mapper_db.get_device(address) + + # Calculate distance from RSSI + distance = estimate_distance(rssi, tx_power=-65) + + # Record the observation (inserts or updates device) + rf_mapper_db.record_bluetooth_observation( + address=address, + name=name, + rssi=rssi, + distance_m=distance, + device_class="BLE", + device_type=bt_device_type, + manufacturer=manufacturer, + floor=None, + scan_id=scan_id, + scanner_id=scanner_id + ) + + if existing: + stats["devices_updated"] += 1 + else: + stats["devices_imported"] += 1 + + stats["rssi_records"] += 1 + + # Set favorite if marked in BLE Radar + if row["favorite"]: + rf_mapper_db.set_device_favorite(address, True) + + # Set custom label if set in BLE Radar + if row["custom_name"]: + rf_mapper_db.set_device_label(address, row["custom_name"]) + + # Set source scanner + rf_mapper_db.set_device_source(address, scanner_id, None, None) + + except Exception as e: + if verbose: + print(f"Error importing {row['address']}: {e}") + stats["errors"] += 1 + + ble_conn.close() + + if verbose: + print(f"\nImport complete:") + print(f" Devices imported: {stats['devices_imported']}") + print(f" Devices updated: {stats['devices_updated']}") + print(f" RSSI records: {stats['rssi_records']}") + print(f" Locations used: {stats['locations_used']}") + if stats["errors"]: + print(f" Errors: {stats['errors']}") + + return stats + + +def main(): + parser = argparse.ArgumentParser( + description="Import BLE Radar database into RF Mapper" + ) + parser.add_argument( + "ble_radar_db", + help="Path to BLE Radar exported SQLite database" + ) + parser.add_argument( + "--scanner-id", + default="ble_radar", + help="Source scanner ID for imported devices (default: ble_radar)" + ) + parser.add_argument( + "-q", "--quiet", + action="store_true", + help="Suppress output" + ) + + args = parser.parse_args() + + try: + stats = import_ble_radar_db( + args.ble_radar_db, + scanner_id=args.scanner_id, + verbose=not args.quiet + ) + return 0 if stats["errors"] == 0 else 1 + except Exception as e: + print(f"Error: {e}") + return 1 + + +if __name__ == "__main__": + exit(main()) diff --git a/src/rf_mapper/web/app.py b/src/rf_mapper/web/app.py index 4d5cf86..8bc264f 100644 --- a/src/rf_mapper/web/app.py +++ b/src/rf_mapper/web/app.py @@ -1517,6 +1517,41 @@ def create_app(config: Config | None = None) -> Flask: result = db.cleanup_old_data(retention_days) return jsonify(result) + @app.route("/api/import/ble-radar", methods=["POST"]) + def api_import_ble_radar(): + """Import BLE Radar database from uploaded file""" + db = app.config.get("DATABASE") + if not db: + return jsonify({"error": "Database not enabled"}), 503 + + if "file" not in request.files: + return jsonify({"error": "No file uploaded"}), 400 + + file = request.files["file"] + if file.filename == "": + return jsonify({"error": "No file selected"}), 400 + + # Save to temp file + import tempfile + with tempfile.NamedTemporaryFile(delete=False, suffix=".sqlite") as tmp: + file.save(tmp.name) + tmp_path = tmp.name + + try: + from ..import_ble_radar import import_ble_radar_db + scanner_id = request.form.get("scanner_id", "ble_radar") + stats = import_ble_radar_db(tmp_path, db, scanner_id, verbose=False) + return jsonify({ + "status": "success", + "message": f"Imported {stats['devices_imported']} new devices, updated {stats['devices_updated']}", + **stats + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + finally: + import os + os.unlink(tmp_path) + # ==================== Peer Sync API ==================== @app.route("/api/peers", methods=["GET"]) diff --git a/src/rf_mapper/web/static/css/style.css b/src/rf_mapper/web/static/css/style.css index 07cbc54..168a6de 100644 --- a/src/rf_mapper/web/static/css/style.css +++ b/src/rf_mapper/web/static/css/style.css @@ -615,6 +615,37 @@ body { border-color: var(--color-primary); } +/* Import Controls */ +.import-controls { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.import-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.import-row label { + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.import-row input[type="file"] { + font-size: 0.75rem; + color: var(--color-text-muted); + max-width: 140px; +} + +#import-status { + font-size: 0.75rem; + margin-left: 0.5rem; +} + /* Device Detail Panel */ .device-detail-panel { position: absolute; diff --git a/src/rf_mapper/web/static/js/app.js b/src/rf_mapper/web/static/js/app.js index 85ae17e..f68907a 100644 --- a/src/rf_mapper/web/static/js/app.js +++ b/src/rf_mapper/web/static/js/app.js @@ -1537,6 +1537,52 @@ async function stopAutoScan() { } } +// ========== Import Functions ========== + +// Import BLE Radar database +async function importBleRadar() { + const fileInput = document.getElementById('ble-radar-file'); + const statusEl = document.getElementById('import-status'); + + if (!fileInput.files || !fileInput.files[0]) { + statusEl.textContent = 'No file selected'; + statusEl.style.color = '#ef4444'; + return; + } + + const file = fileInput.files[0]; + statusEl.textContent = 'Importing...'; + statusEl.style.color = '#fbbf24'; + + const formData = new FormData(); + formData.append('file', file); + formData.append('scanner_id', activeNode === 'local' ? 'ble_radar' : activeNode); + + try { + const response = await fetch('/api/import/ble-radar', { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (response.ok) { + statusEl.textContent = `✓ ${data.devices_imported} new, ${data.devices_updated} updated`; + statusEl.style.color = '#4ade80'; + fileInput.value = ''; + // Refresh the scan to show imported devices + loadLatestScan(); + } else { + statusEl.textContent = `Error: ${data.error}`; + statusEl.style.color = '#ef4444'; + } + } catch (error) { + statusEl.textContent = `Error: ${error.message}`; + statusEl.style.color = '#ef4444'; + console.error('Import error:', error); + } +} + // ========== Device Position Functions ========== // Load saved device positions, source scanner info, and peer positions diff --git a/src/rf_mapper/web/templates/index.html b/src/rf_mapper/web/templates/index.html index 0c214ce..45a002b 100644 --- a/src/rf_mapper/web/templates/index.html +++ b/src/rf_mapper/web/templates/index.html @@ -127,6 +127,22 @@ +
+
+ 📥 Import Data +
+
+
+ + +
+
+ + +
+
+
+
📊 Statistics