Первая итерация заработала

This commit is contained in:
Maxim
2026-05-01 08:22:49 +03:00
parent 3b2a257c2b
commit e2fb7409e4
5 changed files with 249 additions and 213 deletions

View File

@@ -4,136 +4,135 @@ package main
import (
"context"
"encoding/json"
"fmt"
"flag"
"log"
"net/http"
"os"
"time"
"gpio-monitor/internal/adapter"
)
// API обработчики HTTP
type API struct {
buf *adapter.RingBuffer
startTime time.Time
}
func (a *API) HandleLatest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
func (a *API) HandleHealth(w http.ResponseWriter, r *http.Request) {
latest, ok := a.buf.GetLatest()
if !ok {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"error": "no data yet",
})
return
idle := time.Since(a.buf.LastWriteTime())
status := "ok"
if idle > 15*time.Second {
status = "degraded"
}
// Явное преобразование в []int для читаемого JSON
lastN := a.buf.GetLast(10)
historyInts := make([]int, len(lastN))
for i, v := range lastN {
historyInts[i] = int(v)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"latest_byte": latest,
"latest_hex": fmt.Sprintf("0x%02x", latest),
"latest_char": string(rune(latest)),
"history": historyInts, // теперь будет [1,2,3,...]
"stats": a.buf.Stats(),
"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) HandleNibbles(w http.ResponseWriter, r *http.Request) {
// Для 4-битной шины — показываем последние 16 полубайт
data := a.buf.GetLast(16)
nibbles := make([]map[string]interface{}, len(data))
for i, b := range data {
nibbles[i] = map[string]interface{}{
"index": i,
"value": int(b),
"hex": fmt.Sprintf("0x%02x", b),
"bin": fmt.Sprintf("%04b", b), // 4 бита
"bin_full": fmt.Sprintf("%08b", b), // полный байт
}
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
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"nibbles": nibbles,
"count": len(nibbles),
"latest": latest,
"history": a.buf.GetLast(10),
})
}
func (a *API) HandleHistory(w http.ResponseWriter, r *http.Request) {
// Можно добавить параметр ?n=100
n := 100
data := a.buf.GetLast(n)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"bytes": data,
"count": len(data),
"bytes": a.buf.GetLast(100),
})
}
// CORS middleware для удобства разработки
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
// === FIFO READER ===
func startPipeReader(ctx context.Context, buf *adapter.RingBuffer, path string) {
go func() {
buffer := make([]byte, 64)
for {
select {
case <-ctx.Done():
return
default:
}
f, err := openPipe(path)
if err != nil {
time.Sleep(time.Second)
continue
}
log.Println("pipe connected")
for {
n, err := f.Read(buffer)
if err != nil {
f.Close()
break
}
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)
}
func cors(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next(w, r)
}
}
func main() {
// Инициализация
updates := make(chan byte, 256)
buf := adapter.NewRingBuffer(1024 * 10) // 10KB буфер
pipePath := flag.String("pipe", "/tmp/gpio_pipe", "pipe path")
flag.Parse()
// Запускаем чтение stdin в фоне
buf := adapter.NewRingBuffer(1024 * 10)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
reader := adapter.NewStdinReader(updates)
if err := reader.Start(ctx, buf); err != nil {
log.Fatal("Ошибка запуска читателя stdin:", err)
startPipeReader(ctx, buf, *pipePath)
api := &API{
buf: buf,
startTime: time.Now(),
}
fs := http.FileServer(http.Dir("/home/user/temp/golang/web"))
http.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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)
}
})))
// Инициализация API
api := &API{buf: buf}
// HTTP маршруты (каждый только ОДИН раз!)
http.HandleFunc("/api/latest", corsMiddleware(api.HandleLatest))
http.HandleFunc("/api/history", corsMiddleware(api.HandleHistory))
http.HandleFunc("/api/nibbles", corsMiddleware(api.HandleNibbles))
// Статические файлы для веб-интерфейса
fs := http.FileServer(http.Dir("web"))
http.Handle("/", fs)
// Запуск сервера
log.Println("=== GPIO Monitor Server ===")
log.Println("Сервер запущен на http://localhost:8080")
log.Println("API endpoints:")
log.Println(" GET /api/latest - последний байт + история")
log.Println(" GET /api/history - последние 100 байт")
log.Println(" GET /api/nibbles - 4-битное представление")
log.Println("Ожидание данных из stdin...")
log.Println("")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("Ошибка сервера:", err)
}
log.Println("server :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}