chore: commit all pending changes

This commit is contained in:
Smittix
2026-02-23 16:51:32 +00:00
parent 94b358f686
commit 7241dbed35
19 changed files with 946 additions and 308 deletions

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% elif offline_settings.tile_provider == 'cartodb_dark_flir' %}map-flir-enabled{% endif %}">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -423,9 +423,16 @@
let alertsEnabled = true;
let detectionSoundEnabled = localStorage.getItem('adsb_detectionSound') !== 'false'; // Default on
let soundedAircraft = {}; // Track aircraft we've played detection sound for
const MAP_CROSSHAIR_DURATION_MS = 620;
const MAP_CROSSHAIR_DURATION_MS = 1500;
const PANEL_SELECTION_BASE_ZOOM = 10;
const PANEL_SELECTION_MAX_ZOOM = 12;
const PANEL_SELECTION_ZOOM_INCREMENT = 1.4;
const PANEL_SELECTION_STAGE1_DURATION_SEC = 1.05;
const PANEL_SELECTION_STAGE2_DURATION_SEC = 1.15;
const PANEL_SELECTION_STAGE_GAP_MS = 180;
let mapCrosshairResetTimer = null;
let mapCrosshairFallbackTimer = null;
let panelSelectionFallbackTimer = null;
let panelSelectionStageTimer = null;
let mapCrosshairRequestId = 0;
// Watchlist - persisted to localStorage
let watchlist = JSON.parse(localStorage.getItem('adsb_watchlist') || '[]');
@@ -2792,18 +2799,32 @@ sudo make install</code>
`;
}
function triggerMapCrosshairAnimation(lat, lon) {
function triggerMapCrosshairAnimation(lat, lon, durationMs = MAP_CROSSHAIR_DURATION_MS, lockToMapCenter = false) {
if (!radarMap) return;
const overlay = document.getElementById('mapCrosshairOverlay');
if (!overlay) return;
const point = radarMap.latLngToContainerPoint([lat, lon]);
const size = radarMap.getSize();
const targetX = Math.max(0, Math.min(size.x, point.x));
const targetY = Math.max(0, Math.min(size.y, point.y));
let targetX;
let targetY;
overlay.style.setProperty('--target-x', `${targetX}px`);
overlay.style.setProperty('--target-y', `${targetY}px`);
if (lockToMapCenter) {
targetX = size.x / 2;
targetY = size.y / 2;
} else {
const point = radarMap.latLngToContainerPoint([lat, lon]);
targetX = Math.max(0, Math.min(size.x, point.x));
targetY = Math.max(0, Math.min(size.y, point.y));
}
const startX = size.x + 8;
const startY = size.y + 8;
overlay.style.setProperty('--crosshair-x-start', `${startX}px`);
overlay.style.setProperty('--crosshair-y-start', `${startY}px`);
overlay.style.setProperty('--crosshair-x-end', `${targetX}px`);
overlay.style.setProperty('--crosshair-y-end', `${targetY}px`);
overlay.style.setProperty('--crosshair-duration', `${durationMs}ms`);
overlay.classList.remove('active');
void overlay.offsetWidth;
overlay.classList.add('active');
@@ -2814,16 +2835,102 @@ sudo make install</code>
mapCrosshairResetTimer = setTimeout(() => {
overlay.classList.remove('active');
mapCrosshairResetTimer = null;
}, MAP_CROSSHAIR_DURATION_MS + 40);
}, durationMs + 100);
}
function getPanelSelectionFinalZoom() {
if (!radarMap) return PANEL_SELECTION_BASE_ZOOM;
const currentZoom = radarMap.getZoom();
const maxZoom = typeof radarMap.getMaxZoom === 'function' ? radarMap.getMaxZoom() : PANEL_SELECTION_MAX_ZOOM;
return Math.min(
PANEL_SELECTION_MAX_ZOOM,
maxZoom,
Math.max(PANEL_SELECTION_BASE_ZOOM, currentZoom + PANEL_SELECTION_ZOOM_INCREMENT)
);
}
function getPanelSelectionIntermediateZoom(finalZoom) {
if (!radarMap) return finalZoom;
const currentZoom = radarMap.getZoom();
if (finalZoom - currentZoom < 0.8) {
return finalZoom;
}
const midpointZoom = currentZoom + ((finalZoom - currentZoom) * 0.55);
return Math.min(finalZoom - 0.45, midpointZoom);
}
function runPanelSelectionAnimation(lat, lon, requestId) {
if (!radarMap) return;
const finalZoom = getPanelSelectionFinalZoom();
const intermediateZoom = getPanelSelectionIntermediateZoom(finalZoom);
const sequenceDurationMs = Math.round(
((PANEL_SELECTION_STAGE1_DURATION_SEC + PANEL_SELECTION_STAGE2_DURATION_SEC) * 1000) +
PANEL_SELECTION_STAGE_GAP_MS + 260
);
const startSecondStage = () => {
if (requestId !== mapCrosshairRequestId) return;
radarMap.flyTo([lat, lon], finalZoom, {
animate: true,
duration: PANEL_SELECTION_STAGE2_DURATION_SEC,
easeLinearity: 0.2
});
};
triggerMapCrosshairAnimation(
lat,
lon,
Math.max(MAP_CROSSHAIR_DURATION_MS, sequenceDurationMs),
true
);
if (intermediateZoom >= finalZoom - 0.1) {
radarMap.flyTo([lat, lon], finalZoom, {
animate: true,
duration: PANEL_SELECTION_STAGE2_DURATION_SEC,
easeLinearity: 0.2
});
return;
}
let stage1Handled = false;
const finishStage1 = () => {
if (stage1Handled || requestId !== mapCrosshairRequestId) return;
stage1Handled = true;
if (panelSelectionFallbackTimer) {
clearTimeout(panelSelectionFallbackTimer);
panelSelectionFallbackTimer = null;
}
panelSelectionStageTimer = setTimeout(() => {
panelSelectionStageTimer = null;
startSecondStage();
}, PANEL_SELECTION_STAGE_GAP_MS);
};
radarMap.once('moveend', finishStage1);
panelSelectionFallbackTimer = setTimeout(
finishStage1,
Math.round(PANEL_SELECTION_STAGE1_DURATION_SEC * 1000) + 160
);
radarMap.flyTo([lat, lon], intermediateZoom, {
animate: true,
duration: PANEL_SELECTION_STAGE1_DURATION_SEC,
easeLinearity: 0.2
});
}
function selectAircraft(icao, source = 'map') {
const prevSelected = selectedIcao;
selectedIcao = icao;
mapCrosshairRequestId += 1;
if (mapCrosshairFallbackTimer) {
clearTimeout(mapCrosshairFallbackTimer);
mapCrosshairFallbackTimer = null;
if (panelSelectionFallbackTimer) {
clearTimeout(panelSelectionFallbackTimer);
panelSelectionFallbackTimer = null;
}
if (panelSelectionStageTimer) {
clearTimeout(panelSelectionStageTimer);
panelSelectionStageTimer = null;
}
// Update marker icons for both previous and new selection
@@ -2853,19 +2960,8 @@ sudo make install</code>
const targetLon = ac.lon;
if (source === 'panel' && radarMap) {
const requestId = mapCrosshairRequestId;
let crosshairTriggered = false;
const runCrosshair = () => {
if (crosshairTriggered || requestId !== mapCrosshairRequestId) return;
crosshairTriggered = true;
if (mapCrosshairFallbackTimer) {
clearTimeout(mapCrosshairFallbackTimer);
mapCrosshairFallbackTimer = null;
}
triggerMapCrosshairAnimation(targetLat, targetLon);
};
radarMap.once('moveend', runCrosshair);
mapCrosshairFallbackTimer = setTimeout(runCrosshair, 450);
runPanelSelectionAnimation(targetLat, targetLon, mapCrosshairRequestId);
return;
}
radarMap.setView([targetLat, targetLon], 10);

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% elif offline_settings.tile_provider == 'cartodb_dark_flir' %}map-flir-enabled{% endif %}">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% elif offline_settings.tile_provider == 'cartodb_dark_flir' %}map-flir-enabled{% endif %}">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% elif offline_settings.tile_provider == 'cartodb_dark_flir' %}map-flir-enabled{% endif %}">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% elif offline_settings.tile_provider == 'cartodb_dark_flir' %}map-flir-enabled{% endif %}">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
@@ -2193,7 +2193,7 @@
<div id="sstvScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1e1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Signal Scope</span>
<span>Audio Waveform</span>
<div style="display: flex; gap: 14px;">
<span>RMS: <span id="sstvScopeRmsLabel" style="color: #c080ff; font-variant-numeric: tabular-nums;">0</span></span>
<span>PEAK: <span id="sstvScopePeakLabel" style="color: #f44; font-variant-numeric: tabular-nums;">0</span></span>
@@ -2461,7 +2461,7 @@
<div id="sstvGeneralScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1e1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Signal Scope</span>
<span>Audio Waveform</span>
<div style="display: flex; gap: 14px;">
<span>RMS: <span id="sstvGeneralScopeRmsLabel" style="color: #c080ff; font-variant-numeric: tabular-nums;">0</span></span>
<span>PEAK: <span id="sstvGeneralScopePeakLabel" style="color: #f44; font-variant-numeric: tabular-nums;">0</span></span>
@@ -2835,7 +2835,7 @@
<div id="pagerScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1a1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Signal Scope</span>
<span>Audio Waveform</span>
<div style="display: flex; gap: 14px;">
<span>RMS: <span id="scopeRmsLabel" style="color: #0ff; font-variant-numeric: tabular-nums;">0</span></span>
<span>PEAK: <span id="scopePeakLabel" style="color: #f44; font-variant-numeric: tabular-nums;">0</span></span>
@@ -2853,7 +2853,7 @@
<div id="sensorScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Signal Scope</span>
<span>Audio Waveform</span>
<div style="display: flex; gap: 14px;">
<span>RSSI: <span id="sensorScopeRssiLabel" style="color: #0f0; font-variant-numeric: tabular-nums;">--</span><span style="color: #444;"> dB</span></span>
<span>SNR: <span id="sensorScopeSnrLabel" style="color: #fa0; font-variant-numeric: tabular-nums;">--</span><span style="color: #444;"> dB</span></span>
@@ -3934,61 +3934,157 @@
let sensorScopeCtx = null;
let sensorScopeAnim = null;
let sensorScopeHistory = [];
let sensorScopeWaveBuffer = [];
let sensorScopeDisplayWave = [];
const SENSOR_SCOPE_LEN = 200;
const SENSOR_SCOPE_WAVE_BUFFER_LEN = 2048;
const SENSOR_SCOPE_WAVE_INPUT_SMOOTH_ALPHA = 0.55;
const SENSOR_SCOPE_WAVE_DISPLAY_SMOOTH_ALPHA = 0.22;
const SENSOR_SCOPE_WAVE_IDLE_DECAY = 0.96;
let sensorScopeRssi = 0;
let sensorScopeSnr = 0;
let sensorScopeTargetRssi = 0;
let sensorScopeTargetSnr = 0;
let sensorScopeMsgBurst = 0;
let sensorScopeLastPulse = 0;
let sensorScopeLastWaveAt = 0;
let sensorScopeLastInputSample = 0;
function resizeSensorScopeCanvas(canvas) {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const width = Math.max(1, Math.floor(rect.width * dpr));
const height = Math.max(1, Math.floor(rect.height * dpr));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
}
function buildSensorWaveformFallback(rssi, snr, noise, points = 160) {
const rssiNorm = Math.min(Math.max(Math.abs(rssi) / 40, 0), 1);
const snrNorm = Math.min(Math.max((snr + 5) / 35, 0), 1);
const noiseNorm = Math.min(Math.max(Math.abs(noise) / 40, 0), 1);
const amplitude = Math.max(0.06, Math.min(1.0, (0.6 * rssiNorm + 0.4 * snrNorm) - (0.22 * noiseNorm)));
const cycles = 3 + (snrNorm * 8);
const harmonic = 0.25 + (0.35 * snrNorm);
const hiss = 0.08 + (0.18 * noiseNorm);
const phase = (performance.now() * 0.002 * (1.4 + (snrNorm * 2.2))) % (Math.PI * 2);
const waveform = [];
for (let i = 0; i < points; i++) {
const t = points > 1 ? (i / (points - 1)) : 0;
const base = Math.sin((Math.PI * 2 * cycles * t) + phase);
const overtone = Math.sin((Math.PI * 2 * (cycles * 2.4) * t) + (phase * 0.7));
const noiseWobble = Math.sin((Math.PI * 2 * (cycles * 7.0) * t) + (phase * 2.1));
let sample = amplitude * (base + (harmonic * overtone) + (hiss * noiseWobble));
sample /= (1 + harmonic + hiss);
waveform.push(Math.round(Math.max(-1, Math.min(1, sample)) * 127));
}
return waveform;
}
function appendSensorWaveformSamples(waveform) {
if (!Array.isArray(waveform) || waveform.length === 0) return;
for (const packedSample of waveform) {
const sample = Number(packedSample);
if (!Number.isFinite(sample)) continue;
const normalized = Math.max(-127, Math.min(127, sample)) / 127;
sensorScopeLastInputSample += (normalized - sensorScopeLastInputSample) * SENSOR_SCOPE_WAVE_INPUT_SMOOTH_ALPHA;
sensorScopeWaveBuffer.push(sensorScopeLastInputSample);
}
if (sensorScopeWaveBuffer.length > SENSOR_SCOPE_WAVE_BUFFER_LEN) {
sensorScopeWaveBuffer.splice(0, sensorScopeWaveBuffer.length - SENSOR_SCOPE_WAVE_BUFFER_LEN);
}
sensorScopeLastWaveAt = performance.now();
}
function applySensorScopeData(scopeData) {
if (!scopeData || typeof scopeData !== 'object') return;
const parsedRssi = Number(scopeData.rssi);
const parsedSnr = Number(scopeData.snr);
const parsedNoise = Number(scopeData.noise);
const rssi = Number.isFinite(parsedRssi) ? parsedRssi : 0;
const snr = Number.isFinite(parsedSnr) ? parsedSnr : 0;
const noise = Number.isFinite(parsedNoise) ? parsedNoise : 0;
sensorScopeTargetRssi = rssi;
sensorScopeTargetSnr = snr;
if (Array.isArray(scopeData.waveform) && scopeData.waveform.length) {
appendSensorWaveformSamples(scopeData.waveform);
} else {
appendSensorWaveformSamples(buildSensorWaveformFallback(rssi, snr, noise));
}
}
function initSensorScope() {
const canvas = document.getElementById('sensorScopeCanvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * (window.devicePixelRatio || 1);
canvas.height = rect.height * (window.devicePixelRatio || 1);
if (sensorScopeAnim) {
cancelAnimationFrame(sensorScopeAnim);
sensorScopeAnim = null;
}
resizeSensorScopeCanvas(canvas);
sensorScopeCtx = canvas.getContext('2d');
sensorScopeHistory = new Array(SENSOR_SCOPE_LEN).fill(0);
sensorScopeWaveBuffer = [];
sensorScopeDisplayWave = [];
sensorScopeRssi = 0;
sensorScopeSnr = 0;
sensorScopeTargetRssi = 0;
sensorScopeTargetSnr = 0;
sensorScopeMsgBurst = 0;
sensorScopeLastPulse = 0;
sensorScopeLastWaveAt = 0;
sensorScopeLastInputSample = 0;
drawSensorScope();
}
function drawSensorScope() {
const ctx = sensorScopeCtx;
if (!ctx) return;
resizeSensorScopeCanvas(ctx.canvas);
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const midY = H / 2;
// Phosphor persistence
ctx.fillStyle = 'rgba(5, 5, 16, 0.3)';
ctx.fillStyle = 'rgba(5, 5, 16, 0.26)';
ctx.fillRect(0, 0, W, H);
// Smooth towards targets (decay when no new packets)
// Smooth towards targets
sensorScopeRssi += (sensorScopeTargetRssi - sensorScopeRssi) * 0.25;
sensorScopeSnr += (sensorScopeTargetSnr - sensorScopeSnr) * 0.15;
// Decay targets back to zero between packets
// Decay targets back to idle between updates
sensorScopeTargetRssi *= 0.97;
sensorScopeTargetSnr *= 0.97;
// RSSI is typically negative dBm (e.g. -0.1 to -30+)
// Normalize: map absolute RSSI to 0-1 range (0 dB = max, -40 dB = min)
// Keep amplitude envelope for context
const rssiNorm = Math.min(Math.max(Math.abs(sensorScopeRssi) / 40, 0), 1.0);
sensorScopeHistory.push(rssiNorm);
if (sensorScopeHistory.length > SENSOR_SCOPE_LEN) {
sensorScopeHistory.shift();
}
// Grid lines
// Grid lines (horizontal + vertical)
ctx.strokeStyle = 'rgba(40, 80, 40, 0.4)';
ctx.lineWidth = 1;
ctx.lineWidth = 0.8;
for (let i = 1; i < 8; i++) {
const gx = (W / 8) * i;
ctx.beginPath();
ctx.moveTo(gx, 0);
ctx.lineTo(gx, H);
ctx.stroke();
}
for (let g = 0.25; g < 1; g += 0.25) {
const gy = midY - g * midY;
const gy2 = midY + g * midY;
@@ -4000,40 +4096,92 @@
// Center baseline
ctx.strokeStyle = 'rgba(60, 100, 60, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, midY);
ctx.lineTo(W, midY);
ctx.stroke();
// Waveform (mirrored, green theme for 433)
const stepX = W / SENSOR_SCOPE_LEN;
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 1.5;
ctx.shadowColor = '#0f0';
ctx.shadowBlur = 4;
// Upper half
// Slow envelope as context around baseline
const envStepX = W / (SENSOR_SCOPE_LEN - 1);
ctx.strokeStyle = 'rgba(86, 230, 120, 0.45)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i < sensorScopeHistory.length; i++) {
const x = i * stepX;
const amp = sensorScopeHistory[i] * midY * 0.9;
const x = i * envStepX;
const amp = sensorScopeHistory[i] * midY * 0.85;
const y = midY - amp;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Lower half (mirror)
ctx.beginPath();
for (let i = 0; i < sensorScopeHistory.length; i++) {
const x = i * stepX;
const amp = sensorScopeHistory[i] * midY * 0.9;
const x = i * envStepX;
const amp = sensorScopeHistory[i] * midY * 0.85;
const y = midY + amp;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Actual waveform trace
const waveformPointCount = Math.min(Math.max(120, Math.floor(W / 3.2)), 420);
if (sensorScopeWaveBuffer.length > 1) {
const waveIsFresh = (performance.now() - sensorScopeLastWaveAt) < 1000;
const sourceLen = sensorScopeWaveBuffer.length;
const sourceWindow = Math.min(sourceLen, 1536);
const sourceStart = sourceLen - sourceWindow;
if (sensorScopeDisplayWave.length !== waveformPointCount) {
sensorScopeDisplayWave = new Array(waveformPointCount).fill(0);
}
for (let i = 0; i < waveformPointCount; i++) {
const a = sourceStart + Math.floor((i / waveformPointCount) * sourceWindow);
const b = sourceStart + Math.floor(((i + 1) / waveformPointCount) * sourceWindow);
const start = Math.max(sourceStart, Math.min(sourceLen - 1, a));
const end = Math.max(start + 1, Math.min(sourceLen, b));
let sum = 0;
let count = 0;
for (let j = start; j < end; j++) {
sum += sensorScopeWaveBuffer[j];
count++;
}
const targetSample = count > 0 ? (sum / count) : 0;
sensorScopeDisplayWave[i] += (targetSample - sensorScopeDisplayWave[i]) * SENSOR_SCOPE_WAVE_DISPLAY_SMOOTH_ALPHA;
}
ctx.strokeStyle = waveIsFresh ? '#40ff7a' : 'rgba(64, 255, 122, 0.45)';
ctx.lineWidth = 1.7;
ctx.shadowColor = '#40ff7a';
ctx.shadowBlur = waveIsFresh ? 6 : 2;
const stepX = waveformPointCount > 1 ? (W / (waveformPointCount - 1)) : W;
ctx.beginPath();
const firstY = midY - (sensorScopeDisplayWave[0] * midY * 0.9);
ctx.moveTo(0, firstY);
for (let i = 1; i < waveformPointCount - 1; i++) {
const x = i * stepX;
const y = midY - (sensorScopeDisplayWave[i] * midY * 0.9);
const nx = (i + 1) * stepX;
const ny = midY - (sensorScopeDisplayWave[i + 1] * midY * 0.9);
const cx = (x + nx) / 2;
const cy = (y + ny) / 2;
ctx.quadraticCurveTo(x, y, cx, cy);
}
const lastX = (waveformPointCount - 1) * stepX;
const lastY = midY - (sensorScopeDisplayWave[waveformPointCount - 1] * midY * 0.9);
ctx.lineTo(lastX, lastY);
ctx.stroke();
if (!waveIsFresh) {
for (let i = 0; i < sensorScopeDisplayWave.length; i++) {
sensorScopeDisplayWave[i] *= SENSOR_SCOPE_WAVE_IDLE_DECAY;
}
}
}
ctx.shadowBlur = 0;
// SNR indicator (amber dashed line)
@@ -4050,7 +4198,7 @@
ctx.setLineDash([]);
}
// Sensor decode flash (green overlay)
// Sensor decode flash
if (sensorScopeMsgBurst > 0.01) {
ctx.fillStyle = `rgba(0, 255, 100, ${sensorScopeMsgBurst * 0.15})`;
ctx.fillRect(0, 0, W, H);
@@ -4064,11 +4212,15 @@
if (rssiLabel) rssiLabel.textContent = sensorScopeRssi < -0.5 ? sensorScopeRssi.toFixed(1) : '--';
if (snrLabel) snrLabel.textContent = sensorScopeSnr > 0.5 ? sensorScopeSnr.toFixed(1) : '--';
if (statusLabel) {
if (Math.abs(sensorScopeRssi) > 1) {
statusLabel.textContent = 'SIGNAL';
statusLabel.style.color = '#0f0';
const waveIsFresh = (performance.now() - sensorScopeLastWaveAt) < 1000;
if (Math.abs(sensorScopeRssi) > 1.2 && waveIsFresh) {
statusLabel.textContent = 'DEMODULATING';
statusLabel.style.color = '#40ff7a';
} else if (Math.abs(sensorScopeRssi) > 0.6) {
statusLabel.textContent = 'CARRIER';
statusLabel.style.color = '#78ff9a';
} else {
statusLabel.textContent = 'MONITORING';
statusLabel.textContent = 'QUIET';
statusLabel.style.color = '#555';
}
}
@@ -4082,6 +4234,11 @@
sensorScopeAnim = null;
}
sensorScopeCtx = null;
sensorScopeWaveBuffer = [];
sensorScopeDisplayWave = [];
sensorScopeHistory = [];
sensorScopeLastWaveAt = 0;
sensorScopeLastInputSample = 0;
}
// Start sensor decoding
@@ -4256,6 +4413,11 @@
const placeholder = output.querySelector('.placeholder');
if (placeholder) placeholder.style.display = 'none';
// Agent polling may only return decoded packets, so derive scope updates from packet levels.
if (msg && (msg.rssi !== undefined || msg.snr !== undefined || msg.noise !== undefined)) {
applySensorScopeData(msg);
}
// Create signal card if SignalCards is available
if (typeof SignalCards !== 'undefined' && SignalCards.createFromSensor) {
const card = SignalCards.createFromSensor(msg);
@@ -4303,8 +4465,7 @@
if (data.type === 'sensor') {
addSensorReading(data);
} else if (data.type === 'scope') {
sensorScopeTargetRssi = data.rssi;
sensorScopeTargetSnr = data.snr;
applySensorScopeData(data);
} else if (data.type === 'status') {
if (data.text === 'stopped') {
setSensorRunning(false);
@@ -4332,6 +4493,12 @@
// Flash sensor scope green on decode
sensorScopeMsgBurst = 1.0;
// Fallback when no dedicated scope packet has arrived recently.
if ((data.rssi !== undefined || data.snr !== undefined || data.noise !== undefined)
&& ((performance.now() - sensorScopeLastWaveAt) > 250)) {
applySensorScopeData(data);
}
sensorCount++;
document.getElementById('sensorCount').textContent = sensorCount;
@@ -5208,54 +5375,111 @@
let pagerScopeCtx = null;
let pagerScopeAnim = null;
let pagerScopeHistory = [];
let pagerScopeWaveBuffer = [];
let pagerScopeDisplayWave = [];
const SCOPE_HISTORY_LEN = 200;
const SCOPE_WAVE_BUFFER_LEN = 2048;
const SCOPE_WAVE_INPUT_SMOOTH_ALPHA = 0.55;
const SCOPE_WAVE_DISPLAY_SMOOTH_ALPHA = 0.22;
const SCOPE_WAVE_IDLE_DECAY = 0.96;
let pagerScopeRms = 0;
let pagerScopePeak = 0;
let pagerScopeTargetRms = 0;
let pagerScopeTargetPeak = 0;
let pagerScopeMsgBurst = 0;
let pagerScopeLastWaveAt = 0;
let pagerScopeLastInputSample = 0;
function resizePagerScopeCanvas(canvas) {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const width = Math.max(1, Math.floor(rect.width * dpr));
const height = Math.max(1, Math.floor(rect.height * dpr));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
}
function applyPagerScopeData(scopeData) {
if (!scopeData || typeof scopeData !== 'object') return;
pagerScopeTargetRms = Number(scopeData.rms) || 0;
pagerScopeTargetPeak = Number(scopeData.peak) || 0;
if (Array.isArray(scopeData.waveform) && scopeData.waveform.length) {
for (const packedSample of scopeData.waveform) {
const sample = Number(packedSample);
if (!Number.isFinite(sample)) continue;
const normalized = Math.max(-127, Math.min(127, sample)) / 127;
pagerScopeLastInputSample += (normalized - pagerScopeLastInputSample) * SCOPE_WAVE_INPUT_SMOOTH_ALPHA;
pagerScopeWaveBuffer.push(pagerScopeLastInputSample);
}
if (pagerScopeWaveBuffer.length > SCOPE_WAVE_BUFFER_LEN) {
pagerScopeWaveBuffer.splice(0, pagerScopeWaveBuffer.length - SCOPE_WAVE_BUFFER_LEN);
}
pagerScopeLastWaveAt = performance.now();
}
}
function initPagerScope() {
const canvas = document.getElementById('pagerScopeCanvas');
if (!canvas) return;
// Set actual pixel resolution
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * (window.devicePixelRatio || 1);
canvas.height = rect.height * (window.devicePixelRatio || 1);
if (pagerScopeAnim) {
cancelAnimationFrame(pagerScopeAnim);
pagerScopeAnim = null;
}
resizePagerScopeCanvas(canvas);
pagerScopeCtx = canvas.getContext('2d');
pagerScopeHistory = new Array(SCOPE_HISTORY_LEN).fill(0);
pagerScopeWaveBuffer = [];
pagerScopeDisplayWave = [];
pagerScopeRms = 0;
pagerScopePeak = 0;
pagerScopeTargetRms = 0;
pagerScopeTargetPeak = 0;
pagerScopeMsgBurst = 0;
pagerScopeLastWaveAt = 0;
pagerScopeLastInputSample = 0;
drawPagerScope();
}
function drawPagerScope() {
const ctx = pagerScopeCtx;
if (!ctx) return;
resizePagerScopeCanvas(ctx.canvas);
const W = ctx.canvas.width;
const H = ctx.canvas.height;
const midY = H / 2;
// Phosphor persistence: semi-transparent clear
ctx.fillStyle = 'rgba(5, 5, 16, 0.3)';
// Phosphor persistence
ctx.fillStyle = 'rgba(5, 5, 16, 0.26)';
ctx.fillRect(0, 0, W, H);
// Smooth towards target values
pagerScopeRms += (pagerScopeTargetRms - pagerScopeRms) * 0.25;
pagerScopePeak += (pagerScopeTargetPeak - pagerScopePeak) * 0.15;
// Push current RMS into history (normalized 0-1 against 32768)
// Keep a slow amplitude envelope for readability
pagerScopeHistory.push(Math.min(pagerScopeRms / 32768, 1.0));
if (pagerScopeHistory.length > SCOPE_HISTORY_LEN) {
pagerScopeHistory.shift();
}
// Grid lines
// Grid lines (horizontal + vertical)
ctx.strokeStyle = 'rgba(40, 40, 80, 0.4)';
ctx.lineWidth = 1;
ctx.lineWidth = 0.8;
for (let i = 1; i < 8; i++) {
const gx = (W / 8) * i;
ctx.beginPath();
ctx.moveTo(gx, 0);
ctx.lineTo(gx, H);
ctx.stroke();
}
for (let g = 0.25; g < 1; g += 0.25) {
const gy = midY - g * midY;
const gy2 = midY + g * midY;
@@ -5267,40 +5491,92 @@
// Center baseline
ctx.strokeStyle = 'rgba(60, 60, 100, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, midY);
ctx.lineTo(W, midY);
ctx.stroke();
// Waveform (mirrored)
const stepX = W / SCOPE_HISTORY_LEN;
ctx.strokeStyle = '#0ff';
ctx.lineWidth = 1.5;
ctx.shadowColor = '#0ff';
ctx.shadowBlur = 4;
// Upper half
// Slow envelope as context around baseline
const envStepX = W / (SCOPE_HISTORY_LEN - 1);
ctx.strokeStyle = 'rgba(90, 180, 255, 0.45)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i < pagerScopeHistory.length; i++) {
const x = i * stepX;
const amp = pagerScopeHistory[i] * midY * 0.9;
const x = i * envStepX;
const amp = pagerScopeHistory[i] * midY * 0.85;
const y = midY - amp;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Lower half (mirror)
ctx.beginPath();
for (let i = 0; i < pagerScopeHistory.length; i++) {
const x = i * stepX;
const amp = pagerScopeHistory[i] * midY * 0.9;
const x = i * envStepX;
const amp = pagerScopeHistory[i] * midY * 0.85;
const y = midY + amp;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// Actual waveform from real incoming audio samples
const waveformPointCount = Math.min(Math.max(120, Math.floor(W / 3.2)), 420);
if (pagerScopeWaveBuffer.length > 1) {
const waveIsFresh = (performance.now() - pagerScopeLastWaveAt) < 700;
const sourceLen = pagerScopeWaveBuffer.length;
const sourceWindow = Math.min(sourceLen, 1536);
const sourceStart = sourceLen - sourceWindow;
if (pagerScopeDisplayWave.length !== waveformPointCount) {
pagerScopeDisplayWave = new Array(waveformPointCount).fill(0);
}
for (let i = 0; i < waveformPointCount; i++) {
const a = sourceStart + Math.floor((i / waveformPointCount) * sourceWindow);
const b = sourceStart + Math.floor(((i + 1) / waveformPointCount) * sourceWindow);
const start = Math.max(sourceStart, Math.min(sourceLen - 1, a));
const end = Math.max(start + 1, Math.min(sourceLen, b));
let sum = 0;
let count = 0;
for (let j = start; j < end; j++) {
sum += pagerScopeWaveBuffer[j];
count++;
}
const targetSample = count > 0 ? (sum / count) : 0;
pagerScopeDisplayWave[i] += (targetSample - pagerScopeDisplayWave[i]) * SCOPE_WAVE_DISPLAY_SMOOTH_ALPHA;
}
ctx.strokeStyle = waveIsFresh ? '#2efbff' : 'rgba(46, 251, 255, 0.45)';
ctx.lineWidth = 1.7;
ctx.shadowColor = '#2efbff';
ctx.shadowBlur = waveIsFresh ? 6 : 2;
const stepX = waveformPointCount > 1 ? (W / (waveformPointCount - 1)) : W;
ctx.beginPath();
const firstY = midY - (pagerScopeDisplayWave[0] * midY * 0.9);
ctx.moveTo(0, firstY);
for (let i = 1; i < waveformPointCount - 1; i++) {
const x = i * stepX;
const y = midY - (pagerScopeDisplayWave[i] * midY * 0.9);
const nx = (i + 1) * stepX;
const ny = midY - (pagerScopeDisplayWave[i + 1] * midY * 0.9);
const cx = (x + nx) / 2;
const cy = (y + ny) / 2;
ctx.quadraticCurveTo(x, y, cx, cy);
}
const lastX = (waveformPointCount - 1) * stepX;
const lastY = midY - (pagerScopeDisplayWave[waveformPointCount - 1] * midY * 0.9);
ctx.lineTo(lastX, lastY);
ctx.stroke();
if (!waveIsFresh) {
for (let i = 0; i < pagerScopeDisplayWave.length; i++) {
pagerScopeDisplayWave[i] *= SCOPE_WAVE_IDLE_DECAY;
}
}
}
ctx.shadowBlur = 0;
// Peak indicator (dashed red line)
@@ -5331,11 +5607,15 @@
if (rmsLabel) rmsLabel.textContent = Math.round(pagerScopeRms);
if (peakLabel) peakLabel.textContent = Math.round(pagerScopePeak);
if (statusLabel) {
if (pagerScopeRms > 500) {
statusLabel.textContent = 'SIGNAL';
statusLabel.style.color = '#0f0';
const waveIsFresh = (performance.now() - pagerScopeLastWaveAt) < 700;
if (pagerScopeRms > 1300 && waveIsFresh) {
statusLabel.textContent = 'DEMODULATING';
statusLabel.style.color = '#00ff88';
} else if (pagerScopeRms > 500) {
statusLabel.textContent = 'CARRIER';
statusLabel.style.color = '#2efbff';
} else {
statusLabel.textContent = 'MONITORING';
statusLabel.textContent = 'QUIET';
statusLabel.style.color = '#555';
}
}
@@ -5349,6 +5629,11 @@
pagerScopeAnim = null;
}
pagerScopeCtx = null;
pagerScopeWaveBuffer = [];
pagerScopeDisplayWave = [];
pagerScopeHistory = [];
pagerScopeLastWaveAt = 0;
pagerScopeLastInputSample = 0;
}
function startDecoding() {
@@ -5578,8 +5863,7 @@
} else if (payload.type === 'info') {
showInfo(`[${data.agent_name}] ${payload.text}`);
} else if (payload.type === 'scope') {
pagerScopeTargetRms = payload.rms;
pagerScopeTargetPeak = payload.peak;
applyPagerScopeData(payload);
}
} else if (data.type === 'keepalive') {
// Ignore keepalive messages
@@ -5599,8 +5883,7 @@
} else if (data.type === 'raw') {
showInfo(data.text);
} else if (data.type === 'scope') {
pagerScopeTargetRms = data.rms;
pagerScopeTargetPeak = data.peak;
applyPagerScopeData(data);
}
}
};

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% elif offline_settings.tile_provider == 'cartodb_dark_flir' %}map-flir-enabled{% endif %}">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@@ -73,7 +73,6 @@
</div>
<select id="tileProvider" class="settings-select" onchange="Settings.setTileProvider(this.value)">
<option value="cartodb_dark_cyan">Intercept Default</option>
<option value="cartodb_dark_flir">FLIR Thermal</option>
<option value="cartodb_dark">CartoDB Dark</option>
<option value="openstreetmap">OpenStreetMap</option>
<option value="cartodb_light">CartoDB Positron</option>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% elif offline_settings.tile_provider == 'cartodb_dark_flir' %}map-flir-enabled{% endif %}">
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">