Отображение в графике буфера

This commit is contained in:
Tot Maxim
2026-05-09 13:39:14 +03:00
parent 3db3a9bdcd
commit 549b0c5e63
4 changed files with 95 additions and 75 deletions

View File

@@ -53,16 +53,17 @@
</div>
</div>
<div class="card wide">
<h3>ГРАФИК КАНАЛА 1</h3>
<canvas id="signalChart" height="120"></canvas>
</div>
<div class="card wide">
<h3 class="accordion-header" id="history-header">▶ ПРИНЯТЫЕ ДАННЫЕ</h3>
<div class="accordion-body" id="history-body">
<div id="history"></div>
</div>
</div>
<div class="card wide">
<h3>ГРАФИК СИГНАЛА</h3>
<canvas id="signalChart" height="120"></canvas>
</div>
</div>

View File

@@ -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] Окно в фокусе, ускоряем опрос");

View File

@@ -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("ЗНАЧЕНИЕ (063)", 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);
}
}

View File

@@ -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;
}