From 4fef21c06fd450a0a06bf3df0492c301918a6065 Mon Sep 17 00:00:00 2001 From: User Date: Sun, 1 Feb 2026 13:00:42 +0100 Subject: [PATCH] feat: filter scanner Bluetooth devices from display - Add bt_mac field to scanner config for identifying scanner BT adapters - Store bt_mac in peers table for peer scanners - Filter out devices matching scanner BT MACs from all views - Prevents scanners from appearing as devices in device lists/maps Config: scanner.bt_mac = "XX:XX:XX:XX:XX:XX" API: /api/peers/register accepts bt_mac field Co-Authored-By: Claude Opus 4.5 --- config.yaml | 5 ++- src/rf_mapper/config.py | 6 ++- src/rf_mapper/database.py | 16 ++++++-- src/rf_mapper/web/app.py | 3 +- src/rf_mapper/web/static/js/app.js | 61 +++++++++++++++++++++++++----- 5 files changed, 73 insertions(+), 18 deletions(-) diff --git a/config.yaml b/config.yaml index cca8f6f..506c691 100644 --- a/config.yaml +++ b/config.yaml @@ -6,12 +6,13 @@ web: port: 5000 debug: false scanner: - id: '' - name: '' + id: rpios + name: rpios latitude: null 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/config.py b/src/rf_mapper/config.py index bfa4341..caa91f6 100644 --- a/src/rf_mapper/config.py +++ b/src/rf_mapper/config.py @@ -29,6 +29,7 @@ class ScannerConfig: longitude: float | None = None # Scanner position (falls back to gps.longitude) floor: int | None = None # Scanner's floor (falls back to building.current_floor) is_master: bool = False # Master node can view other nodes' data in dashboard + bt_mac: str = "" # Scanner's Bluetooth MAC address (for filtering from device lists) # Scanning configuration wifi_interface: str = "wlan0" @@ -181,6 +182,7 @@ class Config: longitude=data["scanner"].get("longitude", config.scanner.longitude), floor=data["scanner"].get("floor", config.scanner.floor), is_master=data["scanner"].get("is_master", config.scanner.is_master), + bt_mac=data["scanner"].get("bt_mac", config.scanner.bt_mac), # Scanning configuration wifi_interface=data["scanner"].get("wifi_interface", config.scanner.wifi_interface), bt_scan_timeout=data["scanner"].get("bt_scan_timeout", config.scanner.bt_scan_timeout), @@ -297,6 +299,7 @@ class Config: - latitude: Scanner position (from scanner config or gps config) - longitude: Scanner position (from scanner config or gps config) - floor: Scanner's floor (from scanner config or building config) + - bt_mac: Bluetooth MAC address (for filtering from device lists) """ import socket @@ -306,7 +309,8 @@ class Config: "name": self.scanner.name or scanner_id, "latitude": self.scanner.latitude if self.scanner.latitude is not None else self.gps.latitude, "longitude": self.scanner.longitude if self.scanner.longitude is not None else self.gps.longitude, - "floor": self.scanner.floor if self.scanner.floor is not None else self.building.current_floor + "floor": self.scanner.floor if self.scanner.floor is not None else self.building.current_floor, + "bt_mac": self.scanner.bt_mac or None } def save(self, path: Path | None = None): diff --git a/src/rf_mapper/database.py b/src/rf_mapper/database.py index 0b9bba2..19bb8a6 100644 --- a/src/rf_mapper/database.py +++ b/src/rf_mapper/database.py @@ -210,6 +210,12 @@ class DeviceDatabase: ) """) + # Add bt_mac column to peers table if missing (for scanner BT filtering) + try: + cursor.execute("ALTER TABLE peers ADD COLUMN bt_mac TEXT") + except sqlite3.OperationalError: + pass # Column already exists + # Add notes column to devices table if missing (for sync) try: cursor.execute("ALTER TABLE devices ADD COLUMN notes TEXT") @@ -941,7 +947,7 @@ class DeviceDatabase: def register_peer(self, scanner_id: str, name: str, url: str, floor: Optional[int] = None, latitude: Optional[float] = None, - longitude: Optional[float] = None) -> bool: + longitude: Optional[float] = None, bt_mac: Optional[str] = None) -> bool: """Register a peer scanner. Args: @@ -951,6 +957,7 @@ class DeviceDatabase: floor: Floor where peer scanner is located latitude: GPS latitude of peer longitude: GPS longitude of peer + bt_mac: Bluetooth MAC address of the scanner (for filtering from device lists) Returns: True if newly registered, False if updated existing @@ -964,16 +971,17 @@ class DeviceDatabase: exists = cursor.fetchone() is not None cursor.execute(""" - INSERT INTO peers (scanner_id, name, url, floor, latitude, longitude, last_seen, registered_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO peers (scanner_id, name, url, floor, latitude, longitude, bt_mac, last_seen, registered_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(scanner_id) DO UPDATE SET name = excluded.name, url = excluded.url, floor = excluded.floor, latitude = excluded.latitude, longitude = excluded.longitude, + bt_mac = COALESCE(excluded.bt_mac, peers.bt_mac), last_seen = excluded.last_seen - """, (scanner_id, name, url, floor, latitude, longitude, timestamp, timestamp)) + """, (scanner_id, name, url, floor, latitude, longitude, bt_mac, timestamp, timestamp)) conn.commit() return not exists diff --git a/src/rf_mapper/web/app.py b/src/rf_mapper/web/app.py index dea5d93..7754604 100644 --- a/src/rf_mapper/web/app.py +++ b/src/rf_mapper/web/app.py @@ -1562,7 +1562,8 @@ def create_app(config: Config | None = None) -> Flask: url=peer_url, floor=data.get("floor"), latitude=data.get("latitude"), - longitude=data.get("longitude") + longitude=data.get("longitude"), + bt_mac=data.get("bt_mac") ) action = "registered" if is_new else "updated" diff --git a/src/rf_mapper/web/static/js/app.js b/src/rf_mapper/web/static/js/app.js index eef02d0..3b3b12b 100644 --- a/src/rf_mapper/web/static/js/app.js +++ b/src/rf_mapper/web/static/js/app.js @@ -24,6 +24,9 @@ let deviceSources = {}; // { deviceId: { scanner_id, lat, lon } } // Peer scanner positions - loaded from /api/peers (live positions) let peerScanners = {}; // { scanner_id: { lat, lon, floor, name } } +// Scanner Bluetooth MACs - for filtering scanners from device lists +let scannerBtMacs = new Set(); // Set of BT MAC addresses belonging to scanners + // Trilateration state - positions calculated from multiple scanner RSSI let trilateratedPositions = {}; // { deviceId: { lat, lon, confidence, scanners, method } } let trilaterationEnabled = true; @@ -120,6 +123,25 @@ function isDeviceMoving(address, newDistance) { return isMoving; } +// Filter out scanner Bluetooth devices from scan data +// Removes devices whose address matches a known scanner's BT MAC +function filterScannerDevices(data) { + if (!data || scannerBtMacs.size === 0) return data; + + if (data.bluetooth_devices) { + const before = data.bluetooth_devices.length; + data.bluetooth_devices = data.bluetooth_devices.filter(dev => { + const addr = (dev.address || '').toUpperCase(); + return !scannerBtMacs.has(addr); + }); + const filtered = before - data.bluetooth_devices.length; + if (filtered > 0) { + console.log(`[Filter] Removed ${filtered} scanner BT device(s)`); + } + } + return data; +} + // Device positions for hit detection (radar view) let devicePositions = []; @@ -313,7 +335,7 @@ async function switchNode(nodeId) { ]); if (latestResp.ok) { - scanData = await latestResp.json(); + scanData = filterScannerDevices(await latestResp.json()); if (floorsResp.ok) { const floorsData = await floorsResp.json(); updateDeviceFloors(floorsData); @@ -390,7 +412,11 @@ function handleWebSocketScanUpdate(data) { // Handle Bluetooth scan results if (data.type === 'bluetooth' && data.devices) { - const newBt = data.devices; + // Filter out scanner Bluetooth devices + const newBt = data.devices.filter(dev => { + const addr = (dev.address || '').toUpperCase(); + return !scannerBtMacs.has(addr); + }); // Track which devices were detected in this scan const detectedAddresses = new Set(newBt.map(d => d.address)); @@ -588,7 +614,7 @@ async function loadLatestScan() { try { const response = await fetch('/api/latest'); if (response.ok) { - scanData = await response.json(); + scanData = filterScannerDevices(await response.json()); updateUI(); } else { document.getElementById('wifi-list').innerHTML = '
No scans yet. Click "New Scan" to start.
'; @@ -624,7 +650,7 @@ async function triggerScan() { }); if (response.ok) { - scanData = await response.json(); + scanData = filterScannerDevices(await response.json()); updateUI(); status.textContent = `Scanned at ${new Date().toLocaleTimeString()}`; } else { @@ -1435,11 +1461,18 @@ async function loadDevicePositions() { } } - // Load peer scanner positions (live/current positions) + // Load peer scanner positions (live/current positions) and BT MACs const peersResponse = await fetch('/api/peers'); if (peersResponse.ok) { const peersData = await peersResponse.json(); peerScanners = {}; + scannerBtMacs = new Set(); + + // Add local scanner's BT MAC if available + if (peersData.this_scanner?.bt_mac) { + scannerBtMacs.add(peersData.this_scanner.bt_mac.toUpperCase()); + } + (peersData.peers || []).forEach(peer => { peerScanners[peer.scanner_id] = { lat: peer.latitude, @@ -1447,8 +1480,12 @@ async function loadDevicePositions() { floor: peer.floor, name: peer.name }; + // Collect peer BT MACs for filtering + if (peer.bt_mac) { + scannerBtMacs.add(peer.bt_mac.toUpperCase()); + } }); - console.log('[Peers] Loaded', Object.keys(peerScanners).length, 'peer positions'); + console.log('[Peers] Loaded', Object.keys(peerScanners).length, 'peer positions,', scannerBtMacs.size, 'scanner BT MACs'); } } catch (error) { console.error('Error loading device positions:', error); @@ -2391,7 +2428,11 @@ async function performLiveBTScan() { if (response.ok) { const data = await response.json(); - const newBt = data.bluetooth_devices || []; + // Filter out scanner Bluetooth devices + const newBt = (data.bluetooth_devices || []).filter(dev => { + const addr = (dev.address || '').toUpperCase(); + return !scannerBtMacs.has(addr); + }); // Track which devices were detected in this scan const detectedAddresses = new Set(newBt.map(d => d.address)); @@ -2453,8 +2494,8 @@ async function performLiveBTScan() { scanData.bluetooth_devices = filteredBt; } else { - // No existing scan data, use BT-only data - data.bluetooth_devices.forEach(dev => { + // No existing scan data, use BT-only data (already filtered above) + newBt.forEach(dev => { // Initialize history with first sample, not moving yet isDeviceMoving(dev.address, dev.estimated_distance_m); dev.is_moving = false; @@ -2462,7 +2503,7 @@ async function performLiveBTScan() { }); scanData = { wifi_networks: [], - bluetooth_devices: data.bluetooth_devices, + bluetooth_devices: newBt, timestamp: data.timestamp }; }