From 58cce4c87acda13458ab1988dfb274aa0a2560e0 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 13 Nov 2025 19:53:11 +0300 Subject: [PATCH] fixed shebang. Now it runs as usual executable --- datagen.py | 666 +++++++++--------- main.py | 1989 ++++++++++++++++++++++++++-------------------------- 2 files changed, 1330 insertions(+), 1325 deletions(-) diff --git a/datagen.py b/datagen.py index 19a0193..b475cc4 100755 --- a/datagen.py +++ b/datagen.py @@ -1,333 +1,333 @@ -#!/usr/bin/python3 -import os -import time -import numpy as np -from datetime import datetime, timedelta -#from builtins import True - -# ================================================================================ -# ПАРАМЕТРЫ ЭМУЛЯЦИИ -# ================================================================================ - -#DATA_DIR = r"D:\data" -DATA_DIR = './data' - -# ✓ ИСПРАВЛЕНИЕ: Выбор типа данных и количество -NUM_FILES = 1000 # Количесйтво файлов для генерации - -# Размеры данных -RAW_SIZE = 64000 -SYNC_DET_SIZE = 1000 -FOURIER_SIZE = SYNC_DET_SIZE // 2 # 500 (положительные частоты) - -# Интервал между файлами (в миллисекундах) -FILE_INTERVAL_MS = 300 # 300 мс между файлами - - -# ================================================================================ -# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ -# ================================================================================ - -def create_raw_data(size=RAW_SIZE, index=0): - """Генерирует RAW данные.""" - # Синусоида + шум, зависит от индекса для разнообразия - t = np.linspace(0, 10 * np.pi, size) - freq_mult = 1.0 + 0.1 * np.sin(index / 100) - signal = np.sin(freq_mult * t) + 0.1 * np.random.randn(size) - return signal - - -def create_sync_det_data(size=SYNC_DET_SIZE, index=0): - """Генерирует SYNC_DET данные.""" - # Модулированная синусоида, зависит от индекса - t = np.linspace(0, 20 * np.pi, size) - damping = np.exp(-t / (20 * np.pi)) * (1 + 0.2 * np.sin(index / 50)) - signal = np.sin(t) * damping - return signal - - -def create_fourier_data(sync_det_data=None, fft_size=SYNC_DET_SIZE, output_size=FOURIER_SIZE, index=0): - """✓ Генерирует FOURIER = |FFT(SYNC_DET)|[:N/2]. - - FFT от сигнала размером N имеет только N/2 независимых значений. - - Args: - sync_det_data: вектор SYNC_DET размером ~1000 - fft_size: размер FFT (1000) - output_size: размер выходного спектра (500) - index: индекс файла для вариативности - - Returns: - fourier_data: амплитудный спектр размером output_size (500) - """ - - if sync_det_data is None: - sync_det_data = create_sync_det_data(index=index) - - # ✓ Вычисляем FFT от SYNC_DET - fft_result = np.fft.fft(sync_det_data[:fft_size]) - - # ✓ Берём амплитудный спектр - amplitude_spectrum = np.abs(fft_result) - - # ✓ Центрируем спектр - fft_shift = np.fft.fftshift(amplitude_spectrum) - - # ✓ Берём только положительные частоты (N/2 = 500) - fft_positive = fft_shift[len(fft_shift) // 2:] - - assert len(fft_positive) == output_size, \ - f"FFT positive frequencies size {len(fft_positive)} != expected {output_size}" - - fourier_data = fft_positive.astype(float) - - # Нормализуем - if fourier_data.max() > 0: - fourier_data = fourier_data / fourier_data.max() * 100 - - return fourier_data - - -# ================================================================================ -# ГЕНЕРАЦИЯ ФАЙЛОВ -# ================================================================================ - -def emit_raw_files(count=NUM_FILES, start_time=None, hex_mode=True): - """Генерирует RAW файлы количеством count.""" - if start_time is None: - start_time = datetime.now() - - print(f"\n{'=' * 80}") - print(f"📝 ГЕНЕРИРОВАНИЕ {count} RAW ФАЙЛОВ") - print(f"{'=' * 80}") - print(f"Размер: {RAW_SIZE} точек") - print(f"Интервал: {FILE_INTERVAL_MS}мс\n") - - for i in range(count): - # Вычисляем время файла - file_time = start_time + timedelta(milliseconds=i * FILE_INTERVAL_MS) - - # Форматируем имя с миллисекундами - time_str = file_time.strftime("%H_%M_%S") - ms = file_time.microsecond // 1000 - filename = f"RAW_{time_str}_{ms:03d}.txt" - - # Генерируем данные - data = create_raw_data(index=i) - - # Добавляем заголовок - filepath = os.path.join(DATA_DIR, filename) - with open(filepath, 'w') as f: - - - if hex_mode: - #f.write("SYNC_DET_HEX\n") - #np.savetxt(f, np.uint32(data*1000), fmt='0xD0%06X') - np.savetxt(f, ((data * 1000).astype(np.int32) & 0xFFFFFF), fmt='0xD0%06X') - - else: - f.write("RAW\n") - np.savetxt(f, data, fmt='%.6f') - - - # Устанавливаем время модификации - timestamp = file_time.timestamp() - os.utime(filepath, (timestamp, timestamp)) - - # Прогресс - if (i + 1) % 100 == 0 or (i + 1) == count: - print(f" ✓ {i + 1}/{count} файлов созданы", end='\r') - - print(f"\n✅ Готово: {count} RAW файлов") - return start_time + timedelta(milliseconds=count * FILE_INTERVAL_MS) - - -def emit_sync_det_files(count=NUM_FILES, start_time=None, hex_mode=True): - """Генерирует SYNC_DET файлы количеством count.""" - if start_time is None: - start_time = datetime.now() - - print(f"\n{'=' * 80}") - print(f"📝 ГЕНЕРИРОВАНИЕ {count} SYNC_DET ФАЙЛОВ") - print(f"{'=' * 80}") - print(f"Размер: {SYNC_DET_SIZE} точек") - print(f"Интервал: {FILE_INTERVAL_MS}мс\n") - - for i in range(count): - # Вычисляем время файла - file_time = start_time + timedelta(milliseconds=i * FILE_INTERVAL_MS) - - # Форматируем имя с миллисекундами - time_str = file_time.strftime("%H_%M_%S") - ms = file_time.microsecond // 1000 - filename = f"SYNC_DET_{time_str}_{ms:03d}.txt" - - # Генерируем данные - data = create_sync_det_data(index=i) - #print("data:", data) - - # Добавляем заголовок - filepath = os.path.join(DATA_DIR, filename) - with open(filepath, 'w') as f: - - if hex_mode: - #f.write("SYNC_DET_HEX\n") - #np.savetxt(f, np.uint32(data*1000), fmt='0xF0%06X') - np.savetxt(f, ((data * 1000).astype(np.int32) & 0xFFFFFF), fmt='0xF0%06X') - else: - f.write("SYNC_DET\n") - np.savetxt(f, data, fmt='%.6f') - - # Устанавливаем время модификации - timestamp = file_time.timestamp() - os.utime(filepath, (timestamp, timestamp)) - - # Прогресс - if (i + 1) % 100 == 0 or (i + 1) == count: - print(f" ✓ {i + 1}/{count} файлов созданы", end='\r') - - print(f"\n✅ Готово: {count} SYNC_DET файлов") - return start_time + timedelta(milliseconds=count * FILE_INTERVAL_MS) - - -def emit_fourier_files(count=NUM_FILES, start_time=None, hex_mode=True): - """✓ Генерирует FOURIER файлы количеством count. - - Каждый файл содержит амплитудный спектр размером 500. - """ - if start_time is None: - start_time = datetime.now() - - print(f"\n{'=' * 80}") - print(f"📝 ГЕНЕРИРОВАНИЕ {count} FOURIER ФАЙЛОВ") - print(f"{'=' * 80}") - print(f"Размер: {FOURIER_SIZE} точек (|FFT|[:N/2])") - print(f"Интервал: {FILE_INTERVAL_MS}мс\n") - - for i in range(count): - # Вычисляем время файла - file_time = start_time + timedelta(milliseconds=i * FILE_INTERVAL_MS) - - # Форматируем имя с миллисекундами - time_str = file_time.strftime("%H_%M_%S") - ms = file_time.microsecond // 1000 - filename = f"FOURIER_{time_str}_{ms:03d}.txt" - - # Генерируем SYNC_DET - sync_det = create_sync_det_data(index=i) - - # Вычисляем FOURIER как |FFT(SYNC_DET)|[:N/2] - data = create_fourier_data( - sync_det_data=sync_det, - fft_size=SYNC_DET_SIZE, - output_size=FOURIER_SIZE, - index=i - ) - - assert len(data) == FOURIER_SIZE, \ - f"FOURIER size {len(data)} != expected {FOURIER_SIZE}" - - # Добавляем заголовок - filepath = os.path.join(DATA_DIR, filename) - with open(filepath, 'w') as f: - - if hex_mode: - #f.write("SYNC_DET_HEX\n") - #np.savetxt(f, np.uint32(data*1000), fmt='0xF4%06X') - np.savetxt(f, ((data * 1000).astype(np.int32) & 0xFFFFFF), fmt='0xF4%06X') - else: - f.write("FOURIER\n") - np.savetxt(f, data, fmt='%.6f') - - - # Устанавливаем время модификации - timestamp = file_time.timestamp() - os.utime(filepath, (timestamp, timestamp)) - - # Прогресс - if (i + 1) % 100 == 0 or (i + 1) == count: - print(f" ✓ {i + 1}/{count} файлов созданы", end='\r') - - print(f"\n✅ Готово: {count} FOURIER файлов") - return start_time + timedelta(milliseconds=count * FILE_INTERVAL_MS) - - -# ================================================================================ -# ОСНОВНАЯ ПРОГРАММА -# ================================================================================ - -def show_menu(): - """Показывает меню выбора типа данных.""" - print("\n" + "=" * 80) - print(" РАДАР ЭМУЛЯТОР - ВЫБОР ТИПА ДАННЫХ") - print("=" * 80) - print() - print(" Выберите тип данных для генерирования:") - print() - print(" 1. RAW - Сырые данные с АЦП ({} точек)".format(RAW_SIZE)) - print(" 2. SYNC_DET - Обработанные данные ({} точек)".format(SYNC_DET_SIZE)) - print(" 3. FOURIER - Амплитудный спектр ({} точек)".format(FOURIER_SIZE)) - print() - print("=" * 80) - print() - - -if __name__ == "__main__": - # Создаём директорию если её нет - os.makedirs(DATA_DIR, exist_ok=True) - - print("\n" + "=" * 80) - print(" РАДАР ЭМУЛЯТОР - ГЕНЕРИРОВАНИЕ ТЕСТОВЫХ ДАННЫХ") - print("=" * 80) - print(f" Директория: {DATA_DIR}") - print(f" Количество файлов: {NUM_FILES}") - print(f" Интервал между файлами: {FILE_INTERVAL_MS}мс") - print(f" Формат времени: HH:MM:SS.mmm") - print("=" * 80) - - show_menu() - - - while True: - try: - hex_choice = input(" Генерировать в HEX (h) или float (f)? 'q' для выхода: ").strip().lower() - if hex_choice == "h": - hex_mode = True - elif hex_choice == "f": - hex_mode = False - else: - print(" ❌ Неверный выбор. Введите h, f или q") - - - - choice = input(" Введите номер (1/2/3) или 'q' для выхода: ").strip().lower() - - if choice == 'q': - print("\n Выход.") - break - elif choice == '1': - start_time = datetime.now().replace(microsecond=0) - emit_raw_files(NUM_FILES, start_time, hex_mode=hex_mode) - break - elif choice == '2': - start_time = datetime.now().replace(microsecond=0) - emit_sync_det_files(NUM_FILES, start_time, hex_mode=hex_mode) - break - elif choice == '3': - start_time = datetime.now().replace(microsecond=0) - emit_fourier_files(NUM_FILES, start_time, hex_mode=hex_mode) - break - else: - print(" ❌ Неверный выбор. Введите 1, 2, 3 или q") - - - except KeyboardInterrupt: - print("\n\n Прервано пользователем.") - break - except Exception as e: - print(f" ❌ Ошибка: {e}") - continue - - print(f"\n📂 Файлы сохранены в: {DATA_DIR}") - print(f"\n💡 Запустите main_analyzer.py и откройте директорию {DATA_DIR}") - print(f" Анализатор автоматически подхватит новые файлы\n") +#!/usr/bin/python3 +import os +import time +import numpy as np +from datetime import datetime, timedelta +#from builtins import True + +# ================================================================================ +# ПАРАМЕТРЫ ЭМУЛЯЦИИ +# ================================================================================ + +#DATA_DIR = r"D:\data" +DATA_DIR = './data' + +# ✓ ИСПРАВЛЕНИЕ: Выбор типа данных и количество +NUM_FILES = 1000 # Количесйтво файлов для генерации + +# Размеры данных +RAW_SIZE = 64000 +SYNC_DET_SIZE = 1000 +FOURIER_SIZE = SYNC_DET_SIZE // 2 # 500 (положительные частоты) + +# Интервал между файлами (в миллисекундах) +FILE_INTERVAL_MS = 300 # 300 мс между файлами + + +# ================================================================================ +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +# ================================================================================ + +def create_raw_data(size=RAW_SIZE, index=0): + """Генерирует RAW данные.""" + # Синусоида + шум, зависит от индекса для разнообразия + t = np.linspace(0, 10 * np.pi, size) + freq_mult = 1.0 + 0.1 * np.sin(index / 100) + signal = np.sin(freq_mult * t) + 0.1 * np.random.randn(size) + return signal + + +def create_sync_det_data(size=SYNC_DET_SIZE, index=0): + """Генерирует SYNC_DET данные.""" + # Модулированная синусоида, зависит от индекса + t = np.linspace(0, 20 * np.pi, size) + damping = np.exp(-t / (20 * np.pi)) * (1 + 0.2 * np.sin(index / 50)) + signal = np.sin(t) * damping + return signal + + +def create_fourier_data(sync_det_data=None, fft_size=SYNC_DET_SIZE, output_size=FOURIER_SIZE, index=0): + """✓ Генерирует FOURIER = |FFT(SYNC_DET)|[:N/2]. + + FFT от сигнала размером N имеет только N/2 независимых значений. + + Args: + sync_det_data: вектор SYNC_DET размером ~1000 + fft_size: размер FFT (1000) + output_size: размер выходного спектра (500) + index: индекс файла для вариативности + + Returns: + fourier_data: амплитудный спектр размером output_size (500) + """ + + if sync_det_data is None: + sync_det_data = create_sync_det_data(index=index) + + # ✓ Вычисляем FFT от SYNC_DET + fft_result = np.fft.fft(sync_det_data[:fft_size]) + + # ✓ Берём амплитудный спектр + amplitude_spectrum = np.abs(fft_result) + + # ✓ Центрируем спектр + fft_shift = np.fft.fftshift(amplitude_spectrum) + + # ✓ Берём только положительные частоты (N/2 = 500) + fft_positive = fft_shift[len(fft_shift) // 2:] + + assert len(fft_positive) == output_size, \ + f"FFT positive frequencies size {len(fft_positive)} != expected {output_size}" + + fourier_data = fft_positive.astype(float) + + # Нормализуем + if fourier_data.max() > 0: + fourier_data = fourier_data / fourier_data.max() * 100 + + return fourier_data + + +# ================================================================================ +# ГЕНЕРАЦИЯ ФАЙЛОВ +# ================================================================================ + +def emit_raw_files(count=NUM_FILES, start_time=None, hex_mode=True): + """Генерирует RAW файлы количеством count.""" + if start_time is None: + start_time = datetime.now() + + print(f"\n{'=' * 80}") + print(f"📝 ГЕНЕРИРОВАНИЕ {count} RAW ФАЙЛОВ") + print(f"{'=' * 80}") + print(f"Размер: {RAW_SIZE} точек") + print(f"Интервал: {FILE_INTERVAL_MS}мс\n") + + for i in range(count): + # Вычисляем время файла + file_time = start_time + timedelta(milliseconds=i * FILE_INTERVAL_MS) + + # Форматируем имя с миллисекундами + time_str = file_time.strftime("%H_%M_%S") + ms = file_time.microsecond // 1000 + filename = f"RAW_{time_str}_{ms:03d}.txt" + + # Генерируем данные + data = create_raw_data(index=i) + + # Добавляем заголовок + filepath = os.path.join(DATA_DIR, filename) + with open(filepath, 'w') as f: + + + if hex_mode: + #f.write("SYNC_DET_HEX\n") + #np.savetxt(f, np.uint32(data*1000), fmt='0xD0%06X') + np.savetxt(f, ((data * 1000).astype(np.int32) & 0xFFFFFF), fmt='0xD0%06X') + + else: + f.write("RAW\n") + np.savetxt(f, data, fmt='%.6f') + + + # Устанавливаем время модификации + timestamp = file_time.timestamp() + os.utime(filepath, (timestamp, timestamp)) + + # Прогресс + if (i + 1) % 100 == 0 or (i + 1) == count: + print(f" ✓ {i + 1}/{count} файлов созданы", end='\r') + + print(f"\n✅ Готово: {count} RAW файлов") + return start_time + timedelta(milliseconds=count * FILE_INTERVAL_MS) + + +def emit_sync_det_files(count=NUM_FILES, start_time=None, hex_mode=True): + """Генерирует SYNC_DET файлы количеством count.""" + if start_time is None: + start_time = datetime.now() + + print(f"\n{'=' * 80}") + print(f"📝 ГЕНЕРИРОВАНИЕ {count} SYNC_DET ФАЙЛОВ") + print(f"{'=' * 80}") + print(f"Размер: {SYNC_DET_SIZE} точек") + print(f"Интервал: {FILE_INTERVAL_MS}мс\n") + + for i in range(count): + # Вычисляем время файла + file_time = start_time + timedelta(milliseconds=i * FILE_INTERVAL_MS) + + # Форматируем имя с миллисекундами + time_str = file_time.strftime("%H_%M_%S") + ms = file_time.microsecond // 1000 + filename = f"SYNC_DET_{time_str}_{ms:03d}.txt" + + # Генерируем данные + data = create_sync_det_data(index=i) + #print("data:", data) + + # Добавляем заголовок + filepath = os.path.join(DATA_DIR, filename) + with open(filepath, 'w') as f: + + if hex_mode: + #f.write("SYNC_DET_HEX\n") + #np.savetxt(f, np.uint32(data*1000), fmt='0xF0%06X') + np.savetxt(f, ((data * 1000).astype(np.int32) & 0xFFFFFF), fmt='0xF0%06X') + else: + f.write("SYNC_DET\n") + np.savetxt(f, data, fmt='%.6f') + + # Устанавливаем время модификации + timestamp = file_time.timestamp() + os.utime(filepath, (timestamp, timestamp)) + + # Прогресс + if (i + 1) % 100 == 0 or (i + 1) == count: + print(f" ✓ {i + 1}/{count} файлов созданы", end='\r') + + print(f"\n✅ Готово: {count} SYNC_DET файлов") + return start_time + timedelta(milliseconds=count * FILE_INTERVAL_MS) + + +def emit_fourier_files(count=NUM_FILES, start_time=None, hex_mode=True): + """✓ Генерирует FOURIER файлы количеством count. + + Каждый файл содержит амплитудный спектр размером 500. + """ + if start_time is None: + start_time = datetime.now() + + print(f"\n{'=' * 80}") + print(f"📝 ГЕНЕРИРОВАНИЕ {count} FOURIER ФАЙЛОВ") + print(f"{'=' * 80}") + print(f"Размер: {FOURIER_SIZE} точек (|FFT|[:N/2])") + print(f"Интервал: {FILE_INTERVAL_MS}мс\n") + + for i in range(count): + # Вычисляем время файла + file_time = start_time + timedelta(milliseconds=i * FILE_INTERVAL_MS) + + # Форматируем имя с миллисекундами + time_str = file_time.strftime("%H_%M_%S") + ms = file_time.microsecond // 1000 + filename = f"FOURIER_{time_str}_{ms:03d}.txt" + + # Генерируем SYNC_DET + sync_det = create_sync_det_data(index=i) + + # Вычисляем FOURIER как |FFT(SYNC_DET)|[:N/2] + data = create_fourier_data( + sync_det_data=sync_det, + fft_size=SYNC_DET_SIZE, + output_size=FOURIER_SIZE, + index=i + ) + + assert len(data) == FOURIER_SIZE, \ + f"FOURIER size {len(data)} != expected {FOURIER_SIZE}" + + # Добавляем заголовок + filepath = os.path.join(DATA_DIR, filename) + with open(filepath, 'w') as f: + + if hex_mode: + #f.write("SYNC_DET_HEX\n") + #np.savetxt(f, np.uint32(data*1000), fmt='0xF4%06X') + np.savetxt(f, ((data * 1000).astype(np.int32) & 0xFFFFFF), fmt='0xF4%06X') + else: + f.write("FOURIER\n") + np.savetxt(f, data, fmt='%.6f') + + + # Устанавливаем время модификации + timestamp = file_time.timestamp() + os.utime(filepath, (timestamp, timestamp)) + + # Прогресс + if (i + 1) % 100 == 0 or (i + 1) == count: + print(f" ✓ {i + 1}/{count} файлов созданы", end='\r') + + print(f"\n✅ Готово: {count} FOURIER файлов") + return start_time + timedelta(milliseconds=count * FILE_INTERVAL_MS) + + +# ================================================================================ +# ОСНОВНАЯ ПРОГРАММА +# ================================================================================ + +def show_menu(): + """Показывает меню выбора типа данных.""" + print("\n" + "=" * 80) + print(" РАДАР ЭМУЛЯТОР - ВЫБОР ТИПА ДАННЫХ") + print("=" * 80) + print() + print(" Выберите тип данных для генерирования:") + print() + print(" 1. RAW - Сырые данные с АЦП ({} точек)".format(RAW_SIZE)) + print(" 2. SYNC_DET - Обработанные данные ({} точек)".format(SYNC_DET_SIZE)) + print(" 3. FOURIER - Амплитудный спектр ({} точек)".format(FOURIER_SIZE)) + print() + print("=" * 80) + print() + + +if __name__ == "__main__": + # Создаём директорию если её нет + os.makedirs(DATA_DIR, exist_ok=True) + + print("\n" + "=" * 80) + print(" РАДАР ЭМУЛЯТОР - ГЕНЕРИРОВАНИЕ ТЕСТОВЫХ ДАННЫХ") + print("=" * 80) + print(f" Директория: {DATA_DIR}") + print(f" Количество файлов: {NUM_FILES}") + print(f" Интервал между файлами: {FILE_INTERVAL_MS}мс") + print(f" Формат времени: HH:MM:SS.mmm") + print("=" * 80) + + show_menu() + + + while True: + try: + hex_choice = input(" Генерировать в HEX (h) или float (f)? 'q' для выхода: ").strip().lower() + if hex_choice == "h": + hex_mode = True + elif hex_choice == "f": + hex_mode = False + else: + print(" ❌ Неверный выбор. Введите h, f или q") + + + + choice = input(" Введите номер (1/2/3) или 'q' для выхода: ").strip().lower() + + if choice == 'q': + print("\n Выход.") + break + elif choice == '1': + start_time = datetime.now().replace(microsecond=0) + emit_raw_files(NUM_FILES, start_time, hex_mode=hex_mode) + break + elif choice == '2': + start_time = datetime.now().replace(microsecond=0) + emit_sync_det_files(NUM_FILES, start_time, hex_mode=hex_mode) + break + elif choice == '3': + start_time = datetime.now().replace(microsecond=0) + emit_fourier_files(NUM_FILES, start_time, hex_mode=hex_mode) + break + else: + print(" ❌ Неверный выбор. Введите 1, 2, 3 или q") + + + except KeyboardInterrupt: + print("\n\n Прервано пользователем.") + break + except Exception as e: + print(f" ❌ Ошибка: {e}") + continue + + print(f"\n📂 Файлы сохранены в: {DATA_DIR}") + print(f"\n💡 Запустите main_analyzer.py и откройте директорию {DATA_DIR}") + print(f" Анализатор автоматически подхватит новые файлы\n") diff --git a/main.py b/main.py index a985ff1..53b9917 100755 --- a/main.py +++ b/main.py @@ -1,68 +1,70 @@ -#!/usr/bin/python3 -import os -import time -import numpy as np -import tkinter as tk -from tkinter import ttk -import matplotlib.pyplot as plt -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from scipy.signal import square -from scipy.ndimage import gaussian_filter1d -from scipy.interpolate import interp1d -from datetime import datetime, timedelta -import threading -import queue - -# ================================================================================ -# ПАРАМЕТРЫ И КОНСТАНТЫ -# ================================================================================ -#data_dir = r"D:\data" -data_dir = "./data" -PeriodIntegrate = 2 -pontInOneFqChange = 86 - -# Высота B-скана для RAW/SYNC_DET обработки -height = 60 - -timePoint = 100 -FQend = 512 -FFT0_delta = 5 - -STANDARD_RAW_SIZE = 64000 -STANDARD_SYNC_SIZE = 1000 - -# FOURIER размер = SYNC_DET_SIZE // 2 (положительные частоты) -STANDARD_FOURIER_SIZE = STANDARD_SYNC_SIZE // 2 # 1000 // 2 = 500 - -STANDARD_FOURIER_ROWS = 60 -STANDARD_FOURIER_COLS = 100 - -MAX_PROCESSING_TIME_MS = 250 - -# ПЕРЕЧИСЛЕНИЕ ТИПОВ ДАННЫХ +#!/usr/bin/python3 + +import os +import time +import numpy as np +import tkinter as tk +from tkinter import ttk +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from scipy.signal import square +from scipy.ndimage import gaussian_filter1d +from scipy.interpolate import interp1d +from datetime import datetime, timedelta +import threading +import queue +from sys import argv + +# ================================================================================ +# ПАРАМЕТРЫ И КОНСТАНТЫ +# ================================================================================ +#data_dir = r"D:\data" +data_dir = "./data" +PeriodIntegrate = 2 +pontInOneFqChange = 86 + +# Высота B-скана для RAW/SYNC_DET обработки +height = 60 + +timePoint = 100 +FQend = 512 +FFT0_delta = 5 + +STANDARD_RAW_SIZE = 64000 +STANDARD_SYNC_SIZE = 1000 + +# FOURIER размер = SYNC_DET_SIZE // 2 (положительные частоты) +STANDARD_FOURIER_SIZE = STANDARD_SYNC_SIZE // 2 # 1000 // 2 = 500 + +STANDARD_FOURIER_ROWS = 60 +STANDARD_FOURIER_COLS = 100 + +MAX_PROCESSING_TIME_MS = 250 + +# ПЕРЕЧИСЛЕНИЕ ТИПОВ ДАННЫХ DATA_TYPE_RAW = "RAW" DATA_TYPE_SYNC_DET = "SYNC_DET" DATA_TYPE_FOURIER = "FOURIER" DATA_TYPE_HEX = "HEX" - -# Режим обработки FOURIER файлов -FOURIER_MODE = 'collapse_mean' -FOURIER_SUBSAMPLE_K = 10 - -# Начальный интервал между файлами (в миллисекундах) -DEFAULT_FILE_INTERVAL_MS = 300 # 300 мс - -# Коэффициент для определения разрыва (1.5x интервала) -GAP_THRESHOLD_MULTIPLIER = 1.5 - -# Начальное время опроса файлов (в миллисекундах) -DEFAULT_FILE_POLL_INTERVAL_MS = 100 # 100 мс - - -# ================================================================================ -# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ -# ================================================================================ - + +# Режим обработки FOURIER файлов +FOURIER_MODE = 'collapse_mean' +FOURIER_SUBSAMPLE_K = 10 + +# Начальный интервал между файлами (в миллисекундах) +DEFAULT_FILE_INTERVAL_MS = 300 # 300 мс + +# Коэффициент для определения разрыва (1.5x интервала) +GAP_THRESHOLD_MULTIPLIER = 1.5 + +# Начальное время опроса файлов (в миллисекундах) +DEFAULT_FILE_POLL_INTERVAL_MS = 100 # 100 мс + + +# ================================================================================ +# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ +# ================================================================================ + def detect_data_type(first_line): """Определяет тип данных по первой строке файла. @@ -80,40 +82,40 @@ def detect_data_type(first_line): return DATA_TYPE_HEX except Exception: return DATA_TYPE_HEX - - -def resize_1d_interpolate(data, target_size): - """Ресайзит одномерный массив с использованием линейной интерполяции.""" - if len(data) == target_size: - return data - - old_indices = np.linspace(0, 1, len(data)) - new_indices = np.linspace(0, 1, target_size) - - f = interp1d(old_indices, data, kind='linear', fill_value='extrapolate') - return f(new_indices) - - -def resize_2d_interpolate(data, target_rows, target_cols): - """Ресайзит двумерный массив на target_rows x target_cols с интерполяцией.""" - rows, cols = data.shape - - if rows == target_rows and cols == target_cols: - return data - - old_row_indices = np.linspace(0, 1, rows) - new_row_indices = np.linspace(0, 1, target_rows) - f_rows = interp1d(old_row_indices, data, axis=0, kind='linear', fill_value='extrapolate') - data_resampled_rows = f_rows(new_row_indices) - - old_col_indices = np.linspace(0, 1, cols) - new_col_indices = np.linspace(0, 1, target_cols) - f_cols = interp1d(old_col_indices, data_resampled_rows, axis=1, kind='linear', fill_value='extrapolate') - data_resampled = f_cols(new_col_indices) - - return data_resampled - - + + +def resize_1d_interpolate(data, target_size): + """Ресайзит одномерный массив с использованием линейной интерполяции.""" + if len(data) == target_size: + return data + + old_indices = np.linspace(0, 1, len(data)) + new_indices = np.linspace(0, 1, target_size) + + f = interp1d(old_indices, data, kind='linear', fill_value='extrapolate') + return f(new_indices) + + +def resize_2d_interpolate(data, target_rows, target_cols): + """Ресайзит двумерный массив на target_rows x target_cols с интерполяцией.""" + rows, cols = data.shape + + if rows == target_rows and cols == target_cols: + return data + + old_row_indices = np.linspace(0, 1, rows) + new_row_indices = np.linspace(0, 1, target_rows) + f_rows = interp1d(old_row_indices, data, axis=0, kind='linear', fill_value='extrapolate') + data_resampled_rows = f_rows(new_row_indices) + + old_col_indices = np.linspace(0, 1, cols) + new_col_indices = np.linspace(0, 1, target_cols) + f_cols = interp1d(old_col_indices, data_resampled_rows, axis=1, kind='linear', fill_value='extrapolate') + data_resampled = f_cols(new_col_indices) + + return data_resampled + + def load_data_with_type(filename): """Загружает данные и определяет их тип по первой строке.""" with open(filename, 'r') as f: @@ -241,686 +243,686 @@ def parse_hex_file(filename): return DATA_TYPE_RAW, seg_raw if len(seg_raw) > 1 else seg_raw[0] return DATA_TYPE_RAW, np.asarray([], dtype=float) - - -def get_file_time_with_milliseconds(filename): - """Получает время файла с миллисекундами из имени или mtime.""" - full_path = os.path.join(os.getcwd(), filename) - - milliseconds = 0 - parts = filename.split('_') - if len(parts) >= 4: - try: - last_part = parts[-1].split('.')[0] - if last_part.isdigit() and len(last_part) <= 3: - milliseconds = int(last_part) - except: - pass - - file_mtime = os.path.getmtime(full_path) - file_time = datetime.fromtimestamp(file_mtime) - - if milliseconds > 0: - file_time = file_time.replace(microsecond=milliseconds * 1000) - else: - now = datetime.now() - file_time = file_time.replace(microsecond=now.microsecond) - - return file_time - - -# ================================================================================ -# КЛАСС ПРИЛОЖЕНИЯ -# ================================================================================ - -class DataAnalyzerApp: - def __init__(self, root): - self.root = root - self.root.title("Radar Data Analyzer (Time Synchronized - Queue Based)") - self.root.geometry("1500x850") - - self.data_dir = data_dir - os.makedirs(self.data_dir, exist_ok=True) - os.chdir(self.data_dir) - - # Инициализируем с существующими файлами + + +def get_file_time_with_milliseconds(filename): + """Получает время файла с миллисекундами из имени или mtime.""" + full_path = os.path.join(os.getcwd(), filename) + + milliseconds = 0 + parts = filename.split('_') + if len(parts) >= 4: + try: + last_part = parts[-1].split('.')[0] + if last_part.isdigit() and len(last_part) <= 3: + milliseconds = int(last_part) + except: + pass + + file_mtime = os.path.getmtime(full_path) + file_time = datetime.fromtimestamp(file_mtime) + + if milliseconds > 0: + file_time = file_time.replace(microsecond=milliseconds * 1000) + else: + now = datetime.now() + file_time = file_time.replace(microsecond=now.microsecond) + + return file_time + + +# ================================================================================ +# КЛАСС ПРИЛОЖЕНИЯ +# ================================================================================ + +class DataAnalyzerApp: + def __init__(self, root): + self.root = root + self.root.title("Radar Data Analyzer (Time Synchronized - Queue Based)") + self.root.geometry("1500x850") + + self.data_dir = data_dir + os.makedirs(self.data_dir, exist_ok=True) + os.chdir(self.data_dir) + + # Инициализируем с существующими файлами existing_files = sorted([ f for f in os.listdir() if f.lower().endswith(('.txt', '.txt1', '.txt2', '.csv')) ]) - self.processed_files = set(existing_files) - - if existing_files: - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print(f"[{timestamp}] ℹ️ Skipping {len(existing_files)} existing files\n") - - # Инициализация состояния обработки - self.SizeFirst = STANDARD_RAW_SIZE - self.time_idx = np.arange(1, self.SizeFirst + 1) - self.k = 1 - self.SUM = 0.0 - self.B = None - self.meandr = None - self.signal = np.array([]) - self.signalView = np.array([]) - self.timeSignal = np.array([]) - - # История B_scan с привязкой к времени файла - self.B_scan_data = [] - self.B_scan_times = [] - self.B_scan_types = [] # RAW, SYNC_DET, FOURIER, GAP - self.last_file_time = None - - # Интервал между файлами (в миллисекундах) - задаётся пользователем - self.file_interval_ms = DEFAULT_FILE_INTERVAL_MS - self.time_gap_threshold_ms = self.file_interval_ms * GAP_THRESHOLD_MULTIPLIER - - # ✓ ИСПРАВЛЕНИЕ: Счётчик пропущенных кадров - self.gap_frames_count = 0 - - self.startPointTime = 0 - self.timestart = time.perf_counter() - self.timestart2 = None - self.FshiftS = None - - # Очередь для добавления колонок (без блокировок) - self.bscan_queue = queue.Queue() - - # Для потоковой отрисовки - self.update_lock = threading.Lock() - self.pending_update = False - self.pending_original_size = None - self.pending_data_type = None - - # Автопрокрутка по времени - self.auto_follow = tk.BooleanVar(value=True) - - # Флаг для отключения автопрокрутки при ручном выборе - self.manual_range_selection = False - - # Время опроса файлов (в миллисекундах) - self.file_poll_interval_ms = DEFAULT_FILE_POLL_INTERVAL_MS - - # Параметры слайдеров для B_scan - self.bscan_row_min = 0 - self.bscan_row_max = height - 1 - self.bscan_col_min = 0 - self.bscan_col_max = timePoint - 1 - - # Главный фрейм с двумя колонками - self.main_frame = ttk.Frame(self.root) - self.main_frame.pack(fill='both', expand=True, padx=3, pady=3) - - # Левая колонка: Графики (65%) - self.left_frame = ttk.Frame(self.main_frame) - self.left_frame.pack(side='left', fill='both', expand=True, padx=(0, 3)) - - # Правая колонка: Настройки (35%) - self.right_frame = ttk.Frame(self.main_frame, width=210) - self.right_frame.pack(side='right', fill='both', padx=(3, 0)) - self.right_frame.pack_propagate(False) - - # Инициализация графиков и настроек - self.init_plots_panel() - self.init_settings_panel() - - self.processed_count = 0 - self.skipped_count = 0 - - # ============================================================================ - # ИНИЦИАЛИЗАЦИЯ ИНТЕРФЕЙСА - # ============================================================================ - - def init_plots_panel(self): - """Инициализация панели с графиками (слева).""" - title = ttk.Label(self.left_frame, text="Графики", font=('Arial', 11, 'bold')) - title.pack() - - fig_frame = ttk.Frame(self.left_frame) - fig_frame.pack(fill='both', expand=True, padx=3, pady=3) - - self.fig = plt.Figure(figsize=(9, 6.5), dpi=100) - gs = self.fig.add_gridspec(2, 2, hspace=0.4, wspace=0.35) - - self.ax_raw = self.fig.add_subplot(gs[0, 0]) - self.ax_processed = self.fig.add_subplot(gs[0, 1]) - self.ax_fourier = self.fig.add_subplot(gs[1, 0]) - self.ax_bscan = self.fig.add_subplot(gs[1, 1]) - - self.canvas = FigureCanvasTkAgg(self.fig, master=fig_frame) - self.canvas.draw() - self.canvas.get_tk_widget().pack(fill='both', expand=True) - - self.draw_empty_plots() - - def init_settings_panel(self): - """Инициализация панели с настройками (справа).""" - title = ttk.Label(self.right_frame, text="⚙️ Настройки", font=('Arial', 10, 'bold')) - title.pack(pady=3, padx=3) - - # Canvas с прокруткой - self.settings_canvas = tk.Canvas(self.right_frame, bg='white', highlightthickness=0) - scrollbar = ttk.Scrollbar(self.right_frame, orient='vertical', command=self.settings_canvas.yview) - scrollable_frame = ttk.Frame(self.settings_canvas) - - scrollable_frame.bind( - "", - lambda e: self.settings_canvas.configure(scrollregion=self.settings_canvas.bbox("all")) - ) - - self.settings_canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") - self.settings_canvas.configure(yscrollcommand=scrollbar.set) - - self.settings_canvas.pack(side='left', fill='both', expand=True) - scrollbar.pack(side='right', fill='y') - - # Контроль интервала между файлами - interval_label = ttk.Label(scrollable_frame, text="📊 File Interval (ms):", - font=('Arial', 9, 'bold'), foreground='darkblue') - interval_label.pack(anchor='w', padx=3, pady=(5, 0)) - - interval_control_frame = ttk.Frame(scrollable_frame) - interval_control_frame.pack(fill='x', padx=3, pady=(0, 2)) - - self.interval_var = tk.IntVar(value=self.file_interval_ms) - interval_spinbox = ttk.Spinbox(interval_control_frame, from_=50, to=5000, - textvariable=self.interval_var, - width=6, command=self.on_file_interval_changed) - interval_spinbox.pack(side='left', padx=(0, 3)) - - self.interval_scale = ttk.Scale(interval_control_frame, from_=50, to=5000, orient='horizontal', - variable=self.interval_var, - command=self.on_interval_scale_changed) - self.interval_scale.pack(side='left', fill='x', expand=True, padx=(0, 3)) - - self.interval_label = ttk.Label(interval_control_frame, text=f"{self.file_interval_ms}ms", - font=('Arial', 8), foreground='darkblue', width=6, - relief='sunken', anchor='center') - self.interval_label.pack(side='left') - - interval_hint = ttk.Label(scrollable_frame, text="Диапазон: 50-5000 мс", - font=('Arial', 7), foreground='gray') - interval_hint.pack(anchor='w', padx=3, pady=(0, 3)) - - # Контроль времени опроса файлов - poll_label = ttk.Label(scrollable_frame, text="⏱️ Poll Interval (ms):", - font=('Arial', 9, 'bold'), foreground='darkred') - poll_label.pack(anchor='w', padx=3, pady=(5, 0)) - - poll_control_frame = ttk.Frame(scrollable_frame) - poll_control_frame.pack(fill='x', padx=3, pady=(0, 2)) - - self.poll_interval_var = tk.IntVar(value=self.file_poll_interval_ms) - poll_spinbox = ttk.Spinbox(poll_control_frame, from_=10, to=5000, - textvariable=self.poll_interval_var, - width=6, command=self.on_poll_interval_changed) - poll_spinbox.pack(side='left', padx=(0, 3)) - - self.poll_scale = ttk.Scale(poll_control_frame, from_=10, to=5000, orient='horizontal', - variable=self.poll_interval_var, - command=self.on_poll_scale_changed) - self.poll_scale.pack(side='left', fill='x', expand=True, padx=(0, 3)) - - self.poll_label = ttk.Label(poll_control_frame, text=f"{self.file_poll_interval_ms}ms", - font=('Arial', 8), foreground='darkred', width=6, - relief='sunken', anchor='center') - self.poll_label.pack(side='left') - - poll_hint = ttk.Label(scrollable_frame, text="Диапазон: 10-5000 мс", - font=('Arial', 7), foreground='gray') - poll_hint.pack(anchor='w', padx=3, pady=(0, 3)) - - ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) - - # Информация о кодировке кадров - legend_label = ttk.Label(scrollable_frame, text="B-Scan Legend:", - font=('Arial', 9, 'bold'), foreground='darkgreen') - legend_label.pack(anchor='w', padx=3, pady=3) - - legend_text = ttk.Label(scrollable_frame, - text="■ Colors: DATA\n■ White: GAP (lost frame)\n■ Black: ZERO (missing)", - font=('Arial', 8), foreground='darkgreen', justify='left') - legend_text.pack(anchor='w', padx=3, pady=(0, 3)) - - ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) - - # Информация о типах данных и размерах - info_label = ttk.Label(scrollable_frame, text=f"B-scan types:", - font=('Arial', 9, 'bold'), foreground='darkblue') - info_label.pack(anchor='w', padx=3, pady=3) - - info_text = ttk.Label(scrollable_frame, - text=f"RAW/SYNC_DET: {height}px\nFOURIER: {STANDARD_FOURIER_SIZE}px", - font=('Arial', 8), foreground='darkblue', justify='left') - info_text.pack(anchor='w', padx=3, pady=(0, 3)) - - ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=2) - - # Row Min/Max для выбора диапазона отображения - row_min_label = ttk.Label(scrollable_frame, text="Row Min (display range):", font=('Arial', 8)) - row_min_label.pack(anchor='w', padx=3, pady=(3, 0)) - self.slider_row_min_var = tk.IntVar(value=0) - self.scale_row_min = ttk.Scale(scrollable_frame, from_=0, to=STANDARD_FOURIER_SIZE - 1, - orient='horizontal', variable=self.slider_row_min_var, - command=self.on_slider_changed) - self.scale_row_min.pack(fill='x', expand=True, padx=3, pady=(0, 1)) - self.label_row_min = ttk.Label(scrollable_frame, text="0", font=('Arial', 8), foreground='blue') - self.label_row_min.pack(anchor='e', padx=3, pady=(0, 3)) - - # Row Max - row_max_label = ttk.Label(scrollable_frame, text="Row Max (display range):", font=('Arial', 8)) - row_max_label.pack(anchor='w', padx=3, pady=(3, 0)) - self.slider_row_max_var = tk.IntVar(value=STANDARD_FOURIER_SIZE - 1) - self.scale_row_max = ttk.Scale(scrollable_frame, from_=0, to=STANDARD_FOURIER_SIZE - 1, - orient='horizontal', variable=self.slider_row_max_var, - command=self.on_slider_changed) - self.scale_row_max.pack(fill='x', expand=True, padx=3, pady=(0, 1)) - self.label_row_max = ttk.Label(scrollable_frame, text=str(STANDARD_FOURIER_SIZE - 1), font=('Arial', 8), - foreground='blue') - self.label_row_max.pack(anchor='e', padx=3, pady=(0, 3)) - - # Col Min - col_min_label = ttk.Label(scrollable_frame, text="Col Min:", font=('Arial', 8)) - col_min_label.pack(anchor='w', padx=3, pady=(3, 0)) - self.slider_col_min_var = tk.IntVar(value=0) - self.scale_col_min = ttk.Scale(scrollable_frame, from_=0, to=1000, - orient='horizontal', variable=self.slider_col_min_var, - command=self.on_col_slider_changed) - self.scale_col_min.pack(fill='x', expand=True, padx=3, pady=(0, 1)) - self.label_col_min = ttk.Label(scrollable_frame, text="0", font=('Arial', 8), foreground='blue') - self.label_col_min.pack(anchor='e', padx=3, pady=(0, 3)) - - # Col Max - col_max_label = ttk.Label(scrollable_frame, text="Col Max:", font=('Arial', 8)) - col_max_label.pack(anchor='w', padx=3, pady=(3, 0)) - self.slider_col_max_var = tk.IntVar(value=timePoint - 1) - self.scale_col_max = ttk.Scale(scrollable_frame, from_=0, to=1000, - orient='horizontal', variable=self.slider_col_max_var, - command=self.on_col_slider_changed) - self.scale_col_max.pack(fill='x', expand=True, padx=3, pady=(0, 1)) - self.label_col_max = ttk.Label(scrollable_frame, text=str(timePoint - 1), font=('Arial', 8), foreground='blue') - self.label_col_max.pack(anchor='e', padx=3, pady=(0, 3)) - - # Разделитель - ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) - - # Кнопка автопрокрутки - follow_check = tk.Checkbutton(scrollable_frame, text="📌 Автопрокрутка", - variable=self.auto_follow, font=('Arial', 8), - bg='white', activebackground='white', activeforeground='black', - command=self.on_auto_follow_changed) - follow_check.pack(anchor='w', padx=3, pady=2) - - # Кнопки - button_frame = ttk.Frame(scrollable_frame) - button_frame.pack(fill='x', padx=3, pady=2) - - reset_btn = ttk.Button(button_frame, text='🔄 Сброс', command=self.on_reset_clicked) - reset_btn.pack(fill='x', pady=1) - - export_btn = ttk.Button(button_frame, text='💾 Экспорт', command=self.on_export_clicked) - export_btn.pack(fill='x', pady=1) - - # Разделитель - ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) - - # Статус - status_label = ttk.Label(scrollable_frame, text="Статус:", font=('Arial', 9, 'bold')) - status_label.pack(anchor='w', padx=3) - - self.status_text = ttk.Label(scrollable_frame, text='Ожидание...', - font=('Arial', 7, 'italic'), foreground='blue', - wraplength=160, justify='left') - self.status_text.pack(anchor='w', padx=3, pady=2) - - # Разделитель - ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) - - # Статистика - stats_label = ttk.Label(scrollable_frame, text="Статистика:", font=('Arial', 9, 'bold')) - stats_label.pack(anchor='w', padx=3) - - self.skipped_text = ttk.Label(scrollable_frame, text='Пропущено: 0', - font=('Arial', 8), foreground='orange') - self.skipped_text.pack(anchor='w', padx=3, pady=1) - - self.processed_text = ttk.Label(scrollable_frame, text='Обработано: 0', - font=('Arial', 8), foreground='green') - self.processed_text.pack(anchor='w', padx=3, pady=1) - - # ✓ ИСПРАВЛЕНИЕ: Счётчик пропущенных кадров - self.gap_frames_text = ttk.Label(scrollable_frame, text='Пропущено кадров: 0', - font=('Arial', 8), foreground='red') - self.gap_frames_text.pack(anchor='w', padx=3, pady=1) - - self.bscan_text = ttk.Label(scrollable_frame, text='B-scan: 0 columns', - font=('Arial', 8), foreground='purple') - self.bscan_text.pack(anchor='w', padx=3, pady=1) - - self.time_text = ttk.Label(scrollable_frame, text='Время: --:--:--', - font=('Arial', 8), foreground='green') - self.time_text.pack(anchor='w', padx=3, pady=1) - - def draw_empty_plots(self): - """Рисует пустые графики с подписями.""" - self.ax_raw.cla() - self.ax_raw.plot([], []) - self.ax_raw.set_xlabel('Такты', fontsize=9) - self.ax_raw.set_ylabel('Сигнал', fontsize=9) - self.ax_raw.set_title('Данные с АЦП', fontsize=10) - self.ax_raw.grid(True, alpha=0.3) - self.ax_raw.tick_params(labelsize=8) - - self.ax_processed.cla() - self.ax_processed.plot([], []) - self.ax_processed.set_xlabel('Частота', fontsize=9) - self.ax_processed.set_ylabel('Сигнал', fontsize=9) - self.ax_processed.set_title('Обработанные данные', fontsize=10) - self.ax_processed.grid(True, alpha=0.3) - self.ax_processed.tick_params(labelsize=8) - - self.ax_fourier.cla() - self.ax_fourier.plot([], []) - self.ax_fourier.set_title('Фурье образ', fontsize=10) - self.ax_fourier.grid(True, alpha=0.3) - self.ax_fourier.tick_params(labelsize=8) - - self.ax_bscan.cla() - self.ax_bscan.set_title('B-Scan (динамический)', fontsize=10) - self.ax_bscan.set_xlabel('Время →', fontsize=9) - self.ax_bscan.set_ylabel('Частота/Дистанция', fontsize=9) - self.ax_bscan.tick_params(labelsize=8) - - self.canvas.draw() - - # ============================================================================ - # CALLBACKS ИНТЕРФЕЙСА - # ============================================================================ - - def on_file_interval_changed(self): - """Callback при изменении интервала между файлами через Spinbox.""" - try: - new_interval = self.interval_var.get() - if 50 <= new_interval <= 5000: - self.file_interval_ms = new_interval - self.time_gap_threshold_ms = new_interval * GAP_THRESHOLD_MULTIPLIER - self.interval_label.config(text=f"{new_interval}ms") - self.interval_scale.set(new_interval) - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print( - f"[{timestamp}] 📊 File interval changed to {new_interval}ms (gap threshold: {self.time_gap_threshold_ms:.0f}ms)") - else: - self.interval_var.set(self.file_interval_ms) - except: - self.interval_var.set(self.file_interval_ms) - - def on_interval_scale_changed(self, val): - """Callback при изменении интервала между файлами через Scale.""" - try: - new_interval = int(float(val)) - if 50 <= new_interval <= 5000: - self.file_interval_ms = new_interval - self.time_gap_threshold_ms = new_interval * GAP_THRESHOLD_MULTIPLIER - self.interval_var.set(new_interval) - self.interval_label.config(text=f"{new_interval}ms") - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print( - f"[{timestamp}] 📊 File interval changed to {new_interval}ms (via slider, gap threshold: {self.time_gap_threshold_ms:.0f}ms)") - else: - self.interval_scale.set(self.file_interval_ms) - except: - self.interval_scale.set(self.file_interval_ms) - - def on_poll_interval_changed(self): - """Callback при изменении времени опроса файлов через Spinbox.""" - try: - new_interval = self.poll_interval_var.get() - if 10 <= new_interval <= 5000: - self.file_poll_interval_ms = new_interval - self.poll_label.config(text=f"{new_interval}ms") - self.poll_scale.set(new_interval) - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print(f"[{timestamp}] ⏱️ Poll interval changed to {new_interval}ms") - else: - self.poll_interval_var.set(self.file_poll_interval_ms) - except: - self.poll_interval_var.set(self.file_poll_interval_ms) - - def on_poll_scale_changed(self, val): - """Callback при изменении времени опроса файлов через Scale.""" - try: - new_interval = int(float(val)) - if 10 <= new_interval <= 5000: - self.file_poll_interval_ms = new_interval - self.poll_interval_var.set(new_interval) - self.poll_label.config(text=f"{new_interval}ms") - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print(f"[{timestamp}] ⏱️ Poll interval changed to {new_interval}ms (via slider)") - else: - self.poll_scale.set(self.file_poll_interval_ms) - except: - self.poll_scale.set(self.file_poll_interval_ms) - - def on_slider_changed(self, val=None): - """Callback при изменении слайдеров Row Min/Max.""" - self.bscan_row_min = self.slider_row_min_var.get() - self.bscan_row_max = self.slider_row_max_var.get() - - self.label_row_min.config(text=str(self.bscan_row_min)) - self.label_row_max.config(text=str(self.bscan_row_max)) - - self.redraw_bscan_immediate() - - def on_col_slider_changed(self, val=None): - """Callback для Col Min/Max - обновляет B_scan и перерисовывает.""" - self.bscan_col_min = self.slider_col_min_var.get() - self.bscan_col_max = self.slider_col_max_var.get() - - self.label_col_min.config(text=str(self.bscan_col_min)) - self.label_col_max.config(text=str(self.bscan_col_max)) - - # Отключаем автопрокрутку при ручном выборе - self.manual_range_selection = True - self.auto_follow.set(False) - - # СРАЗУ перерисовываем B_scan - self.redraw_bscan_immediate() - - def on_auto_follow_changed(self): - """Callback при изменении автопрокрутки.""" - if self.auto_follow.get(): - self.manual_range_selection = False - - def on_reset_clicked(self): - """Callback при нажатии кнопки Reset.""" - self.slider_row_min_var.set(0) - self.slider_row_max_var.set(STANDARD_FOURIER_SIZE - 1) - self.slider_col_min_var.set(0) - self.slider_col_max_var.set(min(timePoint - 1, max(0, len(self.B_scan_data) - 1))) - self.manual_range_selection = False - self.auto_follow.set(True) - - def on_export_clicked(self): - """Callback для экспорта графиков.""" - timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")[:-3] - filename = f"export_{timestamp}.png" - self.fig.savefig(filename, dpi=150, bbox_inches='tight') - print(f"✓ Экспортировано: {filename}") - - def redraw_bscan_immediate(self): - """Немедленная перерисовка B_scan при изменении ползунков.""" - if len(self.B_scan_data) == 0: - return - - total_cols = len(self.B_scan_data) - - col_min = min(self.bscan_col_min, total_cols - 1) - col_max = min(self.bscan_col_max + 1, total_cols) - - # Проверяем корректность диапазона - if col_min >= col_max: - return - - row_min = self.bscan_row_min - row_max = self.bscan_row_max - - # Собираем данные для отображения - display_cols = [] - display_types = [] # Тип каждого столбца - for i in range(col_min, col_max): - col_data = self.B_scan_data[i] - col_type = self.B_scan_types[i] - # Берём диапазон по высоте - if len(col_data) > row_min: - row_end = min(row_max + 1, len(col_data)) - row_start = min(row_min, len(col_data) - 1) - if row_end > row_start: - display_cols.append(col_data[row_start:row_end]) - display_types.append(col_type) - - display_times = self.B_scan_times[col_min:col_max] - - # Перерисовываем B_scan - self.ax_bscan.cla() - if display_cols: - # Находим максимальную длину для создания матрицы - max_len = max(len(col) for col in display_cols) - - # Создаём матрицу, заполняя нулями если необходимо - display_data = np.zeros((max_len, len(display_cols))) - for i, col in enumerate(display_cols): - display_data[:len(col), i] = col - - # Проверяем столбцы на GAP - gap_mask = np.array([col_type == "GAP" for col_type in display_types]) - - # Применяем маску: GAP столбцы становятся белыми (все = max_val * 1.5) - max_val = display_data.max() - if max_val > 0: - for i, is_gap in enumerate(gap_mask): - if is_gap: - display_data[:, i] = max_val * 1.5 # Белый (выше максимума) - - im = self.ax_bscan.imshow(display_data, origin='lower', aspect='auto', cmap='viridis') - - time_labels = [t.strftime("%H:%M:%S.%f")[:-3] if t else "?" for t in display_times] - - if len(time_labels) > 1: - step = max(1, len(time_labels) // 10) - tick_positions = np.arange(0, len(time_labels), step) - tick_labels_subset = [time_labels[i] if i < len(time_labels) else "" for i in tick_positions] - self.ax_bscan.set_xticks(tick_positions) - self.ax_bscan.set_xticklabels(tick_labels_subset, rotation=45, ha='right', fontsize=7) - else: - self.ax_bscan.set_xticks([]) - - self.ax_bscan.set_title(f'B-Scan (Cols {col_min}-{col_max - 1})', fontsize=10) - self.ax_bscan.set_xlabel('Время →', fontsize=9) - self.ax_bscan.set_ylabel(f'Диапазон [{row_min}-{row_max}]', fontsize=9) - self.ax_bscan.tick_params(labelsize=8) - - # Сразу рисуем - self.canvas.draw() - - # ============================================================================ - # ОБРАБОТКА ДАННЫХ - # ============================================================================ - - def process_raw_data(self, A, original_size): - """Обработка сырых данных.""" - A_1d = A[:, 0] if A.ndim > 1 else A - A_resized = resize_1d_interpolate(A_1d, STANDARD_RAW_SIZE) - - if self.SizeFirst != STANDARD_RAW_SIZE: - self.SizeFirst = STANDARD_RAW_SIZE - self.time_idx = np.arange(1, self.SizeFirst + 1) - self.B = None - self.meandr = None - self.k = 1 - self.SUM = 0.0 - - if self.k < PeriodIntegrate: - if isinstance(self.SUM, float) and self.SUM == 0.0: - self.SUM = A_resized.astype(float) - else: - self.SUM = self.SUM + A_resized - - if self.k == 1: - self.B = np.zeros((self.SizeFirst, PeriodIntegrate + 2)) - self.B[:, 0] = A_resized - self.meandr = square(self.time_idx * np.pi) - self.timestart2 = time.perf_counter() - else: - self.B[:, self.k] = A_resized - - self.k += 1 - return False, None - - else: - last_col = PeriodIntegrate + 1 - self.B[:, last_col] = A_resized - self.B = np.roll(self.B, 1, axis=1) - - if PeriodIntegrate > 1: - meanB = np.sum(self.B[:, 1:PeriodIntegrate], axis=1) / PeriodIntegrate - else: - meanB = self.B[:, 0] - - mwanBmeandr = meanB * self.meandr - - signal_list = [] - signalView_list = [] - timeSignal_list = [] - - start = 0 - numberOfFreqChangeStart = 0 - for numberOfFreqChange in range(self.SizeFirst): - if (numberOfFreqChange - start) > pontInOneFqChange: - segment = mwanBmeandr[numberOfFreqChangeStart:numberOfFreqChange] - if segment.size > 0: - signal_list.append(np.sum(segment)) - signalView_list.append(np.mean(segment)) - timeSignal_list.append(numberOfFreqChange) - start = numberOfFreqChange - numberOfFreqChangeStart = numberOfFreqChange - - self.signal = np.array(signal_list, dtype=float) - self.signalView = np.array(signalView_list, dtype=float) - self.timeSignal = np.array(timeSignal_list, dtype=int) - - self.compute_fft() - - if self.FshiftS is not None: - center = len(self.FshiftS) // 2 - src_start = center + FFT0_delta - src_end = src_start + height - if src_end <= len(self.FshiftS): - bscan_col = self.FshiftS[src_start:src_end].copy() - return True, bscan_col - - return True, None - - def process_sync_det_data(self, A, original_size): - """Обработка данных синхронного детектирования.""" - A_1d = A[:, 0] if A.ndim > 1 else A - A_resized = resize_1d_interpolate(A_1d, STANDARD_SYNC_SIZE) - - self.signal = A_resized - self.signalView = self.signal * 0.1 - self.timeSignal = np.arange(len(self.signal)) - - self.compute_fft() - - if self.FshiftS is not None: - center = len(self.FshiftS) // 2 - src_start = center + FFT0_delta - src_end = src_start + height - if src_end <= len(self.FshiftS): - bscan_col = self.FshiftS[src_start:src_end].copy() - return True, bscan_col - - return True, None - + self.processed_files = set(existing_files) + + if existing_files: + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ℹ️ Skipping {len(existing_files)} existing files\n") + + # Инициализация состояния обработки + self.SizeFirst = STANDARD_RAW_SIZE + self.time_idx = np.arange(1, self.SizeFirst + 1) + self.k = 1 + self.SUM = 0.0 + self.B = None + self.meandr = None + self.signal = np.array([]) + self.signalView = np.array([]) + self.timeSignal = np.array([]) + + # История B_scan с привязкой к времени файла + self.B_scan_data = [] + self.B_scan_times = [] + self.B_scan_types = [] # RAW, SYNC_DET, FOURIER, GAP + self.last_file_time = None + + # Интервал между файлами (в миллисекундах) - задаётся пользователем + self.file_interval_ms = DEFAULT_FILE_INTERVAL_MS + self.time_gap_threshold_ms = self.file_interval_ms * GAP_THRESHOLD_MULTIPLIER + + # ✓ ИСПРАВЛЕНИЕ: Счётчик пропущенных кадров + self.gap_frames_count = 0 + + self.startPointTime = 0 + self.timestart = time.perf_counter() + self.timestart2 = None + self.FshiftS = None + + # Очередь для добавления колонок (без блокировок) + self.bscan_queue = queue.Queue() + + # Для потоковой отрисовки + self.update_lock = threading.Lock() + self.pending_update = False + self.pending_original_size = None + self.pending_data_type = None + + # Автопрокрутка по времени + self.auto_follow = tk.BooleanVar(value=True) + + # Флаг для отключения автопрокрутки при ручном выборе + self.manual_range_selection = False + + # Время опроса файлов (в миллисекундах) + self.file_poll_interval_ms = DEFAULT_FILE_POLL_INTERVAL_MS + + # Параметры слайдеров для B_scan + self.bscan_row_min = 0 + self.bscan_row_max = height - 1 + self.bscan_col_min = 0 + self.bscan_col_max = timePoint - 1 + + # Главный фрейм с двумя колонками + self.main_frame = ttk.Frame(self.root) + self.main_frame.pack(fill='both', expand=True, padx=3, pady=3) + + # Левая колонка: Графики (65%) + self.left_frame = ttk.Frame(self.main_frame) + self.left_frame.pack(side='left', fill='both', expand=True, padx=(0, 3)) + + # Правая колонка: Настройки (35%) + self.right_frame = ttk.Frame(self.main_frame, width=210) + self.right_frame.pack(side='right', fill='both', padx=(3, 0)) + self.right_frame.pack_propagate(False) + + # Инициализация графиков и настроек + self.init_plots_panel() + self.init_settings_panel() + + self.processed_count = 0 + self.skipped_count = 0 + + # ============================================================================ + # ИНИЦИАЛИЗАЦИЯ ИНТЕРФЕЙСА + # ============================================================================ + + def init_plots_panel(self): + """Инициализация панели с графиками (слева).""" + title = ttk.Label(self.left_frame, text="Графики", font=('Arial', 11, 'bold')) + title.pack() + + fig_frame = ttk.Frame(self.left_frame) + fig_frame.pack(fill='both', expand=True, padx=3, pady=3) + + self.fig = plt.Figure(figsize=(9, 6.5), dpi=100) + gs = self.fig.add_gridspec(2, 2, hspace=0.4, wspace=0.35) + + self.ax_raw = self.fig.add_subplot(gs[0, 0]) + self.ax_processed = self.fig.add_subplot(gs[0, 1]) + self.ax_fourier = self.fig.add_subplot(gs[1, 0]) + self.ax_bscan = self.fig.add_subplot(gs[1, 1]) + + self.canvas = FigureCanvasTkAgg(self.fig, master=fig_frame) + self.canvas.draw() + self.canvas.get_tk_widget().pack(fill='both', expand=True) + + self.draw_empty_plots() + + def init_settings_panel(self): + """Инициализация панели с настройками (справа).""" + title = ttk.Label(self.right_frame, text="⚙️ Настройки", font=('Arial', 10, 'bold')) + title.pack(pady=3, padx=3) + + # Canvas с прокруткой + self.settings_canvas = tk.Canvas(self.right_frame, bg='white', highlightthickness=0) + scrollbar = ttk.Scrollbar(self.right_frame, orient='vertical', command=self.settings_canvas.yview) + scrollable_frame = ttk.Frame(self.settings_canvas) + + scrollable_frame.bind( + "", + lambda e: self.settings_canvas.configure(scrollregion=self.settings_canvas.bbox("all")) + ) + + self.settings_canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + self.settings_canvas.configure(yscrollcommand=scrollbar.set) + + self.settings_canvas.pack(side='left', fill='both', expand=True) + scrollbar.pack(side='right', fill='y') + + # Контроль интервала между файлами + interval_label = ttk.Label(scrollable_frame, text="📊 File Interval (ms):", + font=('Arial', 9, 'bold'), foreground='darkblue') + interval_label.pack(anchor='w', padx=3, pady=(5, 0)) + + interval_control_frame = ttk.Frame(scrollable_frame) + interval_control_frame.pack(fill='x', padx=3, pady=(0, 2)) + + self.interval_var = tk.IntVar(value=self.file_interval_ms) + interval_spinbox = ttk.Spinbox(interval_control_frame, from_=50, to=5000, + textvariable=self.interval_var, + width=6, command=self.on_file_interval_changed) + interval_spinbox.pack(side='left', padx=(0, 3)) + + self.interval_scale = ttk.Scale(interval_control_frame, from_=50, to=5000, orient='horizontal', + variable=self.interval_var, + command=self.on_interval_scale_changed) + self.interval_scale.pack(side='left', fill='x', expand=True, padx=(0, 3)) + + self.interval_label = ttk.Label(interval_control_frame, text=f"{self.file_interval_ms}ms", + font=('Arial', 8), foreground='darkblue', width=6, + relief='sunken', anchor='center') + self.interval_label.pack(side='left') + + interval_hint = ttk.Label(scrollable_frame, text="Диапазон: 50-5000 мс", + font=('Arial', 7), foreground='gray') + interval_hint.pack(anchor='w', padx=3, pady=(0, 3)) + + # Контроль времени опроса файлов + poll_label = ttk.Label(scrollable_frame, text="⏱️ Poll Interval (ms):", + font=('Arial', 9, 'bold'), foreground='darkred') + poll_label.pack(anchor='w', padx=3, pady=(5, 0)) + + poll_control_frame = ttk.Frame(scrollable_frame) + poll_control_frame.pack(fill='x', padx=3, pady=(0, 2)) + + self.poll_interval_var = tk.IntVar(value=self.file_poll_interval_ms) + poll_spinbox = ttk.Spinbox(poll_control_frame, from_=10, to=5000, + textvariable=self.poll_interval_var, + width=6, command=self.on_poll_interval_changed) + poll_spinbox.pack(side='left', padx=(0, 3)) + + self.poll_scale = ttk.Scale(poll_control_frame, from_=10, to=5000, orient='horizontal', + variable=self.poll_interval_var, + command=self.on_poll_scale_changed) + self.poll_scale.pack(side='left', fill='x', expand=True, padx=(0, 3)) + + self.poll_label = ttk.Label(poll_control_frame, text=f"{self.file_poll_interval_ms}ms", + font=('Arial', 8), foreground='darkred', width=6, + relief='sunken', anchor='center') + self.poll_label.pack(side='left') + + poll_hint = ttk.Label(scrollable_frame, text="Диапазон: 10-5000 мс", + font=('Arial', 7), foreground='gray') + poll_hint.pack(anchor='w', padx=3, pady=(0, 3)) + + ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) + + # Информация о кодировке кадров + legend_label = ttk.Label(scrollable_frame, text="B-Scan Legend:", + font=('Arial', 9, 'bold'), foreground='darkgreen') + legend_label.pack(anchor='w', padx=3, pady=3) + + legend_text = ttk.Label(scrollable_frame, + text="■ Colors: DATA\n■ White: GAP (lost frame)\n■ Black: ZERO (missing)", + font=('Arial', 8), foreground='darkgreen', justify='left') + legend_text.pack(anchor='w', padx=3, pady=(0, 3)) + + ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) + + # Информация о типах данных и размерах + info_label = ttk.Label(scrollable_frame, text=f"B-scan types:", + font=('Arial', 9, 'bold'), foreground='darkblue') + info_label.pack(anchor='w', padx=3, pady=3) + + info_text = ttk.Label(scrollable_frame, + text=f"RAW/SYNC_DET: {height}px\nFOURIER: {STANDARD_FOURIER_SIZE}px", + font=('Arial', 8), foreground='darkblue', justify='left') + info_text.pack(anchor='w', padx=3, pady=(0, 3)) + + ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=2) + + # Row Min/Max для выбора диапазона отображения + row_min_label = ttk.Label(scrollable_frame, text="Row Min (display range):", font=('Arial', 8)) + row_min_label.pack(anchor='w', padx=3, pady=(3, 0)) + self.slider_row_min_var = tk.IntVar(value=0) + self.scale_row_min = ttk.Scale(scrollable_frame, from_=0, to=STANDARD_FOURIER_SIZE - 1, + orient='horizontal', variable=self.slider_row_min_var, + command=self.on_slider_changed) + self.scale_row_min.pack(fill='x', expand=True, padx=3, pady=(0, 1)) + self.label_row_min = ttk.Label(scrollable_frame, text="0", font=('Arial', 8), foreground='blue') + self.label_row_min.pack(anchor='e', padx=3, pady=(0, 3)) + + # Row Max + row_max_label = ttk.Label(scrollable_frame, text="Row Max (display range):", font=('Arial', 8)) + row_max_label.pack(anchor='w', padx=3, pady=(3, 0)) + self.slider_row_max_var = tk.IntVar(value=STANDARD_FOURIER_SIZE - 1) + self.scale_row_max = ttk.Scale(scrollable_frame, from_=0, to=STANDARD_FOURIER_SIZE - 1, + orient='horizontal', variable=self.slider_row_max_var, + command=self.on_slider_changed) + self.scale_row_max.pack(fill='x', expand=True, padx=3, pady=(0, 1)) + self.label_row_max = ttk.Label(scrollable_frame, text=str(STANDARD_FOURIER_SIZE - 1), font=('Arial', 8), + foreground='blue') + self.label_row_max.pack(anchor='e', padx=3, pady=(0, 3)) + + # Col Min + col_min_label = ttk.Label(scrollable_frame, text="Col Min:", font=('Arial', 8)) + col_min_label.pack(anchor='w', padx=3, pady=(3, 0)) + self.slider_col_min_var = tk.IntVar(value=0) + self.scale_col_min = ttk.Scale(scrollable_frame, from_=0, to=1000, + orient='horizontal', variable=self.slider_col_min_var, + command=self.on_col_slider_changed) + self.scale_col_min.pack(fill='x', expand=True, padx=3, pady=(0, 1)) + self.label_col_min = ttk.Label(scrollable_frame, text="0", font=('Arial', 8), foreground='blue') + self.label_col_min.pack(anchor='e', padx=3, pady=(0, 3)) + + # Col Max + col_max_label = ttk.Label(scrollable_frame, text="Col Max:", font=('Arial', 8)) + col_max_label.pack(anchor='w', padx=3, pady=(3, 0)) + self.slider_col_max_var = tk.IntVar(value=timePoint - 1) + self.scale_col_max = ttk.Scale(scrollable_frame, from_=0, to=1000, + orient='horizontal', variable=self.slider_col_max_var, + command=self.on_col_slider_changed) + self.scale_col_max.pack(fill='x', expand=True, padx=3, pady=(0, 1)) + self.label_col_max = ttk.Label(scrollable_frame, text=str(timePoint - 1), font=('Arial', 8), foreground='blue') + self.label_col_max.pack(anchor='e', padx=3, pady=(0, 3)) + + # Разделитель + ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) + + # Кнопка автопрокрутки + follow_check = tk.Checkbutton(scrollable_frame, text="📌 Автопрокрутка", + variable=self.auto_follow, font=('Arial', 8), + bg='white', activebackground='white', activeforeground='black', + command=self.on_auto_follow_changed) + follow_check.pack(anchor='w', padx=3, pady=2) + + # Кнопки + button_frame = ttk.Frame(scrollable_frame) + button_frame.pack(fill='x', padx=3, pady=2) + + reset_btn = ttk.Button(button_frame, text='🔄 Сброс', command=self.on_reset_clicked) + reset_btn.pack(fill='x', pady=1) + + export_btn = ttk.Button(button_frame, text='💾 Экспорт', command=self.on_export_clicked) + export_btn.pack(fill='x', pady=1) + + # Разделитель + ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) + + # Статус + status_label = ttk.Label(scrollable_frame, text="Статус:", font=('Arial', 9, 'bold')) + status_label.pack(anchor='w', padx=3) + + self.status_text = ttk.Label(scrollable_frame, text='Ожидание...', + font=('Arial', 7, 'italic'), foreground='blue', + wraplength=160, justify='left') + self.status_text.pack(anchor='w', padx=3, pady=2) + + # Разделитель + ttk.Separator(scrollable_frame, orient='horizontal').pack(fill='x', padx=3, pady=3) + + # Статистика + stats_label = ttk.Label(scrollable_frame, text="Статистика:", font=('Arial', 9, 'bold')) + stats_label.pack(anchor='w', padx=3) + + self.skipped_text = ttk.Label(scrollable_frame, text='Пропущено: 0', + font=('Arial', 8), foreground='orange') + self.skipped_text.pack(anchor='w', padx=3, pady=1) + + self.processed_text = ttk.Label(scrollable_frame, text='Обработано: 0', + font=('Arial', 8), foreground='green') + self.processed_text.pack(anchor='w', padx=3, pady=1) + + # ✓ ИСПРАВЛЕНИЕ: Счётчик пропущенных кадров + self.gap_frames_text = ttk.Label(scrollable_frame, text='Пропущено кадров: 0', + font=('Arial', 8), foreground='red') + self.gap_frames_text.pack(anchor='w', padx=3, pady=1) + + self.bscan_text = ttk.Label(scrollable_frame, text='B-scan: 0 columns', + font=('Arial', 8), foreground='purple') + self.bscan_text.pack(anchor='w', padx=3, pady=1) + + self.time_text = ttk.Label(scrollable_frame, text='Время: --:--:--', + font=('Arial', 8), foreground='green') + self.time_text.pack(anchor='w', padx=3, pady=1) + + def draw_empty_plots(self): + """Рисует пустые графики с подписями.""" + self.ax_raw.cla() + self.ax_raw.plot([], []) + self.ax_raw.set_xlabel('Такты', fontsize=9) + self.ax_raw.set_ylabel('Сигнал', fontsize=9) + self.ax_raw.set_title('Данные с АЦП', fontsize=10) + self.ax_raw.grid(True, alpha=0.3) + self.ax_raw.tick_params(labelsize=8) + + self.ax_processed.cla() + self.ax_processed.plot([], []) + self.ax_processed.set_xlabel('Частота', fontsize=9) + self.ax_processed.set_ylabel('Сигнал', fontsize=9) + self.ax_processed.set_title('Обработанные данные', fontsize=10) + self.ax_processed.grid(True, alpha=0.3) + self.ax_processed.tick_params(labelsize=8) + + self.ax_fourier.cla() + self.ax_fourier.plot([], []) + self.ax_fourier.set_title('Фурье образ', fontsize=10) + self.ax_fourier.grid(True, alpha=0.3) + self.ax_fourier.tick_params(labelsize=8) + + self.ax_bscan.cla() + self.ax_bscan.set_title('B-Scan (динамический)', fontsize=10) + self.ax_bscan.set_xlabel('Время →', fontsize=9) + self.ax_bscan.set_ylabel('Частота/Дистанция', fontsize=9) + self.ax_bscan.tick_params(labelsize=8) + + self.canvas.draw() + + # ============================================================================ + # CALLBACKS ИНТЕРФЕЙСА + # ============================================================================ + + def on_file_interval_changed(self): + """Callback при изменении интервала между файлами через Spinbox.""" + try: + new_interval = self.interval_var.get() + if 50 <= new_interval <= 5000: + self.file_interval_ms = new_interval + self.time_gap_threshold_ms = new_interval * GAP_THRESHOLD_MULTIPLIER + self.interval_label.config(text=f"{new_interval}ms") + self.interval_scale.set(new_interval) + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print( + f"[{timestamp}] 📊 File interval changed to {new_interval}ms (gap threshold: {self.time_gap_threshold_ms:.0f}ms)") + else: + self.interval_var.set(self.file_interval_ms) + except: + self.interval_var.set(self.file_interval_ms) + + def on_interval_scale_changed(self, val): + """Callback при изменении интервала между файлами через Scale.""" + try: + new_interval = int(float(val)) + if 50 <= new_interval <= 5000: + self.file_interval_ms = new_interval + self.time_gap_threshold_ms = new_interval * GAP_THRESHOLD_MULTIPLIER + self.interval_var.set(new_interval) + self.interval_label.config(text=f"{new_interval}ms") + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print( + f"[{timestamp}] 📊 File interval changed to {new_interval}ms (via slider, gap threshold: {self.time_gap_threshold_ms:.0f}ms)") + else: + self.interval_scale.set(self.file_interval_ms) + except: + self.interval_scale.set(self.file_interval_ms) + + def on_poll_interval_changed(self): + """Callback при изменении времени опроса файлов через Spinbox.""" + try: + new_interval = self.poll_interval_var.get() + if 10 <= new_interval <= 5000: + self.file_poll_interval_ms = new_interval + self.poll_label.config(text=f"{new_interval}ms") + self.poll_scale.set(new_interval) + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ⏱️ Poll interval changed to {new_interval}ms") + else: + self.poll_interval_var.set(self.file_poll_interval_ms) + except: + self.poll_interval_var.set(self.file_poll_interval_ms) + + def on_poll_scale_changed(self, val): + """Callback при изменении времени опроса файлов через Scale.""" + try: + new_interval = int(float(val)) + if 10 <= new_interval <= 5000: + self.file_poll_interval_ms = new_interval + self.poll_interval_var.set(new_interval) + self.poll_label.config(text=f"{new_interval}ms") + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ⏱️ Poll interval changed to {new_interval}ms (via slider)") + else: + self.poll_scale.set(self.file_poll_interval_ms) + except: + self.poll_scale.set(self.file_poll_interval_ms) + + def on_slider_changed(self, val=None): + """Callback при изменении слайдеров Row Min/Max.""" + self.bscan_row_min = self.slider_row_min_var.get() + self.bscan_row_max = self.slider_row_max_var.get() + + self.label_row_min.config(text=str(self.bscan_row_min)) + self.label_row_max.config(text=str(self.bscan_row_max)) + + self.redraw_bscan_immediate() + + def on_col_slider_changed(self, val=None): + """Callback для Col Min/Max - обновляет B_scan и перерисовывает.""" + self.bscan_col_min = self.slider_col_min_var.get() + self.bscan_col_max = self.slider_col_max_var.get() + + self.label_col_min.config(text=str(self.bscan_col_min)) + self.label_col_max.config(text=str(self.bscan_col_max)) + + # Отключаем автопрокрутку при ручном выборе + self.manual_range_selection = True + self.auto_follow.set(False) + + # СРАЗУ перерисовываем B_scan + self.redraw_bscan_immediate() + + def on_auto_follow_changed(self): + """Callback при изменении автопрокрутки.""" + if self.auto_follow.get(): + self.manual_range_selection = False + + def on_reset_clicked(self): + """Callback при нажатии кнопки Reset.""" + self.slider_row_min_var.set(0) + self.slider_row_max_var.set(STANDARD_FOURIER_SIZE - 1) + self.slider_col_min_var.set(0) + self.slider_col_max_var.set(min(timePoint - 1, max(0, len(self.B_scan_data) - 1))) + self.manual_range_selection = False + self.auto_follow.set(True) + + def on_export_clicked(self): + """Callback для экспорта графиков.""" + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")[:-3] + filename = f"export_{timestamp}.png" + self.fig.savefig(filename, dpi=150, bbox_inches='tight') + print(f"✓ Экспортировано: {filename}") + + def redraw_bscan_immediate(self): + """Немедленная перерисовка B_scan при изменении ползунков.""" + if len(self.B_scan_data) == 0: + return + + total_cols = len(self.B_scan_data) + + col_min = min(self.bscan_col_min, total_cols - 1) + col_max = min(self.bscan_col_max + 1, total_cols) + + # Проверяем корректность диапазона + if col_min >= col_max: + return + + row_min = self.bscan_row_min + row_max = self.bscan_row_max + + # Собираем данные для отображения + display_cols = [] + display_types = [] # Тип каждого столбца + for i in range(col_min, col_max): + col_data = self.B_scan_data[i] + col_type = self.B_scan_types[i] + # Берём диапазон по высоте + if len(col_data) > row_min: + row_end = min(row_max + 1, len(col_data)) + row_start = min(row_min, len(col_data) - 1) + if row_end > row_start: + display_cols.append(col_data[row_start:row_end]) + display_types.append(col_type) + + display_times = self.B_scan_times[col_min:col_max] + + # Перерисовываем B_scan + self.ax_bscan.cla() + if display_cols: + # Находим максимальную длину для создания матрицы + max_len = max(len(col) for col in display_cols) + + # Создаём матрицу, заполняя нулями если необходимо + display_data = np.zeros((max_len, len(display_cols))) + for i, col in enumerate(display_cols): + display_data[:len(col), i] = col + + # Проверяем столбцы на GAP + gap_mask = np.array([col_type == "GAP" for col_type in display_types]) + + # Применяем маску: GAP столбцы становятся белыми (все = max_val * 1.5) + max_val = display_data.max() + if max_val > 0: + for i, is_gap in enumerate(gap_mask): + if is_gap: + display_data[:, i] = max_val * 1.5 # Белый (выше максимума) + + im = self.ax_bscan.imshow(display_data, origin='lower', aspect='auto', cmap='viridis') + + time_labels = [t.strftime("%H:%M:%S.%f")[:-3] if t else "?" for t in display_times] + + if len(time_labels) > 1: + step = max(1, len(time_labels) // 10) + tick_positions = np.arange(0, len(time_labels), step) + tick_labels_subset = [time_labels[i] if i < len(time_labels) else "" for i in tick_positions] + self.ax_bscan.set_xticks(tick_positions) + self.ax_bscan.set_xticklabels(tick_labels_subset, rotation=45, ha='right', fontsize=7) + else: + self.ax_bscan.set_xticks([]) + + self.ax_bscan.set_title(f'B-Scan (Cols {col_min}-{col_max - 1})', fontsize=10) + self.ax_bscan.set_xlabel('Время →', fontsize=9) + self.ax_bscan.set_ylabel(f'Диапазон [{row_min}-{row_max}]', fontsize=9) + self.ax_bscan.tick_params(labelsize=8) + + # Сразу рисуем + self.canvas.draw() + + # ============================================================================ + # ОБРАБОТКА ДАННЫХ + # ============================================================================ + + def process_raw_data(self, A, original_size): + """Обработка сырых данных.""" + A_1d = A[:, 0] if A.ndim > 1 else A + A_resized = resize_1d_interpolate(A_1d, STANDARD_RAW_SIZE) + + if self.SizeFirst != STANDARD_RAW_SIZE: + self.SizeFirst = STANDARD_RAW_SIZE + self.time_idx = np.arange(1, self.SizeFirst + 1) + self.B = None + self.meandr = None + self.k = 1 + self.SUM = 0.0 + + if self.k < PeriodIntegrate: + if isinstance(self.SUM, float) and self.SUM == 0.0: + self.SUM = A_resized.astype(float) + else: + self.SUM = self.SUM + A_resized + + if self.k == 1: + self.B = np.zeros((self.SizeFirst, PeriodIntegrate + 2)) + self.B[:, 0] = A_resized + self.meandr = square(self.time_idx * np.pi) + self.timestart2 = time.perf_counter() + else: + self.B[:, self.k] = A_resized + + self.k += 1 + return False, None + + else: + last_col = PeriodIntegrate + 1 + self.B[:, last_col] = A_resized + self.B = np.roll(self.B, 1, axis=1) + + if PeriodIntegrate > 1: + meanB = np.sum(self.B[:, 1:PeriodIntegrate], axis=1) / PeriodIntegrate + else: + meanB = self.B[:, 0] + + mwanBmeandr = meanB * self.meandr + + signal_list = [] + signalView_list = [] + timeSignal_list = [] + + start = 0 + numberOfFreqChangeStart = 0 + for numberOfFreqChange in range(self.SizeFirst): + if (numberOfFreqChange - start) > pontInOneFqChange: + segment = mwanBmeandr[numberOfFreqChangeStart:numberOfFreqChange] + if segment.size > 0: + signal_list.append(np.sum(segment)) + signalView_list.append(np.mean(segment)) + timeSignal_list.append(numberOfFreqChange) + start = numberOfFreqChange + numberOfFreqChangeStart = numberOfFreqChange + + self.signal = np.array(signal_list, dtype=float) + self.signalView = np.array(signalView_list, dtype=float) + self.timeSignal = np.array(timeSignal_list, dtype=int) + + self.compute_fft() + + if self.FshiftS is not None: + center = len(self.FshiftS) // 2 + src_start = center + FFT0_delta + src_end = src_start + height + if src_end <= len(self.FshiftS): + bscan_col = self.FshiftS[src_start:src_end].copy() + return True, bscan_col + + return True, None + + def process_sync_det_data(self, A, original_size): + """Обработка данных синхронного детектирования.""" + A_1d = A[:, 0] if A.ndim > 1 else A + A_resized = resize_1d_interpolate(A_1d, STANDARD_SYNC_SIZE) + + self.signal = A_resized + self.signalView = self.signal * 0.1 + self.timeSignal = np.arange(len(self.signal)) + + self.compute_fft() + + if self.FshiftS is not None: + center = len(self.FshiftS) // 2 + src_start = center + FFT0_delta + src_end = src_start + height + if src_end <= len(self.FshiftS): + bscan_col = self.FshiftS[src_start:src_end].copy() + return True, bscan_col + + return True, None + def process_fourier_data(self, A, original_size): """Обработка FOURIER без интерполяции. Поддерживает несколько сегментов.""" columns_to_add = [] @@ -948,160 +950,160 @@ class DataAnalyzerApp: return True, columns_to_add return True, columns_to_add - - def add_bscan_column(self, data_col, current_time, data_type): - """Добавить колонку в B-скан (может быть разного размера).""" - - if self.last_file_time is None: - self.B_scan_data.append(data_col) - self.B_scan_times.append(current_time) - self.B_scan_types.append(data_type) - self.last_file_time = current_time - return - - # Используем переменный интервал между файлами - time_diff_ms = (current_time - self.last_file_time).total_seconds() * 1000 - - if time_diff_ms > self.time_gap_threshold_ms: - missing_count = int(round(time_diff_ms / self.file_interval_ms)) - 1 - - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print( - f"[{timestamp}] ⚠️ Gap detected: {time_diff_ms:.1f}ms (missing ~{missing_count} columns, threshold: {self.time_gap_threshold_ms:.0f}ms)") - - # ✓ ИСПРАВЛЕНИЕ: Увеличиваем счётчик пропущенных кадров - self.gap_frames_count += missing_count - - last_size = len(self.B_scan_data[-1]) if self.B_scan_data else height - for i in range(missing_count): - zero_col = np.zeros(last_size) - self.B_scan_data.append(zero_col) - gap_time = self.last_file_time + timedelta(milliseconds=self.file_interval_ms * (i + 1)) - self.B_scan_times.append(gap_time) - # Отмечаем как GAP (пропущенный кадр) - self.B_scan_types.append("GAP") - - self.B_scan_data.append(data_col) - self.B_scan_times.append(current_time) - self.B_scan_types.append(data_type) - self.last_file_time = current_time - - def compute_fft(self): - """Вычисляем FFT спектр.""" - if self.signal.size > 0: - sig_cut = self.signal[:FQend] if len(self.signal) >= FQend else self.signal - sig_cut = np.sqrt(np.abs(sig_cut)) - F = np.fft.fft(sig_cut) - Fshift = np.abs(np.fft.fftshift(F)) - center = len(sig_cut) // 2 - if center < len(Fshift): - Fshift[max(center - 0, 0):min(center + 1, len(Fshift))] = 0 - self.FshiftS = gaussian_filter1d(Fshift, 5) - - # ============================================================================ - # ОБНОВЛЕНИЕ GUI - # ============================================================================ - - def schedule_update(self, original_size, data_type): - """Отложить обновление GUI.""" - with self.update_lock: - self.pending_original_size = original_size - self.pending_data_type = data_type - self.pending_update = True - - def do_pending_update(self): - """Обновляем GUI.""" - with self.update_lock: - if not self.pending_update: - return - - original_size = self.pending_original_size - data_type = self.pending_data_type - self.pending_update = False - - # Извлекаем все элементы из очереди - while not self.bscan_queue.empty(): - try: - bscan_col, file_time, data_type_col = self.bscan_queue.get_nowait() - self.add_bscan_column(bscan_col, file_time, data_type_col) - except queue.Empty: - break - - # Обновление графиков - self.ax_raw.cla() - if self.B is not None and self.meandr is not None: - self.ax_raw.plot(self.time_idx, np.abs(self.B[:, 0] if self.B.ndim > 1 else self.B), - label='B', linewidth=0.5, alpha=0.7) - if self.timeSignal.size > 0: - self.ax_raw.plot(self.timeSignal, np.abs(self.signalView), linewidth=0.5) - self.ax_raw.set_xlabel('Такты', fontsize=9) - self.ax_raw.set_ylabel('Сигнал', fontsize=9) - self.ax_raw.set_title('Данные с АЦП', fontsize=10) - self.ax_raw.grid(True, alpha=0.3) - self.ax_raw.tick_params(labelsize=8) - - self.ax_processed.cla() - if self.signal.size > 0: - ssS = self.signal.size - perpointFq = 10.67 / ssS - XSignal = 3 + (np.arange(1, ssS + 1) * perpointFq) - self.ax_processed.plot(XSignal, np.abs(self.signal), linewidth=1) - self.ax_processed.set_xlabel('Частота', fontsize=9) - self.ax_processed.set_ylabel('Сигнал', fontsize=9) - self.ax_processed.set_title('Обработанные данные', fontsize=10) - self.ax_processed.grid(True, alpha=0.3) - self.ax_processed.tick_params(labelsize=8) - - self.ax_fourier.cla() - if self.FshiftS is not None: - self.ax_fourier.plot(self.FshiftS, linewidth=1) - self.ax_fourier.set_xlim([0, min(FQend, len(self.FshiftS))]) - self.ax_fourier.set_title('Фурье образ', fontsize=10) - self.ax_fourier.grid(True, alpha=0.3) - self.ax_fourier.tick_params(labelsize=8) - - # Отрисовываем B_scan с автопрокруткой - if len(self.B_scan_data) > 0: - total_cols = len(self.B_scan_data) - - if self.auto_follow.get() and not self.manual_range_selection: - self.bscan_col_min = max(0, total_cols - timePoint) - self.bscan_col_max = total_cols - 1 - self.slider_col_min_var.set(self.bscan_col_min) - self.slider_col_max_var.set(self.bscan_col_max) - - self.redraw_bscan_immediate() - - total_cols = len(self.B_scan_data) - self.bscan_text.config(text=f'B-scan: {total_cols} columns') - - # Обновляем статус - status_msg = f"Тип: {data_type}\nИсх: {original_size}" - self.status_text.config(text=status_msg) - - self.skipped_text.config(text=f'Пропущено: {self.skipped_count}') - self.processed_text.config(text=f'Обработано: {self.processed_count}') - - # ✓ ИСПРАВЛЕНИЕ: Обновляем счётчик пропущенных кадров - self.gap_frames_text.config(text=f'Пропущено кадров: {self.gap_frames_count}') - - if self.B_scan_times: - last_time = self.B_scan_times[-1].strftime("%H:%M:%S.%f")[:-3] - self.time_text.config(text=f'Время: {last_time}') - - # Обновляем диапазоны слайдеров - if len(self.B_scan_data) > 0: - self.scale_col_max.config(to=len(self.B_scan_data) - 1) - if not self.auto_follow.get(): - self.slider_col_max_var.set(min(self.slider_col_max_var.get(), len(self.B_scan_data) - 1)) - - # Принудительный draw() - self.canvas.draw() - - # ============================================================================ - # ОСНОВНОЙ ЦИКЛ ОБРАБОТКИ ФАЙЛОВ - # ============================================================================ - + + def add_bscan_column(self, data_col, current_time, data_type): + """Добавить колонку в B-скан (может быть разного размера).""" + + if self.last_file_time is None: + self.B_scan_data.append(data_col) + self.B_scan_times.append(current_time) + self.B_scan_types.append(data_type) + self.last_file_time = current_time + return + + # Используем переменный интервал между файлами + time_diff_ms = (current_time - self.last_file_time).total_seconds() * 1000 + + if time_diff_ms > self.time_gap_threshold_ms: + missing_count = int(round(time_diff_ms / self.file_interval_ms)) - 1 + + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print( + f"[{timestamp}] ⚠️ Gap detected: {time_diff_ms:.1f}ms (missing ~{missing_count} columns, threshold: {self.time_gap_threshold_ms:.0f}ms)") + + # ✓ ИСПРАВЛЕНИЕ: Увеличиваем счётчик пропущенных кадров + self.gap_frames_count += missing_count + + last_size = len(self.B_scan_data[-1]) if self.B_scan_data else height + for i in range(missing_count): + zero_col = np.zeros(last_size) + self.B_scan_data.append(zero_col) + gap_time = self.last_file_time + timedelta(milliseconds=self.file_interval_ms * (i + 1)) + self.B_scan_times.append(gap_time) + # Отмечаем как GAP (пропущенный кадр) + self.B_scan_types.append("GAP") + + self.B_scan_data.append(data_col) + self.B_scan_times.append(current_time) + self.B_scan_types.append(data_type) + self.last_file_time = current_time + + def compute_fft(self): + """Вычисляем FFT спектр.""" + if self.signal.size > 0: + sig_cut = self.signal[:FQend] if len(self.signal) >= FQend else self.signal + sig_cut = np.sqrt(np.abs(sig_cut)) + F = np.fft.fft(sig_cut) + Fshift = np.abs(np.fft.fftshift(F)) + center = len(sig_cut) // 2 + if center < len(Fshift): + Fshift[max(center - 0, 0):min(center + 1, len(Fshift))] = 0 + self.FshiftS = gaussian_filter1d(Fshift, 5) + + # ============================================================================ + # ОБНОВЛЕНИЕ GUI + # ============================================================================ + + def schedule_update(self, original_size, data_type): + """Отложить обновление GUI.""" + with self.update_lock: + self.pending_original_size = original_size + self.pending_data_type = data_type + self.pending_update = True + + def do_pending_update(self): + """Обновляем GUI.""" + with self.update_lock: + if not self.pending_update: + return + + original_size = self.pending_original_size + data_type = self.pending_data_type + self.pending_update = False + + # Извлекаем все элементы из очереди + while not self.bscan_queue.empty(): + try: + bscan_col, file_time, data_type_col = self.bscan_queue.get_nowait() + self.add_bscan_column(bscan_col, file_time, data_type_col) + except queue.Empty: + break + + # Обновление графиков + self.ax_raw.cla() + if self.B is not None and self.meandr is not None: + self.ax_raw.plot(self.time_idx, np.abs(self.B[:, 0] if self.B.ndim > 1 else self.B), + label='B', linewidth=0.5, alpha=0.7) + if self.timeSignal.size > 0: + self.ax_raw.plot(self.timeSignal, np.abs(self.signalView), linewidth=0.5) + self.ax_raw.set_xlabel('Такты', fontsize=9) + self.ax_raw.set_ylabel('Сигнал', fontsize=9) + self.ax_raw.set_title('Данные с АЦП', fontsize=10) + self.ax_raw.grid(True, alpha=0.3) + self.ax_raw.tick_params(labelsize=8) + + self.ax_processed.cla() + if self.signal.size > 0: + ssS = self.signal.size + perpointFq = 10.67 / ssS + XSignal = 3 + (np.arange(1, ssS + 1) * perpointFq) + self.ax_processed.plot(XSignal, np.abs(self.signal), linewidth=1) + self.ax_processed.set_xlabel('Частота', fontsize=9) + self.ax_processed.set_ylabel('Сигнал', fontsize=9) + self.ax_processed.set_title('Обработанные данные', fontsize=10) + self.ax_processed.grid(True, alpha=0.3) + self.ax_processed.tick_params(labelsize=8) + + self.ax_fourier.cla() + if self.FshiftS is not None: + self.ax_fourier.plot(self.FshiftS, linewidth=1) + self.ax_fourier.set_xlim([0, min(FQend, len(self.FshiftS))]) + self.ax_fourier.set_title('Фурье образ', fontsize=10) + self.ax_fourier.grid(True, alpha=0.3) + self.ax_fourier.tick_params(labelsize=8) + + # Отрисовываем B_scan с автопрокруткой + if len(self.B_scan_data) > 0: + total_cols = len(self.B_scan_data) + + if self.auto_follow.get() and not self.manual_range_selection: + self.bscan_col_min = max(0, total_cols - timePoint) + self.bscan_col_max = total_cols - 1 + self.slider_col_min_var.set(self.bscan_col_min) + self.slider_col_max_var.set(self.bscan_col_max) + + self.redraw_bscan_immediate() + + total_cols = len(self.B_scan_data) + self.bscan_text.config(text=f'B-scan: {total_cols} columns') + + # Обновляем статус + status_msg = f"Тип: {data_type}\nИсх: {original_size}" + self.status_text.config(text=status_msg) + + self.skipped_text.config(text=f'Пропущено: {self.skipped_count}') + self.processed_text.config(text=f'Обработано: {self.processed_count}') + + # ✓ ИСПРАВЛЕНИЕ: Обновляем счётчик пропущенных кадров + self.gap_frames_text.config(text=f'Пропущено кадров: {self.gap_frames_count}') + + if self.B_scan_times: + last_time = self.B_scan_times[-1].strftime("%H:%M:%S.%f")[:-3] + self.time_text.config(text=f'Время: {last_time}') + + # Обновляем диапазоны слайдеров + if len(self.B_scan_data) > 0: + self.scale_col_max.config(to=len(self.B_scan_data) - 1) + if not self.auto_follow.get(): + self.slider_col_max_var.set(min(self.slider_col_max_var.get(), len(self.B_scan_data) - 1)) + + # Принудительный draw() + self.canvas.draw() + + # ============================================================================ + # ОСНОВНОЙ ЦИКЛ ОБРАБОТКИ ФАЙЛОВ + # ============================================================================ + def process_file_thread(self, fname, data_type, A, original_size): """Обработка файла в отдельном потоке.""" try: @@ -1116,10 +1118,10 @@ class DataAnalyzerApp: original_size = 0 file_time = get_file_time_with_milliseconds(fname) - - bscan_col = None - add_to_bscan = False - + + bscan_col = None + add_to_bscan = False + if data_type == DATA_TYPE_RAW: # Может прийти список сегментов (HEX с FE) if isinstance(A, list): @@ -1148,39 +1150,39 @@ class DataAnalyzerApp: col_time = file_time + timedelta(milliseconds=i * 10) self.bscan_queue.put((col, col_time, DATA_TYPE_FOURIER)) bscan_col = None - - if add_to_bscan and bscan_col is not None and data_type != DATA_TYPE_FOURIER: - self.bscan_queue.put((bscan_col, file_time, data_type)) - + + if add_to_bscan and bscan_col is not None and data_type != DATA_TYPE_FOURIER: + self.bscan_queue.put((bscan_col, file_time, data_type)) + self.schedule_update(original_size, data_type) - self.processed_count += 1 - - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - file_time_str = file_time.strftime("%H:%M:%S.%f")[:-3] - print(f"[{timestamp}] ✓ {fname} ({data_type}) [FileTime: {file_time_str}]") - - except Exception as e: - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print(f"[{timestamp}] ✗ Error: {str(e)[:50]}") - - def run(self): - """Основной цикл.""" - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] - print("=" * 80) - print(f" Radar Data Analyzer - Started: {timestamp}") - print(f" Waiting for .txt files in: {self.data_dir}") - print(f" RAW/SYNC_DET A-scan height: {height} pixels") - print(f" FOURIER A-scan size: {STANDARD_FOURIER_SIZE} pixels (N/2)") - print(f" B-scan: Time-synchronized (milliseconds), display range selectable") - print(f" File interval: {self.file_interval_ms}ms (user adjustable)") - print(f" Gap threshold: {self.time_gap_threshold_ms:.0f}ms ({GAP_THRESHOLD_MULTIPLIER}x interval)") - print(f" Poll interval: {self.file_poll_interval_ms}ms (user adjustable)") - print(f" FOURIER_MODE: {FOURIER_MODE}") - print(f" B-Scan display: GAP (lost frames) shown as WHITE") - print("=" * 80 + "\n") - - self.process_files() - + self.processed_count += 1 + + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + file_time_str = file_time.strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ✓ {fname} ({data_type}) [FileTime: {file_time_str}]") + + except Exception as e: + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ✗ Error: {str(e)[:50]}") + + def run(self): + """Основной цикл.""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + print("=" * 80) + print(f" Radar Data Analyzer - Started: {timestamp}") + print(f" Waiting for .txt files in: {self.data_dir}") + print(f" RAW/SYNC_DET A-scan height: {height} pixels") + print(f" FOURIER A-scan size: {STANDARD_FOURIER_SIZE} pixels (N/2)") + print(f" B-scan: Time-synchronized (milliseconds), display range selectable") + print(f" File interval: {self.file_interval_ms}ms (user adjustable)") + print(f" Gap threshold: {self.time_gap_threshold_ms:.0f}ms ({GAP_THRESHOLD_MULTIPLIER}x interval)") + print(f" Poll interval: {self.file_poll_interval_ms}ms (user adjustable)") + print(f" FOURIER_MODE: {FOURIER_MODE}") + print(f" B-Scan display: GAP (lost frames) shown as WHITE") + print("=" * 80 + "\n") + + self.process_files() + def process_files(self): """Обработка файлов в цикле.""" files = sorted([ @@ -1189,10 +1191,10 @@ class DataAnalyzerApp: ]) new_files = [f for f in files if f not in self.processed_files] - - for fname in new_files: - time_start = time.perf_counter() - + + for fname in new_files: + time_start = time.perf_counter() + try: # Быстро определим тип по первой строке (без полного чтения файла) with open(fname, 'r') as f: @@ -1242,30 +1244,33 @@ class DataAnalyzerApp: thread.start() self.processed_files.add(fname) - - except Exception as e: - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print(f"[{timestamp}] ✗ Load error: {str(e)[:50]}") - self.processed_files.add(fname) - - self.do_pending_update() - # Используем переменное время опроса - self.root.after(self.file_poll_interval_ms, self.process_files) - - -# ================================================================================ -# ТОЧКА ВХОДА -# ================================================================================ - -if __name__ == "__main__": - root = tk.Tk() - app = DataAnalyzerApp(root) - app.run() - - try: - root.mainloop() - except KeyboardInterrupt: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] - print("\n" + "=" * 80) - print(f" Program stopped by user: {timestamp}") - print("=" * 80) + + except Exception as e: + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ✗ Load error: {str(e)[:50]}") + self.processed_files.add(fname) + + self.do_pending_update() + # Используем переменное время опроса + self.root.after(self.file_poll_interval_ms, self.process_files) + + +# ================================================================================ +# ТОЧКА ВХОДА +# ================================================================================ + +if __name__ == "__main__": + if len(argv) == 2: + data_dir = argv[1] + print("data dir:", data_dir) + root = tk.Tk() + app = DataAnalyzerApp(root) + app.run() + + try: + root.mainloop() + except KeyboardInterrupt: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + print("\n" + "=" * 80) + print(f" Program stopped by user: {timestamp}") + print("=" * 80)