diff --git a/static/js/modes/bluetooth.js b/static/js/modes/bluetooth.js index a15ccc7..9486d26 100644 --- a/static/js/modes/bluetooth.js +++ b/static/js/modes/bluetooth.js @@ -12,12 +12,27 @@ const BluetoothMode = (function() { let devices = new Map(); let baselineSet = false; let baselineCount = 0; + let selectedDeviceId = null; // DOM elements (cached) let startBtn, stopBtn, messageContainer, deviceContainer; let adapterSelect, scanModeSelect, transportSelect, durationInput, minRssiInput; let baselineStatusEl, capabilityStatusEl; + // Stats tracking + let deviceStats = { + phones: 0, + computers: 0, + audio: 0, + wearables: 0, + other: 0, + strong: 0, + medium: 0, + weak: 0, + trackers: [], + findmy: [] + }; + /** * Initialize the Bluetooth mode */ @@ -37,18 +52,197 @@ const BluetoothMode = (function() { baselineStatusEl = document.getElementById('btBaselineStatus'); capabilityStatusEl = document.getElementById('btCapabilityStatus'); - console.log('[BT] DOM elements:', { - startBtn: !!startBtn, - stopBtn: !!stopBtn, - deviceContainer: !!deviceContainer, - adapterSelect: !!adapterSelect - }); + // Create modal if it doesn't exist + createModal(); // Check capabilities on load checkCapabilities(); // Check scan status (in case page was reloaded during scan) checkScanStatus(); + + // Initialize radar canvas + initRadar(); + } + + /** + * Create the device details modal + */ + function createModal() { + if (document.getElementById('btDeviceModal')) return; + + const modal = document.createElement('div'); + modal.id = 'btDeviceModal'; + modal.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.8);z-index:10000;align-items:center;justify-content:center;'; + modal.innerHTML = ` +
+
+

Device Details

+ +
+
+
+ `; + modal.onclick = (e) => { + if (e.target === modal) closeModal(); + }; + document.body.appendChild(modal); + } + + /** + * Show device details modal + */ + function showModal(deviceId) { + const device = devices.get(deviceId); + if (!device) return; + + selectedDeviceId = deviceId; + const modal = document.getElementById('btDeviceModal'); + const title = document.getElementById('btModalTitle'); + const body = document.getElementById('btModalBody'); + + title.textContent = device.name || formatDeviceId(device.address); + + const rssi = device.rssi_current; + const rssiColor = getRssiColor(rssi); + const flags = device.heuristic_flags || []; + + body.innerHTML = ` + +
+ ${(device.protocol || 'ble').toUpperCase()} + ${flags.map(f => `${f.replace('_', ' ').toUpperCase()}`).join('')} + ${device.in_baseline ? '✓ In Baseline' : '● New Device'} +
+ + +
+
+
+
Signal Strength
+
${rssi !== null ? rssi : '--'}dBm
+
+
+
Range
+
${device.range_band || 'Unknown'}
+
+
+
+
+
Min
+
${device.rssi_min !== null ? device.rssi_min : '--'}
+
+
+
Max
+
${device.rssi_max !== null ? device.rssi_max : '--'}
+
+
+
Median
+
${device.rssi_median !== null ? device.rssi_median : '--'}
+
+
+
Confidence
+
${device.rssi_confidence ? Math.round(device.rssi_confidence * 100) + '%' : '--'}
+
+
+
+ + +
+
+
Address
+
${device.address}
+
+
+
Address Type
+
${device.address_type || 'Unknown'}
+
+
+
Manufacturer
+
${device.manufacturer_name || 'Unknown'}
+
+
+
Manufacturer ID
+
${device.manufacturer_id !== null ? '0x' + device.manufacturer_id.toString(16).toUpperCase().padStart(4, '0') : '--'}
+
+
+ + +
+
Observation Stats
+
+
+
First Seen
+
${device.first_seen ? new Date(device.first_seen).toLocaleTimeString() : '--'}
+
+
+
Last Seen
+
${device.last_seen ? new Date(device.last_seen).toLocaleTimeString() : '--'}
+
+
+
Seen Count
+
${device.seen_count || 0} times
+
+
+
+ + + ${device.service_uuids && device.service_uuids.length > 0 ? ` +
+
Service UUIDs
+
+ ${device.service_uuids.map(uuid => `${uuid}`).join('')} +
+
+ ` : ''} + + +
+ + +
+ `; + + modal.style.display = 'flex'; + } + + /** + * Close device details modal + */ + function closeModal() { + const modal = document.getElementById('btDeviceModal'); + if (modal) modal.style.display = 'none'; + selectedDeviceId = null; + } + + /** + * Copy address to clipboard + */ + function copyAddress(address) { + navigator.clipboard.writeText(address).then(() => { + // Brief visual feedback + const btn = event.target; + const originalText = btn.textContent; + btn.textContent = 'Copied!'; + btn.style.background = '#22c55e'; + setTimeout(() => { + btn.textContent = originalText; + btn.style.background = '#252538'; + }, 1500); + }); + } + + /** + * Format device ID for display (when no name available) + */ + function formatDeviceId(address) { + if (!address) return 'Unknown Device'; + // Return shortened format: first 2 and last 2 octets + const parts = address.split(':'); + if (parts.length === 6) { + return parts[0] + ':' + parts[1] + ':...:' + parts[4] + ':' + parts[5]; + } + return address; } /** @@ -97,23 +291,14 @@ const BluetoothMode = (function() { * Show capability warning */ function showCapabilityWarning(issues) { - if (!capabilityStatusEl || !messageContainer) return; + if (!capabilityStatusEl) return; capabilityStatusEl.style.display = 'block'; - - if (typeof MessageCard !== 'undefined') { - const card = MessageCard.createCapabilityWarning(issues); - if (card) { - capabilityStatusEl.innerHTML = ''; - capabilityStatusEl.appendChild(card); - } - } else { - capabilityStatusEl.innerHTML = ` -
- ${issues.map(i => `
${i}
`).join('')} -
- `; - } + capabilityStatusEl.innerHTML = ` +
+ ${issues.map(i => `
⚠ ${i}
`).join('')} +
+ `; } /** @@ -179,7 +364,6 @@ const BluetoothMode = (function() { if (data.status === 'started' || data.status === 'already_scanning') { setScanning(true); startEventStream(); - showScanningMessage(mode); } else { showErrorMessage(data.message || 'Failed to start scan'); } @@ -198,7 +382,6 @@ const BluetoothMode = (function() { await fetch('/api/bluetooth/scan/stop', { method: 'POST' }); setScanning(false); stopEventStream(); - removeScanningMessage(); } catch (err) { console.error('Failed to stop scan:', err); } @@ -213,10 +396,11 @@ const BluetoothMode = (function() { if (startBtn) startBtn.style.display = scanning ? 'none' : 'block'; if (stopBtn) stopBtn.style.display = scanning ? 'block' : 'none'; - // Clear container when starting scan (removes legacy cards and placeholder) + // Clear container when starting scan if (scanning && deviceContainer) { deviceContainer.innerHTML = ''; - devices.clear(); // Also clear our device map + devices.clear(); + resetStats(); } // Update global status if available @@ -226,6 +410,25 @@ const BluetoothMode = (function() { if (statusText) statusText.textContent = scanning ? 'Scanning...' : 'Idle'; } + /** + * Reset stats + */ + function resetStats() { + deviceStats = { + phones: 0, + computers: 0, + audio: 0, + wearables: 0, + other: 0, + strong: 0, + medium: 0, + weak: 0, + trackers: [], + findmy: [] + }; + updateVisualizationPanels(); + } + /** * Start SSE event stream */ @@ -233,10 +436,8 @@ const BluetoothMode = (function() { if (eventSource) eventSource.close(); eventSource = new EventSource('/api/bluetooth/stream'); - console.log('[BT] SSE stream connected'); eventSource.addEventListener('device_update', (e) => { - console.log('[BT] SSE device_update event:', e.data); try { const device = JSON.parse(e.data); handleDeviceUpdate(device); @@ -245,31 +446,14 @@ const BluetoothMode = (function() { } }); - // Also listen for generic messages as fallback - eventSource.onmessage = (e) => { - console.log('[BT] SSE generic message:', e.data); - }; - eventSource.addEventListener('scan_started', (e) => { const data = JSON.parse(e.data); setScanning(true); - showScanningMessage(data.mode); }); eventSource.addEventListener('scan_stopped', (e) => { setScanning(false); - removeScanningMessage(); const data = JSON.parse(e.data); - showScanCompleteMessage(data.device_count, data.duration); - }); - - eventSource.addEventListener('error', (e) => { - try { - const data = JSON.parse(e.data); - showErrorMessage(data.message); - } catch { - // Connection error - } }); eventSource.onerror = () => { @@ -291,10 +475,237 @@ const BluetoothMode = (function() { * Handle device update from SSE */ function handleDeviceUpdate(device) { - console.log('[BT] Device update received:', device); devices.set(device.device_id, device); renderDevice(device); updateDeviceCount(); + updateStatsFromDevice(device); + updateVisualizationPanels(); + updateRadar(); + } + + /** + * Update stats from device + */ + function updateStatsFromDevice(device) { + // Categorize by manufacturer/type + const mfr = (device.manufacturer_name || '').toLowerCase(); + const name = (device.name || '').toLowerCase(); + + // Reset counts and recalculate from all devices + deviceStats.phones = 0; + deviceStats.computers = 0; + deviceStats.audio = 0; + deviceStats.wearables = 0; + deviceStats.other = 0; + deviceStats.strong = 0; + deviceStats.medium = 0; + deviceStats.weak = 0; + deviceStats.trackers = []; + deviceStats.findmy = []; + + devices.forEach(d => { + const m = (d.manufacturer_name || '').toLowerCase(); + const n = (d.name || '').toLowerCase(); + const rssi = d.rssi_current; + + // Device type classification + if (n.includes('iphone') || n.includes('phone') || n.includes('pixel') || n.includes('galaxy') || n.includes('android')) { + deviceStats.phones++; + } else if (n.includes('macbook') || n.includes('laptop') || n.includes('pc') || n.includes('computer') || n.includes('imac')) { + deviceStats.computers++; + } else if (n.includes('airpod') || n.includes('headphone') || n.includes('speaker') || n.includes('buds') || n.includes('audio') || n.includes('beats')) { + deviceStats.audio++; + } else if (n.includes('watch') || n.includes('band') || n.includes('fitbit') || n.includes('garmin')) { + deviceStats.wearables++; + } else { + deviceStats.other++; + } + + // Signal strength classification + if (rssi !== null && rssi !== undefined) { + if (rssi >= -50) deviceStats.strong++; + else if (rssi >= -70) deviceStats.medium++; + else deviceStats.weak++; + } + + // Tracker detection (Apple, Tile, etc.) + if (m.includes('apple') && (d.heuristic_flags || []).includes('beacon_like')) { + if (!deviceStats.findmy.find(t => t.address === d.address)) { + deviceStats.findmy.push(d); + } + } + if (n.includes('tile') || n.includes('airtag') || n.includes('smarttag')) { + if (!deviceStats.trackers.find(t => t.address === d.address)) { + deviceStats.trackers.push(d); + } + } + }); + } + + /** + * Update visualization panels + */ + function updateVisualizationPanels() { + // Device Types + const phoneCount = document.getElementById('btPhoneCount'); + const computerCount = document.getElementById('btComputerCount'); + const audioCount = document.getElementById('btAudioCount'); + const wearableCount = document.getElementById('btWearableCount'); + const otherCount = document.getElementById('btOtherCount'); + + if (phoneCount) phoneCount.textContent = deviceStats.phones; + if (computerCount) computerCount.textContent = deviceStats.computers; + if (audioCount) audioCount.textContent = deviceStats.audio; + if (wearableCount) wearableCount.textContent = deviceStats.wearables; + if (otherCount) otherCount.textContent = deviceStats.other; + + // Signal Distribution + const total = devices.size || 1; + const strongBar = document.getElementById('btSignalStrong'); + const mediumBar = document.getElementById('btSignalMedium'); + const weakBar = document.getElementById('btSignalWeak'); + const strongCount = document.getElementById('btSignalStrongCount'); + const mediumCount = document.getElementById('btSignalMediumCount'); + const weakCount = document.getElementById('btSignalWeakCount'); + + if (strongBar) strongBar.style.width = (deviceStats.strong / total * 100) + '%'; + if (mediumBar) mediumBar.style.width = (deviceStats.medium / total * 100) + '%'; + if (weakBar) weakBar.style.width = (deviceStats.weak / total * 100) + '%'; + if (strongCount) strongCount.textContent = deviceStats.strong; + if (mediumCount) mediumCount.textContent = deviceStats.medium; + if (weakCount) weakCount.textContent = deviceStats.weak; + + // Tracker Detection + const trackerList = document.getElementById('btTrackerList'); + if (trackerList) { + if (deviceStats.trackers.length === 0) { + trackerList.innerHTML = '
No trackers detected
'; + } else { + trackerList.innerHTML = deviceStats.trackers.map(t => ` +
+
+ ${t.name || formatDeviceId(t.address)} + ${t.rssi_current || '--'} dBm +
+
${t.address}
+
+ `).join(''); + } + } + + // FindMy Detection + const findmyList = document.getElementById('btFindMyList'); + if (findmyList) { + if (deviceStats.findmy.length === 0) { + findmyList.innerHTML = '
No FindMy devices detected
'; + } else { + findmyList.innerHTML = deviceStats.findmy.map(t => ` +
+
+ ${t.name || 'Apple Device'} + ${t.rssi_current || '--'} dBm +
+
${t.address}
+
+ `).join(''); + } + } + } + + /** + * Initialize radar canvas + */ + function initRadar() { + const canvas = document.getElementById('btRadarCanvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + drawRadarBackground(ctx, canvas.width, canvas.height); + } + + /** + * Draw radar background + */ + function drawRadarBackground(ctx, width, height) { + const centerX = width / 2; + const centerY = height / 2; + const maxRadius = Math.min(width, height) / 2 - 10; + + ctx.clearRect(0, 0, width, height); + + // Draw concentric circles + ctx.strokeStyle = 'rgba(0, 212, 255, 0.2)'; + ctx.lineWidth = 1; + for (let i = 1; i <= 4; i++) { + ctx.beginPath(); + ctx.arc(centerX, centerY, maxRadius * i / 4, 0, Math.PI * 2); + ctx.stroke(); + } + + // Draw cross lines + ctx.beginPath(); + ctx.moveTo(centerX, 10); + ctx.lineTo(centerX, height - 10); + ctx.moveTo(10, centerY); + ctx.lineTo(width - 10, centerY); + ctx.stroke(); + + // Center dot + ctx.fillStyle = '#00d4ff'; + ctx.beginPath(); + ctx.arc(centerX, centerY, 3, 0, Math.PI * 2); + ctx.fill(); + } + + /** + * Update radar with device positions + */ + function updateRadar() { + const canvas = document.getElementById('btRadarCanvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const width = canvas.width; + const height = canvas.height; + const centerX = width / 2; + const centerY = height / 2; + const maxRadius = Math.min(width, height) / 2 - 15; + + // Redraw background + drawRadarBackground(ctx, width, height); + + // Plot devices + let angle = 0; + const angleStep = (Math.PI * 2) / Math.max(devices.size, 1); + + devices.forEach(device => { + const rssi = device.rssi_current; + if (rssi === null || rssi === undefined) return; + + // Convert RSSI to distance (closer = smaller radius) + // RSSI typically ranges from -30 (very close) to -100 (far) + const normalizedRssi = Math.max(0, Math.min(1, (rssi + 100) / 70)); + const radius = maxRadius * (1 - normalizedRssi); + + const x = centerX + Math.cos(angle) * radius; + const y = centerY + Math.sin(angle) * radius; + + // Color based on signal strength + const color = getRssiColor(rssi); + + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(x, y, 4, 0, Math.PI * 2); + ctx.fill(); + + // Glow effect + ctx.fillStyle = color.replace(')', ', 0.3)').replace('rgb', 'rgba'); + ctx.beginPath(); + ctx.arc(x, y, 8, 0, Math.PI * 2); + ctx.fill(); + + angle += angleStep; + }); } /** @@ -311,35 +722,24 @@ const BluetoothMode = (function() { * Render a device card */ function renderDevice(device) { - console.log('[BT] Rendering device:', device.device_id, device); if (!deviceContainer) { deviceContainer = document.getElementById('btDeviceListContent'); - if (!deviceContainer) { - console.error('[BT] No container - cannot render'); - return; - } + if (!deviceContainer) return; } - // Use simple inline rendering with NO CSS classes to avoid any interference const escapedId = CSS.escape(device.device_id); const existingCard = deviceContainer.querySelector('[data-bt-device-id="' + escapedId + '"]'); const cardHtml = createSimpleDeviceCard(device); - console.log('[BT] Card HTML length:', cardHtml.length, 'existing:', !!existingCard); - if (existingCard) { existingCard.outerHTML = cardHtml; } else { deviceContainer.insertAdjacentHTML('afterbegin', cardHtml); } - - // Log container state - console.log('[BT] Container now has', deviceContainer.children.length, 'children'); } /** - * Simple device card - pure inline rendering with NO CSS classes - * This avoids any CSS conflicts by using only inline styles + * Simple device card with click handler */ function createSimpleDeviceCard(device) { const protocol = device.protocol || 'ble'; @@ -356,7 +756,9 @@ const BluetoothMode = (function() { badgesHtml += 'PERSISTENT'; } - const name = escapeHtml(device.name || device.device_id || 'Unknown'); + // Use device name if available, otherwise format the address nicely + const displayName = device.name || formatDeviceId(device.address); + const name = escapeHtml(displayName); const addr = escapeHtml(device.address || 'Unknown'); const addrType = escapeHtml(device.address_type || 'unknown'); const rssi = device.rssi_current; @@ -367,19 +769,20 @@ const BluetoothMode = (function() { const rangeBand = device.range_band || 'unknown'; const inBaseline = device.in_baseline || false; - // Use a div with NO classes at all - pure inline styles to avoid any CSS interference - const cardStyle = 'display:block;background:#1a1a2e;border:1px solid #444;border-radius:8px;padding:14px;margin-bottom:10px;box-sizing:border-box;overflow:visible;'; + const cardStyle = 'display:block;background:#1a1a2e;border:1px solid #444;border-radius:8px;padding:14px;margin-bottom:10px;cursor:pointer;transition:border-color 0.2s;'; const headerStyle = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;'; - const nameStyle = 'font-size:15px;font-weight:600;color:#e0e0e0;margin-bottom:4px;'; - const addrStyle = 'font-family:monospace;font-size:12px;color:#00d4ff;'; - const rssiRowStyle = 'display:flex;justify-content:space-between;align-items:center;background:#141428;padding:12px;border-radius:6px;margin:10px 0;'; - const rssiValueStyle = 'font-family:monospace;font-size:18px;font-weight:700;color:' + rssiColor + ';'; - const rangeBandStyle = 'font-size:11px;color:#888;text-transform:uppercase;letter-spacing:0.5px;'; - const mfrStyle = 'font-size:11px;color:#888;margin-bottom:8px;'; + const nameStyle = 'font-size:14px;font-weight:600;color:#e0e0e0;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; + const addrStyle = 'font-family:monospace;font-size:11px;color:#00d4ff;'; + const rssiRowStyle = 'display:flex;justify-content:space-between;align-items:center;background:#141428;padding:10px;border-radius:6px;margin:10px 0;'; + const rssiValueStyle = 'font-family:monospace;font-size:16px;font-weight:700;color:' + rssiColor + ';'; + const rangeBandStyle = 'font-size:10px;color:#888;text-transform:uppercase;letter-spacing:0.5px;'; + const mfrStyle = 'font-size:11px;color:#888;margin-bottom:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; const metaStyle = 'display:flex;justify-content:space-between;font-size:10px;color:#666;'; const statusPillStyle = 'background:' + (inBaseline ? 'rgba(34,197,94,0.15)' : 'rgba(59,130,246,0.15)') + ';color:' + (inBaseline ? '#22c55e' : '#3b82f6') + ';padding:3px 10px;border-radius:12px;font-size:10px;font-weight:500;'; - return '
' + + const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'"); + + return '
' + '
' + '
' + protoBadge + badgesHtml + '
' + '' + (inBaseline ? '✓ Known' : '● New') + '' + @@ -392,9 +795,9 @@ const BluetoothMode = (function() { '' + rssiStr + '' + '' + rangeBand + '' + '
' + - (mfr ? '
Manufacturer: ' + mfr + '
' : '') + + (mfr ? '
' + mfr + '
' : '') + '
' + - 'Seen: ' + seenCount + ' times' + + 'Seen ' + seenCount + '×' + 'Just now' + '
' + '
'; @@ -422,30 +825,6 @@ const BluetoothMode = (function() { return div.innerHTML; } - /** - * Show device details - */ - async function showDeviceDetails(deviceId) { - try { - const response = await fetch(`/api/bluetooth/devices/${encodeURIComponent(deviceId)}`); - const device = await response.json(); - - // Toggle advanced panel or show modal - const card = deviceContainer?.querySelector(`[data-device-id="${deviceId}"]`); - if (card) { - const panel = card.querySelector('.signal-advanced-panel'); - if (panel) { - panel.classList.toggle('show'); - if (panel.classList.contains('show')) { - panel.innerHTML = `
${JSON.stringify(device, null, 2)}
`; - } - } - } - } catch (err) { - console.error('Failed to get device details:', err); - } - } - /** * Set baseline */ @@ -458,13 +837,9 @@ const BluetoothMode = (function() { baselineSet = true; baselineCount = data.device_count; updateBaselineStatus(); - showBaselineSetMessage(data.device_count); - } else { - showErrorMessage(data.message || 'Failed to set baseline'); } } catch (err) { console.error('Failed to set baseline:', err); - showErrorMessage('Failed to set baseline'); } } @@ -493,10 +868,10 @@ const BluetoothMode = (function() { if (!baselineStatusEl) return; if (baselineSet) { - baselineStatusEl.textContent = `Baseline set: ${baselineCount} device${baselineCount !== 1 ? 's' : ''}`; + baselineStatusEl.textContent = `Baseline: ${baselineCount} devices`; baselineStatusEl.style.color = '#22c55e'; } else { - baselineStatusEl.textContent = 'No baseline set'; + baselineStatusEl.textContent = 'No baseline'; baselineStatusEl.style.color = ''; } } @@ -508,55 +883,12 @@ const BluetoothMode = (function() { window.open(`/api/bluetooth/export?format=${format}`, '_blank'); } - /** - * Show scanning message - */ - function showScanningMessage(mode) { - if (!messageContainer || typeof MessageCard === 'undefined') return; - - removeScanningMessage(); - const card = MessageCard.createScanningCard({ - backend: mode, - deviceCount: devices.size - }); - messageContainer.appendChild(card); - } - - /** - * Remove scanning message - */ - function removeScanningMessage() { - MessageCard?.removeMessage?.('btScanningStatus'); - } - - /** - * Show scan complete message - */ - function showScanCompleteMessage(deviceCount, duration) { - if (!messageContainer || typeof MessageCard === 'undefined') return; - - const card = MessageCard.createScanCompleteCard(deviceCount, duration || 0); - messageContainer.appendChild(card); - } - - /** - * Show baseline set message - */ - function showBaselineSetMessage(count) { - if (!messageContainer || typeof MessageCard === 'undefined') return; - - const card = MessageCard.createBaselineCard(count, true); - messageContainer.appendChild(card); - } - /** * Show error message */ function showErrorMessage(message) { - if (!messageContainer || typeof MessageCard === 'undefined') return; - - const card = MessageCard.createErrorCard(message, () => startScan()); - messageContainer.appendChild(card); + console.error('[BT] Error:', message); + // Could show a toast notification here } // Public API @@ -568,6 +900,9 @@ const BluetoothMode = (function() { setBaseline, clearBaseline, exportData, + showModal, + closeModal, + copyAddress, getDevices: () => Array.from(devices.values()), isScanning: () => isScanning }; @@ -584,7 +919,6 @@ function btExport(format) { BluetoothMode.exportData(format); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { - // Only init if we're on a page with Bluetooth mode if (document.getElementById('bluetoothMode')) { BluetoothMode.init(); }