рабочая версия
This commit is contained in:
@@ -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("<br>");
|
||||
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("<br>");
|
||||
}
|
||||
|
||||
// ===== ПОСЛЕДНЕЕ ЗНАЧЕНИЕ (без фильтра) =====
|
||||
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();
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user