Compare commits
3 Commits
5d2d37fc7b
...
d785f4a481
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d785f4a481 | ||
|
|
580b78e5c0 | ||
|
|
30923eb9ce |
@@ -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
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ h1 {
|
||||
|
||||
.fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
width: 0;
|
||||
background: #e6852d;
|
||||
box-shadow:
|
||||
inset 0 0 4px rgba(255,255,255,0.2),
|
||||
@@ -177,7 +177,7 @@ h1 {
|
||||
.fill.no-connection {
|
||||
background: #555555 !important;
|
||||
box-shadow: none !important;
|
||||
width: 0% !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
/* ===== SIGNAL ICON ===== */
|
||||
@@ -194,7 +194,7 @@ h1 {
|
||||
flex: 1;
|
||||
border-radius: 2px;
|
||||
min-width: 5px;
|
||||
border: 1.5px solid #e6852d;
|
||||
border: 2px solid #e6852d;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
@@ -22,17 +22,21 @@ export class AudioEngine {
|
||||
/**
|
||||
* Инициализация AudioContext (требует взаимодействия пользователя)
|
||||
*/
|
||||
private initAudio(): boolean {
|
||||
private async initAudio(): Promise<boolean> {
|
||||
try {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
const AudioContextClass = (window as any).AudioContext || (window as any).webkitAudioContext;
|
||||
this.audioContext = new AudioContextClass();
|
||||
}
|
||||
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
const context = this.audioContext;
|
||||
if (!context) return false;
|
||||
|
||||
if (context.state === 'suspended') {
|
||||
await context.resume();
|
||||
}
|
||||
|
||||
return this.audioContext.state === 'running';
|
||||
return context.state === 'running';
|
||||
} catch (error) {
|
||||
console.warn('[Audio] Ошибка инициализации:', error);
|
||||
return false;
|
||||
@@ -104,18 +108,26 @@ export class AudioEngine {
|
||||
* Обновление уровня сигнала
|
||||
* @param level - уровень сигнала (0-3)
|
||||
*/
|
||||
public updateLevel(level: number): void {
|
||||
public async updateLevel(level: number): Promise<void> {
|
||||
// Инициализация при первом вызове
|
||||
if (!this.audioContext) {
|
||||
if (!this.initAudio()) {
|
||||
const initialized = await this.initAudio();
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const context = this.audioContext;
|
||||
if (!context) return;
|
||||
|
||||
// Если AudioContext не готов, пробуем восстановить
|
||||
if (this.audioContext?.state !== 'running') {
|
||||
this.audioContext?.resume();
|
||||
return;
|
||||
if (context.state !== 'running') {
|
||||
try {
|
||||
await context.resume();
|
||||
} catch (error) {
|
||||
console.warn('[Audio] Ошибка возобновления контекста:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Приводим уровень к числу
|
||||
@@ -260,9 +272,14 @@ export class AudioEngine {
|
||||
console.log('[Audio] Звук остановлен');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ПУБЛИЧНЫЕ МЕТОДЫ
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Проверка, активен ли звук
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public isActive(): boolean {
|
||||
return this.isPlaying && this.currentLevel > 0;
|
||||
}
|
||||
@@ -270,6 +287,7 @@ export class AudioEngine {
|
||||
/**
|
||||
* Получение текущей частоты
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public getCurrentFrequency(): number {
|
||||
return this.currentFrequency;
|
||||
}
|
||||
@@ -277,6 +295,7 @@ export class AudioEngine {
|
||||
/**
|
||||
* Получение текущего уровня
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public getCurrentLevel(): number {
|
||||
return this.currentLevel;
|
||||
}
|
||||
@@ -284,11 +303,12 @@ export class AudioEngine {
|
||||
/**
|
||||
* Полная остановка и освобождение ресурсов
|
||||
*/
|
||||
public dispose(): void {
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public async dispose(): Promise<void> {
|
||||
this.stopSound();
|
||||
if (this.audioContext) {
|
||||
try {
|
||||
this.audioContext.close();
|
||||
await this.audioContext.close();
|
||||
} catch (e) {
|
||||
// Игнорируем
|
||||
}
|
||||
@@ -300,5 +320,6 @@ export class AudioEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем одиночный экземпляр
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export const audioEngine = new AudioEngine();
|
||||
@@ -1,23 +1,18 @@
|
||||
// camera.ts
|
||||
import { updateResizerVisibility } from './layout.js';
|
||||
import { updateResizerVisibility, openRightPanel, closeRightPanel } from './layout.js';
|
||||
|
||||
let rightPanel: HTMLElement | null = null;
|
||||
let camImage: HTMLImageElement | null = null;
|
||||
let crosshair: HTMLElement | null = null;
|
||||
let isOpen: boolean = false;
|
||||
let streamCheckInterval: number | null = null;
|
||||
|
||||
interface StreamResponse {
|
||||
cam: string;
|
||||
available: boolean;
|
||||
source: string;
|
||||
}
|
||||
let resizer: HTMLElement | null = null;
|
||||
|
||||
export function initCamera(): void {
|
||||
rightPanel = document.getElementById("rightPanel");
|
||||
camImage = document.getElementById("camImage") as HTMLImageElement;
|
||||
crosshair = document.getElementById("crosshair");
|
||||
|
||||
resizer = document.getElementById("dragBar");
|
||||
|
||||
const btn = document.getElementById("camToggle");
|
||||
|
||||
@@ -28,9 +23,8 @@ export function initCamera(): void {
|
||||
|
||||
isOpen = true;
|
||||
|
||||
if (rightPanel) {
|
||||
rightPanel.classList.remove("hidden");
|
||||
}
|
||||
// Открываем правую панель через layout
|
||||
openRightPanel();
|
||||
|
||||
// Показываем перекрестие
|
||||
if (crosshair) {
|
||||
@@ -44,13 +38,15 @@ export function initCamera(): void {
|
||||
|
||||
if (btn) {
|
||||
const textSpan = btn.querySelector('.cam-btn-text');
|
||||
if (textSpan) textSpan.textContent = 'Скрыть';
|
||||
if (textSpan) {
|
||||
textSpan.textContent = 'Скрыть';
|
||||
} else {
|
||||
btn.innerHTML = '📷 Скрыть';
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[camera] opened");
|
||||
|
||||
updateResizerVisibility();
|
||||
|
||||
// Запускаем мониторинг потока
|
||||
startStreamMonitoring();
|
||||
}
|
||||
@@ -65,24 +61,25 @@ export function initCamera(): void {
|
||||
crosshair.style.display = "none";
|
||||
}
|
||||
|
||||
// остановка stream
|
||||
// Останавливаем поток
|
||||
if (camImage) {
|
||||
camImage.src = "";
|
||||
}
|
||||
|
||||
if (rightPanel) {
|
||||
rightPanel.classList.add("hidden");
|
||||
}
|
||||
// Закрываем правую панель через layout
|
||||
closeRightPanel();
|
||||
|
||||
if (btn) {
|
||||
const textSpan = btn.querySelector('.cam-btn-text');
|
||||
if (textSpan) textSpan.textContent = 'Камера';
|
||||
if (textSpan) {
|
||||
textSpan.textContent = 'Камера';
|
||||
} else {
|
||||
btn.innerHTML = '📷 Камера';
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[camera] closed");
|
||||
|
||||
updateResizerVisibility();
|
||||
|
||||
// Останавливаем мониторинг
|
||||
stopStreamMonitoring();
|
||||
}
|
||||
@@ -139,7 +136,15 @@ export function initCamera(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем для возможности ручного управления
|
||||
// ============================================
|
||||
// ПУБЛИЧНЫЕ ФУНКЦИИ (используются в других модулях)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Показать/скрыть перекрестие на видео
|
||||
* @param show - показывать или скрыть
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function showCrosshair(show: boolean = true): void {
|
||||
const crosshairElem = document.getElementById("crosshair");
|
||||
if (crosshairElem) {
|
||||
@@ -147,9 +152,14 @@ export function showCrosshair(show: boolean = true): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить прозрачность перекрестия
|
||||
* @param opacity - значение прозрачности (0-1)
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function setCrosshairOpacity(opacity: string | number): void {
|
||||
const crosshairElem = document.getElementById("crosshair");
|
||||
if (crosshairElem) {
|
||||
crosshairElem.style.opacity = String(opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +1,138 @@
|
||||
// chart.js
|
||||
export function drawChart(canvas: HTMLCanvasElement, history: number[]): void {
|
||||
if (!canvas || !history?.length) return;
|
||||
if (!canvas || !history?.length) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const displayHeight = rect.height || 200;
|
||||
const displayWidth = rect.width || 800;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const displayHeight = rect.height || 200;
|
||||
canvas.width = rect.width || 800;
|
||||
canvas.height = displayHeight;
|
||||
|
||||
canvas.width = displayWidth;
|
||||
canvas.height = displayHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const padLeft = 45;
|
||||
const padRight = 15;
|
||||
const padTop = 10;
|
||||
const padBottom = 25;
|
||||
const plotW = w - padLeft - padRight;
|
||||
const plotH = h - padTop - padBottom;
|
||||
|
||||
const padLeft = 45;
|
||||
const padRight = 15;
|
||||
const padTop = 10;
|
||||
const padBottom = 25;
|
||||
const plotW = w - padLeft - padRight;
|
||||
const plotH = h - padTop - padBottom;
|
||||
// Фон
|
||||
ctx.fillStyle = "#0a0f0a";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
// Фон
|
||||
ctx.fillStyle = "#0a0f0a";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
// Сетка
|
||||
const maxVal = 63;
|
||||
const gridLines = 4;
|
||||
|
||||
// Сетка
|
||||
const maxVal = 63;
|
||||
const gridLines = 4;
|
||||
ctx.strokeStyle = "#0d2a0d";
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.setLineDash([4, 4]);
|
||||
|
||||
ctx.strokeStyle = "#0d2a0d";
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.setLineDash([4, 4]);
|
||||
for (let i = 0; i <= gridLines; i++) {
|
||||
const val = (maxVal / gridLines) * i;
|
||||
const y = padTop + plotH - (val / maxVal) * plotH;
|
||||
|
||||
for (let i = 0; i <= gridLines; i++) {
|
||||
const val = (maxVal / gridLines) * i;
|
||||
const y = padTop + plotH - (val / maxVal) * plotH;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padLeft, y);
|
||||
ctx.lineTo(w - padRight, y);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padLeft, y);
|
||||
ctx.lineTo(w - padRight, y);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = "#00ff88";
|
||||
ctx.font = "11px monospace";
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText(Math.round(val).toString(), padLeft - 8, y + 4);
|
||||
}
|
||||
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Подписи осей
|
||||
ctx.fillStyle = "#00ff88";
|
||||
ctx.font = "bold 11px monospace";
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(12, padTop + plotH / 2);
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("ЗНАЧЕНИЕ", 0, 0);
|
||||
ctx.restore();
|
||||
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("БУФЕР ПРИНЯТЫХ ДАННЫХ", padLeft + plotW / 2, h - 4);
|
||||
|
||||
const hasSignal = history.some(v => v > 0);
|
||||
|
||||
if (!hasSignal) {
|
||||
ctx.fillStyle = "#ff4444";
|
||||
ctx.font = "14px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("Ожидание сигнала...", w / 2, h / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
let plotData = history;
|
||||
if (history.length > plotW) {
|
||||
const step = history.length / plotW;
|
||||
plotData = [];
|
||||
for (let i = 0; i < plotW; i++) {
|
||||
const idx = Math.floor(i * step);
|
||||
plotData.push(history[idx]);
|
||||
ctx.fillStyle = "#00ff88";
|
||||
ctx.font = "11px monospace";
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText(Math.round(val).toString(), padLeft - 8, y + 4);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.strokeStyle = "#e6852d";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.shadowColor = "#e6852d";
|
||||
ctx.shadowBlur = 3;
|
||||
|
||||
ctx.beginPath();
|
||||
const stepX = plotW / Math.max(plotData.length - 1, 1);
|
||||
|
||||
for (let i = 0; i < plotData.length; i++) {
|
||||
const x = padLeft + i * stepX;
|
||||
const y = padTop + plotH - (plotData[i] / maxVal) * plotH;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
if (history.length > 0) {
|
||||
const lastVal = history[history.length - 1];
|
||||
const lastX = padLeft + (plotData.length - 1) * stepX;
|
||||
const lastY = padTop + plotH - (lastVal / maxVal) * plotH;
|
||||
|
||||
ctx.fillStyle = "#c46b1f";
|
||||
ctx.beginPath();
|
||||
ctx.arc(lastX, lastY, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
const labelText = `${lastVal}`;
|
||||
const labelWidth = ctx.measureText(labelText).width;
|
||||
|
||||
ctx.fillStyle = "rgba(0,0,0,0.8)";
|
||||
ctx.fillRect(lastX - labelWidth / 2 - 4, lastY - 22, labelWidth + 8, 18);
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Подписи осей
|
||||
ctx.fillStyle = "#00ff88";
|
||||
ctx.font = "bold 12px monospace";
|
||||
ctx.font = "bold 11px monospace";
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(12, padTop + plotH / 2);
|
||||
ctx.rotate(-Math.PI / 2);
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(labelText, lastX, lastY - 8);
|
||||
ctx.fillText("ЗНАЧЕНИЕ", 0, 0);
|
||||
ctx.restore();
|
||||
|
||||
const peak = Math.max(...history);
|
||||
const min = Math.min(...history);
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("БУФЕР ПРИНЯТЫХ ДАННЫХ", padLeft + plotW / 2, h - 4);
|
||||
|
||||
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||
ctx.fillRect(w - 120, padTop, 110, 42);
|
||||
const hasSignal = history.some(v => v > 0);
|
||||
|
||||
ctx.fillStyle = "#00ff88";
|
||||
ctx.font = "10px monospace";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(`Пик: ${peak}`, w - 110, padTop + 14);
|
||||
ctx.fillText(`Мин: ${min}`, w - 110, padTop + 28);
|
||||
ctx.fillText(`Точек: ${history.length}`, w - 110, padTop + 42);
|
||||
}
|
||||
if (!hasSignal) {
|
||||
ctx.fillStyle = "#ff4444";
|
||||
ctx.font = "14px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("Ожидание сигнала...", w / 2, h / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
let plotData = history;
|
||||
if (history.length > plotW) {
|
||||
const step = history.length / plotW;
|
||||
plotData = [];
|
||||
for (let i = 0; i < plotW; i++) {
|
||||
const idx = Math.floor(i * step);
|
||||
plotData.push(history[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.strokeStyle = "#e6852d";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.shadowColor = "#e6852d";
|
||||
ctx.shadowBlur = 3;
|
||||
|
||||
ctx.beginPath();
|
||||
const stepX = plotW / Math.max(plotData.length - 1, 1);
|
||||
|
||||
for (let i = 0; i < plotData.length; i++) {
|
||||
const x = padLeft + i * stepX;
|
||||
const y = padTop + plotH - (plotData[i] / maxVal) * plotH;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
if (history.length > 0) {
|
||||
const lastVal = history[history.length - 1];
|
||||
const lastX = padLeft + (plotData.length - 1) * stepX;
|
||||
const lastY = padTop + plotH - (lastVal / maxVal) * plotH;
|
||||
|
||||
ctx.fillStyle = "#c46b1f";
|
||||
ctx.beginPath();
|
||||
ctx.arc(lastX, lastY, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
const labelText = `${lastVal}`;
|
||||
const labelWidth = ctx.measureText(labelText).width;
|
||||
|
||||
ctx.fillStyle = "rgba(0,0,0,0.8)";
|
||||
ctx.fillRect(lastX - labelWidth / 2 - 4, lastY - 22, labelWidth + 8, 18);
|
||||
|
||||
ctx.fillStyle = "#00ff88";
|
||||
ctx.font = "bold 12px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(labelText, lastX, lastY - 8);
|
||||
|
||||
const peak = Math.max(...history);
|
||||
const min = Math.min(...history);
|
||||
|
||||
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||
ctx.fillRect(w - 120, padTop, 110, 42);
|
||||
|
||||
ctx.fillStyle = "#00ff88";
|
||||
ctx.font = "10px monospace";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(`Пик: ${peak}`, w - 110, padTop + 14);
|
||||
ctx.fillText(`Мин: ${min}`, w - 110, padTop + 28);
|
||||
ctx.fillText(`Точек: ${history.length}`, w - 110, padTop + 42);
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,236 @@
|
||||
// layout.ts
|
||||
let resizer: HTMLElement | null = null;
|
||||
let leftPanel: HTMLElement | null = null;
|
||||
let rightPanel: HTMLElement | null = null;
|
||||
let dragBar: HTMLElement | null = null;
|
||||
let isDragging: boolean = false;
|
||||
let pendingX: number | null = null;
|
||||
let isOpen: boolean = false;
|
||||
let lastLeftWidth: number = 0;
|
||||
let lastRightWidth: number = 0;
|
||||
|
||||
export function initResize(): void {
|
||||
resizer = document.getElementById("dragBar");
|
||||
leftPanel = document.getElementById("leftPanel");
|
||||
rightPanel = document.getElementById("rightPanel");
|
||||
dragBar = document.getElementById("dragBar");
|
||||
|
||||
if (!leftPanel || !rightPanel || !dragBar) return;
|
||||
|
||||
const savedRatio = localStorage.getItem('layoutRatio');
|
||||
if (savedRatio) {
|
||||
applyRatio(parseFloat(savedRatio));
|
||||
if (!resizer || !leftPanel || !rightPanel) {
|
||||
console.warn("[layout] Элементы не найдены");
|
||||
return;
|
||||
}
|
||||
|
||||
dragBar.addEventListener("mousedown", (e) => {
|
||||
if (rightPanel!.classList.contains("hidden")) return;
|
||||
// Проверяем видимость при инициализации
|
||||
updateResizerVisibility();
|
||||
|
||||
let isDragging = false;
|
||||
|
||||
// Обработчик начала перетаскивания
|
||||
resizer.addEventListener("mousedown", (e: MouseEvent) => {
|
||||
// Сохраняем ссылки на элементы в локальные переменные
|
||||
const left = leftPanel;
|
||||
const right = rightPanel;
|
||||
|
||||
// Проверяем, что элементы существуют
|
||||
if (!left || !right) {
|
||||
console.warn("[layout] Панели не найдены");
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что правая панель видима
|
||||
if (right.classList.contains("hidden")) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDragging = true;
|
||||
document.body.style.cursor = "col-resize";
|
||||
e.preventDefault();
|
||||
});
|
||||
document.body.style.userSelect = "none";
|
||||
|
||||
document.addEventListener("mouseup", () => {
|
||||
if (!isDragging) return;
|
||||
// Сохраняем начальные размеры
|
||||
const startX = e.clientX;
|
||||
const leftWidth = left.getBoundingClientRect().width;
|
||||
const rightWidth = right.getBoundingClientRect().width;
|
||||
const totalWidth = leftWidth + rightWidth;
|
||||
|
||||
isDragging = false;
|
||||
document.body.style.cursor = "default";
|
||||
function onMouseMove(moveEvent: MouseEvent): void {
|
||||
if (!isDragging) return;
|
||||
|
||||
if (rightPanel && !rightPanel.classList.contains("hidden")) {
|
||||
const total = window.innerWidth;
|
||||
const leftWidth = leftPanel!.getBoundingClientRect().width;
|
||||
localStorage.setItem('layoutRatio', String(leftWidth / total));
|
||||
// Проверяем, что элементы всё ещё существуют
|
||||
if (!left || !right) return;
|
||||
|
||||
const deltaX = moveEvent.clientX - startX;
|
||||
let newLeftWidth = leftWidth + deltaX;
|
||||
|
||||
// Ограничиваем минимальные размеры
|
||||
const minWidth = 200;
|
||||
newLeftWidth = Math.max(minWidth, Math.min(totalWidth - minWidth, newLeftWidth));
|
||||
|
||||
const newRightWidth = totalWidth - newLeftWidth;
|
||||
|
||||
left.style.flex = `0 0 ${newLeftWidth}px`;
|
||||
right.style.flex = `0 0 ${newRightWidth}px`;
|
||||
|
||||
// Сохраняем последние размеры
|
||||
lastLeftWidth = newLeftWidth;
|
||||
lastRightWidth = newRightWidth;
|
||||
}
|
||||
|
||||
function onMouseUp(): void {
|
||||
isDragging = false;
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
if (!isDragging) return;
|
||||
if (rightPanel && rightPanel.classList.contains("hidden")) return;
|
||||
|
||||
pendingX = e.clientX;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (pendingX === null) return;
|
||||
if (!leftPanel || !rightPanel || !dragBar) return;
|
||||
|
||||
const total = window.innerWidth;
|
||||
let leftWidth = pendingX;
|
||||
|
||||
const min = 300;
|
||||
if (leftWidth < min) leftWidth = min;
|
||||
if (leftWidth > total - min) leftWidth = total - min;
|
||||
|
||||
const rightWidth = total - leftWidth - dragBar.offsetWidth;
|
||||
|
||||
leftPanel.style.flex = `0 0 ${leftWidth}px`;
|
||||
rightPanel.style.flex = `0 0 ${rightWidth}px`;
|
||||
|
||||
pendingX = null;
|
||||
});
|
||||
// Обработчик изменения размера окна
|
||||
window.addEventListener("resize", () => {
|
||||
handleWindowResize();
|
||||
});
|
||||
}
|
||||
|
||||
function applyRatio(ratio: number): void {
|
||||
const total = window.innerWidth;
|
||||
const leftWidth = Math.floor(total * ratio);
|
||||
const rightWidth = total - leftWidth;
|
||||
/**
|
||||
* Обработка изменения размера окна
|
||||
*/
|
||||
function handleWindowResize(): void {
|
||||
if (!leftPanel || !rightPanel) return;
|
||||
|
||||
if (leftPanel && rightPanel) {
|
||||
leftPanel.style.flex = `0 0 ${leftWidth}px`;
|
||||
rightPanel.style.flex = `0 0 ${rightWidth}px`;
|
||||
// Если правая панель видима, пересчитываем размеры
|
||||
if (!rightPanel.classList.contains("hidden")) {
|
||||
// Если есть сохраненные размеры, используем их пропорционально
|
||||
if (lastLeftWidth > 0 && lastRightWidth > 0) {
|
||||
const totalWidth = window.innerWidth;
|
||||
const ratio = lastLeftWidth / (lastLeftWidth + lastRightWidth);
|
||||
const newLeftWidth = totalWidth * ratio;
|
||||
const newRightWidth = totalWidth - newLeftWidth;
|
||||
|
||||
// Ограничиваем минимальные размеры
|
||||
const minWidth = 200;
|
||||
const finalLeftWidth = Math.max(minWidth, Math.min(totalWidth - minWidth, newLeftWidth));
|
||||
const finalRightWidth = totalWidth - finalLeftWidth;
|
||||
|
||||
leftPanel.style.flex = `0 0 ${finalLeftWidth}px`;
|
||||
rightPanel.style.flex = `0 0 ${finalRightWidth}px`;
|
||||
|
||||
lastLeftWidth = finalLeftWidth;
|
||||
lastRightWidth = finalRightWidth;
|
||||
} else {
|
||||
// Если нет сохраненных размеров, делим пополам
|
||||
const totalWidth = window.innerWidth;
|
||||
const halfWidth = totalWidth / 2;
|
||||
leftPanel.style.flex = `0 0 ${halfWidth}px`;
|
||||
rightPanel.style.flex = `0 0 ${halfWidth}px`;
|
||||
|
||||
lastLeftWidth = halfWidth;
|
||||
lastRightWidth = halfWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление видимости разделителя в зависимости от состояния правой панели
|
||||
*/
|
||||
export function updateResizerVisibility(): void {
|
||||
if (!dragBar || !rightPanel || !leftPanel) return;
|
||||
if (!resizer || !rightPanel) return;
|
||||
|
||||
// Если правая панель скрыта - скрываем разделитель
|
||||
if (rightPanel.classList.contains("hidden")) {
|
||||
resizer.classList.add("hidden");
|
||||
} else {
|
||||
resizer.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть правую панель
|
||||
*/
|
||||
export function openRightPanel(): void {
|
||||
if (!leftPanel || !rightPanel) return;
|
||||
|
||||
// Убираем класс hidden
|
||||
rightPanel.classList.remove("hidden");
|
||||
|
||||
// Обновляем разделитель
|
||||
updateResizerVisibility();
|
||||
|
||||
// Устанавливаем размеры
|
||||
const totalWidth = window.innerWidth;
|
||||
const halfWidth = totalWidth / 2;
|
||||
|
||||
// Проверяем, есть ли сохраненные размеры
|
||||
if (lastLeftWidth > 0 && lastRightWidth > 0) {
|
||||
// Используем сохраненные пропорции
|
||||
const ratio = lastLeftWidth / (lastLeftWidth + lastRightWidth);
|
||||
const newLeftWidth = totalWidth * ratio;
|
||||
const newRightWidth = totalWidth - newLeftWidth;
|
||||
|
||||
const minWidth = 200;
|
||||
const finalLeftWidth = Math.max(minWidth, Math.min(totalWidth - minWidth, newLeftWidth));
|
||||
const finalRightWidth = totalWidth - finalLeftWidth;
|
||||
|
||||
leftPanel.style.flex = `0 0 ${finalLeftWidth}px`;
|
||||
rightPanel.style.flex = `0 0 ${finalRightWidth}px`;
|
||||
|
||||
lastLeftWidth = finalLeftWidth;
|
||||
lastRightWidth = finalRightWidth;
|
||||
} else {
|
||||
// Делим пополам
|
||||
leftPanel.style.flex = `0 0 ${halfWidth}px`;
|
||||
rightPanel.style.flex = `0 0 ${halfWidth}px`;
|
||||
|
||||
lastLeftWidth = halfWidth;
|
||||
lastRightWidth = halfWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрыть правую панель
|
||||
*/
|
||||
export function closeRightPanel(): void {
|
||||
if (!rightPanel) return;
|
||||
|
||||
// Добавляем класс hidden
|
||||
rightPanel.classList.add("hidden");
|
||||
|
||||
// Обновляем разделитель
|
||||
updateResizerVisibility();
|
||||
|
||||
// Сбрасываем flex для левой панели
|
||||
if (leftPanel) {
|
||||
leftPanel.style.flex = "1 1 auto";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ПУБЛИЧНЫЕ ФУНКЦИИ (используются в других модулях)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Установить видимость разделителя вручную
|
||||
* @param visible - показывать или скрыть
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function setResizerVisible(visible: boolean): void {
|
||||
if (resizer) {
|
||||
if (visible) {
|
||||
resizer.classList.remove("hidden");
|
||||
} else {
|
||||
resizer.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить состояние правой панели
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function toggleRightPanel(): void {
|
||||
if (!rightPanel) return;
|
||||
|
||||
if (rightPanel.classList.contains("hidden")) {
|
||||
dragBar.classList.add("hidden");
|
||||
leftPanel.style.flex = "1";
|
||||
openRightPanel();
|
||||
} else {
|
||||
dragBar.classList.remove("hidden");
|
||||
closeRightPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,192 +1,243 @@
|
||||
// state.ts - Peak Holder алгоритм с таймером
|
||||
|
||||
interface AppState {
|
||||
maxPoints: number;
|
||||
signalHistory: number[];
|
||||
lastBufferSize: number;
|
||||
maxPoints: number;
|
||||
signalHistory: number[];
|
||||
lastBufferSize: number;
|
||||
|
||||
// Peak Holder
|
||||
peakValue: number; // Текущее удерживаемое пиковое значение амплитуды
|
||||
currentValue: number; // Текущее реальное значение амплитуды
|
||||
peakTimer: number | null; // Таймер для сброса пика амплитуды
|
||||
peakHoldTime: number; // Время удержания пика в миллисекундах
|
||||
// Peak Holder
|
||||
peakValue: number; // Текущее удерживаемое пиковое значение амплитуды
|
||||
currentValue: number; // Текущее реальное значение амплитуды
|
||||
peakTimer: number | null; // Таймер для сброса пика амплитуды
|
||||
peakHoldTime: number; // Время удержания пика в миллисекундах
|
||||
|
||||
// Peak Holder для уровня сигнала
|
||||
peakSignalLevel: number;
|
||||
currentSignalLevel: number;
|
||||
signalPeakTimer: number | null;
|
||||
signalPeakHoldTime: number;
|
||||
// Peak Holder для уровня сигнала
|
||||
peakSignalLevel: number;
|
||||
currentSignalLevel: number;
|
||||
signalPeakTimer: number | null;
|
||||
signalPeakHoldTime: number;
|
||||
|
||||
hasPeak: boolean;
|
||||
hasPeak: boolean;
|
||||
}
|
||||
|
||||
type PeakHolderListener = () => void;
|
||||
|
||||
export const state: AppState = {
|
||||
maxPoints: 1000,
|
||||
signalHistory: [],
|
||||
lastBufferSize: 0,
|
||||
maxPoints: 1000,
|
||||
signalHistory: [],
|
||||
lastBufferSize: 0,
|
||||
|
||||
peakValue: 0,
|
||||
currentValue: 0,
|
||||
peakTimer: null,
|
||||
peakHoldTime: 3000, // 3 секунды
|
||||
peakValue: 0,
|
||||
currentValue: 0,
|
||||
peakTimer: null,
|
||||
peakHoldTime: 3000, // 3 секунды
|
||||
|
||||
peakSignalLevel: 0,
|
||||
currentSignalLevel: 0,
|
||||
signalPeakTimer: null,
|
||||
signalPeakHoldTime: 3000,
|
||||
peakSignalLevel: 0,
|
||||
currentSignalLevel: 0,
|
||||
signalPeakTimer: null,
|
||||
signalPeakHoldTime: 3000,
|
||||
|
||||
hasPeak: false,
|
||||
hasPeak: false,
|
||||
};
|
||||
|
||||
let peakHolderListener: PeakHolderListener | null = null;
|
||||
|
||||
/** Обновление UI при сбросе peak holder по таймеру (между опросами API). */
|
||||
export function setPeakHolderListener(listener: PeakHolderListener | null): void {
|
||||
peakHolderListener = listener;
|
||||
// ============================================
|
||||
// ПУБЛИЧНЫЕ ФУНКЦИИ
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Установка времени удержания пика сигнала
|
||||
* @param ms - время в миллисекундах
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function setSignalPeakHoldTime(ms: number): void {
|
||||
state.signalPeakHoldTime = ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о пике сигнала
|
||||
* @returns { peak: number; current: number; timerActive: boolean }
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function getSignalPeakInfo(): { peak: number; current: number; timerActive: boolean } {
|
||||
return {
|
||||
peak: state.peakSignalLevel,
|
||||
current: state.currentSignalLevel,
|
||||
timerActive: state.signalPeakTimer !== null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление нового значения сигнала в историю
|
||||
* @param value - значение сигнала
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function addSignal(value: number): void {
|
||||
state.signalHistory.push(value);
|
||||
if (state.signalHistory.length > state.maxPoints) {
|
||||
state.signalHistory = state.signalHistory.slice(-state.maxPoints);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Установка слушателя для обновления пика
|
||||
* @param listener - функция-слушатель
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function setPeakHolderListener(listener: PeakHolderListener | null): void {
|
||||
peakHolderListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление пикового значения амплитуды
|
||||
* @param target - текущее значение
|
||||
* @returns пиковое значение
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function updatePeakHolder(target: number): number {
|
||||
// Обновляем текущее значение
|
||||
state.currentValue = target;
|
||||
|
||||
// Если новое значение больше текущего пика
|
||||
if (target > state.peakValue) {
|
||||
// Устанавливаем новый пик
|
||||
state.peakValue = target;
|
||||
state.hasPeak = true;
|
||||
scheduleAmplitudePeakRelease();
|
||||
} else if (target < state.peakValue && state.peakTimer === null) {
|
||||
// Пик выше текущего значения, но таймер пропал — перезапускаем удержание
|
||||
scheduleAmplitudePeakRelease();
|
||||
} // Возвращаем текущий пик
|
||||
return state.peakValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление пикового уровня сигнала
|
||||
* @param level - текущий уровень
|
||||
* @returns пиковый уровень
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function updateSignalPeakHolder(level: number): number {
|
||||
// Обновляем текущее значение
|
||||
state.currentSignalLevel = level;
|
||||
|
||||
// Если новый уровень больше пика — устанавливаем новый пик
|
||||
if (level > state.peakSignalLevel) {
|
||||
state.peakSignalLevel = level;
|
||||
state.hasPeak = true;
|
||||
scheduleSignalPeakRelease();
|
||||
} else if (level < state.peakSignalLevel && state.signalPeakTimer === null) {
|
||||
scheduleSignalPeakRelease();
|
||||
}
|
||||
|
||||
// Возвращаем текущий пик
|
||||
return state.peakSignalLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительный сброс пика сигнала
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function forceResetSignalPeak(): void {
|
||||
if (state.signalPeakTimer !== null) {
|
||||
clearTimeout(state.signalPeakTimer);
|
||||
state.signalPeakTimer = null;
|
||||
}
|
||||
state.peakSignalLevel = 0;
|
||||
state.currentSignalLevel = 0;
|
||||
state.hasPeak = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительный сброс всех пиков
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function forceResetPeak(): void {
|
||||
if (state.peakTimer !== null) {
|
||||
clearTimeout(state.peakTimer);
|
||||
state.peakTimer = null;
|
||||
}
|
||||
state.peakValue = 0;
|
||||
state.currentValue = 0;
|
||||
state.hasPeak = false;
|
||||
forceResetSignalPeak();
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление массива значений сигнала
|
||||
* @param values - массив значений
|
||||
* @param bufferSize - размер буфера
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function addSignals(values: number[], bufferSize: number): void {
|
||||
if (!Array.isArray(values) || values.length === 0) return;
|
||||
|
||||
const newBytes = bufferSize - state.lastBufferSize;
|
||||
|
||||
if (newBytes > 0 && newBytes <= values.length) {
|
||||
const fresh = values.slice(-newBytes);
|
||||
const decoded = fresh.map(b => b & 0x3F);
|
||||
state.signalHistory.push(...decoded);
|
||||
}
|
||||
|
||||
state.lastBufferSize = bufferSize;
|
||||
|
||||
if (state.signalHistory.length > state.maxPoints) {
|
||||
state.signalHistory = state.signalHistory.slice(-state.maxPoints);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сброс отслеживания буфера
|
||||
*/
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export function resetBufferTracking(): void {
|
||||
state.lastBufferSize = 0;
|
||||
forceResetPeak();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ВНУТРЕННИЕ ФУНКЦИИ
|
||||
// ============================================
|
||||
|
||||
function notifyPeakHolderChange(): void {
|
||||
if (peakHolderListener) {
|
||||
peakHolderListener();
|
||||
}
|
||||
if (peakHolderListener) {
|
||||
peakHolderListener();
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleAmplitudePeakRelease(): void {
|
||||
// Сбрасываем старый таймер
|
||||
if (state.peakTimer !== null) {
|
||||
clearTimeout(state.peakTimer);
|
||||
state.peakTimer = null;
|
||||
}
|
||||
// Сбрасываем старый таймер
|
||||
if (state.peakTimer !== null) {
|
||||
clearTimeout(state.peakTimer);
|
||||
state.peakTimer = null;
|
||||
}
|
||||
|
||||
// Запускаем новый таймер на 3 секунды
|
||||
state.peakTimer = window.setTimeout(() => {
|
||||
state.peakValue = state.currentValue;
|
||||
state.peakTimer = null;
|
||||
state.hasPeak = false;
|
||||
console.log(`[Peak] Сброс пика до: ${state.peakValue}`);
|
||||
notifyPeakHolderChange();
|
||||
}, state.peakHoldTime);
|
||||
// Запускаем новый таймер на 3 секунды
|
||||
state.peakTimer = window.setTimeout(() => {
|
||||
state.peakValue = state.currentValue;
|
||||
state.peakTimer = null;
|
||||
state.hasPeak = false;
|
||||
console.log(`[Peak] Сброс пика до: ${state.peakValue}`);
|
||||
notifyPeakHolderChange();
|
||||
}, state.peakHoldTime);
|
||||
}
|
||||
|
||||
function scheduleSignalPeakRelease(): void {
|
||||
// Сбрасываем старый таймер
|
||||
if (state.signalPeakTimer !== null) {
|
||||
clearTimeout(state.signalPeakTimer);
|
||||
state.signalPeakTimer = null;
|
||||
}
|
||||
// Сбрасываем старый таймер
|
||||
if (state.signalPeakTimer !== null) {
|
||||
clearTimeout(state.signalPeakTimer);
|
||||
state.signalPeakTimer = null;
|
||||
}
|
||||
|
||||
// Запускаем новый таймер на 3 секунды
|
||||
state.signalPeakTimer = window.setTimeout(() => {
|
||||
// Сбрасываем пик к текущему уровню
|
||||
state.peakSignalLevel = state.currentSignalLevel;
|
||||
state.signalPeakTimer = null;
|
||||
state.hasPeak = false;
|
||||
console.log(`[SignalPeak] Сброс пика до: ${state.peakSignalLevel}`);
|
||||
notifyPeakHolderChange();
|
||||
}, state.signalPeakHoldTime);
|
||||
}
|
||||
|
||||
// ========== ФУНКЦИИ ДЛЯ АМПЛИТУДЫ ==========
|
||||
|
||||
export function updatePeakHolder(target: number): number {
|
||||
// Обновляем текущее значение
|
||||
state.currentValue = target;
|
||||
|
||||
// Если новое значение больше текущего пика
|
||||
if (target > state.peakValue) {
|
||||
// Устанавливаем новый пик
|
||||
state.peakValue = target;
|
||||
state.hasPeak = true;
|
||||
scheduleAmplitudePeakRelease();
|
||||
} else if (target < state.peakValue && state.peakTimer === null) {
|
||||
// Пик выше текущего значения, но таймер пропал — перезапускаем удержание
|
||||
scheduleAmplitudePeakRelease();
|
||||
} // Возвращаем текущий пик
|
||||
return state.peakValue;
|
||||
}
|
||||
|
||||
// ========== ФУНКЦИИ ДЛЯ УРОВНЯ СИГНАЛА ==========
|
||||
|
||||
export function updateSignalPeakHolder(level: number): number {
|
||||
// Обновляем текущее значение
|
||||
state.currentSignalLevel = level;
|
||||
|
||||
// Если новый уровень больше пика — устанавливаем новый пик
|
||||
if (level > state.peakSignalLevel) {
|
||||
state.peakSignalLevel = level;
|
||||
state.hasPeak = true;
|
||||
scheduleSignalPeakRelease();
|
||||
} else if (level < state.peakSignalLevel && state.signalPeakTimer === null) {
|
||||
scheduleSignalPeakRelease();
|
||||
}
|
||||
|
||||
// Возвращаем текущий пик
|
||||
return state.peakSignalLevel;
|
||||
}
|
||||
|
||||
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
|
||||
|
||||
export function forceResetSignalPeak(): void {
|
||||
if (state.signalPeakTimer !== null) {
|
||||
clearTimeout(state.signalPeakTimer);
|
||||
state.signalPeakTimer = null;
|
||||
}
|
||||
state.peakSignalLevel = 0;
|
||||
state.currentSignalLevel = 0;
|
||||
state.hasPeak = false;
|
||||
}
|
||||
|
||||
export function setSignalPeakHoldTime(ms: number): void {
|
||||
state.signalPeakHoldTime = ms;
|
||||
}
|
||||
|
||||
export function getSignalPeakInfo(): { peak: number; current: number; timerActive: boolean } {
|
||||
return {
|
||||
peak: state.peakSignalLevel,
|
||||
current: state.currentSignalLevel,
|
||||
timerActive: state.signalPeakTimer !== null,
|
||||
};
|
||||
}
|
||||
|
||||
export function forceResetPeak(): void {
|
||||
if (state.peakTimer !== null) {
|
||||
clearTimeout(state.peakTimer);
|
||||
state.peakTimer = null;
|
||||
}
|
||||
state.peakValue = 0;
|
||||
state.currentValue = 0;
|
||||
state.hasPeak = false;
|
||||
forceResetSignalPeak();
|
||||
}
|
||||
|
||||
export function addSignal(value: number): void {
|
||||
state.signalHistory.push(value);
|
||||
if (state.signalHistory.length > state.maxPoints) {
|
||||
state.signalHistory = state.signalHistory.slice(-state.maxPoints);
|
||||
}
|
||||
}
|
||||
|
||||
export function addSignals(values: number[], bufferSize: number): void {
|
||||
if (!Array.isArray(values) || values.length === 0) return;
|
||||
|
||||
const newBytes = bufferSize - state.lastBufferSize;
|
||||
|
||||
if (newBytes > 0 && newBytes <= values.length) {
|
||||
const fresh = values.slice(-newBytes);
|
||||
const decoded = fresh.map(b => b & 0x3F);
|
||||
state.signalHistory.push(...decoded);
|
||||
}
|
||||
|
||||
state.lastBufferSize = bufferSize;
|
||||
|
||||
if (state.signalHistory.length > state.maxPoints) {
|
||||
state.signalHistory = state.signalHistory.slice(-state.maxPoints);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetBufferTracking(): void {
|
||||
state.lastBufferSize = 0;
|
||||
forceResetPeak();
|
||||
}
|
||||
// Запускаем новый таймер на 3 секунды
|
||||
state.signalPeakTimer = window.setTimeout(() => {
|
||||
// Сбрасываем пик к текущему уровню
|
||||
state.peakSignalLevel = state.currentSignalLevel;
|
||||
state.signalPeakTimer = null;
|
||||
state.hasPeak = false;
|
||||
console.log(`[SignalPeak] Сброс пика до: ${state.peakSignalLevel}`);
|
||||
notifyPeakHolderChange();
|
||||
}, state.signalPeakHoldTime);
|
||||
}
|
||||
@@ -166,7 +166,6 @@
|
||||
<div class="logs-content">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="data">📊 Данные</button>
|
||||
<button class="tab-btn" data-tab="stats">📈 Статистика</button>
|
||||
<button class="tab-btn" data-tab="events">📋 События</button>
|
||||
</div>
|
||||
|
||||
@@ -178,10 +177,6 @@
|
||||
<div id="dataTableContainer" style="max-height:400px; overflow-y:auto;"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-stats" class="tab-content">
|
||||
<div id="statsContent">Выберите файл для просмотра статистики</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-events" class="tab-content">
|
||||
<div id="eventsContent" class="events-list">Загрузка...</div>
|
||||
</div>
|
||||
@@ -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 = `
|
||||
<div class="log-stat-card">
|
||||
<div class="value">${samples.length}</div>
|
||||
<div class="value">${totalPoints}</div>
|
||||
<div class="label">Всего точек</div>
|
||||
</div>
|
||||
<div class="log-stat-card">
|
||||
<div class="value">${max}</div>
|
||||
<div class="label">Максимум</div>
|
||||
<div class="value">${amplitudeOver0Count}</div>
|
||||
<div class="label">Амплитуда > 0</div>
|
||||
</div>
|
||||
<div class="log-stat-card">
|
||||
<div class="value">${avg.toFixed(1)}</div>
|
||||
<div class="label">Среднее</div>
|
||||
<div class="value">${signalOver10Count}</div>
|
||||
<div class="label">Сигнал > 10</div>
|
||||
</div>
|
||||
<div class="log-stat-card">
|
||||
<div class="value">${totalObjects}</div>
|
||||
@@ -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 = `<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Время</th>
|
||||
<th>Объектов</th>
|
||||
<th>Сила</th>
|
||||
<th>Амплитуда</th>
|
||||
<th>Сигнал</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
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 += `<tr>
|
||||
<td>${s.time}</td>
|
||||
<td>${s.count}</td>
|
||||
<td class="${strengthClass}">${s.strength_name}</td>
|
||||
<td class="${ampClass}">${s.amplitude}</td>
|
||||
<td>${s.signal}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user