From d785f4a481a82068d3c4da9c746eddeeb14bdeed Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 17 Jun 2026 16:35:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BE=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=B2=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 388 ++++++++++++++++++++------------------- cmd/server/web/logs.html | 66 ++++--- 2 files changed, 234 insertions(+), 220 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index eb6f502..f1673f6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -40,211 +40,213 @@ func writeJSON(w http.ResponseWriter, v any) { // HandleLogFiles - список доступных лог-файлов func (a *API) HandleLogFiles(w http.ResponseWriter, r *http.Request) { - dataDir, err := logger.GetDataLogsDir() - if err != nil { - http.Error(w, err.Error(), 500) - return - } + dataDir, err := logger.GetDataLogsDir() + if err != nil { + http.Error(w, err.Error(), 500) + return + } - files, err := filepath.Glob(filepath.Join(dataDir, "gpio-*.bin")) - if err != nil { - http.Error(w, err.Error(), 500) - return - } + files, err := filepath.Glob(filepath.Join(dataDir, "gpio-*.bin")) + if err != nil { + http.Error(w, err.Error(), 500) + return + } - type LogFileInfo struct { - Name string `json:"name"` - Path string `json:"path"` - Size int64 `json:"size"` - ModTime time.Time `json:"mod_time"` - Time string `json:"time"` - Date string `json:"date"` - Hour int `json:"hour"` - IsActive bool `json:"is_active"` - } + type LogFileInfo struct { + Name string `json:"name"` + Path string `json:"path"` + Size int64 `json:"size"` + ModTime time.Time `json:"mod_time"` + Time string `json:"time"` + Date string `json:"date"` + Hour int `json:"hour"` + IsActive bool `json:"is_active"` + } - var result []LogFileInfo - for _, file := range files { - info, err := os.Stat(file) - if err != nil { - continue - } + var result []LogFileInfo + for _, file := range files { + info, err := os.Stat(file) + if err != nil { + continue + } - // Парсим имя файла: gpio-2026-06-16-11.bin - base := filepath.Base(file) - parts := strings.Split(strings.TrimSuffix(base, ".bin"), "-") + base := filepath.Base(file) + parts := strings.Split(strings.TrimSuffix(base, ".bin"), "-") - var fileTime time.Time - if len(parts) == 5 { - year, _ := strconv.Atoi(parts[1]) - month, _ := strconv.Atoi(parts[2]) - day, _ := strconv.Atoi(parts[3]) - hour, _ := strconv.Atoi(parts[4]) - fileTime = time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.Local) - } else { - fileTime = info.ModTime() - } + var fileTime time.Time + if len(parts) == 5 { + year, _ := strconv.Atoi(parts[1]) + month, _ := strconv.Atoi(parts[2]) + day, _ := strconv.Atoi(parts[3]) + hour, _ := strconv.Atoi(parts[4]) + fileTime = time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.Local) + } else { + fileTime = info.ModTime() + } - result = append(result, LogFileInfo{ - Name: base, - Path: file, - Size: info.Size(), - ModTime: info.ModTime(), - Time: fileTime.Format("02.01.2006 15:00"), - Date: fileTime.Format("2006-01-02"), - Hour: fileTime.Hour(), - IsActive: false, - }) - } + result = append(result, LogFileInfo{ + Name: base, + Path: file, + Size: info.Size(), + ModTime: info.ModTime(), + Time: fileTime.Format("02.01.2006 15:00"), + Date: fileTime.Format("2006-01-02"), + Hour: fileTime.Hour(), + IsActive: false, + }) + } - // Сортируем по времени (новые сверху) - sort.Slice(result, func(i, j int) bool { - return result[i].ModTime.After(result[j].ModTime) - }) + // Сортируем по времени (новые сверху) + sort.Slice(result, func(i, j int) bool { + return result[i].ModTime.After(result[j].ModTime) + }) - writeJSON(w, result) + writeJSON(w, result) +} + +// HandleLogEvents - чтение событий +func (a *API) HandleLogEvents(w http.ResponseWriter, r *http.Request) { + limit := 100 + if l := r.URL.Query().Get("limit"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil { + limit = parsed + } + } + + eventsPath, err := logger.GetEventHumanLogPath() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + content, err := os.ReadFile(eventsPath) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + lines := strings.Split(string(content), "\n") + // Берем последние N строк + if len(lines) > limit+1 { + lines = lines[len(lines)-limit-1:] + } + + // Парсим строки для JSON + type Event struct { + Time string `json:"time"` + Event string `json:"event"` + } + + var events []Event + for _, line := range lines { + if line == "" { + continue + } + // Формат: [2026-06-16 11:23:45.123] EVENT: СЕРВЕР_ЗАПУЩЕН + parts := strings.SplitN(line, "] EVENT: ", 2) + if len(parts) == 2 { + events = append(events, Event{ + Time: strings.TrimPrefix(parts[0], "["), + Event: parts[1], + }) + } + } + + writeJSON(w, map[string]any{ + "events": events, + "total": len(events), + }) } // HandleLogData - чтение данных из конкретного bin файла func (a *API) HandleLogData(w http.ResponseWriter, r *http.Request) { - filename := r.URL.Query().Get("file") - if filename == "" { - http.Error(w, "missing file parameter", 400) - return - } + filename := r.URL.Query().Get("file") + if filename == "" { + http.Error(w, "отсутствует параметр file", 400) + return + } - // Защита от path traversal - filename = filepath.Base(filename) - dataDir, err := logger.GetDataLogsDir() - if err != nil { - http.Error(w, err.Error(), 500) - return - } + // Защита от path traversal + filename = filepath.Base(filename) + dataDir, err := logger.GetDataLogsDir() + if err != nil { + http.Error(w, err.Error(), 500) + return + } - filePath := filepath.Join(dataDir, filename) - if _, err := os.Stat(filePath); os.IsNotExist(err) { - http.Error(w, "file not found", 404) - return - } + filePath := filepath.Join(dataDir, filename) + if _, err := os.Stat(filePath); os.IsNotExist(err) { + http.Error(w, "файл не найден", 404) + return + } - // Читаем бинарный файл - f, err := os.Open(filePath) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - defer f.Close() + // Читаем бинарный файл + f, err := os.Open(filePath) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + defer f.Close() type Sample struct { - Timestamp int64 `json:"ts"` - Time string `json:"time"` - Value byte `json:"value"` - Count byte `json:"count"` - Strength byte `json:"strength"` + Timestamp int64 `json:"ts"` + Time string `json:"time"` + Value byte `json:"value"` + Count byte `json:"count"` + Strength byte `json:"strength"` StrengthName string `json:"strength_name"` + Amplitude byte `json:"amplitude"` // Старшие 2 бита (0-3) + Signal byte `json:"signal"` // Младшие 6 бит (0-63) } - var samples []Sample + var samples []Sample buf := make([]byte, 9) // 8 байт timestamp + 1 байт value - for { - n, err := f.Read(buf) - if err == io.EOF { - break - } - if err != nil { - http.Error(w, err.Error(), 500) - return - } - if n < 9 { - continue - } + for { + n, err := f.Read(buf) + if err == io.EOF { + break + } + if err != nil { + http.Error(w, err.Error(), 500) + return + } + if n < 9 { + continue + } - ts := int64(binary.LittleEndian.Uint64(buf[0:8])) + ts := int64(binary.LittleEndian.Uint64(buf[0:8])) value := buf[8] data := logger.ParseGPIO(value) samples = append(samples, Sample{ - Timestamp: ts, - Time: time.UnixMicro(ts).Format("2006-01-02 15:04:05.000"), - Value: value, - Count: data.Count, - Strength: data.Strength, + Timestamp: ts, + Time: time.UnixMicro(ts).Format("02.01.2006 15:04:05"), + Value: value, + Count: data.Count, + Strength: data.Strength, StrengthName: logger.GetStrengthName(data.Strength), + Amplitude: (value >> 6) & 0x3, // Старшие 2 бита (0-3) - АМПЛИТУДА + Signal: value & 0x3F, // Младшие 6 бит (0-63) - СИГНАЛ }) - } + } - // Ограничиваем количество точек для производительности - if len(samples) > 10000 { - // Берем каждую N-ю точку - step := len(samples) / 10000 - var filtered []Sample - for i := 0; i < len(samples); i += step { - filtered = append(filtered, samples[i]) - } - samples = filtered - } + // Ограничиваем количество точек для производительности + if len(samples) > 10000 { + step := len(samples) / 10000 + var filtered []Sample + for i := 0; i < len(samples); i += step { + filtered = append(filtered, samples[i]) + } + samples = filtered + } - writeJSON(w, map[string]any{ - "filename": filename, - "total": len(samples), - "samples": samples, - }) -} - -// HandleLogEvents - чтение событий -func (a *API) HandleLogEvents(w http.ResponseWriter, r *http.Request) { - limit := 100 - if l := r.URL.Query().Get("limit"); l != "" { - if parsed, err := strconv.Atoi(l); err == nil { - limit = parsed - } - } - - eventsPath, err := logger.GetEventHumanLogPath() - if err != nil { - http.Error(w, err.Error(), 500) - return - } - - content, err := os.ReadFile(eventsPath) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - - lines := strings.Split(string(content), "\n") - // Берем последние N строк - if len(lines) > limit+1 { - lines = lines[len(lines)-limit-1:] - } - - // Парсим строки для JSON - type Event struct { - Time string `json:"time"` - Event string `json:"event"` - } - - var events []Event - for _, line := range lines { - if line == "" { - continue - } - // Формат: [2026-06-16 11:23:45.123] EVENT: СЕРВЕР_ЗАПУЩЕН - parts := strings.SplitN(line, "] EVENT: ", 2) - if len(parts) == 2 { - events = append(events, Event{ - Time: strings.TrimPrefix(parts[0], "["), - Event: parts[1], - }) - } - } - - writeJSON(w, map[string]any{ - "events": events, - "total": len(events), - }) + writeJSON(w, map[string]any{ + "filename": filename, + "total": len(samples), + "samples": samples, + }) } func (a *API) HandleHealth(w http.ResponseWriter, r *http.Request) { @@ -313,12 +315,12 @@ func (a *API) HandleStream(w http.ResponseWriter, r *http.Request) { // Прокси для MJPEG потока — просто ретранслирует готовый поток из go2rtc func handleCamProxy(w http.ResponseWriter, r *http.Request) { - log.Printf("[cam] proxy request from %s", r.RemoteAddr) + log.Printf("[cam] запрос от %s", r.RemoteAddr) resp, err := http.Get("http://127.0.0.1:1984/api/stream.mjpeg?src=cam_mjpeg") if err != nil { - log.Printf("[cam] go2rtc unavailable: %v", err) - http.Error(w, "camera unavailable", 503) + log.Printf("[cam] go2rtc недоступен: %v", err) + http.Error(w, "камера недоступна", 503) return } defer resp.Body.Close() @@ -335,7 +337,7 @@ func handleCamProxy(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { - http.Error(w, "stream unsupported", 500) + http.Error(w, "поток не поддерживается", 500) return } @@ -346,14 +348,14 @@ func handleCamProxy(w http.ResponseWriter, r *http.Request) { if n > 0 { _, err = w.Write(buf[:n]) if err != nil { - log.Printf("[cam] client disconnected") + log.Printf("[cam] клиент отключился") return } flusher.Flush() } if err != nil { if err != io.EOF { - log.Printf("[cam] stream ended: %v", err) + log.Printf("[cam] поток завершен: %v", err) } return } @@ -429,7 +431,7 @@ func main() { // Логируем конфигурацию log.Printf("=== КОНФИГУРАЦИЯ ===") - log.Printf("Retention: %d часов, %d MB (проверка каждые %d мин)", + log.Printf("Хранение: %d часов, %d MB (проверка каждые %d мин)", *retentionHours, *retentionMB, *retentionIntervalMin) log.Printf("Ротация: проверка каждые %d мин", *rotationCheckIntervalMin) log.Printf("Буфер: %d элементов", *bufferSize) @@ -514,11 +516,11 @@ func main() { // Передаем URL камеры из конфигурации handleCamProxyWithURL(w, r, *cameraURL) case "/log/files": - cors(api.HandleLogFiles)(w, r) - case "/log/data": - cors(api.HandleLogData)(w, r) - case "/log/events": - cors(api.HandleLogEvents)(w, r) + cors(api.HandleLogFiles)(w, r) + case "/log/data": + cors(api.HandleLogData)(w, r) + case "/log/events": + cors(api.HandleLogEvents)(w, r) default: http.Error(w, "not found", 404) } @@ -531,19 +533,19 @@ func main() { } http.Handle("/", http.FileServer(http.FS(webFS))) - log.Printf("Server started on %s", *serverPort) - log.Printf("Camera proxy available at /api/cam (source: %s)", *cameraURL) + log.Printf("Сервер запущен на %s", *serverPort) + log.Printf("Прокси камеры доступен по адресу /api/cam (источник: %s)", *cameraURL) log.Fatal(http.ListenAndServe(*serverPort, nil)) } // handleCamProxyWithURL проксирует MJPEG поток с указанным URL func handleCamProxyWithURL(w http.ResponseWriter, r *http.Request, cameraURL string) { - log.Printf("[cam] proxy request from %s to %s", r.RemoteAddr, cameraURL) + log.Printf("[cam] запрос от %s к %s", r.RemoteAddr, cameraURL) resp, err := http.Get(cameraURL) if err != nil { - log.Printf("[cam] camera unavailable: %v", err) - http.Error(w, "camera unavailable", 503) + log.Printf("[cam] камера недоступна: %v", err) + http.Error(w, "камера недоступна", 503) return } defer resp.Body.Close() @@ -560,7 +562,7 @@ func handleCamProxyWithURL(w http.ResponseWriter, r *http.Request, cameraURL str flusher, ok := w.(http.Flusher) if !ok { - http.Error(w, "stream unsupported", 500) + http.Error(w, "поток не поддерживается", 500) return } @@ -571,14 +573,14 @@ func handleCamProxyWithURL(w http.ResponseWriter, r *http.Request, cameraURL str if n > 0 { _, err = w.Write(buf[:n]) if err != nil { - log.Printf("[cam] client disconnected") + log.Printf("[cam] клиент отключился") return } flusher.Flush() } if err != nil { if err != io.EOF { - log.Printf("[cam] stream ended: %v", err) + log.Printf("[cam] поток завершен: %v", err) } return } diff --git a/cmd/server/web/logs.html b/cmd/server/web/logs.html index 097cfcf..a1314bb 100644 --- a/cmd/server/web/logs.html +++ b/cmd/server/web/logs.html @@ -166,7 +166,6 @@
-
@@ -178,10 +177,6 @@
-
-
Выберите файл для просмотра статистики
-
-
Загрузка...
@@ -262,25 +257,29 @@ function renderData(data) { // Статистика if (samples.length > 0) { - const values = samples.map(s => s.value); - const counts = samples.map(s => s.count); - const max = Math.max(...values); - const min = Math.min(...values); - const avg = values.reduce((a,b) => a + b, 0) / values.length; - const totalObjects = counts.reduce((a,b) => a + b, 0); + const totalPoints = samples.length; + + // Амплитуда > 0 (старшие 2 бита) + const amplitudeOver0Count = samples.filter(s => s.amplitude > 0).length; + + // Сигнал > 10 (младшие 6 бит) + const signalOver10Count = samples.filter(s => s.signal > 10).length; + + // Общее количество объектов (сумма всех сигналов) + const totalObjects = samples.reduce((sum, s) => sum + s.signal, 0); logStats.innerHTML = `
-
${samples.length}
+
${totalPoints}
Всего точек
-
${max}
-
Максимум
+
${amplitudeOver0Count}
+
Амплитуда > 0
-
${avg.toFixed(1)}
-
Среднее
+
${signalOver10Count}
+
Сигнал > 10
${totalObjects}
@@ -291,29 +290,35 @@ function renderData(data) { // График if (logChart && samples.length > 0) { - const history = samples.map(s => s.count); + const history = samples.map(s => s.signal); // Используем signal вместо count drawChart(logChart, history); } - // Таблица — только Время, Объектов, Сила + // Таблица - убрали колонку "Объектов" if (samples.length > 0) { let html = ` - - + + `; const displaySamples = samples.slice(-200); displaySamples.forEach(s => { - const strengthClass = `strength-${s.strength}`; + // Цвет в зависимости от амплитуды + let ampClass = ''; + if (s.amplitude === 0) ampClass = 'strength-0'; + else if (s.amplitude === 1) ampClass = 'strength-1'; + else if (s.amplitude === 2) ampClass = 'strength-2'; + else if (s.amplitude === 3) ampClass = 'strength-3'; + html += ` - - + + `; }); @@ -360,10 +365,17 @@ document.querySelectorAll('.tab-btn[data-tab]').forEach(btn => { // Инициализация loadFileList(); -// Обновление времени -function updateTime() { - const now = new Date(); - document.getElementById('server-time').textContent = now.toLocaleTimeString(); +// Обновление времени с сервера (единый формат) +async function updateTime() { + try { + const response = await fetch('/api/health'); + const data = await response.json(); + document.getElementById('server-time').textContent = data.server_time || new Date().toLocaleTimeString(); + } catch (error) { + // Если сервер недоступен, используем локальное время + const now = new Date(); + document.getElementById('server-time').textContent = now.toLocaleTimeString(); + } } setInterval(updateTime, 1000); updateTime();
ВремяОбъектовСилаАмплитудаСигнал
${s.time}${s.count}${s.strength_name}${s.amplitude}${s.signal}