рабочая версия
This commit is contained in:
@@ -1,113 +1,148 @@
|
|||||||
|
// ===== SMOOTH STATE =====
|
||||||
|
let lastValue = 0;
|
||||||
|
|
||||||
// ===== ACCORDION =====
|
// ===== ACCORDION =====
|
||||||
const historyHeader = document.getElementById("history-header");
|
const historyHeader = document.getElementById("history-header");
|
||||||
const historyBody = document.getElementById("history-body");
|
const historyBody = document.getElementById("history-body");
|
||||||
|
|
||||||
let accordionOpen = false;
|
let accordionOpen = false;
|
||||||
historyBody.classList.add("collapsed"); // изначально свёрнут
|
|
||||||
|
|
||||||
historyHeader.addEventListener("click", () => {
|
if (historyBody && historyHeader) {
|
||||||
accordionOpen = !accordionOpen;
|
historyBody.classList.add("collapsed");
|
||||||
historyHeader.textContent = (accordionOpen ? "▼" : "▶") + " ПРИНЯТЫЕ ДАННЫЕ";
|
|
||||||
historyBody.classList.toggle("collapsed", !accordionOpen);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== PROGRESS BAR =====
|
historyHeader.addEventListener("click", () => {
|
||||||
function setBar(id, value) {
|
accordionOpen = !accordionOpen;
|
||||||
value = Math.max(0, Math.min(100, value));
|
historyHeader.textContent =
|
||||||
document.getElementById(id).style.width = value + "%";
|
(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) {
|
function setSignal(level) {
|
||||||
const bars = document.querySelectorAll(".sig-bar");
|
const bars = document.querySelectorAll(".sig-bar");
|
||||||
bars.forEach(b => {
|
if (!bars.length) return;
|
||||||
b.classList.remove("active-1", "active-2", "active-3", "active-4");
|
|
||||||
b.classList.add("dim");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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) {
|
if (level >= 3) {
|
||||||
bars[0].classList.add("active-1"); bars[0].classList.remove("dim");
|
bars[2].classList.add("active-3");
|
||||||
bars[1].classList.add("active-2"); bars[1].classList.remove("dim");
|
bars[2].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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SMOOTHING =====
|
// ===== SMOOTH =====
|
||||||
let lastValue = 0;
|
|
||||||
|
|
||||||
function smoothBar(target) {
|
function smoothBar(target) {
|
||||||
const alpha = 0.2;
|
const alpha = 0.2;
|
||||||
lastValue = lastValue + alpha * (target - lastValue);
|
lastValue = lastValue + alpha * (target - lastValue);
|
||||||
return lastValue;
|
return lastValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== MEDIAN FILTER =====
|
// ===== MEDIAN =====
|
||||||
function median(values) {
|
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 sorted = [...values].sort((a, b) => a - b);
|
||||||
const mid = Math.floor(sorted.length / 2);
|
const mid = Math.floor(sorted.length / 2);
|
||||||
|
|
||||||
return sorted.length % 2 !== 0
|
return sorted.length % 2
|
||||||
? sorted[mid]
|
? sorted[mid]
|
||||||
: (sorted[mid - 1] + sorted[mid]) / 2;
|
: (sorted[mid - 1] + sorted[mid]) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== MAIN LOOP =====
|
// ===== UPDATE LOOP =====
|
||||||
async function update() {
|
async function update() {
|
||||||
try {
|
try {
|
||||||
const r1 = await fetch("/api/health");
|
const [r1, r2] = await Promise.all([
|
||||||
const r2 = await fetch("/api/history");
|
fetch("/api/health"),
|
||||||
|
fetch("/api/history")
|
||||||
|
]);
|
||||||
|
|
||||||
const h = await r1.json();
|
const h = await r1.json();
|
||||||
const hist = await r2.json();
|
const hist = await r2.json();
|
||||||
|
|
||||||
// ===== STATUS =====
|
// ===== 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 filled = h.stats?.filled ?? 0;
|
||||||
const uptime = Math.round(h.uptime_sec || 0);
|
const uptime = Math.round(h.uptime_sec || 0);
|
||||||
|
|
||||||
document.getElementById("meta").textContent =
|
const bps = Math.round(h.stats?.bits_per_sec ?? 0);
|
||||||
"работа: " + uptime + "s | буфер: " + filled;
|
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 =====
|
// ===== HISTORY =====
|
||||||
const arr = Array.isArray(hist.bytes) ? hist.bytes : [];
|
const arr = Array.isArray(hist.bytes) ? hist.bytes : [];
|
||||||
|
|
||||||
document.getElementById("history").innerHTML =
|
const historyEl = document.getElementById("history");
|
||||||
arr.map((b, i) =>
|
if (historyEl) {
|
||||||
"[" + i + "] " +
|
historyEl.innerHTML = arr
|
||||||
(Number(b) & 0xFF).toString(2).padStart(8, "0") +
|
.map((b, i) =>
|
||||||
" = значение: " + (Number(b) & 0xFF)
|
`[${i}] ${(Number(b) & 0xFF).toString(2).padStart(8, "0")} = ${Number(b) & 0xFF}`
|
||||||
).join("<br>");
|
)
|
||||||
|
.join("<br>");
|
||||||
|
}
|
||||||
|
|
||||||
// ===== ПОСЛЕДНЕЕ ЗНАЧЕНИЕ (без фильтра) =====
|
// ===== SIGNAL PARSING =====
|
||||||
const raw = arr.length > 0 ? (arr[arr.length - 1] & 0xFF) : 0;
|
const raw = arr.length ? (arr[arr.length - 1] & 0xFF) : 0;
|
||||||
|
|
||||||
// ===== ПАРСИНГ БИТ =====
|
const barValue = raw & 0x3F; // 0–63
|
||||||
const barValue = raw & 0x3F; // биты 0-5 (0-63)
|
const signalLevel = (raw >> 6) & 0x3; // 0–3
|
||||||
const signalLevel = (raw >> 6) & 0x3; // биты 6-7 (0-3)
|
|
||||||
|
|
||||||
// ===== PROGRESS BAR (0-63 -> 0-100%) =====
|
|
||||||
const percent = (barValue / 63) * 100;
|
const percent = (barValue / 63) * 100;
|
||||||
const smooth = smoothBar(percent);
|
const smooth = smoothBar(percent);
|
||||||
|
|
||||||
setBar("bar1", smooth);
|
setBar("bar1", smooth);
|
||||||
|
setSignal(signalLevel);
|
||||||
|
|
||||||
// ===== SIGNAL ICON =====
|
const lastByte = document.getElementById("last-byte");
|
||||||
setSignal(signalLevel, raw);
|
if (lastByte) lastByte.textContent = barValue;
|
||||||
|
|
||||||
// ===== LAST BYTE =====
|
|
||||||
document.getElementById("last-byte").textContent = barValue;
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("UPDATE ERROR:", e);
|
console.error("UPDATE ERROR:", e);
|
||||||
document.getElementById("status").textContent = "СЕРВЕР НЕДОСТУПЕН";
|
|
||||||
|
const status = document.getElementById("status");
|
||||||
|
if (status) status.textContent = "СЕРВЕР НЕДОСТУПЕН";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== START =====
|
||||||
setInterval(update, 500);
|
setInterval(update, 500);
|
||||||
update();
|
update();
|
||||||
@@ -1,99 +1,140 @@
|
|||||||
// internal/adapter/buffer.go
|
|
||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RingBuffer struct {
|
type RingBuffer struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
data []byte
|
data []byte
|
||||||
writePtr int
|
writePtr int
|
||||||
count 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 {
|
func NewRingBuffer(size int) *RingBuffer {
|
||||||
return &RingBuffer{
|
return &RingBuffer{
|
||||||
data: make([]byte, size),
|
data: make([]byte, size),
|
||||||
lastWrite: time.Now(),
|
lastWrite: time.Now(),
|
||||||
}
|
lastTick: time.Now(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *RingBuffer) Write(b byte) {
|
func (rb *RingBuffer) Write(b byte) {
|
||||||
rb.mu.Lock()
|
rb.mu.Lock()
|
||||||
defer rb.mu.Unlock()
|
|
||||||
|
|
||||||
rb.data[rb.writePtr] = b
|
// ===== WRITE =====
|
||||||
rb.writePtr = (rb.writePtr + 1) % len(rb.data)
|
rb.data[rb.writePtr] = b
|
||||||
|
rb.writePtr = (rb.writePtr + 1) % len(rb.data)
|
||||||
|
|
||||||
if rb.count < len(rb.data) {
|
if rb.count < len(rb.data) {
|
||||||
rb.count++
|
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 {
|
func (rb *RingBuffer) LastWriteTime() time.Time {
|
||||||
rb.mu.RLock()
|
rb.mu.RLock()
|
||||||
defer rb.mu.RUnlock()
|
defer rb.mu.RUnlock()
|
||||||
return rb.lastWrite
|
|
||||||
|
return rb.lastWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *RingBuffer) GetLatest() (byte, bool) {
|
func (rb *RingBuffer) GetLatest() (byte, bool) {
|
||||||
rb.mu.RLock()
|
rb.mu.RLock()
|
||||||
defer rb.mu.RUnlock()
|
defer rb.mu.RUnlock()
|
||||||
|
|
||||||
if rb.count == 0 {
|
if rb.count == 0 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
idx := rb.writePtr - 1
|
idx := rb.writePtr - 1
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
idx += len(rb.data)
|
idx += len(rb.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rb.data[idx], true
|
return rb.data[idx], true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *RingBuffer) GetLast(n int) []byte {
|
func (rb *RingBuffer) GetLast(n int) []byte {
|
||||||
rb.mu.RLock()
|
rb.mu.RLock()
|
||||||
defer rb.mu.RUnlock()
|
defer rb.mu.RUnlock()
|
||||||
|
|
||||||
if n > rb.count {
|
if n > rb.count {
|
||||||
n = rb.count
|
n = rb.count
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]byte, n)
|
res := make([]byte, n)
|
||||||
|
|
||||||
start := rb.writePtr - 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])
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
if start < 0 {
|
||||||
}
|
start += len(rb.data)
|
||||||
|
|
||||||
func (rb *RingBuffer) Stats() map[string]interface{} {
|
copy(res, rb.data[start:])
|
||||||
rb.mu.RLock()
|
copy(res[len(rb.data)-start:], rb.data[:rb.writePtr])
|
||||||
defer rb.mu.RUnlock()
|
} else {
|
||||||
|
copy(res, rb.data[start:rb.writePtr])
|
||||||
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return res
|
||||||
"size": len(rb.data),
|
|
||||||
"filled": rb.count,
|
|
||||||
"last_write": rb.lastWrite,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *RingBuffer) IsAlive(timeout time.Duration) bool {
|
func (rb *RingBuffer) IsAlive(timeout time.Duration) bool {
|
||||||
rb.mu.RLock()
|
rb.mu.RLock()
|
||||||
defer rb.mu.RUnlock()
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user