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]