Отображение в графике буфера
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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] Окно в фокусе, ускоряем опрос");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user