mirror of
https://github.com/smittix/intercept.git
synced 2026-07-23 16:38:11 -07:00
Fix BT Locate startup/map rendering and CelesTrak import reliability
This commit is contained in:
+16
-3
@@ -109,9 +109,22 @@ def start_session():
|
|||||||
f"env={environment.name}, fallback=({fallback_lat}, {fallback_lon})"
|
f"env={environment.name}, fallback=({fallback_lat}, {fallback_lon})"
|
||||||
)
|
)
|
||||||
|
|
||||||
session = start_locate_session(
|
try:
|
||||||
target, environment, custom_exponent, fallback_lat, fallback_lon
|
session = start_locate_session(
|
||||||
)
|
target, environment, custom_exponent, fallback_lat, fallback_lon
|
||||||
|
)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
logger.warning(f"Unable to start BT Locate session: {exc}")
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error': 'Bluetooth scanner could not be started. Check adapter permissions/capabilities.',
|
||||||
|
}), 503
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception(f"Unexpected error starting BT Locate session: {exc}")
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'error': 'Failed to start locate session',
|
||||||
|
}), 500
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'started',
|
'status': 'started',
|
||||||
|
|||||||
+34
-7
@@ -588,14 +588,14 @@ def list_tracked_satellites():
|
|||||||
def add_tracked_satellites_endpoint():
|
def add_tracked_satellites_endpoint():
|
||||||
"""Add one or more tracked satellites."""
|
"""Add one or more tracked satellites."""
|
||||||
global _tle_cache
|
global _tle_cache
|
||||||
data = request.json
|
data = request.get_json(silent=True)
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({'status': 'error', 'message': 'No data provided'}), 400
|
return jsonify({'status': 'error', 'message': 'No data provided'}), 400
|
||||||
|
|
||||||
# Accept a single satellite dict or a list
|
# Accept a single satellite dict or a list
|
||||||
sat_list = data if isinstance(data, list) else [data]
|
sat_list = data if isinstance(data, list) else [data]
|
||||||
|
|
||||||
added = 0
|
normalized: list[dict] = []
|
||||||
for sat in sat_list:
|
for sat in sat_list:
|
||||||
norad_id = str(sat.get('norad_id', sat.get('norad', '')))
|
norad_id = str(sat.get('norad_id', sat.get('norad', '')))
|
||||||
name = sat.get('name', '')
|
name = sat.get('name', '')
|
||||||
@@ -605,19 +605,46 @@ def add_tracked_satellites_endpoint():
|
|||||||
tle2 = sat.get('tle_line2', sat.get('tle2'))
|
tle2 = sat.get('tle_line2', sat.get('tle2'))
|
||||||
enabled = sat.get('enabled', True)
|
enabled = sat.get('enabled', True)
|
||||||
|
|
||||||
if add_tracked_satellite(norad_id, name, tle1, tle2, enabled):
|
normalized.append({
|
||||||
added += 1
|
'norad_id': norad_id,
|
||||||
|
'name': name,
|
||||||
|
'tle_line1': tle1,
|
||||||
|
'tle_line2': tle2,
|
||||||
|
'enabled': bool(enabled),
|
||||||
|
'builtin': False,
|
||||||
|
})
|
||||||
|
|
||||||
# Also inject into TLE cache if we have TLE data
|
# Also inject into TLE cache if we have TLE data
|
||||||
if tle1 and tle2:
|
if tle1 and tle2:
|
||||||
cache_key = name.replace(' ', '-').upper()
|
cache_key = name.replace(' ', '-').upper()
|
||||||
_tle_cache[cache_key] = (name, tle1, tle2)
|
_tle_cache[cache_key] = (name, tle1, tle2)
|
||||||
|
|
||||||
return jsonify({
|
# Single inserts preserve previous behavior; list inserts use DB-level bulk path.
|
||||||
|
if len(normalized) == 1:
|
||||||
|
sat = normalized[0]
|
||||||
|
added = 1 if add_tracked_satellite(
|
||||||
|
sat['norad_id'],
|
||||||
|
sat['name'],
|
||||||
|
sat.get('tle_line1'),
|
||||||
|
sat.get('tle_line2'),
|
||||||
|
sat.get('enabled', True),
|
||||||
|
sat.get('builtin', False),
|
||||||
|
) else 0
|
||||||
|
else:
|
||||||
|
added = bulk_add_tracked_satellites(normalized)
|
||||||
|
|
||||||
|
response_payload = {
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'added': added,
|
'added': added,
|
||||||
'satellites': get_tracked_satellites(),
|
'processed': len(normalized),
|
||||||
})
|
}
|
||||||
|
|
||||||
|
# Returning all tracked satellites for very large imports can stall the UI.
|
||||||
|
include_satellites = request.args.get('include_satellites', '').lower() == 'true'
|
||||||
|
if include_satellites or len(normalized) <= 32:
|
||||||
|
response_payload['satellites'] = get_tracked_satellites()
|
||||||
|
|
||||||
|
return jsonify(response_payload)
|
||||||
|
|
||||||
|
|
||||||
@satellite_bp.route('/tracked/<norad_id>', methods=['PUT'])
|
@satellite_bp.route('/tracked/<norad_id>', methods=['PUT'])
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const BtLocate = (function() {
|
|||||||
let lastRenderedDetectionKey = null;
|
let lastRenderedDetectionKey = null;
|
||||||
let pendingHeatSync = false;
|
let pendingHeatSync = false;
|
||||||
let mapStabilizeTimer = null;
|
let mapStabilizeTimer = null;
|
||||||
|
let modeActive = false;
|
||||||
|
|
||||||
const MAX_HEAT_POINTS = 1200;
|
const MAX_HEAT_POINTS = 1200;
|
||||||
const MAX_TRAIL_POINTS = 1200;
|
const MAX_TRAIL_POINTS = 1200;
|
||||||
@@ -85,6 +86,7 @@ const BtLocate = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
modeActive = true;
|
||||||
loadOverlayPreferences();
|
loadOverlayPreferences();
|
||||||
syncOverlayControls();
|
syncOverlayControls();
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ const BtLocate = (function() {
|
|||||||
// Re-invalidate map on re-entry and ensure tiles are present
|
// Re-invalidate map on re-entry and ensure tiles are present
|
||||||
if (map) {
|
if (map) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
safeInvalidateMap();
|
safeInvalidateMap(true);
|
||||||
// Re-apply user's tile layer if tiles were lost
|
// Re-apply user's tile layer if tiles were lost
|
||||||
let hasTiles = false;
|
let hasTiles = false;
|
||||||
map.eachLayer(layer => {
|
map.eachLayer(layer => {
|
||||||
@@ -142,7 +144,7 @@ const BtLocate = (function() {
|
|||||||
flushPendingHeatSync();
|
flushPendingHeatSync();
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
safeInvalidateMap();
|
safeInvalidateMap(true);
|
||||||
flushPendingHeatSync();
|
flushPendingHeatSync();
|
||||||
}, 100);
|
}, 100);
|
||||||
scheduleMapStabilization();
|
scheduleMapStabilization();
|
||||||
@@ -173,8 +175,18 @@ const BtLocate = (function() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMacInput(value) {
|
||||||
|
const raw = (value || '').trim().toUpperCase().replace(/-/g, ':');
|
||||||
|
if (!raw) return '';
|
||||||
|
const compact = raw.replace(/[^0-9A-F]/g, '');
|
||||||
|
if (compact.length === 12) {
|
||||||
|
return compact.match(/.{1,2}/g).join(':');
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
const mac = document.getElementById('btLocateMac')?.value.trim();
|
const mac = normalizeMacInput(document.getElementById('btLocateMac')?.value);
|
||||||
const namePattern = document.getElementById('btLocateNamePattern')?.value.trim();
|
const namePattern = document.getElementById('btLocateNamePattern')?.value.trim();
|
||||||
const irk = document.getElementById('btLocateIrk')?.value.trim();
|
const irk = document.getElementById('btLocateIrk')?.value.trim();
|
||||||
|
|
||||||
@@ -210,7 +222,19 @@ const BtLocate = (function() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(async (r) => {
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = await r.json();
|
||||||
|
} catch (_) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
if (!r.ok || data.status !== 'started') {
|
||||||
|
const message = data.error || data.message || ('HTTP ' + r.status);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'started') {
|
if (data.status === 'started') {
|
||||||
sessionStartedAt = data.session?.started_at ? new Date(data.session.started_at).getTime() : Date.now();
|
sessionStartedAt = data.session?.started_at ? new Date(data.session.started_at).getTime() : Date.now();
|
||||||
@@ -224,7 +248,11 @@ const BtLocate = (function() {
|
|||||||
restoreTrail();
|
restoreTrail();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => console.error('[BtLocate] Start error:', err));
|
.catch(err => {
|
||||||
|
console.error('[BtLocate] Start error:', err);
|
||||||
|
alert('BT Locate failed to start: ' + (err?.message || 'Unknown error'));
|
||||||
|
showIdleUI();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
@@ -888,7 +916,10 @@ const BtLocate = (function() {
|
|||||||
if (!map) return;
|
if (!map) return;
|
||||||
ensureHeatLayer();
|
ensureHeatLayer();
|
||||||
if (!heatLayer) return;
|
if (!heatLayer) return;
|
||||||
if (!isMapContainerVisible()) {
|
if (!modeActive || !isMapContainerVisible()) {
|
||||||
|
if (map.hasLayer(heatLayer)) {
|
||||||
|
map.removeLayer(heatLayer);
|
||||||
|
}
|
||||||
pendingHeatSync = true;
|
pendingHeatSync = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -918,6 +949,40 @@ const BtLocate = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setActiveMode(active) {
|
||||||
|
modeActive = !!active;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
if (!modeActive) {
|
||||||
|
stopMapStabilization();
|
||||||
|
if (heatLayer && map.hasLayer(heatLayer)) {
|
||||||
|
map.removeLayer(heatLayer);
|
||||||
|
}
|
||||||
|
pendingHeatSync = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!modeActive) return;
|
||||||
|
safeInvalidateMap(true);
|
||||||
|
if (typeof window.requestAnimationFrame === 'function') {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (!modeActive) return;
|
||||||
|
safeInvalidateMap(true);
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (!modeActive) return;
|
||||||
|
safeInvalidateMap(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
syncHeatLayer();
|
||||||
|
syncMovementLayer();
|
||||||
|
syncStrongestMarker();
|
||||||
|
updateConfidenceLayer();
|
||||||
|
scheduleMapStabilization(14);
|
||||||
|
}, 80);
|
||||||
|
}
|
||||||
|
|
||||||
function isMapRenderable() {
|
function isMapRenderable() {
|
||||||
if (!map || !isMapContainerVisible()) return false;
|
if (!map || !isMapContainerVisible()) return false;
|
||||||
if (typeof map.getSize === 'function') {
|
if (typeof map.getSize === 'function') {
|
||||||
@@ -927,9 +992,26 @@ const BtLocate = (function() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeInvalidateMap() {
|
function refreshBaseTiles() {
|
||||||
|
if (!map || typeof L === 'undefined' || typeof map.eachLayer !== 'function') return;
|
||||||
|
map.eachLayer((layer) => {
|
||||||
|
if (layer instanceof L.TileLayer && typeof layer.redraw === 'function') {
|
||||||
|
try {
|
||||||
|
layer.redraw();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeInvalidateMap(forceRecenter = false) {
|
||||||
if (!map || !isMapContainerVisible()) return false;
|
if (!map || !isMapContainerVisible()) return false;
|
||||||
map.invalidateSize({ pan: false, animate: false });
|
map.invalidateSize({ pan: !!forceRecenter, animate: false });
|
||||||
|
if (forceRecenter) {
|
||||||
|
const center = map.getCenter();
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
map.setView(center, zoom, { animate: false });
|
||||||
|
}
|
||||||
|
refreshBaseTiles();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -950,7 +1032,7 @@ const BtLocate = (function() {
|
|||||||
stopMapStabilization();
|
stopMapStabilization();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (safeInvalidateMap()) {
|
if (safeInvalidateMap(true)) {
|
||||||
flushPendingHeatSync();
|
flushPendingHeatSync();
|
||||||
syncMovementLayer();
|
syncMovementLayer();
|
||||||
syncStrongestMarker();
|
syncStrongestMarker();
|
||||||
@@ -1624,7 +1706,7 @@ const BtLocate = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function invalidateMap() {
|
function invalidateMap() {
|
||||||
if (safeInvalidateMap()) {
|
if (safeInvalidateMap(true)) {
|
||||||
flushPendingHeatSync();
|
flushPendingHeatSync();
|
||||||
syncMovementLayer();
|
syncMovementLayer();
|
||||||
syncStrongestMarker();
|
syncStrongestMarker();
|
||||||
@@ -1635,6 +1717,7 @@ const BtLocate = (function() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
init,
|
init,
|
||||||
|
setActiveMode,
|
||||||
start,
|
start,
|
||||||
stop,
|
stop,
|
||||||
handoff,
|
handoff,
|
||||||
@@ -1652,3 +1735,5 @@ const BtLocate = (function() {
|
|||||||
fetchPairedIrks,
|
fetchPairedIrks,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
window.BtLocate = BtLocate;
|
||||||
|
|||||||
+31
-17
@@ -4004,6 +4004,11 @@
|
|||||||
if (btLocateVisuals) btLocateVisuals.style.display = mode === 'bt_locate' ? 'flex' : 'none';
|
if (btLocateVisuals) btLocateVisuals.style.display = mode === 'bt_locate' ? 'flex' : 'none';
|
||||||
if (spaceWeatherVisuals) spaceWeatherVisuals.style.display = mode === 'spaceweather' ? 'flex' : 'none';
|
if (spaceWeatherVisuals) spaceWeatherVisuals.style.display = mode === 'spaceweather' ? 'flex' : 'none';
|
||||||
|
|
||||||
|
// Prevent Leaflet heatmap redraws on hidden BT Locate map containers.
|
||||||
|
if (typeof BtLocate !== 'undefined' && BtLocate.setActiveMode) {
|
||||||
|
BtLocate.setActiveMode(mode === 'bt_locate');
|
||||||
|
}
|
||||||
|
|
||||||
// Hide sidebar by default for Meshtastic mode, show for others
|
// Hide sidebar by default for Meshtastic mode, show for others
|
||||||
const mainContent = document.querySelector('.main-content');
|
const mainContent = document.querySelector('.main-content');
|
||||||
if (mainContent) {
|
if (mainContent) {
|
||||||
@@ -10403,7 +10408,7 @@
|
|||||||
|
|
||||||
fetch('/satellite/celestrak/' + category)
|
fetch('/satellite/celestrak/' + category)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(async data => {
|
||||||
if (data.status === 'success' && data.satellites) {
|
if (data.status === 'success' && data.satellites) {
|
||||||
const toAdd = data.satellites
|
const toAdd = data.satellites
|
||||||
.filter(sat => !trackedSatellites.find(s => s.norad === String(sat.norad)))
|
.filter(sat => !trackedSatellites.find(s => s.norad === String(sat.norad)))
|
||||||
@@ -10420,27 +10425,36 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('/satellite/tracked', {
|
const batchSize = 250;
|
||||||
method: 'POST',
|
let addedTotal = 0;
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(toAdd)
|
for (let i = 0; i < toAdd.length; i += batchSize) {
|
||||||
})
|
const batch = toAdd.slice(i, i + batchSize);
|
||||||
.then(r => r.json())
|
const completed = Math.min(i + batch.length, toAdd.length);
|
||||||
.then(result => {
|
status.innerHTML = `<span style="color: var(--accent-cyan);">Importing ${completed}/${toAdd.length} from ${category}...</span>`;
|
||||||
if (result.status === 'success') {
|
|
||||||
_loadSatellitesFromAPI();
|
const resp = await fetch('/satellite/tracked', {
|
||||||
status.innerHTML = `<span style="color: var(--accent-green);">Added ${result.added} satellites (${data.satellites.length} total in category)</span>`;
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(batch)
|
||||||
|
});
|
||||||
|
const result = await resp.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (!resp.ok || result.status !== 'success') {
|
||||||
|
throw new Error(result.message || result.error || `HTTP ${resp.status}`);
|
||||||
}
|
}
|
||||||
})
|
addedTotal += Number(result.added || 0);
|
||||||
.catch(() => {
|
}
|
||||||
status.innerHTML = `<span style="color: var(--accent-red);">Failed to save satellites</span>`;
|
|
||||||
});
|
_loadSatellitesFromAPI();
|
||||||
|
status.innerHTML = `<span style="color: var(--accent-green);">Added ${addedTotal} satellites (${data.satellites.length} total in category)</span>`;
|
||||||
} else {
|
} else {
|
||||||
status.innerHTML = `<span style="color: var(--accent-red);">Error: ${data.message || 'Failed to fetch'}</span>`;
|
status.innerHTML = `<span style="color: var(--accent-red);">Error: ${data.message || 'Failed to fetch'}</span>`;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
status.innerHTML = `<span style="color: var(--accent-red);">Network error</span>`;
|
const msg = err && err.message ? err.message : 'Network error';
|
||||||
|
status.innerHTML = `<span style="color: var(--accent-red);">Import failed: ${msg}</span>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,14 @@ class TestLocateTarget:
|
|||||||
device.name = None
|
device.name = None
|
||||||
assert target.matches(device) is True
|
assert target.matches(device) is True
|
||||||
|
|
||||||
|
def test_match_by_mac_without_separators(self):
|
||||||
|
target = LocateTarget(mac_address='aabbccddeeff')
|
||||||
|
device = MagicMock()
|
||||||
|
device.device_id = 'other'
|
||||||
|
device.address = 'AA:BB:CC:DD:EE:FF'
|
||||||
|
device.name = None
|
||||||
|
assert target.matches(device) is True
|
||||||
|
|
||||||
def test_match_by_name_pattern(self):
|
def test_match_by_name_pattern(self):
|
||||||
target = LocateTarget(name_pattern='iPhone')
|
target = LocateTarget(name_pattern='iPhone')
|
||||||
device = MagicMock()
|
device = MagicMock()
|
||||||
@@ -276,3 +284,16 @@ class TestModuleLevelSessionManagement:
|
|||||||
assert session2.active is True
|
assert session2.active is True
|
||||||
|
|
||||||
stop_locate_session()
|
stop_locate_session()
|
||||||
|
|
||||||
|
@patch('utils.bt_locate.get_bluetooth_scanner')
|
||||||
|
def test_start_raises_when_scanner_cannot_start(self, mock_get_scanner):
|
||||||
|
mock_scanner = MagicMock()
|
||||||
|
mock_scanner.is_scanning = False
|
||||||
|
mock_scanner.start_scan.return_value = False
|
||||||
|
status = MagicMock()
|
||||||
|
status.error = 'No adapter'
|
||||||
|
mock_scanner.get_status.return_value = status
|
||||||
|
mock_get_scanner.return_value = mock_scanner
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
start_locate_session(LocateTarget(mac_address='AA:BB:CC:DD:EE:FF'))
|
||||||
|
|||||||
+37
-7
@@ -27,6 +27,28 @@ MAX_TRAIL_POINTS = 500
|
|||||||
EMA_ALPHA = 0.3
|
EMA_ALPHA = 0.3
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
class Environment(Enum):
|
class Environment(Enum):
|
||||||
"""RF propagation environment presets."""
|
"""RF propagation environment presets."""
|
||||||
FREE_SPACE = 2.0
|
FREE_SPACE = 2.0
|
||||||
@@ -114,9 +136,9 @@ class LocateTarget:
|
|||||||
|
|
||||||
# Match by MAC/address (case-insensitive, normalize separators)
|
# Match by MAC/address (case-insensitive, normalize separators)
|
||||||
if self.mac_address:
|
if self.mac_address:
|
||||||
dev_addr = (device.address or '').upper().replace('-', ':')
|
dev_addr = _normalize_mac(device.address)
|
||||||
target_addr = self.mac_address.upper().replace('-', ':')
|
target_addr = _normalize_mac(self.mac_address)
|
||||||
if dev_addr == target_addr:
|
if dev_addr and target_addr and dev_addr == target_addr:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Match by payload fingerprint (guard against low-stability generic fingerprints)
|
# Match by payload fingerprint (guard against low-stability generic fingerprints)
|
||||||
@@ -276,15 +298,21 @@ class LocateSession:
|
|||||||
"""
|
"""
|
||||||
self._scanner = get_bluetooth_scanner()
|
self._scanner = get_bluetooth_scanner()
|
||||||
self._scanner.add_device_callback(self._on_device)
|
self._scanner.add_device_callback(self._on_device)
|
||||||
|
self._scanner_started_by_us = False
|
||||||
|
|
||||||
# Ensure BLE scanning is active
|
# Ensure BLE scanning is active
|
||||||
if not self._scanner.is_scanning:
|
if not self._scanner.is_scanning:
|
||||||
logger.info("BT scanner not running, starting scan for locate session")
|
logger.info("BT scanner not running, starting scan for locate session")
|
||||||
self._scanner_started_by_us = True
|
self._scanner_started_by_us = True
|
||||||
if not self._scanner.start_scan(mode='auto'):
|
if not self._scanner.start_scan(mode='auto'):
|
||||||
logger.warning("Failed to start BT scanner for locate session")
|
# Surface startup failure to caller and avoid leaving stale callbacks.
|
||||||
else:
|
status = self._scanner.get_status()
|
||||||
self._scanner_started_by_us = False
|
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.active = True
|
||||||
self.started_at = datetime.now()
|
self.started_at = datetime.now()
|
||||||
@@ -567,7 +595,9 @@ def start_locate_session(
|
|||||||
_session = LocateSession(
|
_session = LocateSession(
|
||||||
target, environment, custom_exponent, fallback_lat, fallback_lon
|
target, environment, custom_exponent, fallback_lat, fallback_lon
|
||||||
)
|
)
|
||||||
_session.start()
|
if not _session.start():
|
||||||
|
_session = None
|
||||||
|
raise RuntimeError("Bluetooth scanner failed to start")
|
||||||
return _session
|
return _session
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user