добавлен парсер HEX с разбиением по FE; обрабатываем только строки 0x; D0→RAW; сегменты с F0→SYNC_DET (расчёт спектра и добавление в B‑scan), F4 в таких сегментах игнорируется; поддержка .csv и отложенный парсинг HEX для избежания SKIP по таймауту.
This commit is contained in:
35
AGENTS.md
Normal file
35
AGENTS.md
Normal file
@ -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.
|
||||||
|
|
||||||
6
datagen.py
Normal file → Executable file
6
datagen.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import numpy as np
|
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 # Количесйтво файлов для генерации
|
NUM_FILES = 1000 # Количесйтво файлов для генерации
|
||||||
@ -291,4 +293,4 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
print(f"\n📂 Файлы сохранены в: {DATA_DIR}")
|
print(f"\n📂 Файлы сохранены в: {DATA_DIR}")
|
||||||
print(f"\n💡 Запустите main_analyzer.py и откройте директорию {DATA_DIR}")
|
print(f"\n💡 Запустите main_analyzer.py и откройте директорию {DATA_DIR}")
|
||||||
print(f" Анализатор автоматически подхватит новые файлы\n")
|
print(f" Анализатор автоматически подхватит новые файлы\n")
|
||||||
|
|||||||
106
main.py
106
main.py
@ -155,18 +155,23 @@ def parse_hex_file(filename):
|
|||||||
|
|
||||||
def finalize_segment():
|
def finalize_segment():
|
||||||
nonlocal cur
|
nonlocal cur
|
||||||
# Приоритет выбора, что считать сегментом
|
# Приоритет выбора сегмента:
|
||||||
if cur["F4"]:
|
# 1) Если есть F0 — используем как SYNC_DET (F4 игнорируем временно)
|
||||||
seg_fourier.append(np.asarray(cur["F4"], dtype=float))
|
# 2) Иначе F1+F2 → амплитуда
|
||||||
elif cur["F3"]:
|
# 3) Иначе F4 (если нет F0)
|
||||||
arr = np.asarray(cur["F3"], dtype=float)
|
# 4) Иначе F3 (sqrt)
|
||||||
seg_fourier.append(np.sqrt(np.maximum(0.0, arr)))
|
# 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"]):
|
elif cur["F1"] and cur["F2"] and len(cur["F1"]) == len(cur["F2"]):
|
||||||
re = np.asarray(cur["F1"], dtype=float)
|
re = np.asarray(cur["F1"], dtype=float)
|
||||||
im = np.asarray(cur["F2"], dtype=float)
|
im = np.asarray(cur["F2"], dtype=float)
|
||||||
seg_fourier.append(np.sqrt(re * re + im * im))
|
seg_fourier.append(np.sqrt(re * re + im * im))
|
||||||
elif cur["F0"]:
|
elif cur["F4"]:
|
||||||
seg_sync.append(np.asarray(cur["F0"], dtype=float))
|
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"]:
|
elif cur["D0"]:
|
||||||
seg_raw.append(np.asarray(cur["D0"], dtype=float))
|
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):
|
def process_file_thread(self, fname, data_type, A, original_size):
|
||||||
"""Обработка файла в отдельном потоке."""
|
"""Обработка файла в отдельном потоке."""
|
||||||
try:
|
try:
|
||||||
file_time = get_file_time_with_milliseconds(fname)
|
# Если данные не были загружены в главном потоке (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
|
bscan_col = None
|
||||||
add_to_bscan = False
|
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:
|
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.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
|
self.processed_count += 1
|
||||||
|
|
||||||
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
@ -1166,20 +1181,37 @@ class DataAnalyzerApp:
|
|||||||
|
|
||||||
self.process_files()
|
self.process_files()
|
||||||
|
|
||||||
def process_files(self):
|
def process_files(self):
|
||||||
"""Обработка файлов в цикле."""
|
"""Обработка файлов в цикле."""
|
||||||
files = sorted([f for f in os.listdir() if f.endswith('.csv') or
|
files = sorted([
|
||||||
f.endswith('.txt1') or f.endswith('.txt2') or f.endswith('.csv')])
|
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]
|
])
|
||||||
print("new files:", new_files, files)
|
|
||||||
|
new_files = [f for f in files if f not in self.processed_files]
|
||||||
|
|
||||||
for fname in new_files:
|
for fname in new_files:
|
||||||
time_start = time.perf_counter()
|
time_start = time.perf_counter()
|
||||||
|
|
||||||
try:
|
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)
|
data_type, A = load_data_with_type(fname)
|
||||||
# Поддержка списка сегментов (HEX с FE)
|
|
||||||
if isinstance(A, list):
|
if isinstance(A, list):
|
||||||
original_size = len(A[0]) if len(A) > 0 else 0
|
original_size = len(A[0]) if len(A) > 0 else 0
|
||||||
elif isinstance(A, np.ndarray):
|
elif isinstance(A, np.ndarray):
|
||||||
@ -1194,22 +1226,22 @@ class DataAnalyzerApp:
|
|||||||
self.skipped_count += 1
|
self.skipped_count += 1
|
||||||
self.processed_files.add(fname)
|
self.processed_files.add(fname)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elapsed_time_ms = (time.perf_counter() - time_start) * 1000
|
elapsed_time_ms = (time.perf_counter() - time_start) * 1000
|
||||||
|
|
||||||
if elapsed_time_ms > MAX_PROCESSING_TIME_MS:
|
if elapsed_time_ms > MAX_PROCESSING_TIME_MS:
|
||||||
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
print(f"[{timestamp}] ⏭️ SKIP {fname} (load time: {elapsed_time_ms:.1f}ms)")
|
print(f"[{timestamp}] ⏭️ SKIP {fname} (load time: {elapsed_time_ms:.1f}ms)")
|
||||||
self.skipped_count += 1
|
self.skipped_count += 1
|
||||||
else:
|
else:
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=self.process_file_thread,
|
target=self.process_file_thread,
|
||||||
args=(fname, data_type, A, original_size),
|
args=(fname, data_type, A, original_size),
|
||||||
daemon=True
|
daemon=True
|
||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
self.processed_files.add(fname)
|
self.processed_files.add(fname)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
|
|||||||
Reference in New Issue
Block a user