feat: add BLE Radar database import

- Add import_ble_radar.py module for importing BLE Radar exports
- Add /api/import/ble-radar endpoint for file upload
- Add import section to dashboard with file picker
- CLI: python -m rf_mapper.import_ble_radar <file.sqlite>
- Imports devices, RSSI, favorites, and custom labels

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
User
2026-02-01 17:54:31 +01:00
parent b1efb4ae3c
commit cc6d9ee58d
6 changed files with 320 additions and 3 deletions

View File

@@ -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

View File

@@ -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())

View File

@@ -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"])

View File

@@ -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;

View File

@@ -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

View File

@@ -127,6 +127,22 @@
</div>
</div>
<div class="section">
<div class="section-header">
<span class="section-title">📥 Import Data</span>
</div>
<div class="import-controls">
<div class="import-row">
<label>BLE Radar Database:</label>
<input type="file" id="ble-radar-file" accept=".sqlite,.db">
</div>
<div class="import-row">
<button class="btn btn-small" onclick="importBleRadar()">Import</button>
<span id="import-status"></span>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<span class="section-title">📊 Statistics</span>