Первая итерация заработала
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
4
start-server.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd /home/user/temp/golang || exit 1
|
||||
|
||||
./gpio-monitor-server -pipe /tmp/gpio_pipe
|
||||
147
web/index.html
147
web/index.html
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user