From 3b2a257c2bc50f09a086997772c93ae8e9fb6586 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 30 Apr 2026 22:06:05 +0300 Subject: [PATCH] Initial commit: GPIO monitoring server --- .gitignore | 32 +++++++++ README.md | 16 +++++ cmd/server/main.go | 139 +++++++++++++++++++++++++++++++++++++ go.mod | 3 + internal/adapter/buffer.go | 95 +++++++++++++++++++++++++ internal/adapter/reader.go | 60 ++++++++++++++++ progress.txt | 1 + web/index.html | 36 ++++++++++ 8 files changed, 382 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/server/main.go create mode 100644 go.mod create mode 100644 internal/adapter/buffer.go create mode 100644 internal/adapter/reader.go create mode 100644 progress.txt create mode 100644 web/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..842aa3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Бинарные файлы (скомпилированные программы) +gpio-monitor-server +progress-server +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Тестовые бинарники +*.test + +# Результаты тестов с покрытием +*.out + +# Зависимости +vendor/ + +# Файлы IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Системные файлы +.DS_Store +Thumbs.db + +# Временные файлы +tmp/ +temp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b5d2ac --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# GPIO Monitor Server + +Сервер для мониторинга GPIO на Raspberry Pi. + +## Структура проекта +- `cmd/server/` - точка входа приложения +- `internal/` - внутренняя логика +- `web/` - веб-интерфейс +- `scripts/` - вспомогательные скрипты + +## Сборка +go build -o gpio-monitor-server ./cmd/server + +## Запуск +./gpio-monitor-server + diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..c9fe224 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,139 @@ +// cmd/server/main.go +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "gpio-monitor/internal/adapter" +) + +// API обработчики HTTP +type API struct { + buf *adapter.RingBuffer +} + +func (a *API) HandleLatest(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + 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 + } + + // Явное преобразование в []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(), + }) +} + +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), // полный байт + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "nibbles": nibbles, + "count": len(nibbles), + }) +} + +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), + }) +} + +// CORS middleware для удобства разработки +func corsMiddleware(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 буфер + + // Запускаем чтение stdin в фоне + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + reader := adapter.NewStdinReader(updates) + if err := reader.Start(ctx, buf); err != nil { + log.Fatal("Ошибка запуска читателя stdin:", err) + } + + // Инициализация 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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6ce6a67 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gpio-monitor + +go 1.21 diff --git a/internal/adapter/buffer.go b/internal/adapter/buffer.go new file mode 100644 index 0000000..aacba73 --- /dev/null +++ b/internal/adapter/buffer.go @@ -0,0 +1,95 @@ +// internal/adapter/buffer.go +package adapter + +import ( + "sync" + "time" +) + +// RingBuffer - потокобезопасный кольцевой буфер для байт GPIO +type RingBuffer struct { + mu sync.RWMutex + data []byte + writePtr int + count int // сколько байт записано (до заполнения буфера) + lastWrite time.Time +} + +// NewRingBuffer создает буфер заданного размера +func NewRingBuffer(size int) *RingBuffer { + return &RingBuffer{ + data: make([]byte, size), + lastWrite: time.Now(), + } +} + +// Write добавляет байт в буфер +func (rb *RingBuffer) Write(b byte) { + rb.mu.Lock() + defer rb.mu.Unlock() + + rb.data[rb.writePtr] = b + rb.writePtr = (rb.writePtr + 1) % len(rb.data) + if rb.count < len(rb.data) { + rb.count++ + } + rb.lastWrite = time.Now() +} + +// GetLatest возвращает последний записанный байт +func (rb *RingBuffer) GetLatest() (byte, bool) { + rb.mu.RLock() + defer rb.mu.RUnlock() + + if rb.count == 0 { + return 0, false + } + + idx := rb.writePtr - 1 + if idx < 0 { + idx += len(rb.data) + } + return rb.data[idx], true +} + +// GetLast возвращает последние N байт +func (rb *RingBuffer) GetLast(n int) []byte { + rb.mu.RLock() + defer rb.mu.RUnlock() + + if n > rb.count { + n = rb.count + } + if n == 0 { + return []byte{} + } + + result := make([]byte, n) + start := rb.writePtr - n + if start < 0 { + start += len(rb.data) + copy(result, rb.data[start:]) + copy(result[len(rb.data)-start:], rb.data[:rb.writePtr]) + } else { + copy(result, rb.data[start:rb.writePtr]) + } + return result +} + +// GetAll возвращает все данные из буфера +func (rb *RingBuffer) GetAll() []byte { + return rb.GetLast(rb.count) +} + +// Stats возвращает статистику буфера +func (rb *RingBuffer) Stats() map[string]interface{} { + rb.mu.RLock() + defer rb.mu.RUnlock() + + return map[string]interface{}{ + "total_bytes": rb.count, + "buffer_size": len(rb.data), + "last_write": rb.lastWrite.Format(time.RFC3339), + "is_full": rb.count == len(rb.data), + } +} diff --git a/internal/adapter/reader.go b/internal/adapter/reader.go new file mode 100644 index 0000000..f106b8e --- /dev/null +++ b/internal/adapter/reader.go @@ -0,0 +1,60 @@ +// internal/adapter/reader.go +package adapter + +import ( + "bufio" + "context" + "io" + "log" + "os" +) + +// StdinReader читает бинарные данные из stdin и пишет в буфер +type StdinReader struct { + updates chan byte // канал для real-time уведомлений (0 = без уведомлений) +} + +// NewStdinReader создает читалку stdin +func NewStdinReader(updates chan byte) *StdinReader { + return &StdinReader{ + updates: updates, + } +} + +// Start начинает чтение stdin в отдельной горутине +func (r *StdinReader) Start(ctx context.Context, buf *RingBuffer) error { + reader := bufio.NewReader(os.Stdin) + + go func() { + for { + select { + case <-ctx.Done(): + log.Println("StdinReader: остановлен") + return + default: + b, err := reader.ReadByte() + if err != nil { + if err == io.EOF { + log.Println("StdinReader: stdin закрыт (EOF)") + return + } + // Другие ошибки игнорируем, продолжаем читать + continue + } + + buf.Write(b) + + // Неблокирующая отправка в канал уведомлений + if r.updates != nil { + select { + case r.updates <- b: + default: + // Канал заполнен, пропускаем уведомление + } + } + } + } + }() + + return nil +} diff --git a/progress.txt b/progress.txt new file mode 100644 index 0000000..8e14edc --- /dev/null +++ b/progress.txt @@ -0,0 +1 @@ +78 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e61f23b --- /dev/null +++ b/web/index.html @@ -0,0 +1,36 @@ + + + + + GPIO Monitor + + + +

GPIO Monitor (4-bit bus)

+
--
+
+ + + +