Merge branch 'master' of https://git.radiophotonics.ru/ChTheo/RFG_Receiver_GUI
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.
|
||||
|
||||
65
datagen.py
Normal file → Executable file
65
datagen.py
Normal file → Executable file
@ -1,13 +1,16 @@
|
||||
#!/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 = r"D:\data"
|
||||
DATA_DIR = './data'
|
||||
|
||||
# ✓ ИСПРАВЛЕНИЕ: Выбор типа данных и количество
|
||||
NUM_FILES = 1000 # Количесйтво файлов для генерации
|
||||
@ -89,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()
|
||||
@ -115,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()
|
||||
@ -130,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()
|
||||
@ -152,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()
|
||||
@ -171,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.
|
||||
@ -211,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()
|
||||
@ -261,8 +287,19 @@ if __name__ == "__main__":
|
||||
|
||||
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':
|
||||
@ -270,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
|
||||
|
||||
400
main.py
400
main.py
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import time
|
||||
import numpy as np
|
||||
@ -12,6 +13,8 @@ from scipy.interpolate import interp1d
|
||||
from datetime import datetime, timedelta
|
||||
import threading
|
||||
import queue
|
||||
from sys import argv
|
||||
from collections import deque
|
||||
|
||||
# ================================================================================
|
||||
# ПАРАМЕТРЫ И КОНСТАНТЫ
|
||||
@ -38,6 +41,17 @@ STANDARD_FOURIER_ROWS = 60
|
||||
STANDARD_FOURIER_COLS = 100
|
||||
|
||||
MAX_PROCESSING_TIME_MS = 250
|
||||
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"
|
||||
@ -56,7 +70,13 @@ DEFAULT_FILE_INTERVAL_MS = 300 # 300 мс
|
||||
GAP_THRESHOLD_MULTIPLIER = 1.5
|
||||
|
||||
# Начальное время опроса файлов (в миллисекундах)
|
||||
DEFAULT_FILE_POLL_INTERVAL_MS = 100 # 100 мс
|
||||
DEFAULT_FILE_POLL_INTERVAL_MS = 40 # 100 мс
|
||||
|
||||
# Игнорировать пропущенные данные (не добавлять GAP-колонки)
|
||||
IGNORE_LOST = True
|
||||
|
||||
# Игнорировать спектры из F4 и рассчитывать FFT из временных данных
|
||||
IGNORE_F4_FFT_DATA = True
|
||||
|
||||
|
||||
# ================================================================================
|
||||
@ -114,6 +134,53 @@ 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).
|
||||
|
||||
Принимает 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 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:
|
||||
@ -153,20 +220,48 @@ 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
|
||||
# Приоритет выбора, что считать сегментом
|
||||
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) Если есть F4 — используем как FOURIER; F0 (если есть) передаём для отображения без расчёта FFT
|
||||
# 2) Иначе F1+F2 → амплитуда
|
||||
# 3) Иначе F3 (sqrt)
|
||||
# 4) Иначе F0 как SYNC_DET
|
||||
# 5) Иначе D0 как RAW
|
||||
if cur["F4"] and not IGNORE_F4_FFT_DATA:
|
||||
# FOURIER данные получены напрямую из файла (F4)
|
||||
col = np.asarray(cur["F4"], dtype=float)
|
||||
col = BF_fft_postprocessor(col)
|
||||
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))
|
||||
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"]:
|
||||
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))
|
||||
# Сброс
|
||||
@ -272,12 +367,16 @@ 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)
|
||||
os.chdir(self.data_dir)
|
||||
|
||||
# Настройка: игнорировать пропуски кадров
|
||||
self.ignore_lost = IGNORE_LOST
|
||||
|
||||
# Инициализируем с существующими файлами
|
||||
existing_files = sorted([
|
||||
f for f in os.listdir()
|
||||
@ -362,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
|
||||
|
||||
# ============================================================================
|
||||
# ИНИЦИАЛИЗАЦИЯ ИНТЕРФЕЙСА
|
||||
# ============================================================================
|
||||
@ -374,7 +480,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])
|
||||
@ -762,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]
|
||||
@ -770,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]
|
||||
@ -923,8 +1043,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:
|
||||
@ -952,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
|
||||
|
||||
# Используем переменный интервал между файлами
|
||||
@ -962,24 +1096,81 @@ 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)
|
||||
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 спектр."""
|
||||
@ -991,7 +1182,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
|
||||
@ -1100,7 +1292,19 @@ class DataAnalyzerApp:
|
||||
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)
|
||||
#print("file:", fname)
|
||||
#print("A:", A)
|
||||
|
||||
bscan_col = None
|
||||
add_to_bscan = False
|
||||
@ -1132,6 +1336,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:
|
||||
@ -1168,18 +1377,35 @@ class DataAnalyzerApp:
|
||||
|
||||
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')])
|
||||
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]
|
||||
print("new files:", new_files, 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):
|
||||
@ -1187,6 +1413,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]
|
||||
@ -1217,15 +1484,94 @@ 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
|
||||
|
||||
|
||||
# ================================================================================
|
||||
# ТОЧКА ВХОДА
|
||||
# ================================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(argv) == 2:
|
||||
data_dir = argv[1]
|
||||
print("data dir:", data_dir)
|
||||
root = tk.Tk()
|
||||
app = DataAnalyzerApp(root)
|
||||
app.run()
|
||||
|
||||
Reference in New Issue
Block a user