Initial commit: GPIO monitoring server

This commit is contained in:
Maxim
2026-04-30 22:06:05 +03:00
commit 3b2a257c2b
8 changed files with 382 additions and 0 deletions

32
.gitignore vendored Normal file
View File

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

16
README.md Normal file
View File

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

139
cmd/server/main.go Normal file
View File

@@ -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)
}
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module gpio-monitor
go 1.21

View File

@@ -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),
}
}

View File

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

1
progress.txt Normal file
View File

@@ -0,0 +1 @@
78

36
web/index.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GPIO Monitor</title>
<style>
body { font-family: monospace; padding: 20px; background: #1a1a1a; color: #0f0; }
.data { display: flex; flex-wrap: wrap; gap: 10px; }
.byte { padding: 10px; border: 1px solid #0f0; border-radius: 5px; text-align: center; }
.latest { font-size: 48px; margin: 20px 0; padding: 20px; border: 2px solid #0f0; text-align: center; }
</style>
</head>
<body>
<h1>GPIO Monitor (4-bit bus)</h1>
<div class="latest" id="latest">--</div>
<div class="data" id="history"></div>
<script>
async function update() {
const resp = await fetch('/api/latest');
const data = await resp.json();
document.getElementById('latest').textContent =
`Byte: ${data.latest_hex} (${data.latest_byte})`;
const historyDiv = document.getElementById('history');
historyDiv.innerHTML = data.history.map(v =>
`<div class="byte">0x${v.toString(16).padStart(2,'0')}<br>${v}</div>`
).join('');
}
setInterval(update, 1000);
update();
</script>
</body>
</html>