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 @@ + + + + +
+ +