Compare commits
6 Commits
322c53d513
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fc7c65454 | ||
|
|
579cea57dc | ||
|
|
5def3e2214 | ||
|
|
cc6d9ee58d | ||
|
|
b1efb4ae3c | ||
|
|
91536860ad |
46
CLAUDE.md
46
CLAUDE.md
@@ -2,11 +2,23 @@
|
||||
|
||||
RF Environment Scanner for WiFi and Bluetooth signal mapping on Linux.
|
||||
|
||||
## Key Documentation
|
||||
## Key Documentation (Maintain These!)
|
||||
|
||||
| File | Purpose | When to Update |
|
||||
|------|---------|----------------|
|
||||
| **[TASKS.md](TASKS.md)** | Current sprint tasks, priorities (P0-P3), status | Start/end of each work session |
|
||||
| **[TODO.md](TODO.md)** | Backlog by category, completed items | When adding/completing features |
|
||||
| **[ROADMAP.md](ROADMAP.md)** | Version milestones, long-term vision | When milestones change |
|
||||
| **[CHANGELOG.md](CHANGELOG.md)** | Version history, notable changes | Each release |
|
||||
| **[PROJECT.md](PROJECT.md)** | Goals, architecture, dependencies | Major architectural changes |
|
||||
| **[USAGE.md](USAGE.md)** | User guide, CLI, web interface, API | When adding features |
|
||||
| **[docs/CHEATSHEET.md](docs/CHEATSHEET.md)** | Quick reference commands | When adding features |
|
||||
| **[INVENTORY.md](INVENTORY.md)** | Multi-node deployment info (gitignored) | When nodes change |
|
||||
|
||||
## Configuration
|
||||
|
||||
- **[USAGE.md](USAGE.md)** - User guide with CLI commands, web interface, configuration, and API reference
|
||||
- **[TODO.md](TODO.md)** - Pending features and improvements
|
||||
- **[config.yaml](config.yaml)** - Current configuration (GPS, web server, scanner, building settings)
|
||||
- **[docs/HOME_ASSISTANT.md](docs/HOME_ASSISTANT.md)** - Home Assistant webhook integration
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -22,6 +34,8 @@ src/rf_mapper/
|
||||
├── bluetooth_*.py # Bluetooth device identification and classification
|
||||
├── visualize.py # ASCII radar and chart generation
|
||||
├── profiling.py # CPU/memory profiling utilities
|
||||
├── termux.py # Termux/Android environment detection
|
||||
├── sync.py # Multi-scanner peer sync
|
||||
└── web/
|
||||
├── app.py # Flask application and API endpoints
|
||||
├── templates/ # Jinja2 HTML templates (base.html, index.html)
|
||||
@@ -38,21 +52,35 @@ src/rf_mapper/
|
||||
| Change web UI | `web/templates/index.html`, `static/js/app.js`, `static/css/style.css` |
|
||||
| Add configuration | `src/rf_mapper/config.py`, `config.yaml` |
|
||||
| Home Assistant integration | `src/rf_mapper/homeassistant.py`, `docs/HOME_ASSISTANT.md` |
|
||||
| Multi-scanner sync | `src/rf_mapper/sync.py`, `web/app.py` |
|
||||
| Termux/Android support | `src/rf_mapper/termux.py` |
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
rf-mapper start # Start web server (background)
|
||||
rf-mapper status # Check if running
|
||||
rf-mapper stop # Stop server
|
||||
rf-mapper scan -l room # CLI scan
|
||||
rf-mapper --help # All commands
|
||||
python -m rf_mapper start # Start web server (background)
|
||||
python -m rf_mapper status # Check if running
|
||||
python -m rf_mapper stop # Stop server
|
||||
python -m rf_mapper restart # Restart server
|
||||
python -m rf_mapper scan -l room # CLI scan
|
||||
python -m rf_mapper --help # All commands
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
See [INVENTORY.md](INVENTORY.md) for multi-node deployment details.
|
||||
|
||||
```bash
|
||||
# Update and restart all nodes
|
||||
cd ~/git/rf-mapper && source venv/bin/activate && git pull && python -m rf_mapper restart
|
||||
ssh grokbox "cd ~/git/rf-mapper && source venv/bin/activate && git pull && python -m rf_mapper restart"
|
||||
ssh jellystar "cd ~/git/rf-mapper && source venv/bin/activate && git pull && python -m rf_mapper restart"
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Python 3.10+, Flask, PyYAML, requests
|
||||
- Python 3.10+, Flask, PyYAML, requests, bleak
|
||||
- Leaflet.js (2D maps), MapLibre GL JS (3D maps)
|
||||
- Linux tools: `iw`, bleak (BLE via D-Bus)
|
||||
- SQLite for device history
|
||||
|
||||
@@ -122,14 +122,17 @@ pip install -e .
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Start web server (background)
|
||||
rf-mapper start
|
||||
python -m rf_mapper start
|
||||
|
||||
# Check status
|
||||
rf-mapper status
|
||||
python -m rf_mapper status
|
||||
|
||||
# CLI scan
|
||||
rf-mapper scan
|
||||
python -m rf_mapper scan
|
||||
|
||||
# Open http://localhost:5000
|
||||
```
|
||||
|
||||
53
USAGE.md
53
USAGE.md
@@ -65,7 +65,7 @@ For auto-start on device boot, create `~/.termux/boot/start-rf-mapper.sh`:
|
||||
termux-wake-lock
|
||||
cd ~/git/rf-mapper
|
||||
source venv/bin/activate
|
||||
rf-mapper start
|
||||
python -m rf_mapper start
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
@@ -91,92 +91,97 @@ pip install -e .
|
||||
source venv/bin/activate
|
||||
|
||||
# Run interactive scan
|
||||
rf-mapper
|
||||
python -m rf_mapper
|
||||
|
||||
# Start web server
|
||||
rf-mapper start
|
||||
python -m rf_mapper start
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
All commands require activating the virtual environment first:
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
### Scanning
|
||||
|
||||
```bash
|
||||
# Basic scan (interactive mode)
|
||||
rf-mapper
|
||||
python -m rf_mapper
|
||||
|
||||
# Scan with location label
|
||||
rf-mapper scan -l kitchen
|
||||
python -m rf_mapper scan -l kitchen
|
||||
|
||||
# Scan WiFi only
|
||||
rf-mapper scan --no-bt
|
||||
python -m rf_mapper scan --no-bt
|
||||
|
||||
# Scan Bluetooth only
|
||||
rf-mapper scan --no-wifi
|
||||
python -m rf_mapper scan --no-wifi
|
||||
|
||||
# Use specific WiFi interface
|
||||
rf-mapper scan -i wlan1
|
||||
python -m rf_mapper scan -i wlan1
|
||||
```
|
||||
|
||||
### Visualization (CLI)
|
||||
|
||||
```bash
|
||||
# Visualize latest scan (ASCII radar + charts)
|
||||
rf-mapper visualize
|
||||
python -m rf_mapper visualize
|
||||
|
||||
# Visualize specific scan file
|
||||
rf-mapper visualize -f data/scan_20240131_120000_kitchen.json
|
||||
python -m rf_mapper visualize -f data/scan_20240131_120000_kitchen.json
|
||||
|
||||
# Analyze RF environment
|
||||
rf-mapper analyze
|
||||
python -m rf_mapper analyze
|
||||
```
|
||||
|
||||
### Scan History
|
||||
|
||||
```bash
|
||||
# List saved scans
|
||||
rf-mapper list
|
||||
python -m rf_mapper list
|
||||
```
|
||||
|
||||
### Web Server
|
||||
|
||||
```bash
|
||||
# Start web server (background daemon)
|
||||
rf-mapper start
|
||||
python -m rf_mapper start
|
||||
|
||||
# Start in foreground (for debugging)
|
||||
rf-mapper start --foreground
|
||||
python -m rf_mapper start --foreground
|
||||
|
||||
# Custom host/port
|
||||
rf-mapper start -H 127.0.0.1 -p 8080
|
||||
python -m rf_mapper start -H 127.0.0.1 -p 8080
|
||||
|
||||
# With debug mode
|
||||
rf-mapper start --foreground --debug
|
||||
python -m rf_mapper start --foreground --debug
|
||||
|
||||
# With request profiling
|
||||
rf-mapper start --profile-requests
|
||||
python -m rf_mapper start --profile-requests
|
||||
|
||||
# With request logging
|
||||
rf-mapper start --log-requests
|
||||
python -m rf_mapper start --log-requests
|
||||
|
||||
# Stop the server
|
||||
rf-mapper stop
|
||||
python -m rf_mapper stop
|
||||
|
||||
# Restart the server
|
||||
rf-mapper restart
|
||||
python -m rf_mapper restart
|
||||
|
||||
# Check server status
|
||||
rf-mapper status
|
||||
python -m rf_mapper status
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# Show current configuration
|
||||
rf-mapper config
|
||||
python -m rf_mapper config
|
||||
|
||||
# Set GPS coordinates
|
||||
rf-mapper config --set-gps 50.8585 4.3978 --save
|
||||
python -m rf_mapper config --set-gps 50.8585 4.3978 --save
|
||||
|
||||
# Check Termux prerequisites (Android only)
|
||||
rf-mapper check-termux
|
||||
@@ -462,7 +467,7 @@ adb shell "settings put global settings_enable_monitor_phantom_procs false"
|
||||
|
||||
### Master Dashboard: Node selector not appearing
|
||||
1. Verify `is_master: true` in config.yaml
|
||||
2. Restart rf-mapper: `rf-mapper restart`
|
||||
2. Restart rf-mapper: `python -m rf_mapper restart`
|
||||
3. Check peers are registered: `curl http://localhost:5000/api/peers | jq '.peers | length'`
|
||||
4. At least one peer must be registered for selector to appear
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,21 +6,26 @@ Quick reference for RF Mapper commands and configuration.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
All commands require activating the virtual environment first:
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `rf-mapper` | Interactive scan mode |
|
||||
| `rf-mapper scan` | Run scan with defaults |
|
||||
| `rf-mapper scan -l kitchen` | Scan with location label |
|
||||
| `rf-mapper scan --no-bt` | WiFi only |
|
||||
| `rf-mapper scan --no-wifi` | Bluetooth only |
|
||||
| `rf-mapper visualize` | ASCII radar display |
|
||||
| `rf-mapper analyze` | RF environment analysis |
|
||||
| `rf-mapper list` | List saved scans |
|
||||
| `rf-mapper start` | Start web server (background) |
|
||||
| `rf-mapper stop` | Stop web server |
|
||||
| `rf-mapper restart` | Restart web server |
|
||||
| `rf-mapper status` | Check if server is running |
|
||||
| `rf-mapper config` | Show configuration |
|
||||
| `python -m rf_mapper` | Interactive scan mode |
|
||||
| `python -m rf_mapper scan` | Run scan with defaults |
|
||||
| `python -m rf_mapper scan -l kitchen` | Scan with location label |
|
||||
| `python -m rf_mapper scan --no-bt` | WiFi only |
|
||||
| `python -m rf_mapper scan --no-wifi` | Bluetooth only |
|
||||
| `python -m rf_mapper visualize` | ASCII radar display |
|
||||
| `python -m rf_mapper analyze` | RF environment analysis |
|
||||
| `python -m rf_mapper list` | List saved scans |
|
||||
| `python -m rf_mapper start` | Start web server (background) |
|
||||
| `python -m rf_mapper stop` | Stop web server |
|
||||
| `python -m rf_mapper restart` | Restart web server |
|
||||
| `python -m rf_mapper status` | Check if server is running |
|
||||
| `python -m rf_mapper config` | Show configuration |
|
||||
|
||||
---
|
||||
|
||||
@@ -28,18 +33,18 @@ Quick reference for RF Mapper commands and configuration.
|
||||
|
||||
```bash
|
||||
# Lifecycle
|
||||
rf-mapper start # Start (background daemon)
|
||||
rf-mapper stop # Stop
|
||||
rf-mapper restart # Restart
|
||||
rf-mapper status # Check if running
|
||||
python -m rf_mapper start # Start (background daemon)
|
||||
python -m rf_mapper stop # Stop
|
||||
python -m rf_mapper restart # Restart
|
||||
python -m rf_mapper status # Check if running
|
||||
|
||||
# Start options
|
||||
rf-mapper start -f # Foreground mode
|
||||
rf-mapper start -H 127.0.0.1 # Bind to localhost only
|
||||
rf-mapper start -p 8080 # Custom port
|
||||
rf-mapper start --debug # Debug mode (requires -f)
|
||||
rf-mapper start --profile-requests # Per-request profiling
|
||||
rf-mapper start --log-requests # Request logging
|
||||
python -m rf_mapper start -f # Foreground mode
|
||||
python -m rf_mapper start -H 127.0.0.1 # Bind to localhost only
|
||||
python -m rf_mapper start -p 8080 # Custom port
|
||||
python -m rf_mapper start --debug # Debug mode (requires -f)
|
||||
python -m rf_mapper start --profile-requests # Per-request profiling
|
||||
python -m rf_mapper start --log-requests # Request logging
|
||||
```
|
||||
|
||||
---
|
||||
@@ -48,10 +53,10 @@ rf-mapper start --log-requests # Request logging
|
||||
|
||||
```bash
|
||||
# Show current config
|
||||
rf-mapper config
|
||||
python -m rf_mapper config
|
||||
|
||||
# Set GPS coordinates
|
||||
rf-mapper config --set-gps 50.8585 4.3978 --save
|
||||
python -m rf_mapper config --set-gps 50.8585 4.3978 --save
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -258,7 +258,7 @@ template:
|
||||
|
||||
1. **Enable integration**: Set `home_assistant.enabled: true` in config.yaml
|
||||
2. **Add HA automations**: Copy webhook automations to HA
|
||||
3. **Restart RF Mapper**: `rf-mapper restart`
|
||||
3. **Restart RF Mapper**: `source venv/bin/activate && python -m rf_mapper restart`
|
||||
4. **Run scan**: Trigger BT scan in RF Mapper web UI
|
||||
5. **Check HA**: Verify `device_tracker.rf_*` entities appear
|
||||
6. **Test new device**: Clear device from DB, re-scan, verify notification
|
||||
|
||||
@@ -340,6 +340,11 @@ class DeviceDatabase:
|
||||
cursor = conn.cursor()
|
||||
timestamp = datetime.now().isoformat()
|
||||
|
||||
# Skip if this is a known scanner's BT MAC (don't record scanners as devices)
|
||||
cursor.execute("SELECT 1 FROM peers WHERE UPPER(bt_mac) = UPPER(?)", (address,))
|
||||
if cursor.fetchone():
|
||||
return # Skip scanner device
|
||||
|
||||
# Get previous observation for movement detection
|
||||
cursor.execute("""
|
||||
SELECT rssi, distance_m, timestamp FROM rssi_history
|
||||
|
||||
212
src/rf_mapper/import_ble_radar.py
Normal file
212
src/rf_mapper/import_ble_radar.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""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 scanner BT MACs to filter out (don't import scanners as devices)
|
||||
scanner_bt_macs = set()
|
||||
peers = rf_mapper_db.get_peers()
|
||||
for peer in peers:
|
||||
if peer.get("bt_mac"):
|
||||
scanner_bt_macs.add(peer["bt_mac"].upper())
|
||||
|
||||
if verbose and scanner_bt_macs:
|
||||
print(f"Filtering {len(scanner_bt_macs)} scanner BT MAC(s)")
|
||||
|
||||
stats["scanners_filtered"] = 0
|
||||
|
||||
# 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"]
|
||||
|
||||
# Skip scanner devices (don't import scanners as regular devices)
|
||||
if address.upper() in scanner_bt_macs:
|
||||
stats["scanners_filtered"] += 1
|
||||
if verbose:
|
||||
print(f"Skipping scanner: {address}")
|
||||
continue
|
||||
|
||||
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.get("scanners_filtered"):
|
||||
print(f" Scanners skipped: {stats['scanners_filtered']}")
|
||||
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())
|
||||
@@ -666,10 +666,17 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
with open(scan_files[0]) as f:
|
||||
scan = json.load(f)
|
||||
|
||||
# Get saved floor assignments from database
|
||||
# Get saved floor assignments and scanner BT MACs from database
|
||||
db = app.config.get("DATABASE")
|
||||
saved_floors = db.get_all_device_floors() if db else {}
|
||||
|
||||
# Get scanner BT MACs to filter out
|
||||
scanner_bt_macs = set()
|
||||
if db:
|
||||
for peer in db.get_peers():
|
||||
if peer.get("bt_mac"):
|
||||
scanner_bt_macs.add(peer["bt_mac"].upper())
|
||||
|
||||
# Enrich with distance estimates and saved floor assignments
|
||||
for net in scan.get("wifi_networks", []):
|
||||
net["estimated_distance_m"] = round(estimate_distance(net["rssi"]), 2)
|
||||
@@ -678,12 +685,18 @@ def create_app(config: Config | None = None) -> Flask:
|
||||
if "height_m" not in net:
|
||||
net["height_m"] = None
|
||||
|
||||
# Filter out scanner devices and enrich BT devices
|
||||
filtered_bt = []
|
||||
for dev in scan.get("bluetooth_devices", []):
|
||||
address = dev.get("address", "").upper()
|
||||
if address in scanner_bt_macs:
|
||||
continue # Skip scanner devices
|
||||
dev["estimated_distance_m"] = round(estimate_distance(dev["rssi"], tx_power=-65), 2)
|
||||
address = dev.get("address")
|
||||
dev["floor"] = saved_floors.get(address) if address else dev.get("floor")
|
||||
dev["floor"] = saved_floors.get(dev.get("address")) if dev.get("address") else dev.get("floor")
|
||||
if "height_m" not in dev:
|
||||
dev["height_m"] = None
|
||||
filtered_bt.append(dev)
|
||||
scan["bluetooth_devices"] = filtered_bt
|
||||
|
||||
scan["gps"] = {
|
||||
"lat": app.config["CURRENT_LAT"],
|
||||
@@ -1517,6 +1530,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