Отлаженная версия в кабинете от 06.05.2026

This commit is contained in:
Maxim
2026-05-06 11:41:48 +03:00
parent f9b5ab27d5
commit 80aeb1a5eb
12 changed files with 305 additions and 240 deletions

21
build.sh Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
APP_NAME="gpio-monitor-server"
CMD_PATH="./cmd/server"
BUILD_DIR="./build"
echo ">> Cleaning old build..."
rm -rf ${BUILD_DIR}
mkdir -p ${BUILD_DIR}
echo ">> Downloading dependencies..."
go mod tidy
echo ">> Building ${APP_NAME}..."
GOOS=linux GOARCH=arm64 go build -o ${BUILD_DIR}/${APP_NAME} ${CMD_PATH}
echo ">> Build complete:"
ls -lh ${BUILD_DIR}/${APP_NAME}

View File

@@ -2,151 +2,162 @@
package main
import (
"context"
"encoding/json"
"flag"
"log"
"net/http"
"os"
"time"
"context"
"embed"
"encoding/json"
"flag"
"io/fs"
"log"
"net/http"
"os"
"time"
"gpio-monitor/internal/adapter"
"gpio-monitor/internal/adapter"
)
// ===== EMBED WEB =====
//go:embed web/*
var webFiles embed.FS
type API struct {
buf *adapter.RingBuffer
startTime time.Time
buf *adapter.RingBuffer
startTime time.Time
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
func (a *API) HandleHealth(w http.ResponseWriter, r *http.Request) {
latest, ok := a.buf.GetLatest()
idle := time.Since(a.buf.LastWriteTime())
latest, ok := a.buf.GetLatest()
idle := time.Since(a.buf.LastWriteTime())
status := "ok"
if idle > 15*time.Second {
status = "degraded"
}
status := "ok"
if idle > 15*time.Second {
status = "degraded"
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": status,
"uptime_sec": time.Since(a.startTime).Seconds(),
"last_data_ms": idle.Milliseconds(),
"pipe_alive": a.buf.IsAlive(3*time.Second),
"has_data": ok,
"latest": latest,
"stats": a.buf.Stats(),
})
writeJSON(w, map[string]any{
"status": status,
"uptime_sec": time.Since(a.startTime).Seconds(),
"last_data_ms": idle.Milliseconds(),
"pipe_alive": a.buf.IsAlive(3 * time.Second),
"has_data": ok,
"latest": latest,
"stats": a.buf.Stats(),
})
}
func (a *API) HandleLatest(w http.ResponseWriter, r *http.Request) {
latest, ok := a.buf.GetLatest()
if !ok {
json.NewEncoder(w).Encode(map[string]string{"error": "no data"})
return
}
latest, ok := a.buf.GetLatest()
if !ok {
writeJSON(w, map[string]string{"error": "no data"})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"latest": latest,
"history": a.buf.GetLast(10),
})
writeJSON(w, map[string]any{
"latest": latest,
"history": a.buf.GetLast(10),
})
}
func (a *API) HandleHistory(w http.ResponseWriter, r *http.Request) {
raw := a.buf.GetLast(100)
raw := a.buf.GetLast(100)
out := make([]int, len(raw))
for i, v := range raw {
out[i] = int(v)
}
out := make([]int, len(raw))
for i, v := range raw {
out[i] = int(v)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"bytes": out,
})
writeJSON(w, map[string]any{
"bytes": out,
})
}
// ===== PIPE READER =====
// === FIFO READER ===
func startPipeReader(ctx context.Context, buf *adapter.RingBuffer, path string) {
go func() {
buffer := make([]byte, 64)
go func() {
buffer := make([]byte, 64)
for {
select {
case <-ctx.Done():
return
default:
}
for {
select {
case <-ctx.Done():
return
default:
}
f, err := openPipe(path)
if err != nil {
time.Sleep(time.Second)
continue
}
f, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
time.Sleep(time.Second)
continue
}
log.Println("pipe connected")
log.Println("pipe connected")
for {
n, err := f.Read(buffer)
if err != nil {
f.Close()
break
}
for {
n, err := f.Read(buffer)
if err != nil {
f.Close()
break
}
for i := 0; i < n; i++ {
buf.Write(buffer[i])
}
}
for i := 0; i < n; i++ {
buf.Write(buffer[i])
}
}
log.Println("pipe disconnected, retrying...")
}
}()
}
func openPipe(path string) (*os.File, error) {
return os.OpenFile(path, os.O_RDONLY, 0)
log.Println("pipe disconnected, retry...")
}
}()
}
func cors(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
next(w, r)
}
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
next(w, r)
}
}
func main() {
pipePath := flag.String("pipe", "/tmp/gpio_pipe", "pipe path")
flag.Parse()
pipePath := flag.String("pipe", "/tmp/gpio_pipe", "pipe path")
flag.Parse()
buf := adapter.NewRingBuffer(1024 * 10)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
buf := adapter.NewRingBuffer(1024 * 10)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
startPipeReader(ctx, buf, *pipePath)
startPipeReader(ctx, buf, *pipePath)
api := &API{
buf: buf,
startTime: time.Now(),
}
api := &API{
buf: buf,
startTime: time.Now(),
}
http.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ===== API =====
http.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/latest":
cors(api.HandleLatest)(w, r)
case "/history":
cors(api.HandleHistory)(w, r)
case "/health":
cors(api.HandleHealth)(w, r)
default:
http.Error(w, "not found", 404)
}
})))
switch r.URL.Path {
case "/latest":
cors(api.HandleLatest)(w, r)
case "/history":
cors(api.HandleHistory)(w, r)
case "/health":
cors(api.HandleHealth)(w, r)
default:
http.Error(w, "not found", 404)
}
})))
http.Handle("/", http.FileServer(http.Dir("/home/user/temp/golang/web")))
// ===== WEB (EMBEDDED) =====
webFS, err := fs.Sub(webFiles, "web")
if err != nil {
log.Fatal(err)
}
log.Println("server :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
http.Handle("/", http.FileServer(http.FS(webFS)))
log.Println("server :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

View File

@@ -0,0 +1,65 @@
<!-- web/dashboard.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>GPIO Панель</title>
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<h1>GPIO МОНИТОРИНГ В РЕАЛЬНОМ ВРЕМЕНИ</h1>
<div class="grid">
<div class="card">
<h3>СОСТОЯНИЕ</h3>
<div id="status">...</div>
<div id="meta"></div>
</div>
<div class="card">
<h3>ПОСЛЕДНИЙ СИГНАЛ</h3>
<div id="latest">--</div>
<div id="bits"></div>
</div>
<div class="card wide">
<h3>УРОВНИ КАНАЛОВ</h3>
<div class="progress-row">
<div class="progress-label">Канал 1</div>
<div class="progress">
<div class="fill" id="bar1"></div>
</div>
</div>
<div class="progress-row">
<div class="progress-label">Звук</div>
<div class="progress">
<div class="fill" id="bar2"></div>
</div>
</div>
<div class="progress-row">
<div class="progress-label">Высота</div>
<div class="progress">
<div class="fill" id="bar3"></div>
</div>
</div>
</div>
<div class="card wide">
<h3>ПОТОК ДАННЫХ (последние 32)</h3>
<div id="history"></div>
</div>
</div>
<script src="/js/app.js"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 572 KiB

After

Width:  |  Height:  |  Size: 572 KiB

101
cmd/server/web/js/app.js Normal file
View File

@@ -0,0 +1,101 @@
// ===== BIT RENDER =====
function renderBits(byte) {
const bits = document.getElementById("bits");
bits.innerHTML = "";
byte = Number(byte || 0);
for (let i = 7; i >= 0; i--) {
const v = (byte >> i) & 1;
const d = document.createElement("div");
d.className = "bit" + (v ? " on" : "");
d.textContent = v;
bits.appendChild(d);
}
}
// ===== PROGRESS BAR =====
function setBar(id, value) {
value = Math.max(0, Math.min(100, value));
document.getElementById(id).style.width = value + "%";
}
// ===== SMOOTHING =====
let lastValue = 0;
function smoothBar(target) {
const alpha = 0.2;
lastValue = lastValue + alpha * (target - lastValue);
return lastValue;
}
// ===== MEDIAN FILTER =====
function median(values) {
if (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
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
}
// ===== MAIN LOOP =====
async function update() {
try {
const r1 = await fetch("/api/health");
const r2 = await fetch("/api/history");
const h = await r1.json();
const hist = await r2.json();
// ===== STATUS =====
document.getElementById("status").textContent = h.status || "неизвестно";
const filled = h.stats?.filled ?? 0;
const uptime = Math.round(h.uptime_sec || 0);
document.getElementById("meta").textContent =
"аптайм: " + uptime + "s | буфер: " + filled;
// ===== LATEST =====
const latest = Number(h.latest || 0);
document.getElementById("latest").textContent = latest;
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>");
// ===== FILTER (последние 9 значений) =====
const last = arr.slice(-9).map(v => Number(v) & 0xFF);
const filtered = median(last);
// ===== NORMALIZE =====
const percent = (filtered / 255) * 100;
// ===== SMOOTH =====
const smooth = smoothBar(percent);
// ===== ONLY CHANNEL 1 =====
setBar("bar1", smooth);
} catch (e) {
console.error("UPDATE ERROR:", e);
document.getElementById("status").textContent = "СЕРВЕР НЕДОСТУПЕН";
}
}
// быстрее и плавнее
setInterval(update, 500);
update();

View File

@@ -1 +0,0 @@
78

0
scripts/imi_wire.py Executable file → Normal file
View File

2
start-server.sh Executable file → Normal file
View File

@@ -1,4 +1,4 @@
#!/bin/bash
cd /home/user/temp/golang || exit 1
./gpio-monitor-server -pipe /tmp/gpio_pipe
./build/gpio-monitor-server -pipe /tmp/gpio_pipe

View File

@@ -1,65 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GPIO Dashboard</title>
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<h1>GPIO REALTIME DASHBOARD</h1>
<div class="grid">
<div class="card">
<h3>STATUS</h3>
<div id="status">...</div>
<div id="meta"></div>
</div>
<div class="card">
<h3>LATEST SIGNAL</h3>
<div id="latest">--</div>
<div id="bits"></div>
</div>
<div class="card wide">
<h3>CHANNEL LEVELS</h3>
<div class="progress-row">
<div class="progress-label">Канал 1</div>
<div class="progress">
<div class="fill" id="bar1"></div>
</div>
</div>
<div class="progress-row">
<div class="progress-label">Звук</div>
<div class="progress">
<div class="fill" id="bar2"></div>
</div>
</div>
<div class="progress-row">
<div class="progress-label">Высота</div>
<div class="progress">
<div class="fill" id="bar3"></div>
</div>
</div>
</div>
<div class="card wide">
<h3>LIVE STREAM (last 32)</h3>
<div id="history"></div>
</div>
</div>
<script src="/js/app.js"></script>
</body>
</html>

View File

@@ -1,67 +0,0 @@
function renderBits(byte) {
const bits = document.getElementById("bits");
bits.innerHTML = "";
byte = Number(byte || 0);
for (let i = 7; i >= 0; i--) {
const v = (byte >> i) & 1;
const d = document.createElement("div");
d.className = "bit" + (v ? " on" : "");
d.textContent = v;
bits.appendChild(d);
}
}
function setBar(id, value) {
value = Math.max(0, Math.min(100, value));
document.getElementById(id).style.width = value + "%";
}
async function update() {
try {
const r1 = await fetch("/api/health");
const r2 = await fetch("/api/history");
const h = await r1.json();
const hist = await r2.json();
console.log("health:", h);
console.log("history:", hist);
document.getElementById("status").textContent = h.status || "unknown";
const filled = h.stats?.filled ?? 0;
const uptime = Math.round(h.uptime_sec || 0);
document.getElementById("meta").textContent =
"uptime: " + uptime + "s | filled: " + filled;
const latest = Number(h.latest || 0);
document.getElementById("latest").textContent = latest;
renderBits(latest);
setBar("bar1", (latest & 15) * 6.6);
setBar("bar2", ((latest * 3) % 16) * 6.6);
setBar("bar3", ((latest * 5) % 16) * 6.6);
const arr = Array.isArray(hist.bytes) ? hist.bytes : [];
document.getElementById("history").innerHTML =
arr.map((b, i) =>
"[" + i + "] " +
(Number(b) & 15).toString(2).padStart(4, "0") +
" = " + (Number(b) & 15)
).join("<br>");
} catch (e) {
console.error("UPDATE ERROR:", e);
document.getElementById("status").textContent = "SERVER DOWN";
}
}
setInterval(update, 500);
update();