feat: Add Meshtastic, Ubertooth, and Offline Mode support

New Features:
- Meshtastic LoRa mesh network integration
  - Real-time message streaming via SSE
  - Channel configuration with encryption
  - Node information with RSSI/SNR metrics
- Ubertooth One BLE scanner backend
  - Passive capture across all 40 BLE channels
  - Raw advertising payload access
- Offline mode with bundled assets
  - Local Leaflet, Chart.js, and fonts
  - Multiple map tile providers
  - Settings modal for configuration

Technical Changes:
- New routes: meshtastic.py, offline.py
- New utils: ubertooth_scanner.py, meshtastic.py
- New CSS/JS for meshtastic and settings
- Updated dashboard templates with conditional asset loading
- Added context processor for offline settings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-28 20:14:51 +00:00
parent eae1820fda
commit db304631f8
47 changed files with 5948 additions and 128 deletions

View File

@@ -0,0 +1,102 @@
<!-- MESHTASTIC MODE -->
<div id="meshtasticMode" class="mode-content mesh-sidebar-collapsed">
<!-- Hide Sidebar Button -->
<button class="mesh-hide-sidebar-btn" onclick="Meshtastic.toggleSidebar()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="11 17 6 12 11 7"/>
<polyline points="18 17 13 12 18 7"/>
</svg>
Hide Sidebar
</button>
<!-- Collapse Toggle for Options Panel -->
<div class="mesh-sidebar-toggle" onclick="Meshtastic.toggleOptionsPanel()">
<span class="mesh-sidebar-toggle-icon" id="meshSidebarIcon"></span>
<span class="mesh-sidebar-toggle-text">Meshtastic Options</span>
</div>
<!-- Collapsible Content -->
<div class="mesh-sidebar-content" id="meshSidebarContent">
<!-- Channels Panel - shown when connected -->
<div class="section" id="meshChannelsSection" style="display: none;">
<h3>Channels</h3>
<div id="meshChannelsList">
<!-- Populated by JavaScript -->
</div>
<button class="preset-btn" onclick="Meshtastic.refreshChannels()" style="width: 100%; margin-top: 8px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; margin-right: 6px; vertical-align: middle;">
<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
Refresh Channels
</button>
</div>
<div class="section">
<h3>Help</h3>
<button class="preset-btn" onclick="Meshtastic.showHelp()" style="width: 100%;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px; margin-right: 6px; vertical-align: middle;">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
About Meshtastic
</button>
</div>
<div class="section">
<h3>Resources</h3>
<div style="display: flex; flex-direction: column; gap: 6px;">
<a href="https://meshtastic.org" target="_blank" rel="noopener" class="preset-btn" style="text-decoration: none; text-align: center;">
Meshtastic.org
</a>
<a href="https://meshtastic.org/docs/" target="_blank" rel="noopener" class="preset-btn" style="text-decoration: none; text-align: center;">
Documentation
</a>
</div>
</div>
</div>
</div>
<!-- Channel Configuration Modal -->
<div id="meshChannelModal" class="signal-details-modal">
<div class="signal-details-modal-backdrop" onclick="Meshtastic.closeChannelModal()"></div>
<div class="signal-details-modal-content">
<div class="signal-details-modal-header">
<h3>Configure Channel <span id="meshModalChannelIndex">0</span></h3>
<button class="signal-details-modal-close" onclick="Meshtastic.closeChannelModal()">&times;</button>
</div>
<div class="signal-details-modal-body">
<div class="signal-details-section">
<div class="signal-details-title">Channel Settings</div>
<div class="form-group" style="margin-bottom: 12px;">
<label style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Channel Name (max 12 chars)</label>
<input type="text" id="meshModalChannelName" maxlength="12" placeholder="MyChannel" style="width: 100%;">
</div>
<div class="form-group" style="margin-bottom: 12px;">
<label style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Encryption (PSK)</label>
<select id="meshModalPskFormat" onchange="Meshtastic.onPskFormatChange()" style="width: 100%; margin-bottom: 8px;">
<option value="keep">Keep Current</option>
<option value="none">None (No Encryption)</option>
<option value="default">Default (Public Key - NOT SECURE)</option>
<option value="random">Random (Generate AES-256)</option>
<option value="simple">Passphrase (simple:...)</option>
<option value="base64">Base64 Key</option>
<option value="hex">Hex Key (0x...)</option>
</select>
<div id="meshModalPskInputContainer" style="display: none;">
<input type="text" id="meshModalPskValue" placeholder="Enter key..." style="width: 100%;">
</div>
<div id="meshModalPskWarning" style="display: none; background: rgba(255,193,7,0.1); border: 1px solid var(--accent-yellow); border-radius: 4px; padding: 8px; margin-top: 8px; font-size: 10px;">
<strong style="color: var(--accent-yellow);">Warning:</strong>
<span style="color: var(--text-secondary);">The default key is publicly known and provides no security.</span>
</div>
</div>
</div>
</div>
<div class="signal-details-modal-footer" style="display: flex; gap: 8px;">
<button class="preset-btn" onclick="Meshtastic.closeChannelModal()" style="flex: 1;">Cancel</button>
<button class="run-btn" onclick="Meshtastic.saveChannelConfig()" style="flex: 1;">Save Changes</button>
</div>
</div>
</div>