Fix BT/WiFi run-state health and BT Locate tracking continuity

This commit is contained in:
Smittix
2026-02-19 21:39:09 +00:00
parent 9e634ffbd8
commit 7c816e3543
6 changed files with 256 additions and 144 deletions
+61 -5
View File
@@ -671,10 +671,66 @@ def _get_dmr_active() -> bool:
return False return False
def _get_bluetooth_health() -> tuple[bool, int]:
"""Return Bluetooth active state and best-effort device count."""
legacy_running = bt_process is not None and (bt_process.poll() is None if bt_process else False)
scanner_running = False
scanner_count = 0
try:
from utils.bluetooth.scanner import _scanner_instance as bt_scanner
if bt_scanner is not None:
scanner_running = bool(bt_scanner.is_scanning)
scanner_count = int(bt_scanner.device_count)
except Exception:
scanner_running = False
scanner_count = 0
locate_running = False
try:
from utils.bt_locate import get_locate_session
session = get_locate_session()
if session and getattr(session, 'active', False):
scanner = getattr(session, '_scanner', None)
locate_running = bool(scanner and scanner.is_scanning)
except Exception:
locate_running = False
return (legacy_running or scanner_running or locate_running), max(len(bt_devices), scanner_count)
def _get_wifi_health() -> tuple[bool, int, int]:
"""Return WiFi active state and best-effort network/client counts."""
legacy_running = wifi_process is not None and (wifi_process.poll() is None if wifi_process else False)
scanner_running = False
scanner_networks = 0
scanner_clients = 0
try:
from utils.wifi.scanner import _scanner_instance as wifi_scanner
if wifi_scanner is not None:
status = wifi_scanner.get_status()
scanner_running = bool(status.is_scanning)
scanner_networks = int(status.networks_found or 0)
scanner_clients = int(status.clients_found or 0)
except Exception:
scanner_running = False
scanner_networks = 0
scanner_clients = 0
return (
legacy_running or scanner_running,
max(len(wifi_networks), scanner_networks),
max(len(wifi_clients), scanner_clients),
)
@app.route('/health') @app.route('/health')
def health_check() -> Response: def health_check() -> Response:
"""Health check endpoint for monitoring.""" """Health check endpoint for monitoring."""
import time import time
bt_active, bt_device_count = _get_bluetooth_health()
wifi_active, wifi_network_count, wifi_client_count = _get_wifi_health()
return jsonify({ return jsonify({
'status': 'healthy', 'status': 'healthy',
'version': VERSION, 'version': VERSION,
@@ -687,8 +743,8 @@ def health_check() -> Response:
'acars': acars_process is not None and (acars_process.poll() is None if acars_process else False), 'acars': acars_process is not None and (acars_process.poll() is None if acars_process else False),
'vdl2': vdl2_process is not None and (vdl2_process.poll() is None if vdl2_process else False), 'vdl2': vdl2_process is not None and (vdl2_process.poll() is None if vdl2_process else False),
'aprs': aprs_process is not None and (aprs_process.poll() is None if aprs_process else False), 'aprs': aprs_process is not None and (aprs_process.poll() is None if aprs_process else False),
'wifi': wifi_process is not None and (wifi_process.poll() is None if wifi_process else False), 'wifi': wifi_active,
'bluetooth': bt_process is not None and (bt_process.poll() is None if bt_process else False), 'bluetooth': bt_active,
'dsc': dsc_process is not None and (dsc_process.poll() is None if dsc_process else False), 'dsc': dsc_process is not None and (dsc_process.poll() is None if dsc_process else False),
'dmr': _get_dmr_active(), 'dmr': _get_dmr_active(),
'subghz': _get_subghz_active(), 'subghz': _get_subghz_active(),
@@ -696,9 +752,9 @@ def health_check() -> Response:
'data': { 'data': {
'aircraft_count': len(adsb_aircraft), 'aircraft_count': len(adsb_aircraft),
'vessel_count': len(ais_vessels), 'vessel_count': len(ais_vessels),
'wifi_networks_count': len(wifi_networks), 'wifi_networks_count': wifi_network_count,
'wifi_clients_count': len(wifi_clients), 'wifi_clients_count': wifi_client_count,
'bt_devices_count': len(bt_devices), 'bt_devices_count': bt_device_count,
'dsc_messages_count': len(dsc_messages), 'dsc_messages_count': len(dsc_messages),
} }
}) })
+18 -2
View File
@@ -38,6 +38,8 @@ def start_session():
- name_pattern: Target name substring (optional) - name_pattern: Target name substring (optional)
- irk_hex: Identity Resolving Key hex string (optional) - irk_hex: Identity Resolving Key hex string (optional)
- device_id: Device ID from Bluetooth scanner (optional) - device_id: Device ID from Bluetooth scanner (optional)
- device_key: Stable device key from Bluetooth scanner (optional)
- fingerprint_id: Payload fingerprint ID from Bluetooth scanner (optional)
- known_name: Hand-off device name (optional) - known_name: Hand-off device name (optional)
- known_manufacturer: Hand-off manufacturer (optional) - known_manufacturer: Hand-off manufacturer (optional)
- last_known_rssi: Hand-off last RSSI (optional) - last_known_rssi: Hand-off last RSSI (optional)
@@ -55,14 +57,28 @@ def start_session():
name_pattern=data.get('name_pattern'), name_pattern=data.get('name_pattern'),
irk_hex=data.get('irk_hex'), irk_hex=data.get('irk_hex'),
device_id=data.get('device_id'), device_id=data.get('device_id'),
device_key=data.get('device_key'),
fingerprint_id=data.get('fingerprint_id'),
known_name=data.get('known_name'), known_name=data.get('known_name'),
known_manufacturer=data.get('known_manufacturer'), known_manufacturer=data.get('known_manufacturer'),
last_known_rssi=data.get('last_known_rssi'), last_known_rssi=data.get('last_known_rssi'),
) )
# At least one identifier required # At least one identifier required
if not any([target.mac_address, target.name_pattern, target.irk_hex, target.device_id]): if not any([
return jsonify({'error': 'At least one target identifier required (mac_address, name_pattern, irk_hex, or device_id)'}), 400 target.mac_address,
target.name_pattern,
target.irk_hex,
target.device_id,
target.device_key,
target.fingerprint_id,
]):
return jsonify({
'error': (
'At least one target identifier required '
'(mac_address, name_pattern, irk_hex, device_id, device_key, or fingerprint_id)'
)
}), 400
# Parse environment # Parse environment
env_str = data.get('environment', 'OUTDOOR').upper() env_str = data.get('environment', 'OUTDOOR').upper()
+2 -1
View File
@@ -1621,6 +1621,7 @@ const BluetoothMode = (function() {
if (typeof BtLocate !== 'undefined') { if (typeof BtLocate !== 'undefined') {
BtLocate.handoff({ BtLocate.handoff({
device_id: device.device_id, device_id: device.device_id,
device_key: device.device_key || null,
mac_address: device.address, mac_address: device.address,
address_type: device.address_type || null, address_type: device.address_type || null,
irk_hex: device.irk_hex || null, irk_hex: device.irk_hex || null,
@@ -1629,7 +1630,7 @@ const BluetoothMode = (function() {
last_known_rssi: device.rssi_current, last_known_rssi: device.rssi_current,
tx_power: device.tx_power || null, tx_power: device.tx_power || null,
appearance_name: device.appearance_name || null, appearance_name: device.appearance_name || null,
fingerprint_id: device.fingerprint_id || null, fingerprint_id: device.fingerprint_id || device.fingerprint?.id || null,
mac_cluster_count: device.mac_cluster_count || 0 mac_cluster_count: device.mac_cluster_count || 0
}); });
} }
+13 -2
View File
@@ -145,6 +145,8 @@ const BtLocate = (function() {
if (namePattern) body.name_pattern = namePattern; if (namePattern) body.name_pattern = namePattern;
if (irk) body.irk_hex = irk; if (irk) body.irk_hex = irk;
if (handoffData?.device_id) body.device_id = handoffData.device_id; if (handoffData?.device_id) body.device_id = handoffData.device_id;
if (handoffData?.device_key) body.device_key = handoffData.device_key;
if (handoffData?.fingerprint_id) body.fingerprint_id = handoffData.fingerprint_id;
if (handoffData?.known_name) body.known_name = handoffData.known_name; if (handoffData?.known_name) body.known_name = handoffData.known_name;
if (handoffData?.known_manufacturer) body.known_manufacturer = handoffData.known_manufacturer; if (handoffData?.known_manufacturer) body.known_manufacturer = handoffData.known_manufacturer;
if (handoffData?.last_known_rssi) body.last_known_rssi = handoffData.last_known_rssi; if (handoffData?.last_known_rssi) body.last_known_rssi = handoffData.last_known_rssi;
@@ -159,8 +161,9 @@ const BtLocate = (function() {
console.log('[BtLocate] Starting with body:', body); console.log('[BtLocate] Starting with body:', body);
if (!body.mac_address && !body.name_pattern && !body.irk_hex && !body.device_id) { if (!body.mac_address && !body.name_pattern && !body.irk_hex &&
alert('Please provide at least a MAC address, name pattern, IRK, or use hand-off from Bluetooth mode.'); !body.device_id && !body.device_key && !body.fingerprint_id) {
alert('Please provide at least one target identifier or use hand-off from Bluetooth mode.');
return; return;
} }
@@ -255,6 +258,9 @@ const BtLocate = (function() {
eventSource.onerror = function() { eventSource.onerror = function() {
console.warn('[BtLocate] SSE error, polling fallback active'); console.warn('[BtLocate] SSE error, polling fallback active');
if (eventSource && eventSource.readyState === EventSource.CLOSED) {
eventSource = null;
}
}; };
// Start polling fallback (catches data even if SSE fails) // Start polling fallback (catches data even if SSE fails)
@@ -318,6 +324,11 @@ const BtLocate = (function() {
updateScanStatus(data); updateScanStatus(data);
updateHudInfo(data); updateHudInfo(data);
// Recover live stream if browser closed SSE connection.
if (!eventSource || eventSource.readyState === EventSource.CLOSED) {
connectSSE();
}
// Show diagnostics // Show diagnostics
const diagEl = document.getElementById('btLocateDiag'); const diagEl = document.getElementById('btLocateDiag');
if (diagEl) { if (diagEl) {
+4 -1
View File
@@ -3910,7 +3910,10 @@
const btScanActive = (typeof BluetoothMode !== 'undefined' && const btScanActive = (typeof BluetoothMode !== 'undefined' &&
typeof BluetoothMode.isScanning === 'function' && typeof BluetoothMode.isScanning === 'function' &&
BluetoothMode.isScanning()) || isBtRunning; BluetoothMode.isScanning()) || isBtRunning;
if (btScanActive && typeof stopBtScan === 'function') stopBtScan(); const isBtModeTransition =
(currentMode === 'bluetooth' && mode === 'bt_locate') ||
(currentMode === 'bt_locate' && mode === 'bluetooth');
if (btScanActive && !isBtModeTransition && typeof stopBtScan === 'function') stopBtScan();
if (isAprsRunning) stopAprs(); if (isAprsRunning) stopAprs();
if (isTscmRunning) stopTscmSweep(); if (isTscmRunning) stopTscmSweep();
} }
+27 -2
View File
@@ -88,6 +88,8 @@ class LocateTarget:
name_pattern: str | None = None name_pattern: str | None = None
irk_hex: str | None = None irk_hex: str | None = None
device_id: str | None = None device_id: str | None = None
device_key: str | None = None
fingerprint_id: str | None = None
# Hand-off metadata from Bluetooth mode # Hand-off metadata from Bluetooth mode
known_name: str | None = None known_name: str | None = None
known_manufacturer: str | None = None known_manufacturer: str | None = None
@@ -95,6 +97,10 @@ class LocateTarget:
def matches(self, device: BTDeviceAggregate) -> bool: def matches(self, device: BTDeviceAggregate) -> bool:
"""Check if a device matches this target.""" """Check if a device matches this target."""
# Match by stable device key (survives MAC randomization for many devices)
if self.device_key and getattr(device, 'device_key', None) == self.device_key:
return True
# Match by device_id (exact) # Match by device_id (exact)
if self.device_id and device.device_id == self.device_id: if self.device_id and device.device_id == self.device_id:
return True return True
@@ -113,6 +119,13 @@ class LocateTarget:
if dev_addr == target_addr: if dev_addr == target_addr:
return True return True
# Match by payload fingerprint (guard against low-stability generic fingerprints)
if self.fingerprint_id:
dev_fp = getattr(device, 'payload_fingerprint_id', None)
dev_fp_stability = getattr(device, 'payload_fingerprint_stability', 0.0) or 0.0
if dev_fp and dev_fp == self.fingerprint_id and dev_fp_stability >= 0.35:
return True
# Match by RPA resolution # Match by RPA resolution
if self.irk_hex: if self.irk_hex:
try: try:
@@ -126,8 +139,18 @@ class LocateTarget:
if self.name_pattern and device.name and self.name_pattern.lower() in device.name.lower(): if self.name_pattern and device.name and self.name_pattern.lower() in device.name.lower():
return True return True
# Match by known_name from handoff (exact name match) # Match by known_name from handoff (exact or loose normalized match)
return bool(self.known_name and device.name and self.known_name.lower() == device.name.lower()) if self.known_name and device.name:
target_name = self.known_name.strip().lower()
device_name = device.name.strip().lower()
if target_name and (
target_name == device_name
or target_name in device_name
or device_name in target_name
):
return True
return False
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
@@ -135,6 +158,8 @@ class LocateTarget:
'name_pattern': self.name_pattern, 'name_pattern': self.name_pattern,
'irk_hex': self.irk_hex, 'irk_hex': self.irk_hex,
'device_id': self.device_id, 'device_id': self.device_id,
'device_key': self.device_key,
'fingerprint_id': self.fingerprint_id,
'known_name': self.known_name, 'known_name': self.known_name,
'known_manufacturer': self.known_manufacturer, 'known_manufacturer': self.known_manufacturer,
'last_known_rssi': self.last_known_rssi, 'last_known_rssi': self.last_known_rssi,