From 9c28b9501db7412b79ac94b69d021942fd0db36a Mon Sep 17 00:00:00 2001 From: Tot Maxim Date: Thu, 7 May 2026 21:30:48 +0300 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/web/js/app.js | 141 ++++++++++++++++++++------------ internal/adapter/buffer.go | 161 +++++++++++++++++++++++-------------- 2 files changed, 189 insertions(+), 113 deletions(-) diff --git a/cmd/server/web/js/app.js b/cmd/server/web/js/app.js index 850e566..04929fd 100644 --- a/cmd/server/web/js/app.js +++ b/cmd/server/web/js/app.js @@ -1,113 +1,148 @@ +// ===== SMOOTH STATE ===== +let lastValue = 0; + // ===== ACCORDION ===== const historyHeader = document.getElementById("history-header"); const historyBody = document.getElementById("history-body"); + let accordionOpen = false; -historyBody.classList.add("collapsed"); // изначально свёрнут -historyHeader.addEventListener("click", () => { - accordionOpen = !accordionOpen; - historyHeader.textContent = (accordionOpen ? "▼" : "▶") + " ПРИНЯТЫЕ ДАННЫЕ"; - historyBody.classList.toggle("collapsed", !accordionOpen); -}); +if (historyBody && historyHeader) { + historyBody.classList.add("collapsed"); -// ===== PROGRESS BAR ===== -function setBar(id, value) { - value = Math.max(0, Math.min(100, value)); - document.getElementById(id).style.width = value + "%"; + historyHeader.addEventListener("click", () => { + accordionOpen = !accordionOpen; + historyHeader.textContent = + (accordionOpen ? "▼" : "▶") + " ПРИНЯТЫЕ ДАННЫЕ"; + + historyBody.classList.toggle("collapsed", !accordionOpen); + }); } -// ===== SIGNAL ICON (0-3 уровня) ===== +// ===== PROGRESS ===== +function setBar(id, value) { + const el = document.getElementById(id); + if (!el) return; + + value = Math.max(0, Math.min(100, value)); + el.style.width = value + "%"; +} + +// ===== SIGNAL ===== function setSignal(level) { const bars = document.querySelectorAll(".sig-bar"); - bars.forEach(b => { - b.classList.remove("active-1", "active-2", "active-3", "active-4"); - b.classList.add("dim"); - }); + if (!bars.length) return; + bars.forEach(b => b.classList.remove("active-1", "active-2", "active-3", "dim")); + + bars.forEach(b => b.classList.add("dim")); + + if (level >= 1) { + bars[0].classList.add("active-1"); + bars[0].classList.remove("dim"); + } + if (level >= 2) { + bars[1].classList.add("active-2"); + bars[1].classList.remove("dim"); + } if (level >= 3) { - bars[0].classList.add("active-1"); bars[0].classList.remove("dim"); - bars[1].classList.add("active-2"); bars[1].classList.remove("dim"); - bars[2].classList.add("active-3"); bars[2].classList.remove("dim"); - } else if (level >= 2) { - bars[0].classList.add("active-1"); bars[0].classList.remove("dim"); - bars[1].classList.add("active-2"); bars[1].classList.remove("dim"); - } else if (level >= 1) { - bars[0].classList.add("active-1"); bars[0].classList.remove("dim"); + bars[2].classList.add("active-3"); + bars[2].classList.remove("dim"); } } -// ===== SMOOTHING ===== -let lastValue = 0; - +// ===== SMOOTH ===== function smoothBar(target) { const alpha = 0.2; lastValue = lastValue + alpha * (target - lastValue); return lastValue; } -// ===== MEDIAN FILTER ===== +// ===== MEDIAN ===== function median(values) { - if (values.length === 0) return 0; + if (!values || values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); - return sorted.length % 2 !== 0 + return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; } -// ===== MAIN LOOP ===== +// ===== UPDATE LOOP ===== async function update() { try { - const r1 = await fetch("/api/health"); - const r2 = await fetch("/api/history"); + const [r1, r2] = await Promise.all([ + fetch("/api/health"), + fetch("/api/history") + ]); const h = await r1.json(); const hist = await r2.json(); // ===== STATUS ===== - document.getElementById("status").textContent = h.status || "неизвестно"; + const statusEl = document.getElementById("status"); + if (statusEl) statusEl.textContent = h.status || "неизвестно"; const filled = h.stats?.filled ?? 0; const uptime = Math.round(h.uptime_sec || 0); - document.getElementById("meta").textContent = - "работа: " + uptime + "s | буфер: " + filled; + const bps = Math.round(h.stats?.bits_per_sec ?? 0); + const Bps = Math.round(h.stats?.bytes_per_sec ?? 0); + + const meta = document.getElementById("meta"); + if (meta) { + meta.textContent = + `работа: ${uptime}s | буфер: ${filled} | ${bps} bit/s (${Bps} B/s)`; + } + + // ===== LATEST ===== + const latest = Number(h.latest || 0); + + const latestEl = document.getElementById("latest"); + if (latestEl) latestEl.textContent = latest; + + // optional bits view + if (typeof renderBits === "function") { + renderBits(latest); + } // ===== HISTORY ===== const arr = Array.isArray(hist.bytes) ? hist.bytes : []; - document.getElementById("history").innerHTML = - arr.map((b, i) => - "[" + i + "] " + - (Number(b) & 0xFF).toString(2).padStart(8, "0") + - " = значение: " + (Number(b) & 0xFF) - ).join("
"); + const historyEl = document.getElementById("history"); + if (historyEl) { + historyEl.innerHTML = arr + .map((b, i) => + `[${i}] ${(Number(b) & 0xFF).toString(2).padStart(8, "0")} = ${Number(b) & 0xFF}` + ) + .join("
"); + } - // ===== ПОСЛЕДНЕЕ ЗНАЧЕНИЕ (без фильтра) ===== - const raw = arr.length > 0 ? (arr[arr.length - 1] & 0xFF) : 0; + // ===== SIGNAL PARSING ===== + const raw = arr.length ? (arr[arr.length - 1] & 0xFF) : 0; - // ===== ПАРСИНГ БИТ ===== - const barValue = raw & 0x3F; // биты 0-5 (0-63) - const signalLevel = (raw >> 6) & 0x3; // биты 6-7 (0-3) + const barValue = raw & 0x3F; // 0–63 + const signalLevel = (raw >> 6) & 0x3; // 0–3 - // ===== PROGRESS BAR (0-63 -> 0-100%) ===== const percent = (barValue / 63) * 100; const smooth = smoothBar(percent); + setBar("bar1", smooth); + setSignal(signalLevel); - // ===== SIGNAL ICON ===== - setSignal(signalLevel, raw); - - // ===== LAST BYTE ===== - document.getElementById("last-byte").textContent = barValue; + const lastByte = document.getElementById("last-byte"); + if (lastByte) lastByte.textContent = barValue; } catch (e) { console.error("UPDATE ERROR:", e); - document.getElementById("status").textContent = "СЕРВЕР НЕДОСТУПЕН"; + + const status = document.getElementById("status"); + if (status) status.textContent = "СЕРВЕР НЕДОСТУПЕН"; } } +// ===== START ===== setInterval(update, 500); update(); \ No newline at end of file diff --git a/internal/adapter/buffer.go b/internal/adapter/buffer.go index 0cc3ec9..3c4d2f2 100644 --- a/internal/adapter/buffer.go +++ b/internal/adapter/buffer.go @@ -1,99 +1,140 @@ -// internal/adapter/buffer.go package adapter import ( - "sync" - "time" + "sync" + "time" ) type RingBuffer struct { - mu sync.RWMutex - data []byte - writePtr int - count int + mu sync.RWMutex + data []byte + writePtr int + count int - lastWrite time.Time + lastWrite time.Time + + // ===== SPEED METRICS ===== + totalBytes uint64 + + lastTick time.Time + lastBytes uint64 + bytesPerSec float64 + bitsPerSec float64 } func NewRingBuffer(size int) *RingBuffer { - return &RingBuffer{ - data: make([]byte, size), - lastWrite: time.Now(), - } + return &RingBuffer{ + data: make([]byte, size), + lastWrite: time.Now(), + lastTick: time.Now(), + } } func (rb *RingBuffer) Write(b byte) { - rb.mu.Lock() - defer rb.mu.Unlock() + rb.mu.Lock() - rb.data[rb.writePtr] = b - rb.writePtr = (rb.writePtr + 1) % len(rb.data) + // ===== WRITE ===== + rb.data[rb.writePtr] = b + rb.writePtr = (rb.writePtr + 1) % len(rb.data) - if rb.count < len(rb.data) { - rb.count++ - } + if rb.count < len(rb.data) { + rb.count++ + } - rb.lastWrite = time.Now() + rb.lastWrite = time.Now() + rb.totalBytes++ + + rb.mu.Unlock() + + // ===== SPEED CALC (outside lock) ===== + now := rb.lastWrite + + if rb.lastTick.IsZero() { + rb.lastTick = now + rb.lastBytes = rb.totalBytes + return + } + + elapsed := now.Sub(rb.lastTick).Seconds() + + if elapsed >= 1.0 { + diff := rb.totalBytes - rb.lastBytes + + rb.bytesPerSec = float64(diff) / elapsed + rb.bitsPerSec = rb.bytesPerSec * 8 + + rb.lastTick = now + rb.lastBytes = rb.totalBytes + } } func (rb *RingBuffer) LastWriteTime() time.Time { - rb.mu.RLock() - defer rb.mu.RUnlock() - return rb.lastWrite + rb.mu.RLock() + defer rb.mu.RUnlock() + + return rb.lastWrite } func (rb *RingBuffer) GetLatest() (byte, bool) { - rb.mu.RLock() - defer rb.mu.RUnlock() + rb.mu.RLock() + defer rb.mu.RUnlock() - if rb.count == 0 { - return 0, false - } + if rb.count == 0 { + return 0, false + } - idx := rb.writePtr - 1 - if idx < 0 { - idx += len(rb.data) - } + idx := rb.writePtr - 1 + if idx < 0 { + idx += len(rb.data) + } - return rb.data[idx], true + return rb.data[idx], true } func (rb *RingBuffer) GetLast(n int) []byte { - rb.mu.RLock() - defer rb.mu.RUnlock() + rb.mu.RLock() + defer rb.mu.RUnlock() - if n > rb.count { - n = rb.count - } + if n > rb.count { + n = rb.count + } - res := make([]byte, n) + res := make([]byte, n) - start := rb.writePtr - n - if start < 0 { - start += len(rb.data) - copy(res, rb.data[start:]) - copy(res[len(rb.data)-start:], rb.data[:rb.writePtr]) - } else { - copy(res, rb.data[start:rb.writePtr]) - } + start := rb.writePtr - n - return res -} + if start < 0 { + start += len(rb.data) -func (rb *RingBuffer) Stats() map[string]interface{} { - rb.mu.RLock() - defer rb.mu.RUnlock() + copy(res, rb.data[start:]) + copy(res[len(rb.data)-start:], rb.data[:rb.writePtr]) + } else { + copy(res, rb.data[start:rb.writePtr]) + } - return map[string]interface{}{ - "size": len(rb.data), - "filled": rb.count, - "last_write": rb.lastWrite, - } + return res } func (rb *RingBuffer) IsAlive(timeout time.Duration) bool { - rb.mu.RLock() - defer rb.mu.RUnlock() + rb.mu.RLock() + defer rb.mu.RUnlock() - return time.Since(rb.lastWrite) < timeout + return time.Since(rb.lastWrite) < timeout +} + +func (rb *RingBuffer) Stats() map[string]interface{} { + rb.mu.RLock() + defer rb.mu.RUnlock() + + return map[string]interface{}{ + "size": len(rb.data), + "filled": rb.count, + "last_write": rb.lastWrite, + + "total_bytes": rb.totalBytes, + "total_bits": rb.totalBytes * 8, + + "bytes_per_sec": rb.bytesPerSec, + "bits_per_sec": rb.bitsPerSec, + } }