diff --git a/cmd/server/web/dashboard.html b/cmd/server/web/dashboard.html index 1cc9c5e..659600c 100644 --- a/cmd/server/web/dashboard.html +++ b/cmd/server/web/dashboard.html @@ -53,16 +53,17 @@ +
+

ГРАФИК КАНАЛА 1

+ +
+

▶ ПРИНЯТЫЕ ДАННЫЕ

-
-

ГРАФИК СИГНАЛА

- -
diff --git a/cmd/server/web/js/app.js b/cmd/server/web/js/app.js index 5849b15..f3fa7ec 100644 --- a/cmd/server/web/js/app.js +++ b/cmd/server/web/js/app.js @@ -1,6 +1,6 @@ // app.js (оркестратор) import { fetchHealth, fetchHistory } from "./data.js"; -import { state, smooth, addSignal } from "./state.js"; +import { state, smooth, addSignals, resetBufferTracking } from "./state.js"; import { initDOM, setStatus, @@ -15,9 +15,9 @@ import { drawChart } from "./chart.js"; let lastChart = 0; let connectionLost = false; -let consecutiveErrors = 0; // счётчик ошибок подряд -const MAX_ERRORS = 3; // после скольки ошибок замедляем опрос -let pollTimer = null; // храним ID таймера для возможности сброса +let consecutiveErrors = 0; +const MAX_ERRORS = 3; +let pollTimer = null; // ================= INIT ================= window.addEventListener("DOMContentLoaded", () => { @@ -47,20 +47,27 @@ async function update() { connectionLost = false; } else { setStatus("Нет связи", true); + + if (!connectionLost) { + resetBufferTracking(); + } connectionLost = true; } const arr = Array.isArray(hist.bytes) ? hist.bytes : []; const raw = arr[arr.length - 1] ?? 0; + // ===== ДОБАВЛЯЕМ ТОЛЬКО НОВЫЕ БАЙТЫ ===== + if (arr.length > 0 && isOnline) { + const bufferSize = h.stats?.filled ?? 0; + addSignals(arr, bufferSize); + } + // ===== DECODE ===== const value = raw & 0x3F; const level = (raw >> 6) & 0x3; const percent = (value / 63) * 100; - // ===== STATE ===== - addSignal(value); - // ===== UI ===== if (isOnline) { setBars(smooth(percent)); @@ -77,12 +84,10 @@ async function update() { // ===== META ===== const uptime = Math.round(h.uptime_sec || 0); const filled = h.stats?.filled ?? 0; - const bps = Math.round(h.stats?.bits_per_sec ?? 0); const Bps = Math.round(h.stats?.bytes_per_sec ?? 0); - const idle = Math.round((h.last_data_ms || 0) / 1000); setMeta( - `работа: ${uptime}s | буфер: ${filled} | ${bps} bit/s (${Bps} B/s) | idle: ${idle}s` + `Работа: ${uptime}s | Буфер: ${filled} | Приём: ${Bps} B/s` ); // ===== HISTORY ===== @@ -112,6 +117,7 @@ async function update() { setStatus("СЕРВЕР НЕДОСТУПЕН", true); setBars(0, true); setSignal(0, true); + resetBufferTracking(); if (DOM.lastByte) { DOM.lastByte.textContent = "--"; @@ -129,7 +135,6 @@ async function update() { function startPolling() { if (pollTimer) clearInterval(pollTimer); - // Если были ошибки — сразу пробуем переподключиться if (consecutiveErrors > 0) { console.log("[app] Попытка переподключения..."); update(); @@ -138,7 +143,6 @@ function startPolling() { pollTimer = setInterval(update, 500); } -// Сброс при фокусе окна (пользователь вернулся — пробуем быстро) window.addEventListener("focus", () => { if (consecutiveErrors >= MAX_ERRORS) { console.log("[app] Окно в фокусе, ускоряем опрос"); diff --git a/cmd/server/web/js/chart.js b/cmd/server/web/js/chart.js index c803a7b..895bcde 100644 --- a/cmd/server/web/js/chart.js +++ b/cmd/server/web/js/chart.js @@ -3,7 +3,7 @@ export function drawChart(canvas, history) { if (!canvas || !history?.length) return; const rect = canvas.getBoundingClientRect(); - const displayHeight = rect.height || 200; // увеличил для подписей + const displayHeight = rect.height || 200; const displayWidth = rect.width || 800; canvas.width = displayWidth; @@ -13,22 +13,22 @@ export function drawChart(canvas, history) { const w = canvas.width; const h = canvas.height; - // ===== ОТСТУПЫ ДЛЯ ПОДПИСЕЙ ===== - const padLeft = 50; // место под значения Y слева - const padRight = 20; // отступ справа - const padTop = 15; // отступ сверху - const padBottom = 30; // отступ снизу для подписей X + // ===== ОТСТУПЫ ===== + const padLeft = 45; + const padRight = 15; + const padTop = 10; + const padBottom = 25; const plotW = w - padLeft - padRight; const plotH = h - padTop - padBottom; - // ===== ФОН ГРАФИКА ===== + // ===== ФОН ===== ctx.fillStyle = "#0a0f0a"; ctx.fillRect(0, 0, w, h); // ===== СЕТКА ===== const maxVal = 63; - const gridLines = 4; // 4 линии + 0 = 5 уровней: 0, 16, 32, 48, 64 + const gridLines = 4; ctx.strokeStyle = "#0d2a0d"; ctx.lineWidth = 0.5; @@ -38,56 +38,35 @@ export function drawChart(canvas, history) { const val = (maxVal / gridLines) * i; const y = padTop + plotH - (val / maxVal) * plotH; - // Линия сетки ctx.beginPath(); ctx.moveTo(padLeft, y); ctx.lineTo(w - padRight, y); ctx.stroke(); - // Подпись значения слева ctx.fillStyle = "#00ff88"; ctx.font = "11px monospace"; ctx.textAlign = "right"; ctx.fillText(Math.round(val), padLeft - 8, y + 4); } - // Вертикальные линии (временные метки) - const timeMarks = 5; - ctx.setLineDash([4, 4]); - for (let i = 0; i <= timeMarks; i++) { - const x = padLeft + (plotW / timeMarks) * i; - ctx.beginPath(); - ctx.moveTo(x, padTop); - ctx.lineTo(x, padTop + plotH); - ctx.stroke(); + ctx.setLineDash([]); - // Подпись снизу - const pct = Math.round((i / timeMarks) * 100); - ctx.fillStyle = "#008844"; - ctx.font = "9px monospace"; - ctx.textAlign = "center"; - ctx.fillText(`${100 - pct}%`, x, padTop + plotH + 16); - } - - ctx.setLineDash([]); // сброс пунктира - - // ===== ПОДПИСИ ОСЕЙ ===== + // ===== ПОДПИСЬ ОСИ Y ===== ctx.fillStyle = "#00ff88"; - ctx.font = "bold 12px monospace"; + ctx.font = "bold 11px monospace"; - // Y ctx.save(); ctx.translate(12, padTop + plotH / 2); ctx.rotate(-Math.PI / 2); ctx.textAlign = "center"; - ctx.fillText("ЗНАЧЕНИЕ (0–63)", 0, 0); + ctx.fillText("ЗНАЧЕНИЕ", 0, 0); ctx.restore(); - // X + // ===== ПОДПИСЬ ОСИ X ===== ctx.textAlign = "center"; - ctx.fillText("ВРЕМЯ ← новые", w / 2, h - 4); + ctx.fillText("БУФЕР ПРИНЯТЫХ ДАННЫХ", padLeft + plotW / 2, h - 4); - // ===== РИСУЕМ СИГНАЛ ===== + // ===== СИГНАЛ ===== const hasSignal = history.some(v => v > 0); if (!hasSignal) { @@ -98,7 +77,17 @@ export function drawChart(canvas, history) { return; } - // Линия графика + // Прореживание если точек больше чем пикселей + let plotData = history; + if (history.length > plotW) { + const step = history.length / plotW; + plotData = []; + for (let i = 0; i < plotW; i++) { + const idx = Math.floor(i * step); + plotData.push(history[idx]); + } + } + ctx.strokeStyle = "#e6852d"; ctx.lineWidth = 1.5; ctx.shadowColor = "#e6852d"; @@ -106,11 +95,11 @@ export function drawChart(canvas, history) { ctx.beginPath(); - const stepX = plotW / Math.max(history.length - 1, 1); + const stepX = plotW / Math.max(plotData.length - 1, 1); - for (let i = 0; i < history.length; i++) { + for (let i = 0; i < plotData.length; i++) { const x = padLeft + i * stepX; - const y = padTop + plotH - (history[i] / maxVal) * plotH; + const y = padTop + plotH - (plotData[i] / maxVal) * plotH; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); @@ -119,39 +108,44 @@ export function drawChart(canvas, history) { ctx.stroke(); ctx.shadowBlur = 0; - // ===== ПОСЛЕДНЯЯ ТОЧКА ===== + // ===== ПОСЛЕДНЯЯ ТОЧКА + ЗНАЧЕНИЕ ===== if (history.length > 0) { - const lastIdx = history.length - 1; - const lastX = padLeft + lastIdx * stepX; - const lastY = padTop + plotH - (history[lastIdx] / maxVal) * plotH; - const lastVal = history[lastIdx]; + const lastVal = history[history.length - 1]; + const lastX = padLeft + (plotData.length - 1) * stepX; + const lastY = padTop + plotH - (lastVal / maxVal) * plotH; - // Кружок на последней точке + // Кружок ctx.fillStyle = "#c46b1f"; ctx.beginPath(); ctx.arc(lastX, lastY, 4, 0, Math.PI * 2); ctx.fill(); - // Подпись значения над точкой + // Значение последней точки + const labelText = `${lastVal}`; + const labelWidth = ctx.measureText(labelText).width; + + // Фон для читаемости + ctx.fillStyle = "rgba(0,0,0,0.8)"; + ctx.fillRect(lastX - labelWidth / 2 - 4, lastY - 22, labelWidth + 8, 18); + + // Текст по центру над точкой ctx.fillStyle = "#00ff88"; ctx.font = "bold 12px monospace"; - ctx.textAlign = "left"; - ctx.fillText(`${lastVal}`, lastX + 8, lastY - 8); + ctx.textAlign = "center"; + ctx.fillText(labelText, lastX, lastY - 8); - // Карточка статистики в правом верхнем углу - const avg = Math.round(history.reduce((a, b) => a + b, 0) / history.length); + // Статистика в углу const peak = Math.max(...history); const min = Math.min(...history); ctx.fillStyle = "rgba(0,0,0,0.7)"; - ctx.fillRect(w - 140, padTop, 130, 55); + ctx.fillRect(w - 120, padTop, 110, 42); ctx.fillStyle = "#00ff88"; ctx.font = "10px monospace"; ctx.textAlign = "left"; - ctx.fillText(`Пик: ${peak}`, w - 130, padTop + 14); - ctx.fillText(`Сред: ${avg}`, w - 130, padTop + 28); - ctx.fillText(`Мин: ${min}`, w - 130, padTop + 42); - ctx.fillText(`Всего: ${history.length}`, w - 130, padTop + 56); + ctx.fillText(`Пик: ${peak}`, w - 110, padTop + 14); + ctx.fillText(`Мин: ${min}`, w - 110, padTop + 28); + ctx.fillText(`Точек: ${history.length}`, w - 110, padTop + 42); } } \ No newline at end of file diff --git a/cmd/server/web/js/state.js b/cmd/server/web/js/state.js index 80bd150..32a7432 100644 --- a/cmd/server/web/js/state.js +++ b/cmd/server/web/js/state.js @@ -3,19 +3,40 @@ export const state = { maxPoints: 1000, signalHistory: [], smoothValue: 0, + lastBufferSize: 0, }; -// сглаживание export function smooth(target) { state.smoothValue += 0.2 * (target - state.smoothValue); return state.smoothValue; } -// добавление в историю export function addSignal(value) { state.signalHistory.push(value); if (state.signalHistory.length > state.maxPoints) { - state.signalHistory.shift(); + state.signalHistory.splice(0, state.signalHistory.length - state.maxPoints); } +} + +export function addSignals(values, bufferSize) { + if (!Array.isArray(values) || values.length === 0) return; + + const newBytes = bufferSize - state.lastBufferSize; + + if (newBytes > 0 && newBytes <= values.length) { + const fresh = values.slice(-newBytes); + const decoded = fresh.map(b => b & 0x3F); + state.signalHistory.push(...decoded); + } + + state.lastBufferSize = bufferSize; + + if (state.signalHistory.length > state.maxPoints) { + state.signalHistory.splice(0, state.signalHistory.length - state.maxPoints); + } +} + +export function resetBufferTracking() { + state.lastBufferSize = 0; } \ No newline at end of file