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:
@@ -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
|
||||
|
||||
190
src/rf_mapper/import_ble_radar.py
Normal file
190
src/rf_mapper/import_ble_radar.py
Normal 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())
|
||||
@@ -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"])
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user