From 385c2a6dad2cfd74b63c38568f459d69d37281db Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 13 Nov 2025 18:07:59 +0300 Subject: [PATCH 01/14] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BF=D0=B0=D1=80=D1=81=D0=B5=D1=80=20HEX=20?= =?UTF-8?q?=D1=81=20=D1=80=D0=B0=D0=B7=D0=B1=D0=B8=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=BF=D0=BE=20FE;=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=82=D1=8B=D0=B2=D0=B0=D0=B5=D0=BC=20=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=200x;?= =?UTF-8?q?=20D0=E2=86=92RAW;=20=D1=81=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=20F0=E2=86=92SYNC=5FDET=20(=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D1=87=D1=91=D1=82=20=D1=81=D0=BF=D0=B5=D0=BA=D1=82=D1=80=D0=B0?= =?UTF-8?q?=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=20B=E2=80=91scan),=20F4=20=D0=B2=20=D1=82?= =?UTF-8?q?=D0=B0=D0=BA=D0=B8=D1=85=20=D1=81=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=85=20=D0=B8=D0=B3=D0=BD=D0=BE=D1=80=D0=B8=D1=80?= =?UTF-8?q?=D1=83=D0=B5=D1=82=D1=81=D1=8F;=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20.csv=20=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D0=BD=D0=B3=20HEX=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=B1=D0=B5=D0=B6=D0=B0=D0=BD=D0=B8=D1=8F=20SKIP?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83=D1=82?= =?UTF-8?q?=D1=83.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 35 ++++++++++++++++++ datagen.py | 6 ++- main.py | 106 ++++++++++++++++++++++++++++++++++------------------- 3 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 AGENTS.md mode change 100644 => 100755 datagen.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ea22f42 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `main.py`: Tkinter GUI for radar data analysis; watches a data folder, parses RAW/SYNC_DET/FOURIER files, and renders B‑scan/Fourier views. +- `datagen.py`: Test data generator for RAW, SYNC_DET, and FOURIER; produces time‑stamped files to feed the GUI. +- `testLadArrayGround.m`: MATLAB scratch for algorithm experiments. +- Tests: none yet. Add under `tests/` (e.g., `tests/test_io.py`). If processing grows, factor helpers into a `processing/` package (e.g., `processing/signal.py`). + +## Build, Test, and Development Commands +- Create venv: `python3 -m venv .venv && source .venv/bin/activate` (Win: `.venv\Scripts\activate`). +- Install deps: `pip install numpy scipy matplotlib`. Linux may require Tk: `sudo apt-get install -y python3-tk`. +- Run GUI: `python main.py`. +- Generate sample data: `python datagen.py` and choose 1/2/3; files go to the configured data folder. +- Run tests (when added): `pytest -q`. + +## Coding Style & Naming Conventions +- Follow PEP 8 with 4‑space indents; add type hints for new/edited functions. +- Naming: snake_case (functions/vars), PascalCase (classes), UPPER_SNAKE_CASE (constants). +- Keep GUI logic in `main.py`; move pure processing/IO into small, testable modules. + +## Testing Guidelines +- Framework: pytest (recommended). No suite exists yet. +- Naming: `tests/test_*.py`. Use temp dirs for file‑based tests; don’t write to system paths. +- Aim for ≥70% coverage on new modules. Add smoke tests for file parsing and queue/processing logic; use a non‑interactive Matplotlib backend for tests. + +## Commit & Pull Request Guidelines +- Commits: imperative mood with scope, e.g., `gui: improve B‑scan update`, `datagen: add FOURIER mode`. +- PRs: include description, linked issues, reproduction steps, and screenshots/GIFs for UI changes. Keep changes focused and update docs when constants/paths change. + +## Configuration Tips +- Data paths are hardcoded; update before running on non‑Windows systems: + - `main.py:18` — `data_dir = r"D:\\data"` + - `datagen.py:11` — `DATA_DIR = r"D:\\data"` +- Prefer a writable local path (e.g., `/tmp/data`) and do not commit generated data. + diff --git a/datagen.py b/datagen.py old mode 100644 new mode 100755 index 2572451..09b7fdf --- a/datagen.py +++ b/datagen.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 import os import time import numpy as np @@ -7,7 +8,8 @@ from datetime import datetime, timedelta # ПАРАМЕТРЫ ЭМУЛЯЦИИ # ================================================================================ -DATA_DIR = r"D:\data" +#DATA_DIR = r"D:\data" +DATA_DIR = './data' # ✓ ИСПРАВЛЕНИЕ: Выбор типа данных и количество NUM_FILES = 1000 # Количесйтво файлов для генерации @@ -291,4 +293,4 @@ if __name__ == "__main__": print(f"\n📂 Файлы сохранены в: {DATA_DIR}") print(f"\n💡 Запустите main_analyzer.py и откройте директорию {DATA_DIR}") - print(f" Анализатор автоматически подхватит новые файлы\n") \ No newline at end of file + print(f" Анализатор автоматически подхватит новые файлы\n") diff --git a/main.py b/main.py index 82a3cbb..a985ff1 100755 --- a/main.py +++ b/main.py @@ -155,18 +155,23 @@ def parse_hex_file(filename): def finalize_segment(): nonlocal cur - # Приоритет выбора, что считать сегментом - if cur["F4"]: - seg_fourier.append(np.asarray(cur["F4"], dtype=float)) - elif cur["F3"]: - arr = np.asarray(cur["F3"], dtype=float) - seg_fourier.append(np.sqrt(np.maximum(0.0, arr))) + # Приоритет выбора сегмента: + # 1) Если есть F0 — используем как SYNC_DET (F4 игнорируем временно) + # 2) Иначе F1+F2 → амплитуда + # 3) Иначе F4 (если нет F0) + # 4) Иначе F3 (sqrt) + # 5) Иначе D0 как RAW + if cur["F0"]: + seg_sync.append(np.asarray(cur["F0"], dtype=float)) elif cur["F1"] and cur["F2"] and len(cur["F1"]) == len(cur["F2"]): re = np.asarray(cur["F1"], dtype=float) im = np.asarray(cur["F2"], dtype=float) seg_fourier.append(np.sqrt(re * re + im * im)) - elif cur["F0"]: - seg_sync.append(np.asarray(cur["F0"], dtype=float)) + elif cur["F4"]: + seg_fourier.append(np.asarray(cur["F4"], dtype=float)) + elif cur["F3"]: + arr = np.asarray(cur["F3"], dtype=float) + seg_fourier.append(np.sqrt(np.maximum(0.0, arr))) elif cur["D0"]: seg_raw.append(np.asarray(cur["D0"], dtype=float)) # Сброс @@ -1097,10 +1102,20 @@ class DataAnalyzerApp: # ОСНОВНОЙ ЦИКЛ ОБРАБОТКИ ФАЙЛОВ # ============================================================================ - def process_file_thread(self, fname, data_type, A, original_size): - """Обработка файла в отдельном потоке.""" - try: - file_time = get_file_time_with_milliseconds(fname) + def process_file_thread(self, fname, data_type, A, original_size): + """Обработка файла в отдельном потоке.""" + try: + # Если данные не были загружены в главном потоке (HEX отложен) — загрузим здесь + if A is None: + data_type, A = load_data_with_type(fname) + if isinstance(A, list): + original_size = len(A[0]) if len(A) > 0 else 0 + elif isinstance(A, np.ndarray): + original_size = A.shape[0] + else: + original_size = 0 + + file_time = get_file_time_with_milliseconds(fname) bscan_col = None add_to_bscan = False @@ -1137,7 +1152,7 @@ class DataAnalyzerApp: 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.schedule_update(original_size, data_type) self.processed_count += 1 timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] @@ -1166,20 +1181,37 @@ class DataAnalyzerApp: self.process_files() - def process_files(self): - """Обработка файлов в цикле.""" - files = sorted([f for f in os.listdir() if f.endswith('.csv') or - f.endswith('.txt1') or f.endswith('.txt2') or f.endswith('.csv')]) - - new_files = [f for f in files if f not in self.processed_files] - print("new files:", new_files, files) + def process_files(self): + """Обработка файлов в цикле.""" + files = sorted([ + f for f in os.listdir() + if f.lower().endswith(('.txt', '.txt1', '.txt2', '.csv')) + ]) + + new_files = [f for f in files if f not in self.processed_files] for fname in new_files: time_start = time.perf_counter() try: + # Быстро определим тип по первой строке (без полного чтения файла) + with open(fname, 'r') as f: + head = f.readline() + quick_type = detect_data_type(head) + + if quick_type == DATA_TYPE_HEX: + # Отложенный парсинг HEX в фоне, чтобы не блокировать UI и не превышать таймаут + thread = threading.Thread( + target=self.process_file_thread, + args=(fname, DATA_TYPE_HEX, None, 0), + daemon=True + ) + thread.start() + self.processed_files.add(fname) + continue + + # Для остальных типов загрузим сразу и применим лимит времени data_type, A = load_data_with_type(fname) - # Поддержка списка сегментов (HEX с FE) if isinstance(A, list): original_size = len(A[0]) if len(A) > 0 else 0 elif isinstance(A, np.ndarray): @@ -1194,22 +1226,22 @@ class DataAnalyzerApp: self.skipped_count += 1 self.processed_files.add(fname) continue - - elapsed_time_ms = (time.perf_counter() - time_start) * 1000 - - if elapsed_time_ms > MAX_PROCESSING_TIME_MS: - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - print(f"[{timestamp}] ⏭️ SKIP {fname} (load time: {elapsed_time_ms:.1f}ms)") - self.skipped_count += 1 - else: - thread = threading.Thread( - target=self.process_file_thread, - args=(fname, data_type, A, original_size), - daemon=True - ) - thread.start() - - self.processed_files.add(fname) + + elapsed_time_ms = (time.perf_counter() - time_start) * 1000 + + if elapsed_time_ms > MAX_PROCESSING_TIME_MS: + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ⏭️ SKIP {fname} (load time: {elapsed_time_ms:.1f}ms)") + self.skipped_count += 1 + else: + thread = threading.Thread( + target=self.process_file_thread, + args=(fname, data_type, A, original_size), + daemon=True + ) + thread.start() + + self.processed_files.add(fname) except Exception as e: timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] From 3df95279796eedce0417fda8753b502a7230dbf8 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 13 Nov 2025 19:36:54 +0300 Subject: [PATCH 02/14] added hex mode writing (mimics to real data from redar) --- datagen.py | 61 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/datagen.py b/datagen.py index 09b7fdf..19a0193 100755 --- a/datagen.py +++ b/datagen.py @@ -3,6 +3,7 @@ import os import time import numpy as np from datetime import datetime, timedelta +#from builtins import True # ================================================================================ # ПАРАМЕТРЫ ЭМУЛЯЦИИ @@ -91,7 +92,7 @@ def create_fourier_data(sync_det_data=None, fft_size=SYNC_DET_SIZE, output_size= # ГЕНЕРАЦИЯ ФАЙЛОВ # ================================================================================ -def emit_raw_files(count=NUM_FILES, start_time=None): +def emit_raw_files(count=NUM_FILES, start_time=None, hex_mode=True): """Генерирует RAW файлы количеством count.""" if start_time is None: start_time = datetime.now() @@ -117,8 +118,17 @@ def emit_raw_files(count=NUM_FILES, start_time=None): # Добавляем заголовок filepath = os.path.join(DATA_DIR, filename) with open(filepath, 'w') as f: - f.write("RAW\n") - np.savetxt(f, data, fmt='%.6f') + + + 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() @@ -132,7 +142,7 @@ def emit_raw_files(count=NUM_FILES, start_time=None): return start_time + timedelta(milliseconds=count * FILE_INTERVAL_MS) -def emit_sync_det_files(count=NUM_FILES, start_time=None): +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() @@ -154,12 +164,19 @@ def emit_sync_det_files(count=NUM_FILES, start_time=None): # Генерируем данные data = create_sync_det_data(index=i) + #print("data:", data) # Добавляем заголовок filepath = os.path.join(DATA_DIR, filename) with open(filepath, 'w') as f: - f.write("SYNC_DET\n") - np.savetxt(f, data, fmt='%.6f') + + 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() @@ -173,7 +190,7 @@ def emit_sync_det_files(count=NUM_FILES, start_time=None): return start_time + timedelta(milliseconds=count * FILE_INTERVAL_MS) -def emit_fourier_files(count=NUM_FILES, start_time=None): +def emit_fourier_files(count=NUM_FILES, start_time=None, hex_mode=True): """✓ Генерирует FOURIER файлы количеством count. Каждый файл содержит амплитудный спектр размером 500. @@ -213,8 +230,15 @@ def emit_fourier_files(count=NUM_FILES, start_time=None): # Добавляем заголовок filepath = os.path.join(DATA_DIR, filename) with open(filepath, 'w') as f: - f.write("FOURIER\n") - np.savetxt(f, data, fmt='%.6f') + + 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() @@ -262,9 +286,20 @@ if __name__ == "__main__": 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': @@ -272,18 +307,20 @@ if __name__ == "__main__": break elif choice == '1': start_time = datetime.now().replace(microsecond=0) - emit_raw_files(NUM_FILES, start_time) + 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) + 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) + emit_fourier_files(NUM_FILES, start_time, hex_mode=hex_mode) break else: print(" ❌ Неверный выбор. Введите 1, 2, 3 или q") + + except KeyboardInterrupt: print("\n\n Прервано пользователем.") break From 58cce4c87acda13458ab1988dfb274aa0a2560e0 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 13 Nov 2025 19:53:11 +0300 Subject: [PATCH 03/14] 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) From d70b016c0d933ad94d6e2a7306c0a36aa47974cb Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 13 Nov 2025 22:53:43 +0300 Subject: [PATCH 04/14] commented debug print --- main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.py b/main.py index 53b9917..10e8b5e 100755 --- a/main.py +++ b/main.py @@ -1118,6 +1118,8 @@ class DataAnalyzerApp: original_size = 0 file_time = get_file_time_with_milliseconds(fname) + #print("file:", fname) + #print("A:", A) bscan_col = None add_to_bscan = False From fccccc62ff9d31f3374b2491030eeca2f76b4162 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Fri, 14 Nov 2025 00:16:38 +0300 Subject: [PATCH 05/14] implemented cleaner: stored not more than 0.5Gb of data --- main.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 10e8b5e..911c16a 100755 --- a/main.py +++ b/main.py @@ -40,6 +40,7 @@ STANDARD_FOURIER_ROWS = 60 STANDARD_FOURIER_COLS = 100 MAX_PROCESSING_TIME_MS = 250 +FILES_STORED_N_MAX = 100 # ПЕРЕЧИСЛЕНИЕ ТИПОВ ДАННЫХ DATA_TYPE_RAW = "RAW" @@ -58,7 +59,7 @@ DEFAULT_FILE_INTERVAL_MS = 300 # 300 мс GAP_THRESHOLD_MULTIPLIER = 1.5 # Начальное время опроса файлов (в миллисекундах) -DEFAULT_FILE_POLL_INTERVAL_MS = 100 # 100 мс +DEFAULT_FILE_POLL_INTERVAL_MS = 80 # 100 мс # ================================================================================ @@ -1253,9 +1254,85 @@ class DataAnalyzerApp: self.processed_files.add(fname) self.do_pending_update() + # Очистка старых файлов, чтобы хранить не более FILES_STORED_N_MAX + #self.cleanup_old_files() //stores only last FILES_STORED_N_MAX files + self.cleanup_by_size(max_bytes=500*1024*1024) #stores not more than 0.5 Gb of data # Используем переменное время опроса self.root.after(self.file_poll_interval_ms, self.process_files) + def cleanup_old_files(self): + try: + files = [ + f for f in os.listdir() + if f.lower().endswith(('.txt', '.txt1', '.txt2', '.csv')) + ] + if len(files) <= FILES_STORED_N_MAX: + return + # Сортировка по времени изменения (старые сначала) + files_sorted = sorted(files, key=lambda fn: os.path.getmtime(fn)) + to_delete = files_sorted[:len(files) - FILES_STORED_N_MAX] + deleted = 0 + for fn in to_delete: + try: + os.remove(fn) + deleted += 1 + # Поддерживаем список обработанных + if fn in self.processed_files: + self.processed_files.discard(fn) + except Exception: + pass + if deleted: + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] 🧹 Cleanup: removed {deleted} old file(s)") + except Exception: + pass + + def cleanup_by_size(self, max_bytes=500 * 1024 * 1024): + """Альтернативный сборщик: удаляет самые старые .csv/.txt, + если суммарный объём в папке превышает max_bytes (по умолчанию ~500 МБ).""" + try: + # Берём только обычные файлы текущей директории данных + files = [ + f for f in os.listdir() + if os.path.isfile(f) and f.lower().endswith(('.csv', '.txt')) + ] + # Подсчёт общего объёма + meta = [] # (name, size, mtime) + total_size = 0 + for fn in files: + try: + sz = os.path.getsize(fn) + mt = os.path.getmtime(fn) + meta.append((fn, sz, mt)) + total_size += sz + except Exception: + continue + + if total_size <= max_bytes: + return + + # Сортируем по времени изменения (старые сначала) и удаляем, пока не уложимся + meta.sort(key=lambda t: t[2]) + deleted = 0 + for fn, sz, _ in meta: + if total_size <= max_bytes: + break + try: + os.remove(fn) + total_size -= sz + deleted += 1 + if fn in self.processed_files: + self.processed_files.discard(fn) + except Exception: + continue + + if deleted: + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + mb = max_bytes / (1024 * 1024) + print(f"[{timestamp}] 🧹 Collector: removed {deleted} file(s) to keep <= {mb:.0f} MB") + except Exception: + pass + # ================================================================================ # ТОЧКА ВХОДА From dd526c735a7102a6070dc999b5d8b4878534307e Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Fri, 14 Nov 2025 01:13:21 +0300 Subject: [PATCH 06/14] implemented F4-FFT res conditioning --- main.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 911c16a..fa0e3d7 100755 --- a/main.py +++ b/main.py @@ -117,6 +117,29 @@ def resize_2d_interpolate(data, target_rows, target_cols): return data_resampled +def BF_fft_postprocessor(spectrum: np.ndarray) -> np.ndarray: + """Болванка постобработки FFT-данных, полученных из файла (F4). + + Принимает 1D массив амплитуд спектра и возвращает преобразованный массив + той же длины. По умолчанию — тождественное преобразование. + """ + + + + spec_L = len(spectrum) + spectrum_lower = spectrum[:spec_L//2] + spectrum_higher = spectrum[spec_L//2:] + + spectrum[:spec_L//2] = spectrum_higher + spectrum[spec_L//2:] = spectrum_lower[::-1] + + try: + print ("spectrum processed") + return np.asarray(spectrum_tmp, dtype=float) + except Exception: + return spectrum + + def load_data_with_type(filename): """Загружает данные и определяет их тип по первой строке.""" with open(filename, 'r') as f: @@ -164,18 +187,31 @@ def parse_hex_file(filename): # 3) Иначе F4 (если нет F0) # 4) Иначе F3 (sqrt) # 5) Иначе D0 как RAW + #print("cur:", cur) if cur["F0"]: + # print("got F0!") seg_sync.append(np.asarray(cur["F0"], dtype=float)) elif cur["F1"] and cur["F2"] and len(cur["F1"]) == len(cur["F2"]): + print("got F1,F2!") re = np.asarray(cur["F1"], dtype=float) im = np.asarray(cur["F2"], dtype=float) seg_fourier.append(np.sqrt(re * re + im * im)) elif cur["F4"]: - seg_fourier.append(np.asarray(cur["F4"], dtype=float)) + # print("got F4!") + + # print("got fourier!") + # FOURIER данные получены напрямую из файла (F4) + col = np.asarray(cur["F4"], dtype=float) + col = BF_fft_postprocessor(col) + seg_fourier.append(col) elif cur["F3"]: + # print("got F3!") + arr = np.asarray(cur["F3"], dtype=float) seg_fourier.append(np.sqrt(np.maximum(0.0, arr))) elif cur["D0"]: + # print("got D0!") + seg_raw.append(np.asarray(cur["D0"], dtype=float)) # Сброс cur = {"D0": [], "F0": [], "F1": [], "F2": [], "F3": [], "F4": []} @@ -1152,6 +1188,11 @@ class DataAnalyzerApp: for i, col in enumerate(columns): col_time = file_time + timedelta(milliseconds=i * 10) self.bscan_queue.put((col, col_time, DATA_TYPE_FOURIER)) + # Обновляем график Фурье спектром из файла (берём последний столбец) + try: + self.FshiftS = np.asarray(columns[-1], dtype=float) + except Exception: + pass bscan_col = None if add_to_bscan and bscan_col is not None and data_type != DATA_TYPE_FOURIER: From be9991bdd87bb78c27061b1bb237a326bb3fc6d0 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Fri, 14 Nov 2025 01:20:06 +0300 Subject: [PATCH 07/14] implemented lost data ignoration. (control by IGNORE_LOST) --- main.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/main.py b/main.py index fa0e3d7..ec983a6 100755 --- a/main.py +++ b/main.py @@ -61,6 +61,9 @@ GAP_THRESHOLD_MULTIPLIER = 1.5 # Начальное время опроса файлов (в миллисекундах) DEFAULT_FILE_POLL_INTERVAL_MS = 80 # 100 мс +# Игнорировать пропущенные данные (не добавлять GAP-колонки) +IGNORE_LOST = True + # ================================================================================ # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ @@ -322,6 +325,9 @@ class DataAnalyzerApp: os.makedirs(self.data_dir, exist_ok=True) os.chdir(self.data_dir) + # Настройка: игнорировать пропуски кадров + self.ignore_lost = IGNORE_LOST + # Инициализируем с существующими файлами existing_files = sorted([ f for f in os.listdir() @@ -1006,19 +1012,21 @@ class DataAnalyzerApp: 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)") + f"[{timestamp}] ⚠️ Gap detected: {time_diff_ms:.1f}ms (missing ~{missing_count} columns, threshold: {self.time_gap_threshold_ms:.0f}ms)" + + (" — ignored" if self.ignore_lost else "")) # ✓ ИСПРАВЛЕНИЕ: Увеличиваем счётчик пропущенных кадров 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") + if not self.ignore_lost: + 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) From bb09c0d2e7adf5a02d1a2439f0a6d04db4f2bc1c Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Fri, 14 Nov 2025 01:30:21 +0300 Subject: [PATCH 08/14] now if F0 and F4 data both present -- FFT not calculated. It is taken from F4 --- main.py | 67 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index ec983a6..8f8ea20 100755 --- a/main.py +++ b/main.py @@ -120,6 +120,7 @@ def resize_2d_interpolate(data, target_rows, target_cols): return data_resampled + def BF_fft_postprocessor(spectrum: np.ndarray) -> np.ndarray: """Болванка постобработки FFT-данных, полученных из файла (F4). @@ -143,6 +144,21 @@ def BF_fft_postprocessor(spectrum: np.ndarray) -> np.ndarray: return spectrum +''' +def BF_fft_postprocessor(spectrum: np.ndarray) -> np.ndarray: + """Болванка постобработки FFT-данных, полученных из файла (F4). + + Принимает 1D массив амплитуд спектра и возвращает преобразованный массив + той же длины. По умолчанию — тождественное преобразование. + """ + try: + return np.asarray(spectrum, dtype=float) + except Exception: + return spectrum + +''' + + def load_data_with_type(filename): """Загружает данные и определяет их тип по первой строке.""" with open(filename, 'r') as f: @@ -185,36 +201,31 @@ def parse_hex_file(filename): def finalize_segment(): nonlocal cur # Приоритет выбора сегмента: - # 1) Если есть F0 — используем как SYNC_DET (F4 игнорируем временно) + # 1) Если есть F4 — используем как FOURIER; F0 (если есть) передаём для отображения без расчёта FFT # 2) Иначе F1+F2 → амплитуда - # 3) Иначе F4 (если нет F0) - # 4) Иначе F3 (sqrt) + # 3) Иначе F3 (sqrt) + # 4) Иначе F0 как SYNC_DET # 5) Иначе D0 как RAW - #print("cur:", cur) - if cur["F0"]: - # print("got F0!") - seg_sync.append(np.asarray(cur["F0"], dtype=float)) - elif cur["F1"] and cur["F2"] and len(cur["F1"]) == len(cur["F2"]): - print("got F1,F2!") - re = np.asarray(cur["F1"], dtype=float) - im = np.asarray(cur["F2"], dtype=float) - seg_fourier.append(np.sqrt(re * re + im * im)) - elif cur["F4"]: - # print("got F4!") - - # print("got fourier!") + if cur["F4"]: # FOURIER данные получены напрямую из файла (F4) col = np.asarray(cur["F4"], dtype=float) col = BF_fft_postprocessor(col) - seg_fourier.append(col) + if cur["F0"]: + # Сохраняем F0 рядом с F4 для отображения (без расчёта FFT) + f0 = np.asarray(cur["F0"], dtype=float) + seg_fourier.append((col, f0)) + else: + seg_fourier.append(col) + elif cur["F1"] and cur["F2"] and len(cur["F1"]) == len(cur["F2"]): + re = np.asarray(cur["F1"], dtype=float) + im = np.asarray(cur["F2"], dtype=float) + seg_fourier.append(np.sqrt(re * re + im * im)) elif cur["F3"]: - # print("got F3!") - arr = np.asarray(cur["F3"], dtype=float) seg_fourier.append(np.sqrt(np.maximum(0.0, arr))) + elif cur["F0"]: + seg_sync.append(np.asarray(cur["F0"], dtype=float)) elif cur["D0"]: - # print("got D0!") - seg_raw.append(np.asarray(cur["D0"], dtype=float)) # Сброс cur = {"D0": [], "F0": [], "F1": [], "F2": [], "F3": [], "F4": []} @@ -973,8 +984,18 @@ class DataAnalyzerApp: # A может быть: list[np.ndarray] (из HEX) или numpy.ndarray if isinstance(A, list): for seg in A: - col = np.asarray(seg, dtype=float) - columns_to_add.append(col) + # Если сегмент — кортеж (fourier_col, f0), отобразим F0 в временной области, + # но B-scan пополняем только спектром (fourier_col) + if isinstance(seg, tuple) and len(seg) == 2: + col = np.asarray(seg[0], dtype=float) + f0 = np.asarray(seg[1], dtype=float) + self.signal = f0 + self.signalView = f0 * 0.1 + self.timeSignal = np.arange(len(f0)) + columns_to_add.append(col) + else: + col = np.asarray(seg, dtype=float) + columns_to_add.append(col) return True, columns_to_add if A.ndim == 1: From cfce8adce1dadb3bd433ebcb20819167cd449fe9 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Fri, 14 Nov 2025 01:39:43 +0300 Subject: [PATCH 09/14] implemented IGNORE_F4_FFT_DATA flag. If it is true -- F4 data ignored and fft is calculated --- main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 8f8ea20..c5efe2f 100755 --- a/main.py +++ b/main.py @@ -64,6 +64,9 @@ DEFAULT_FILE_POLL_INTERVAL_MS = 80 # 100 мс # Игнорировать пропущенные данные (не добавлять GAP-колонки) IGNORE_LOST = True +# Игнорировать спектры из F4 и рассчитывать FFT из временных данных +IGNORE_F4_FFT_DATA = True + # ================================================================================ # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ @@ -206,7 +209,7 @@ def parse_hex_file(filename): # 3) Иначе F3 (sqrt) # 4) Иначе F0 как SYNC_DET # 5) Иначе D0 как RAW - if cur["F4"]: + if cur["F4"] and not IGNORE_F4_FFT_DATA: # FOURIER данные получены напрямую из файла (F4) col = np.asarray(cur["F4"], dtype=float) col = BF_fft_postprocessor(col) From 0645b7d0c5880f428993300414cbd73642843914 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Fri, 14 Nov 2025 01:44:42 +0300 Subject: [PATCH 10/14] removed gaussian filter. Now result data is more noisy but object is visible! --- main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index c5efe2f..dc8cc2f 100755 --- a/main.py +++ b/main.py @@ -1067,7 +1067,8 @@ class DataAnalyzerApp: 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) + self.FshiftS = Fshift + #self.FshiftS = gaussian_filter1d(Fshift, 5) # ============================================================================ # ОБНОВЛЕНИЕ GUI From 990fbbb26b933049feed2ccaea5ebed200543837 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 20 Nov 2025 18:40:41 +0300 Subject: [PATCH 11/14] decreased DEFAULT_FILE_POLL_INTERVAL_MS to 40 ms (was 80 ms). Because new file appears approx every 50 ms --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index dc8cc2f..f65db76 100755 --- a/main.py +++ b/main.py @@ -59,7 +59,7 @@ DEFAULT_FILE_INTERVAL_MS = 300 # 300 мс GAP_THRESHOLD_MULTIPLIER = 1.5 # Начальное время опроса файлов (в миллисекундах) -DEFAULT_FILE_POLL_INTERVAL_MS = 80 # 100 мс +DEFAULT_FILE_POLL_INTERVAL_MS = 40 # 100 мс # Игнорировать пропущенные данные (не добавлять GAP-колонки) IGNORE_LOST = True From 85843494500ec0026687af518096fc5fb909f6bd Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 20 Nov 2025 19:14:59 +0300 Subject: [PATCH 12/14] decreased window size. Now it fits small screen well --- main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index f65db76..d3fa504 100755 --- a/main.py +++ b/main.py @@ -333,7 +333,8 @@ class DataAnalyzerApp: def __init__(self, root): self.root = root self.root.title("Radar Data Analyzer (Time Synchronized - Queue Based)") - self.root.geometry("1500x850") + # Уменьшено, чтобы помещалось на экране 1024x768 + self.root.geometry("1000x720") self.data_dir = data_dir os.makedirs(self.data_dir, exist_ok=True) @@ -438,7 +439,8 @@ class DataAnalyzerApp: 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) + # Уменьшен стартовый размер фигуры, чтобы влезать в 1000x720 + self.fig = plt.Figure(figsize=(7.5, 6.0), 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]) From be059bad2a39e4b29725f93ae9ef3d2fa29c16bb Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 20 Nov 2025 22:39:21 +0300 Subject: [PATCH 13/14] implemented ignorance of incomplete datafiles. Now files with lower than 100 F0 -- points are rejected --- main.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index d3fa504..ec55bdb 100755 --- a/main.py +++ b/main.py @@ -42,6 +42,9 @@ STANDARD_FOURIER_COLS = 100 MAX_PROCESSING_TIME_MS = 250 FILES_STORED_N_MAX = 100 +# Минимально допустимое число точек F0 для принятия данных +MIN_F0_POINTS = 100 + # ПЕРЕЧИСЛЕНИЕ ТИПОВ ДАННЫХ DATA_TYPE_RAW = "RAW" DATA_TYPE_SYNC_DET = "SYNC_DET" @@ -123,6 +126,14 @@ def resize_2d_interpolate(data, target_rows, target_cols): return data_resampled +def is_all_zero(arr: np.ndarray, eps: float = 0.0) -> bool: + """Возвращает True, если все значения массива близки к нулю.""" + try: + return not np.any(np.abs(arr) > eps) + except Exception: + return False + + def BF_fft_postprocessor(spectrum: np.ndarray) -> np.ndarray: """Болванка постобработки FFT-данных, полученных из файла (F4). @@ -201,6 +212,20 @@ def parse_hex_file(filename): seg_sync = [] seg_fourier = [] + def _is_all_zero_local(arr_like) -> bool: + try: + arr = np.asarray(arr_like, dtype=float) + return not np.any(np.abs(arr) > 0) + except Exception: + return False + + def _f0_is_valid_local(f0_like) -> bool: + try: + arr = np.asarray(f0_like, dtype=float) + return arr.size >= MIN_F0_POINTS and (not _is_all_zero_local(arr)) + except Exception: + return False + def finalize_segment(): nonlocal cur # Приоритет выбора сегмента: @@ -213,7 +238,7 @@ def parse_hex_file(filename): # FOURIER данные получены напрямую из файла (F4) col = np.asarray(cur["F4"], dtype=float) col = BF_fft_postprocessor(col) - if cur["F0"]: + if cur["F0"] and _f0_is_valid_local(cur["F0"]): # Сохраняем F0 рядом с F4 для отображения (без расчёта FFT) f0 = np.asarray(cur["F0"], dtype=float) seg_fourier.append((col, f0)) @@ -227,7 +252,8 @@ def parse_hex_file(filename): arr = np.asarray(cur["F3"], dtype=float) seg_fourier.append(np.sqrt(np.maximum(0.0, arr))) elif cur["F0"]: - seg_sync.append(np.asarray(cur["F0"], dtype=float)) + if _f0_is_valid_local(cur["F0"]): + seg_sync.append(np.asarray(cur["F0"], dtype=float)) elif cur["D0"]: seg_raw.append(np.asarray(cur["D0"], dtype=float)) # Сброс @@ -1300,6 +1326,47 @@ class DataAnalyzerApp: else: original_size = 0 + # Фильтрация неполных/нулевых данных + if data_type == DATA_TYPE_SYNC_DET: + # SYNC_DET соответствует F0; проверяем минимальную длину и ненулевость + if isinstance(A, np.ndarray): + arr = A.ravel() + if arr.size < MIN_F0_POINTS or is_all_zero(arr): + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + reason = f"len={arr.size} < {MIN_F0_POINTS}" if arr.size < MIN_F0_POINTS else "all zeros" + print(f"[{timestamp}] ⏭️ SKIP {fname} (SYNC_DET invalid: {reason})") + self.skipped_count += 1 + self.processed_files.add(fname) + continue + elif isinstance(A, list): + filtered = [] + for seg in A: + try: + arr = np.asarray(seg, dtype=float).ravel() + if arr.size >= MIN_F0_POINTS and (not is_all_zero(arr)): + filtered.append(arr) + except Exception: + continue + if not filtered: + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ⏭️ SKIP {fname} (SYNC_DET segments invalid)") + self.skipped_count += 1 + self.processed_files.add(fname) + continue + A = filtered + original_size = len(A[0]) if len(A) > 0 else 0 + + if data_type == DATA_TYPE_FOURIER: + # Для FOURIER: пропускаем полностью пустые/нулевые массивы + if isinstance(A, np.ndarray): + arr = A.ravel() + if arr.size == 0 or is_all_zero(arr): + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + print(f"[{timestamp}] ⏭️ SKIP {fname} (FOURIER empty or all zeros)") + self.skipped_count += 1 + self.processed_files.add(fname) + continue + # Если после парсинга данных нет — пропускаем файл if (isinstance(A, list) and len(A) == 0) or (isinstance(A, np.ndarray) and A.size == 0): timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] From 696887b4c7144865843b8925ddcd61ae8c86c119 Mon Sep 17 00:00:00 2001 From: Theodor Chikin Date: Thu, 20 Nov 2025 23:00:51 +0300 Subject: [PATCH 14/14] =?UTF-8?q?implemented=20averagignd=20and=20subtract?= =?UTF-8?q?ing=20avg=20from=20B-scan.=20Control=20byBG=5FAVG=5FWINDOW=5FSE?= =?UTF-8?q?C=20=3D=205.0=20BG=5FSUBTRACT=5FALPHA=20=3D=201.0=20BG=5FSUBTRA?= =?UTF-8?q?CT=5FENABLED=20=3D=20True=20#=20=D0=9C=D0=B5=D1=82=D0=BE=D0=B4?= =?UTF-8?q?=20=D1=80=D0=B0=D1=81=D1=87=D1=91=D1=82=D0=B0=20=D1=84=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0:=20'median'=20=D0=B8=D0=BB=D0=B8=20'mean'=20BG=5FB?= =?UTF-8?q?ASELINE=5FMETHOD=20=3D=20'median'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index ec55bdb..1789123 100755 --- a/main.py +++ b/main.py @@ -14,6 +14,7 @@ from datetime import datetime, timedelta import threading import queue from sys import argv +from collections import deque # ================================================================================ # ПАРАМЕТРЫ И КОНСТАНТЫ @@ -45,6 +46,13 @@ FILES_STORED_N_MAX = 100 # Минимально допустимое число точек F0 для принятия данных MIN_F0_POINTS = 100 +# Усреднение B-scan по времени (фон) +BG_AVG_WINDOW_SEC = 5.0 +BG_SUBTRACT_ALPHA = 1.0 +BG_SUBTRACT_ENABLED = True +# Метод расчёта фона: 'median' или 'mean' +BG_BASELINE_METHOD = 'median' + # ПЕРЕЧИСЛЕНИЕ ТИПОВ ДАННЫХ DATA_TYPE_RAW = "RAW" DATA_TYPE_SYNC_DET = "SYNC_DET" @@ -453,6 +461,13 @@ class DataAnalyzerApp: self.processed_count = 0 self.skipped_count = 0 + # Буфер последних колонок для фонового усреднения B-scan + self.bscan_buffer = deque() # элементов: (np.ndarray col, datetime t, str type) + self.bg_avg_window_sec = BG_AVG_WINDOW_SEC + self.bg_subtract_alpha = BG_SUBTRACT_ALPHA + self.bg_subtract_enabled = BG_SUBTRACT_ENABLED + self.bg_method = BG_BASELINE_METHOD + # ============================================================================ # ИНИЦИАЛИЗАЦИЯ ИНТЕРФЕЙСА # ============================================================================ @@ -854,6 +869,13 @@ class DataAnalyzerApp: # Собираем данные для отображения display_cols = [] display_types = [] # Тип каждого столбца + # Предрасчёт фонового усреднения по строкам текущего окна + bg_vec = None + if self.bg_subtract_enabled and self.bscan_buffer: + desired_row_start = row_min + desired_row_end = row_max + 1 + bg_vec = self.compute_background_slice(desired_row_start, desired_row_end) + alpha = self.bg_subtract_alpha if self.bg_subtract_enabled else 0.0 for i in range(col_min, col_max): col_data = self.B_scan_data[i] col_type = self.B_scan_types[i] @@ -862,7 +884,13 @@ class DataAnalyzerApp: 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]) + seg = col_data[row_start:row_end] + if bg_vec is not None and col_type != "GAP": + take = min(len(seg), len(bg_vec)) + if take > 0 and alpha != 0.0: + seg = seg.copy() + seg[:take] = seg[:take] - alpha * bg_vec[:take] + display_cols.append(seg) display_types.append(col_type) display_times = self.B_scan_times[col_min:col_max] @@ -1054,6 +1082,10 @@ class DataAnalyzerApp: self.B_scan_times.append(current_time) self.B_scan_types.append(data_type) self.last_file_time = current_time + # Сохраняем в буфер для фонового усреднения (не добавляем GAP) + if data_type != "GAP": + self.bscan_buffer.append((np.asarray(data_col, dtype=float), current_time, data_type)) + self.prune_bscan_buffer(current_time) return # Используем переменный интервал между файлами @@ -1084,6 +1116,61 @@ class DataAnalyzerApp: self.B_scan_times.append(current_time) self.B_scan_types.append(data_type) self.last_file_time = current_time + # Сохраняем в буфер для фонового усреднения (не добавляем GAP) + if data_type != "GAP": + self.bscan_buffer.append((np.asarray(data_col, dtype=float), current_time, data_type)) + self.prune_bscan_buffer(current_time) + + def prune_bscan_buffer(self, now_time: datetime): + """Удаляет столбцы старше окна усреднения.""" + try: + threshold = now_time - timedelta(seconds=self.bg_avg_window_sec) + while self.bscan_buffer and self.bscan_buffer[0][1] < threshold: + self.bscan_buffer.popleft() + except Exception: + pass + + def compute_background_slice(self, row_start: int, row_end: int) -> np.ndarray: + """Пер-строчная статистика (median/mean) по окну последних колонок для строк [row_start:row_end).""" + n_rows = max(0, row_end - row_start) + if n_rows <= 0 or not self.bscan_buffer: + return np.zeros((0,), dtype=float) + + # Собираем сегменты для указанного диапазона строк + segments = [] + for col, t, tname in self.bscan_buffer: + if col is None: + continue + L = len(col) + if L <= row_start: + continue + take = min(n_rows, L - row_start) + if take <= 0: + continue + seg = np.asarray(col[row_start:row_start + take], dtype=float) + segments.append(seg) + + if not segments: + return np.zeros((n_rows,), dtype=float) + + # Формируем матрицу (num_cols x n_rows) с NaN, заполняя доступные значения + num_cols = len(segments) + M = np.full((num_cols, n_rows), np.nan, dtype=float) + for i, seg in enumerate(segments): + take = min(n_rows, seg.shape[0]) + if take > 0: + M[i, :take] = seg[:take] + + # Агрегирование по времени (ось 0), игнорируя NaN + with np.errstate(all='ignore'): + if getattr(self, 'bg_method', 'median') == 'mean': + bg = np.nanmean(M, axis=0) + else: + bg = np.nanmedian(M, axis=0) + + # Заменим NaN на 0 (строки без данных в окне) + bg = np.nan_to_num(bg, nan=0.0) + return bg def compute_fft(self): """Вычисляем FFT спектр."""