feat: Add BT Locate and GPS modes with IRK auto-detection

New modes:
- BT Locate: SAR Bluetooth device location with GPS-tagged signal trail,
  RSSI-based proximity bands, audio alerts, and IRK auto-extraction from
  paired devices (macOS plist / Linux BlueZ)
- GPS: Real-time position tracking with live map, speed, heading, altitude,
  satellite info, and track recording via gpsd

Bug fixes:
- Fix ABBA deadlock between session lock and aggregator lock in BT Locate
- Fix bleak scan lifecycle tracking in BluetoothScanner (is_scanning property
  now cross-checks backend state)
- Fix map tile persistence when switching modes
- Use 15s max_age window for fresh detections in BT Locate poll loop

Documentation:
- Update README, FEATURES.md, USAGE.md, and GitHub Pages with new modes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-15 21:59:38 +00:00
parent c60769f795
commit d8d08a8b1e
26 changed files with 4481 additions and 510 deletions

View File

@@ -66,7 +66,7 @@ class BluetoothScanner:
self._scan_timer: Optional[threading.Timer] = None
# Callbacks
self._on_device_updated: Optional[Callable[[BTDeviceAggregate], None]] = None
self._on_device_updated_callbacks: list[Callable[[BTDeviceAggregate], None]] = []
# Capability check result
self._capabilities: Optional[SystemCapabilities] = None
@@ -236,9 +236,12 @@ class BluetoothScanner:
'device': device.to_summary_dict(),
})
# Callback
if self._on_device_updated:
self._on_device_updated(device)
# Callbacks
for cb in self._on_device_updated_callbacks:
try:
cb(device)
except Exception as cb_err:
logger.error(f"Device callback error: {cb_err}")
except Exception as e:
logger.error(f"Error handling observation: {e}")
@@ -368,13 +371,39 @@ class BluetoothScanner:
return self._capabilities
def set_on_device_updated(self, callback: Callable[[BTDeviceAggregate], None]) -> None:
"""Set callback for device updates."""
self._on_device_updated = callback
"""Set callback for device updates (legacy, adds to callback list)."""
self.add_device_callback(callback)
def add_device_callback(self, callback: Callable[[BTDeviceAggregate], None]) -> None:
"""Add a callback for device updates."""
if callback not in self._on_device_updated_callbacks:
self._on_device_updated_callbacks.append(callback)
def remove_device_callback(self, callback: Callable[[BTDeviceAggregate], None]) -> None:
"""Remove a device update callback."""
if callback in self._on_device_updated_callbacks:
self._on_device_updated_callbacks.remove(callback)
@property
def is_scanning(self) -> bool:
"""Check if scanning is active."""
return self._status.is_scanning
"""Check if scanning is active.
Cross-checks the backend scanner state, since bleak scans can
expire silently without calling stop_scan().
"""
if not self._status.is_scanning:
return False
# Detect backends that finished on their own (e.g. bleak timeout)
backend_alive = (
(self._dbus_scanner and self._dbus_scanner.is_scanning)
or (self._fallback_scanner and self._fallback_scanner.is_scanning)
)
if not backend_alive:
self._status.is_scanning = False
return False
return True
@property
def device_count(self) -> int: