mirror of
https://github.com/smittix/intercept.git
synced 2026-06-11 07:23:30 -07:00
Merge branch 'smittix:main' into main
This commit is contained in:
+23
-24
@@ -122,14 +122,14 @@ def _get_mode_counts() -> dict[str, int]:
|
||||
except Exception:
|
||||
counts['aprs'] = 0
|
||||
|
||||
# Meshtastic recent messages (route-level list)
|
||||
try:
|
||||
import routes.meshtastic as mesh_route
|
||||
counts['meshtastic'] = len(getattr(mesh_route, '_recent_messages', []))
|
||||
except Exception:
|
||||
counts['meshtastic'] = 0
|
||||
|
||||
return counts
|
||||
# Meshtastic recent messages (route-level list)
|
||||
try:
|
||||
import routes.meshtastic as mesh_route
|
||||
counts['meshtastic'] = len(getattr(mesh_route, '_recent_messages', []))
|
||||
except Exception:
|
||||
counts['meshtastic'] = 0
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def get_cross_mode_summary() -> dict[str, Any]:
|
||||
@@ -160,12 +160,11 @@ def get_mode_health() -> dict[str, dict]:
|
||||
'acars': 'acars_process',
|
||||
'vdl2': 'vdl2_process',
|
||||
'aprs': 'aprs_process',
|
||||
'wifi': 'wifi_process',
|
||||
'bluetooth': 'bt_process',
|
||||
'dsc': 'dsc_process',
|
||||
'rtlamr': 'rtlamr_process',
|
||||
'dmr': 'dmr_process',
|
||||
}
|
||||
'wifi': 'wifi_process',
|
||||
'bluetooth': 'bt_process',
|
||||
'dsc': 'dsc_process',
|
||||
'rtlamr': 'rtlamr_process',
|
||||
}
|
||||
|
||||
for mode, attr in process_map.items():
|
||||
proc = getattr(app_module, attr, None)
|
||||
@@ -187,16 +186,16 @@ def get_mode_health() -> dict[str, dict]:
|
||||
pass
|
||||
|
||||
# Meshtastic: check client connection status
|
||||
try:
|
||||
from utils.meshtastic import get_meshtastic_client
|
||||
client = get_meshtastic_client()
|
||||
health['meshtastic'] = {'running': client._interface is not None}
|
||||
except Exception:
|
||||
health['meshtastic'] = {'running': False}
|
||||
|
||||
try:
|
||||
sdr_status = app_module.get_sdr_device_status()
|
||||
health['sdr_devices'] = {str(k): v for k, v in sdr_status.items()}
|
||||
try:
|
||||
from utils.meshtastic import get_meshtastic_client
|
||||
client = get_meshtastic_client()
|
||||
health['meshtastic'] = {'running': client._interface is not None}
|
||||
except Exception:
|
||||
health['meshtastic'] = {'running': False}
|
||||
|
||||
try:
|
||||
sdr_status = app_module.get_sdr_device_status()
|
||||
health['sdr_devices'] = {str(k): v for k, v in sdr_status.items()}
|
||||
except Exception:
|
||||
health['sdr_devices'] = {}
|
||||
|
||||
|
||||
+193
-114
@@ -7,24 +7,68 @@ distance estimation, and proximity alerts for search and rescue operations.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from utils.bluetooth.models import BTDeviceAggregate
|
||||
from utils.bluetooth.scanner import BluetoothScanner, get_bluetooth_scanner
|
||||
from utils.gps import get_current_position
|
||||
|
||||
logger = logging.getLogger('intercept.bt_locate')
|
||||
logger = logging.getLogger('intercept.bt_locate')
|
||||
|
||||
# Maximum trail points to retain
|
||||
MAX_TRAIL_POINTS = 500
|
||||
|
||||
# EMA smoothing factor for RSSI
|
||||
EMA_ALPHA = 0.3
|
||||
# Maximum trail points to retain
|
||||
MAX_TRAIL_POINTS = 500
|
||||
|
||||
# EMA smoothing factor for RSSI
|
||||
EMA_ALPHA = 0.3
|
||||
|
||||
# Polling/restart tuning for scanner resilience without high CPU churn.
|
||||
POLL_INTERVAL_SECONDS = 1.5
|
||||
SCAN_RESTART_BACKOFF_SECONDS = 8.0
|
||||
NO_MATCH_LOG_EVERY_POLLS = 10
|
||||
|
||||
|
||||
def _normalize_mac(address: str | None) -> str | None:
|
||||
"""Normalize MAC string to colon-separated uppercase form when possible."""
|
||||
if not address:
|
||||
return None
|
||||
|
||||
text = str(address).strip().upper().replace('-', ':')
|
||||
if not text:
|
||||
return None
|
||||
|
||||
# Handle raw 12-hex form: AABBCCDDEEFF
|
||||
raw = ''.join(ch for ch in text if ch in '0123456789ABCDEF')
|
||||
if ':' not in text and len(raw) == 12:
|
||||
text = ':'.join(raw[i:i + 2] for i in range(0, 12, 2))
|
||||
|
||||
parts = text.split(':')
|
||||
if len(parts) == 6 and all(len(p) == 2 and all(c in '0123456789ABCDEF' for c in p) for p in parts):
|
||||
return ':'.join(parts)
|
||||
|
||||
# Return cleaned original when not a strict MAC (caller may still use exact matching)
|
||||
return text
|
||||
|
||||
|
||||
def _address_looks_like_rpa(address: str | None) -> bool:
|
||||
"""
|
||||
Return True when an address looks like a Resolvable Private Address.
|
||||
|
||||
RPA check: most-significant two bits of the first octet are `01`.
|
||||
"""
|
||||
normalized = _normalize_mac(address)
|
||||
if not normalized:
|
||||
return False
|
||||
try:
|
||||
first_octet = int(normalized.split(':', 1)[0], 16)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
return (first_octet >> 6) == 1
|
||||
|
||||
|
||||
class Environment(Enum):
|
||||
@@ -94,8 +138,27 @@ class LocateTarget:
|
||||
known_name: str | None = None
|
||||
known_manufacturer: str | None = None
|
||||
last_known_rssi: int | None = None
|
||||
_cached_irk_hex: str | None = field(default=None, init=False, repr=False)
|
||||
_cached_irk_bytes: bytes | None = field(default=None, init=False, repr=False)
|
||||
|
||||
def matches(self, device: BTDeviceAggregate) -> bool:
|
||||
def _get_irk_bytes(self) -> bytes | None:
|
||||
"""Parse/cache target IRK bytes once for repeated match checks."""
|
||||
if not self.irk_hex:
|
||||
return None
|
||||
if self._cached_irk_hex == self.irk_hex:
|
||||
return self._cached_irk_bytes
|
||||
self._cached_irk_hex = self.irk_hex
|
||||
self._cached_irk_bytes = None
|
||||
try:
|
||||
parsed = bytes.fromhex(self.irk_hex)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
if len(parsed) != 16:
|
||||
return None
|
||||
self._cached_irk_bytes = parsed
|
||||
return parsed
|
||||
|
||||
def matches(self, device: BTDeviceAggregate, irk_bytes: bytes | None = None) -> bool:
|
||||
"""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:
|
||||
@@ -112,28 +175,30 @@ class LocateTarget:
|
||||
if target_addr_part and dev_addr == target_addr_part:
|
||||
return True
|
||||
|
||||
# Match by MAC/address (case-insensitive, normalize separators)
|
||||
# Match by MAC/address (case-insensitive, normalize separators)
|
||||
if self.mac_address:
|
||||
dev_addr = (device.address or '').upper().replace('-', ':')
|
||||
target_addr = self.mac_address.upper().replace('-', ':')
|
||||
if dev_addr == target_addr:
|
||||
dev_addr = _normalize_mac(device.address)
|
||||
target_addr = _normalize_mac(self.mac_address)
|
||||
if dev_addr and target_addr and dev_addr == target_addr:
|
||||
return True
|
||||
|
||||
# Match by payload fingerprint (guard against low-stability generic fingerprints)
|
||||
# Match by payload fingerprint.
|
||||
# For explicit hand-off sessions, allow exact fingerprint matches even if
|
||||
# stability is still warming up.
|
||||
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
|
||||
if dev_fp and dev_fp == self.fingerprint_id:
|
||||
if dev_fp_stability >= 0.35:
|
||||
return True
|
||||
if any([self.device_id, self.device_key, self.mac_address, self.known_name]):
|
||||
return True
|
||||
|
||||
# Match by RPA resolution
|
||||
if self.irk_hex:
|
||||
try:
|
||||
irk = bytes.fromhex(self.irk_hex)
|
||||
if len(irk) == 16 and device.address and resolve_rpa(irk, device.address):
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if self.irk_hex and device.address and _address_looks_like_rpa(device.address):
|
||||
irk = irk_bytes or self._get_irk_bytes()
|
||||
if irk and resolve_rpa(irk, device.address):
|
||||
return True
|
||||
|
||||
# Match by name pattern
|
||||
if self.name_pattern and device.name and self.name_pattern.lower() in device.name.lower():
|
||||
@@ -235,7 +300,7 @@ class LocateSession:
|
||||
self.environment = environment
|
||||
self.fallback_lat = fallback_lat
|
||||
self.fallback_lon = fallback_lon
|
||||
self._lock = threading.Lock()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# Distance estimator
|
||||
n = custom_exponent if environment == Environment.CUSTOM and custom_exponent else environment.value
|
||||
@@ -259,7 +324,9 @@ class LocateSession:
|
||||
# Debug counters
|
||||
self.callback_call_count = 0
|
||||
self.poll_count = 0
|
||||
self._last_seen_device: str | None = None
|
||||
self._last_seen_device: str | None = None
|
||||
self._last_scan_restart_attempt = 0.0
|
||||
self._target_irk = target._get_irk_bytes()
|
||||
|
||||
# Scanner reference
|
||||
self._scanner: BluetoothScanner | None = None
|
||||
@@ -268,27 +335,34 @@ class LocateSession:
|
||||
# Track last RSSI per device to detect changes
|
||||
self._last_cb_rssi: dict[str, int] = {} # Dedup for rapid callbacks only
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Start the locate session.
|
||||
|
||||
Subscribes to scanner callbacks AND runs a polling thread that
|
||||
checks the aggregator directly (handles bleak scan timeout).
|
||||
"""
|
||||
self._scanner = get_bluetooth_scanner()
|
||||
self._scanner.add_device_callback(self._on_device)
|
||||
|
||||
# Ensure BLE scanning is active
|
||||
if not self._scanner.is_scanning:
|
||||
logger.info("BT scanner not running, starting scan for locate session")
|
||||
self._scanner_started_by_us = True
|
||||
if not self._scanner.start_scan(mode='auto'):
|
||||
logger.warning("Failed to start BT scanner for locate session")
|
||||
else:
|
||||
self._scanner_started_by_us = False
|
||||
|
||||
self.active = True
|
||||
self.started_at = datetime.now()
|
||||
self._stop_event.clear()
|
||||
def start(self) -> bool:
|
||||
"""Start the locate session.
|
||||
|
||||
Subscribes to scanner callbacks AND runs a polling thread that
|
||||
checks the aggregator directly (handles bleak scan timeout).
|
||||
"""
|
||||
self._scanner = get_bluetooth_scanner()
|
||||
self._scanner.add_device_callback(self._on_device)
|
||||
self._scanner_started_by_us = False
|
||||
|
||||
# Ensure BLE scanning is active
|
||||
if not self._scanner.is_scanning:
|
||||
logger.info("BT scanner not running, starting scan for locate session")
|
||||
self._scanner_started_by_us = True
|
||||
self._last_scan_restart_attempt = time.monotonic()
|
||||
if not self._scanner.start_scan(mode='auto'):
|
||||
# Surface startup failure to caller and avoid leaving stale callbacks.
|
||||
status = self._scanner.get_status()
|
||||
reason = status.error or "unknown error"
|
||||
logger.warning(f"Failed to start BT scanner for locate session: {reason}")
|
||||
self._scanner.remove_device_callback(self._on_device)
|
||||
self._scanner = None
|
||||
self._scanner_started_by_us = False
|
||||
return False
|
||||
|
||||
self.active = True
|
||||
self.started_at = datetime.now()
|
||||
self._stop_event.clear()
|
||||
|
||||
# Start polling thread as reliable fallback
|
||||
self._poll_thread = threading.Thread(
|
||||
@@ -314,37 +388,40 @@ class LocateSession:
|
||||
|
||||
def _poll_loop(self) -> None:
|
||||
"""Poll scanner aggregator for target device updates."""
|
||||
while not self._stop_event.is_set():
|
||||
self._stop_event.wait(timeout=1.5)
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
try:
|
||||
self._check_aggregator()
|
||||
except Exception as e:
|
||||
logger.error(f"Locate poll error: {e}")
|
||||
while not self._stop_event.is_set():
|
||||
self._stop_event.wait(timeout=POLL_INTERVAL_SECONDS)
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
try:
|
||||
self._check_aggregator()
|
||||
except Exception as e:
|
||||
logger.error(f"Locate poll error: {e}")
|
||||
|
||||
def _check_aggregator(self) -> None:
|
||||
"""Check the scanner's aggregator for the target device."""
|
||||
if not self._scanner:
|
||||
return
|
||||
|
||||
self.poll_count += 1
|
||||
|
||||
# Restart scan if it expired (bleak 10s timeout)
|
||||
if not self._scanner.is_scanning:
|
||||
logger.info("Scanner stopped, restarting for locate session")
|
||||
self._scanner.start_scan(mode='auto')
|
||||
|
||||
# Check devices seen within a recent window. Using a short window
|
||||
self.poll_count += 1
|
||||
|
||||
# Restart scan if it expired (bleak 10s timeout)
|
||||
if not self._scanner.is_scanning:
|
||||
now = time.monotonic()
|
||||
if (now - self._last_scan_restart_attempt) >= SCAN_RESTART_BACKOFF_SECONDS:
|
||||
self._last_scan_restart_attempt = now
|
||||
logger.info("Scanner stopped, restarting for locate session")
|
||||
self._scanner.start_scan(mode='auto')
|
||||
|
||||
# Check devices seen within a recent window. Using a short window
|
||||
# (rather than the aggregator's full 120s) so that once a device
|
||||
# goes silent its stale RSSI stops producing detections. The window
|
||||
# must survive bleak's 10s scan cycle + restart gap (~3s).
|
||||
devices = self._scanner.get_devices(max_age_seconds=15)
|
||||
found_target = False
|
||||
for device in devices:
|
||||
if not self.target.matches(device):
|
||||
continue
|
||||
found_target = True
|
||||
found_target = False
|
||||
for device in devices:
|
||||
if not self.target.matches(device, irk_bytes=self._target_irk):
|
||||
continue
|
||||
found_target = True
|
||||
rssi = device.rssi_current
|
||||
if rssi is None:
|
||||
continue
|
||||
@@ -352,10 +429,14 @@ class LocateSession:
|
||||
break # One match per poll cycle is sufficient
|
||||
|
||||
# Log periodically for debugging
|
||||
if self.poll_count % 20 == 0 or (self.poll_count <= 5) or not found_target:
|
||||
logger.info(
|
||||
f"Poll #{self.poll_count}: {len(devices)} devices, "
|
||||
f"target_found={found_target}, "
|
||||
if (
|
||||
self.poll_count <= 5
|
||||
or self.poll_count % 20 == 0
|
||||
or (not found_target and self.poll_count % NO_MATCH_LOG_EVERY_POLLS == 0)
|
||||
):
|
||||
logger.info(
|
||||
f"Poll #{self.poll_count}: {len(devices)} devices, "
|
||||
f"target_found={found_target}, "
|
||||
f"detections={self.detection_count}, "
|
||||
f"scanning={self._scanner.is_scanning}"
|
||||
)
|
||||
@@ -368,8 +449,8 @@ class LocateSession:
|
||||
self.callback_call_count += 1
|
||||
self._last_seen_device = f"{device.device_id}|{device.name}"
|
||||
|
||||
if not self.target.matches(device):
|
||||
return
|
||||
if not self.target.matches(device, irk_bytes=self._target_irk):
|
||||
return
|
||||
|
||||
rssi = device.rssi_current
|
||||
if rssi is None:
|
||||
@@ -397,13 +478,9 @@ class LocateSession:
|
||||
band = DistanceEstimator.proximity_band(distance)
|
||||
|
||||
# Check RPA resolution
|
||||
rpa_resolved = False
|
||||
if self.target.irk_hex and device.address:
|
||||
try:
|
||||
irk = bytes.fromhex(self.target.irk_hex)
|
||||
rpa_resolved = resolve_rpa(irk, device.address)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
rpa_resolved = False
|
||||
if self._target_irk and device.address and _address_looks_like_rpa(device.address):
|
||||
rpa_resolved = resolve_rpa(self._target_irk, device.address)
|
||||
|
||||
# GPS tag — prefer live GPS, fall back to user-set coordinates
|
||||
gps_pos = get_current_position()
|
||||
@@ -465,15 +542,15 @@ class LocateSession:
|
||||
with self._lock:
|
||||
return [p.to_dict() for p in self.trail if p.lat is not None]
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""Get session status."""
|
||||
gps_pos = get_current_position()
|
||||
def get_status(self, include_debug: bool = False) -> dict:
|
||||
"""Get session status."""
|
||||
gps_pos = get_current_position()
|
||||
|
||||
# Collect scanner/aggregator data OUTSIDE self._lock to avoid ABBA
|
||||
# deadlock: get_status would hold self._lock then wait on
|
||||
# aggregator._lock, while _poll_loop holds aggregator._lock then
|
||||
# waits on self._lock in _record_detection.
|
||||
debug_devices = self._debug_device_sample()
|
||||
debug_devices = self._debug_device_sample() if include_debug else []
|
||||
scanner_running = self._scanner.is_scanning if self._scanner else False
|
||||
scanner_device_count = self._scanner.device_count if self._scanner else 0
|
||||
callback_registered = (
|
||||
@@ -509,8 +586,8 @@ class LocateSession:
|
||||
'latest_rssi_ema': round(self.trail[-1].rssi_ema, 1) if self.trail else None,
|
||||
'latest_distance': round(self.trail[-1].estimated_distance, 2) if self.trail else None,
|
||||
'latest_band': self.trail[-1].proximity_band if self.trail else None,
|
||||
'debug_devices': debug_devices,
|
||||
}
|
||||
'debug_devices': debug_devices,
|
||||
}
|
||||
|
||||
def set_environment(self, environment: Environment, custom_exponent: float | None = None) -> None:
|
||||
"""Update the environment and recalculate distance estimator."""
|
||||
@@ -525,16 +602,16 @@ class LocateSession:
|
||||
return []
|
||||
try:
|
||||
devices = self._scanner.get_devices(max_age_seconds=30)
|
||||
return [
|
||||
{
|
||||
'id': d.device_id,
|
||||
'addr': d.address,
|
||||
'name': d.name,
|
||||
'rssi': d.rssi_current,
|
||||
'match': self.target.matches(d),
|
||||
}
|
||||
for d in devices[:8]
|
||||
]
|
||||
return [
|
||||
{
|
||||
'id': d.device_id,
|
||||
'addr': d.address,
|
||||
'name': d.name,
|
||||
'rssi': d.rssi_current,
|
||||
'match': self.target.matches(d, irk_bytes=self._target_irk),
|
||||
}
|
||||
for d in devices[:8]
|
||||
]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
@@ -550,25 +627,27 @@ _session: LocateSession | None = None
|
||||
_session_lock = threading.Lock()
|
||||
|
||||
|
||||
def start_locate_session(
|
||||
target: LocateTarget,
|
||||
environment: Environment = Environment.OUTDOOR,
|
||||
custom_exponent: float | None = None,
|
||||
fallback_lat: float | None = None,
|
||||
def start_locate_session(
|
||||
target: LocateTarget,
|
||||
environment: Environment = Environment.OUTDOOR,
|
||||
custom_exponent: float | None = None,
|
||||
fallback_lat: float | None = None,
|
||||
fallback_lon: float | None = None,
|
||||
) -> LocateSession:
|
||||
"""Start a new locate session, stopping any existing one."""
|
||||
global _session
|
||||
|
||||
with _session_lock:
|
||||
if _session and _session.active:
|
||||
_session.stop()
|
||||
|
||||
_session = LocateSession(
|
||||
target, environment, custom_exponent, fallback_lat, fallback_lon
|
||||
)
|
||||
_session.start()
|
||||
return _session
|
||||
with _session_lock:
|
||||
if _session and _session.active:
|
||||
_session.stop()
|
||||
|
||||
_session = LocateSession(
|
||||
target, environment, custom_exponent, fallback_lat, fallback_lon
|
||||
)
|
||||
if not _session.start():
|
||||
_session = None
|
||||
raise RuntimeError("Bluetooth scanner failed to start")
|
||||
return _session
|
||||
|
||||
|
||||
def stop_locate_session() -> None:
|
||||
|
||||
+17
-16
@@ -550,12 +550,12 @@ def init_db() -> None:
|
||||
INSERT OR IGNORE INTO tracked_satellites (norad_id, name, tle_line1, tle_line2, enabled, builtin)
|
||||
VALUES ('25544', 'ISS (ZARYA)', NULL, NULL, 1, 1)
|
||||
''')
|
||||
conn.execute('''
|
||||
INSERT OR IGNORE INTO tracked_satellites (norad_id, name, tle_line1, tle_line2, enabled, builtin)
|
||||
VALUES ('40069', 'METEOR-M2', NULL, NULL, 1, 1)
|
||||
''')
|
||||
|
||||
logger.info("Database initialized successfully")
|
||||
conn.execute('''
|
||||
INSERT OR IGNORE INTO tracked_satellites (norad_id, name, tle_line1, tle_line2, enabled, builtin)
|
||||
VALUES ('40069', 'METEOR-M2', NULL, NULL, 1, 1)
|
||||
''')
|
||||
|
||||
logger.info("Database initialized successfully")
|
||||
|
||||
|
||||
def close_db() -> None:
|
||||
@@ -2285,10 +2285,10 @@ def update_tracked_satellite(norad_id: str, enabled: bool) -> bool:
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def remove_tracked_satellite(norad_id: str) -> tuple[bool, str]:
|
||||
"""Delete a tracked satellite by NORAD ID. Refuses to delete builtins."""
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
def remove_tracked_satellite(norad_id: str) -> tuple[bool, str]:
|
||||
"""Delete a tracked satellite by NORAD ID. Refuses to delete builtins."""
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
'SELECT builtin FROM tracked_satellites WHERE norad_id = ?',
|
||||
(str(norad_id),),
|
||||
).fetchone()
|
||||
@@ -2296,9 +2296,10 @@ def remove_tracked_satellite(norad_id: str) -> tuple[bool, str]:
|
||||
return False, 'Satellite not found'
|
||||
if row[0]:
|
||||
return False, 'Cannot remove builtin satellite'
|
||||
conn.execute(
|
||||
'DELETE FROM tracked_satellites WHERE norad_id = ?',
|
||||
(str(norad_id),),
|
||||
)
|
||||
return True, 'Removed'
|
||||
|
||||
conn.execute(
|
||||
'DELETE FROM tracked_satellites WHERE norad_id = ?',
|
||||
(str(norad_id),),
|
||||
)
|
||||
return True, 'Removed'
|
||||
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ def process_event(mode: str, event: dict | Any, event_type: str | None = None) -
|
||||
# Alert failures should never break streaming
|
||||
pass
|
||||
|
||||
|
||||
def _extract_device_id(event: dict) -> str | None:
|
||||
for field in DEVICE_ID_FIELDS:
|
||||
value = event.get(field)
|
||||
|
||||
+4
-20
@@ -343,26 +343,10 @@ SIGNAL_TYPES: list[SignalTypeDefinition] = [
|
||||
regions=["GLOBAL"],
|
||||
),
|
||||
|
||||
# LoRaWAN
|
||||
SignalTypeDefinition(
|
||||
label="LoRaWAN / LoRa Device",
|
||||
tags=["iot", "lora", "lpwan", "telemetry"],
|
||||
description="LoRa long-range IoT device",
|
||||
frequency_ranges=[
|
||||
(863_000_000, 870_000_000), # EU868
|
||||
(902_000_000, 928_000_000), # US915
|
||||
],
|
||||
modulation_hints=["LoRa", "CSS", "FSK"],
|
||||
bandwidth_range=(125_000, 500_000), # LoRa spreading bandwidths
|
||||
base_score=11,
|
||||
is_burst_type=True,
|
||||
regions=["UK/EU", "US"],
|
||||
),
|
||||
|
||||
# Key Fob / Remote
|
||||
SignalTypeDefinition(
|
||||
label="Remote Control / Key Fob",
|
||||
tags=["remote", "keyfob", "automotive", "burst", "ism"],
|
||||
# Key Fob / Remote
|
||||
SignalTypeDefinition(
|
||||
label="Remote Control / Key Fob",
|
||||
tags=["remote", "keyfob", "automotive", "burst", "ism"],
|
||||
description="Wireless remote control or vehicle key fob",
|
||||
frequency_ranges=[
|
||||
(314_900_000, 315_100_000), # 315 MHz (US)
|
||||
|
||||
Reference in New Issue
Block a user