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

@@ -988,6 +988,66 @@ const SignalCards = (function() {
return card;
}
/**
* Build HTML for all meter detail fields from raw message data
*/
function buildMeterDetailsHtml(msg, seenCount) {
let html = '';
const rawMessage = msg.rawMessage || {};
// Display all fields from the raw rtlamr message
for (const [key, value] of Object.entries(rawMessage)) {
if (value === null || value === undefined) continue;
// Format the label (convert camelCase/PascalCase to spaces)
const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()).trim();
// Format the value based on type
let displayValue;
if (Array.isArray(value)) {
// For arrays like DifferentialConsumptionIntervals, show count and values
if (value.length > 10) {
displayValue = `[${value.length} values] ${value.slice(0, 5).join(', ')}...`;
} else {
displayValue = value.join(', ');
}
} else if (typeof value === 'object') {
displayValue = JSON.stringify(value);
} else if (key === 'Consumption') {
displayValue = `${value.toLocaleString()} units`;
} else {
displayValue = String(value);
}
html += `
<div class="signal-advanced-item">
<span class="signal-advanced-label">${escapeHtml(label)}</span>
<span class="signal-advanced-value">${escapeHtml(displayValue)}</span>
</div>
`;
}
// Add message type if not in raw message
if (!rawMessage.Type && msg.type) {
html += `
<div class="signal-advanced-item">
<span class="signal-advanced-label">Message Type</span>
<span class="signal-advanced-value">${escapeHtml(msg.type)}</span>
</div>
`;
}
// Add seen count
html += `
<div class="signal-advanced-item">
<span class="signal-advanced-label">Seen</span>
<span class="signal-advanced-value">${seenCount} time${seenCount > 1 ? 's' : ''}</span>
</div>
`;
return html;
}
/**
* Create a utility meter (rtlamr) card
*/
@@ -1060,30 +1120,7 @@ const SignalCards = (function() {
<div class="signal-advanced-section">
<div class="signal-advanced-title">Meter Details</div>
<div class="signal-advanced-grid">
<div class="signal-advanced-item">
<span class="signal-advanced-label">Meter ID</span>
<span class="signal-advanced-value">${escapeHtml(msg.id || 'N/A')}</span>
</div>
<div class="signal-advanced-item">
<span class="signal-advanced-label">Type</span>
<span class="signal-advanced-value">${escapeHtml(msg.type || 'Unknown')}</span>
</div>
${msg.endpoint_type ? `
<div class="signal-advanced-item">
<span class="signal-advanced-label">Endpoint</span>
<span class="signal-advanced-value">${escapeHtml(msg.endpoint_type)}</span>
</div>
` : ''}
${msg.endpoint_id ? `
<div class="signal-advanced-item">
<span class="signal-advanced-label">Endpoint ID</span>
<span class="signal-advanced-value">${escapeHtml(msg.endpoint_id)}</span>
</div>
` : ''}
<div class="signal-advanced-item">
<span class="signal-advanced-label">Seen</span>
<span class="signal-advanced-value">${seenCount} time${seenCount > 1 ? 's' : ''}</span>
</div>
${buildMeterDetailsHtml(msg, seenCount)}
</div>
</div>
</div>