Initial commit: GPIO monitoring server
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal 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
16
README.md
Normal 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
139
cmd/server/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
95
internal/adapter/buffer.go
Normal file
95
internal/adapter/buffer.go
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
60
internal/adapter/reader.go
Normal file
60
internal/adapter/reader.go
Normal 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
1
progress.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
78
|
||||||
36
web/index.html
Normal file
36
web/index.html
Normal 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>
|
||||||
Reference in New Issue
Block a user