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 <noreply@anthropic.com>
This commit is contained in:
User
2026-02-01 13:00:42 +01:00
parent 446bec278d
commit 4fef21c06f
5 changed files with 73 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = '<div style="color:#888;padding:1rem;">No scans yet. Click "New Scan" to start.</div>';
@@ -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
};
}