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

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

View File

@@ -6,90 +6,94 @@ import (
"time"
)
// RingBuffer - потокобезопасный кольцевой буфер для байт GPIO
type RingBuffer struct {
mu sync.RWMutex
data []byte
writePtr int
count int // сколько байт записано (до заполнения буфера)
mu sync.RWMutex
data []byte
writePtr int
count int
lastWrite time.Time
}
// NewRingBuffer создает буфер заданного размера
func NewRingBuffer(size int) *RingBuffer {
return &RingBuffer{
data: make([]byte, size),
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) LastWriteTime() time.Time {
rb.mu.RLock()
defer rb.mu.RUnlock()
return rb.lastWrite
}
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)
res := 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])
copy(res, rb.data[start:])
copy(res[len(rb.data)-start:], rb.data[:rb.writePtr])
} else {
copy(result, rb.data[start:rb.writePtr])
copy(res, rb.data[start:rb.writePtr])
}
return result
return res
}
// 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),
"size": len(rb.data),
"filled": rb.count,
"last_write": rb.lastWrite,
}
}
func (rb *RingBuffer) IsAlive(timeout time.Duration) bool {
rb.mu.RLock()
defer rb.mu.RUnlock()
return time.Since(rb.lastWrite) < timeout
}

View File

@@ -1,60 +0,0 @@
// 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
}

4
start-server.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
cd /home/user/temp/golang || exit 1
./gpio-monitor-server -pipe /tmp/gpio_pipe

View File

@@ -1,36 +1,125 @@
<!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>
<meta charset="UTF-8">
<title>GPIO Monitor Dashboard</title>
<style>
body {
font-family: monospace;
background: #0b0f0c;
color: #00ff88;
padding: 20px;
}
.card {
border: 1px solid #00ff88;
padding: 12px;
margin-bottom: 12px;
border-radius: 8px;
}
.status-ok { color: #00ff88; }
.status-bad { color: #ff4444; }
.bits {
display: flex;
gap: 8px;
margin-top: 10px;
}
.bit {
width: 40px;
height: 40px;
border: 1px solid #00ff88;
display: flex;
align-items: center;
justify-content: center;
}
.bit.on {
background: #00ff88;
color: black;
}
.small {
opacity: 0.7;
font-size: 12px;
}
</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>
<h1>GPIO Monitor</h1>
<div class="card">
<div>PIPE STATUS: <span id="pipeStatus">...</span></div>
<div>DATA FLOW: <span id="dataFlow">...</span></div>
<div class="small" id="meta"></div>
</div>
<div class="card">
<div>LATEST BYTE: <span id="latest">--</span></div>
<div class="bits" id="bits"></div>
</div>
<div class="card">
<div>HISTORY (last 16 bytes)</div>
<div id="history"></div>
</div>
<script>
function renderBits(byte) {
const bits = document.getElementById("bits");
bits.innerHTML = "";
for (let i = 7; i >= 0; i--) {
const v = (byte >> i) & 1;
const div = document.createElement("div");
div.className = "bit" + (v ? " on" : "");
div.textContent = v;
bits.appendChild(div);
}
}
async function update() {
try {
const r = await fetch("/api/health");
const h = await r.json();
const pipeStatus = document.getElementById("pipeStatus");
const dataFlow = document.getElementById("dataFlow");
pipeStatus.textContent = h.status;
pipeStatus.className = h.status === "ok" ? "status-ok" : "status-bad";
dataFlow.textContent = h.last_data_ms + " ms idle";
document.getElementById("meta").textContent =
"uptime: " + Math.round(h.uptime_sec) + "s | filled: " + h.stats.filled;
const latest = h.latest;
document.getElementById("latest").textContent = latest;
renderBits(latest);
const r2 = await fetch("/api/history");
const hist = await r2.json();
document.getElementById("history").innerHTML =
hist.bytes.map(b => "0x" + b.toString(16).padStart(2,"0")).join(" ");
} catch(e) {
document.getElementById("pipeStatus").textContent = "SERVER DOWN";
}
}
setInterval(update, 500);
update();
</script>
</body>
</html>