diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..5228c5d --- /dev/null +++ b/build.sh @@ -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} diff --git a/cmd/server/main.go b/cmd/server/main.go index 0b050fa..591a96e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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)) } diff --git a/web/css/style.css b/cmd/server/web/css/style.css similarity index 100% rename from web/css/style.css rename to cmd/server/web/css/style.css diff --git a/cmd/server/web/dashboard.html b/cmd/server/web/dashboard.html new file mode 100644 index 0000000..7c361f7 --- /dev/null +++ b/cmd/server/web/dashboard.html @@ -0,0 +1,65 @@ + + + + + + +GPIO Панель + + + + + + +

GPIO МОНИТОРИНГ В РЕАЛЬНОМ ВРЕМЕНИ

+ +
+ +
+

СОСТОЯНИЕ

+
...
+
+
+ +
+

ПОСЛЕДНИЙ СИГНАЛ

+
--
+
+
+ +
+

УРОВНИ КАНАЛОВ

+ +
+
Канал 1
+
+
+
+
+ +
+
Звук
+
+
+
+
+ +
+
Высота
+
+
+
+
+
+ +
+

ПОТОК ДАННЫХ (последние 32)

+
+
+ +
+ + + + + diff --git a/web/favicon.png b/cmd/server/web/favicon.png similarity index 100% rename from web/favicon.png rename to cmd/server/web/favicon.png diff --git a/web/index.html b/cmd/server/web/index.html similarity index 100% rename from web/index.html rename to cmd/server/web/index.html diff --git a/cmd/server/web/js/app.js b/cmd/server/web/js/app.js new file mode 100644 index 0000000..c82f79c --- /dev/null +++ b/cmd/server/web/js/app.js @@ -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("
"); + + // ===== 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(); diff --git a/progress.txt b/progress.txt deleted file mode 100644 index 8e14edc..0000000 --- a/progress.txt +++ /dev/null @@ -1 +0,0 @@ -78 diff --git a/scripts/imi_wire.py b/scripts/imi_wire.py old mode 100755 new mode 100644 diff --git a/start-server.sh b/start-server.sh old mode 100755 new mode 100644 index 60f8bd5..3490a61 --- a/start-server.sh +++ b/start-server.sh @@ -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 diff --git a/web/dashboard.html b/web/dashboard.html deleted file mode 100644 index af40fa6..0000000 --- a/web/dashboard.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - -GPIO Dashboard - - - - - - -

GPIO REALTIME DASHBOARD

- -
- -
-

STATUS

-
...
-
-
- -
-

LATEST SIGNAL

-
--
-
-
- - -
-

CHANNEL LEVELS

- -
-
Канал 1
-
-
-
-
- -
-
Звук
-
-
-
-
- -
-
Высота
-
-
-
-
-
- - -
-

LIVE STREAM (last 32)

-
-
- -
- - - - - diff --git a/web/js/app.js b/web/js/app.js deleted file mode 100644 index c01a138..0000000 --- a/web/js/app.js +++ /dev/null @@ -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("
"); - - } catch (e) { - console.error("UPDATE ERROR:", e); - document.getElementById("status").textContent = "SERVER DOWN"; - } -} - -setInterval(update, 500); -update();