Отлаженная версия в кабинете от 06.05.2026
This commit is contained in:
21
build.sh
Normal file
21
build.sh
Normal 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}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
65
cmd/server/web/dashboard.html
Normal file
65
cmd/server/web/dashboard.html
Normal 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>
|
||||
|
Before Width: | Height: | Size: 572 KiB After Width: | Height: | Size: 572 KiB |
101
cmd/server/web/js/app.js
Normal file
101
cmd/server/web/js/app.js
Normal 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();
|
||||
@@ -1 +0,0 @@
|
||||
78
|
||||
0
scripts/imi_wire.py
Executable file → Normal file
0
scripts/imi_wire.py
Executable file → Normal file
2
start-server.sh
Executable file → Normal file
2
start-server.sh
Executable file → Normal 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
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user