490 lines
19 KiB
Python
490 lines
19 KiB
Python
"""
|
|
Main View for the Radar Data Reader application, now featuring a tabbed interface.
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import scrolledtext, filedialog, ttk
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List
|
|
import queue
|
|
from datetime import datetime, timezone
|
|
|
|
from ..utils import logger
|
|
from ..core.export_profiles import ExportProfile
|
|
|
|
log = logger.get_logger(__name__)
|
|
|
|
|
|
def _format_timetag(timetag: Any) -> str:
|
|
"""Safely formats a numeric timetag into a human-readable UTC string."""
|
|
if not isinstance(timetag, (int, float)) or timetag <= 0:
|
|
return "N/A"
|
|
try:
|
|
dt_object = datetime.fromtimestamp(timetag, tz=timezone.utc)
|
|
return dt_object.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
except (ValueError, OSError):
|
|
return f"Invalid timestamp ({timetag})"
|
|
|
|
|
|
class MainWindow(tk.Frame):
|
|
"""The main application window (View)."""
|
|
|
|
def __init__(self, master: tk.Tk, controller, logging_config: Dict[str, Any]):
|
|
super().__init__(master)
|
|
self.master = master
|
|
self.controller = controller
|
|
|
|
self.gui_update_queue = controller.result_queue
|
|
self.total_blocks_for_progress = 0
|
|
self.profile_editor_window = None
|
|
|
|
self._init_vars()
|
|
self.pack(fill=tk.BOTH, expand=True)
|
|
self.master.title("Radar Data Reader & Processor")
|
|
self.master.geometry("800x850")
|
|
|
|
self._create_widgets()
|
|
self._setup_gui_logging(logging_config)
|
|
|
|
self.master.protocol("WM_DELETE_WINDOW", self.on_close)
|
|
log.info("Main window View initialized.")
|
|
|
|
def _init_vars(self):
|
|
"""Initialize all Tkinter variables."""
|
|
self.status_bar_var = tk.StringVar(value="Ready")
|
|
|
|
self.out_filepath_var = tk.StringVar()
|
|
self.out_output_dir_var = tk.StringVar()
|
|
self.out_basename_var = tk.StringVar()
|
|
self.out_output_csv_var = tk.BooleanVar(value=True)
|
|
self.out_output_json_var = tk.BooleanVar(value=False)
|
|
self.out_csv_profile_var = tk.StringVar()
|
|
self.out_json_profile_var = tk.StringVar()
|
|
|
|
self.rec_filepath_var = tk.StringVar()
|
|
self.rec_file_count_var = tk.IntVar(value=1)
|
|
self.rec_output_dir_var = tk.StringVar()
|
|
self.rec_basename_var = tk.StringVar()
|
|
self.rec_output_out_var = tk.BooleanVar(value=True)
|
|
self.rec_output_csv_var = tk.BooleanVar(value=False)
|
|
self.rec_output_json_var = tk.BooleanVar(value=False)
|
|
self.rec_extract_video_var = tk.BooleanVar(value=False)
|
|
self.rec_csv_profile_var = tk.StringVar()
|
|
self.rec_json_profile_var = tk.StringVar()
|
|
|
|
self.progress_text_var = tk.StringVar(value="N/A")
|
|
self.batches_found_var = tk.StringVar(value="N/A")
|
|
self.file_batch_counter_var = tk.StringVar(value="N/A")
|
|
self.timetag_var = tk.StringVar(value="N/A")
|
|
self.progress_bar_var = tk.DoubleVar(value=0)
|
|
|
|
def _create_widgets(self):
|
|
menu_bar = tk.Menu(self.master)
|
|
self.master.config(menu=menu_bar)
|
|
file_menu = tk.Menu(menu_bar, tearoff=0)
|
|
menu_bar.add_cascade(label="File", menu=file_menu)
|
|
file_menu.add_command(
|
|
label="Manage Export Profiles...",
|
|
command=self.controller.open_profile_editor,
|
|
)
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Exit", command=self.on_close)
|
|
|
|
main_frame = tk.Frame(self)
|
|
main_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
|
|
main_frame.columnconfigure(0, weight=1)
|
|
|
|
notebook = ttk.Notebook(main_frame)
|
|
notebook.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
|
|
|
|
out_processor_tab = ttk.Frame(notebook, padding="10")
|
|
rec_processor_tab = ttk.Frame(notebook, padding="10")
|
|
|
|
notebook.add(out_processor_tab, text="OUT Processor")
|
|
notebook.add(rec_processor_tab, text="REC Processor")
|
|
|
|
self._create_out_processor_tab(out_processor_tab)
|
|
self._create_rec_processor_tab(rec_processor_tab)
|
|
|
|
self._create_live_data_frame(main_frame)
|
|
self._create_log_console_frame(main_frame)
|
|
|
|
self.status_bar = ttk.Label(
|
|
self,
|
|
textvariable=self.status_bar_var,
|
|
relief=tk.SUNKEN,
|
|
anchor=tk.W,
|
|
padding=2,
|
|
)
|
|
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
|
|
|
def _create_out_processor_tab(self, parent):
|
|
parent.columnconfigure(1, weight=1)
|
|
|
|
input_frame = ttk.LabelFrame(parent, text="Input")
|
|
input_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
|
input_frame.columnconfigure(1, weight=1)
|
|
ttk.Label(input_frame, text="Input .out File:").grid(
|
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
out_file_entry = ttk.Entry(
|
|
input_frame, textvariable=self.out_filepath_var, state="readonly"
|
|
)
|
|
out_file_entry.grid(row=0, column=1, sticky="ew", padx=5)
|
|
self.out_browse_button = ttk.Button(
|
|
input_frame, text="Browse...", command=self.controller.select_out_file
|
|
)
|
|
self.out_browse_button.grid(row=0, column=2, padx=5, pady=5)
|
|
self.out_filepath_var.trace_add("write", self.controller.on_out_config_changed)
|
|
|
|
output_frame = ttk.LabelFrame(parent, text="Output Configuration")
|
|
output_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
|
output_frame.columnconfigure(1, weight=1)
|
|
|
|
ttk.Label(output_frame, text="Output Directory:").grid(
|
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
out_dir_entry = ttk.Entry(output_frame, textvariable=self.out_output_dir_var)
|
|
out_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
|
|
out_dir_buttons_frame = ttk.Frame(output_frame)
|
|
out_dir_buttons_frame.grid(row=0, column=2, padx=5)
|
|
ttk.Button(
|
|
out_dir_buttons_frame,
|
|
text="Browse...",
|
|
command=lambda: self.controller.select_output_dir(self.out_output_dir_var),
|
|
).pack(side=tk.LEFT)
|
|
ttk.Button(
|
|
out_dir_buttons_frame,
|
|
text="Open...",
|
|
command=lambda: self.controller.open_folder_from_path(
|
|
self.out_output_dir_var.get()
|
|
),
|
|
).pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
ttk.Label(output_frame, text="Base Filename:").grid(
|
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
out_basename_entry = ttk.Entry(output_frame, textvariable=self.out_basename_var)
|
|
out_basename_entry.grid(row=1, column=1, columnspan=2, sticky="ew", padx=5)
|
|
|
|
options_frame = ttk.LabelFrame(parent, text="Output Formats")
|
|
options_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
|
options_frame.columnconfigure(1, weight=1)
|
|
|
|
ttk.Checkbutton(
|
|
options_frame, text="Generate .csv file", variable=self.out_output_csv_var
|
|
).grid(row=0, column=0, sticky="w", padx=5, pady=2)
|
|
self.out_csv_profile_combobox = ttk.Combobox(
|
|
options_frame,
|
|
textvariable=self.out_csv_profile_var,
|
|
state="readonly",
|
|
width=20,
|
|
)
|
|
self.out_csv_profile_combobox.grid(row=0, column=1, sticky="w", padx=5)
|
|
|
|
ttk.Checkbutton(
|
|
options_frame, text="Generate .json file", variable=self.out_output_json_var
|
|
).grid(row=1, column=0, sticky="w", padx=5, pady=2)
|
|
self.out_json_profile_combobox = ttk.Combobox(
|
|
options_frame,
|
|
textvariable=self.out_json_profile_var,
|
|
state="readonly",
|
|
width=20,
|
|
)
|
|
self.out_json_profile_combobox.grid(row=1, column=1, sticky="w", padx=5)
|
|
|
|
action_frame = ttk.Frame(parent)
|
|
action_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0))
|
|
self.out_process_button = ttk.Button(
|
|
action_frame,
|
|
text="Process .out File",
|
|
command=self.controller.start_out_processing,
|
|
)
|
|
self.out_process_button.pack(side=tk.LEFT, padx=5)
|
|
self.out_stop_button = ttk.Button(
|
|
action_frame,
|
|
text="Stop",
|
|
command=self.controller.stop_processing,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.out_stop_button.pack(side=tk.LEFT, padx=5)
|
|
|
|
def _create_rec_processor_tab(self, parent):
|
|
parent.columnconfigure(1, weight=1)
|
|
|
|
input_frame = ttk.LabelFrame(parent, text="Input REC Sequence")
|
|
input_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
|
input_frame.columnconfigure(1, weight=1)
|
|
|
|
ttk.Label(input_frame, text="First .rec File:").grid(
|
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
rec_file_entry = ttk.Entry(
|
|
input_frame, textvariable=self.rec_filepath_var, state="readonly"
|
|
)
|
|
rec_file_entry.grid(row=0, column=1, sticky="ew", padx=5)
|
|
ttk.Button(
|
|
input_frame, text="Browse...", command=self.controller.select_rec_file
|
|
).grid(row=0, column=2, padx=5)
|
|
|
|
ttk.Label(input_frame, text="Number of Files:").grid(
|
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
rec_file_count_spinbox = ttk.Spinbox(
|
|
input_frame,
|
|
from_=1,
|
|
to=1000,
|
|
textvariable=self.rec_file_count_var,
|
|
width=10,
|
|
)
|
|
rec_file_count_spinbox.grid(row=1, column=1, padx=5, pady=5, sticky="w")
|
|
self.rec_file_count_var.trace_add(
|
|
"write", self.controller.on_rec_config_changed
|
|
)
|
|
self.rec_filepath_var.trace_add("write", self.controller.on_rec_config_changed)
|
|
|
|
output_frame = ttk.LabelFrame(parent, text="Output Configuration")
|
|
output_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
|
output_frame.columnconfigure(1, weight=1)
|
|
|
|
ttk.Label(output_frame, text="Output Directory:").grid(
|
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
rec_dir_entry = ttk.Entry(output_frame, textvariable=self.rec_output_dir_var)
|
|
rec_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
|
|
rec_dir_buttons_frame = ttk.Frame(output_frame)
|
|
rec_dir_buttons_frame.grid(row=0, column=2, padx=5)
|
|
ttk.Button(
|
|
rec_dir_buttons_frame,
|
|
text="Browse...",
|
|
command=lambda: self.controller.select_output_dir(self.rec_output_dir_var),
|
|
).pack(side=tk.LEFT)
|
|
ttk.Button(
|
|
rec_dir_buttons_frame,
|
|
text="Open...",
|
|
command=lambda: self.controller.open_folder_from_path(
|
|
self.rec_output_dir_var.get()
|
|
),
|
|
).pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
ttk.Label(output_frame, text="Base Filename:").grid(
|
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
rec_basename_entry = ttk.Entry(output_frame, textvariable=self.rec_basename_var)
|
|
rec_basename_entry.grid(row=1, column=1, columnspan=2, sticky="ew", padx=5)
|
|
|
|
options_frame = ttk.LabelFrame(parent, text="Output Formats")
|
|
options_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
|
options_frame.columnconfigure(1, weight=1)
|
|
|
|
ttk.Checkbutton(
|
|
options_frame, text="Generate .out file", variable=self.rec_output_out_var
|
|
).grid(row=0, column=0, columnspan=2, sticky="w", padx=5)
|
|
ttk.Checkbutton(
|
|
options_frame,
|
|
text="Extract video stream",
|
|
variable=self.rec_extract_video_var,
|
|
).grid(row=1, column=0, columnspan=2, sticky="w", padx=5)
|
|
ttk.Checkbutton(
|
|
options_frame, text="Generate .csv file", variable=self.rec_output_csv_var
|
|
).grid(row=2, column=0, sticky="w", padx=5)
|
|
self.rec_csv_profile_combobox = ttk.Combobox(
|
|
options_frame,
|
|
textvariable=self.rec_csv_profile_var,
|
|
state="readonly",
|
|
width=20,
|
|
)
|
|
self.rec_csv_profile_combobox.grid(row=2, column=1, sticky="w", padx=5)
|
|
ttk.Checkbutton(
|
|
options_frame, text="Generate .json file", variable=self.rec_output_json_var
|
|
).grid(row=3, column=0, sticky="w", padx=5)
|
|
self.rec_json_profile_combobox = ttk.Combobox(
|
|
options_frame,
|
|
textvariable=self.rec_json_profile_var,
|
|
state="readonly",
|
|
width=20,
|
|
)
|
|
self.rec_json_profile_combobox.grid(row=3, column=1, sticky="w", padx=5)
|
|
|
|
action_frame = ttk.Frame(parent)
|
|
action_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0))
|
|
self.rec_process_button = ttk.Button(
|
|
action_frame,
|
|
text="Process REC Sequence",
|
|
command=self.controller.start_rec_processing,
|
|
)
|
|
self.rec_process_button.pack()
|
|
|
|
def _create_live_data_frame(self, parent):
|
|
status_frame = ttk.LabelFrame(parent, text="Live Data & Progress")
|
|
status_frame.grid(row=1, column=0, sticky="nsew", pady=(0, 5))
|
|
status_frame.columnconfigure(1, weight=1)
|
|
self.progress_bar = ttk.Progressbar(
|
|
status_frame, variable=self.progress_bar_var, maximum=100
|
|
)
|
|
self.progress_bar.grid(
|
|
row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5, 2)
|
|
)
|
|
ttk.Label(status_frame, text="Progress:", anchor="e").grid(
|
|
row=1, column=0, padx=(10, 5), pady=2, sticky="e"
|
|
)
|
|
ttk.Label(status_frame, textvariable=self.progress_text_var, anchor="w").grid(
|
|
row=1, column=1, padx=5, pady=2, sticky="w"
|
|
)
|
|
ttk.Label(status_frame, text="Batches Found:", anchor="e").grid(
|
|
row=2, column=0, padx=(10, 5), pady=2, sticky="e"
|
|
)
|
|
ttk.Label(status_frame, textvariable=self.batches_found_var, anchor="w").grid(
|
|
row=2, column=1, padx=5, pady=2, sticky="w"
|
|
)
|
|
ttk.Label(status_frame, text="File Batch Counter:", anchor="e").grid(
|
|
row=3, column=0, padx=(10, 5), pady=2, sticky="e"
|
|
)
|
|
ttk.Label(
|
|
status_frame, textvariable=self.file_batch_counter_var, anchor="w"
|
|
).grid(row=3, column=1, padx=5, pady=2, sticky="w")
|
|
ttk.Label(status_frame, text="File TimeTag:", anchor="e").grid(
|
|
row=4, column=0, padx=(10, 5), pady=(2, 5), sticky="e"
|
|
)
|
|
ttk.Label(status_frame, textvariable=self.timetag_var, anchor="w").grid(
|
|
row=4, column=1, padx=5, pady=(2, 5), sticky="w"
|
|
)
|
|
|
|
def _create_log_console_frame(self, parent):
|
|
log_frame = ttk.LabelFrame(parent, text="Log Console")
|
|
log_frame.grid(row=2, column=0, sticky="nsew", pady=(5, 0))
|
|
log_frame.rowconfigure(0, weight=1)
|
|
log_frame.columnconfigure(0, weight=1)
|
|
self.log_widget = scrolledtext.ScrolledText(
|
|
log_frame, state=tk.DISABLED, wrap=tk.WORD
|
|
)
|
|
self.log_widget.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
|
parent.rowconfigure(2, weight=1)
|
|
|
|
def _setup_gui_logging(self, logging_config):
|
|
logger.add_tkinter_handler(self.log_widget, self.master, logging_config)
|
|
|
|
def update_export_profiles(
|
|
self,
|
|
profiles: List[ExportProfile],
|
|
active_out_profile: str,
|
|
active_rec_csv: str,
|
|
active_rec_json: str,
|
|
):
|
|
profile_names = [p.name for p in profiles] if profiles else []
|
|
all_combos = [
|
|
(
|
|
self.out_csv_profile_combobox,
|
|
self.out_csv_profile_var,
|
|
active_out_profile,
|
|
),
|
|
(
|
|
self.out_json_profile_combobox,
|
|
self.out_json_profile_var,
|
|
active_out_profile,
|
|
),
|
|
(self.rec_csv_profile_combobox, self.rec_csv_profile_var, active_rec_csv),
|
|
(
|
|
self.rec_json_profile_combobox,
|
|
self.rec_json_profile_var,
|
|
active_rec_json,
|
|
),
|
|
]
|
|
for combo, var, active_name in all_combos:
|
|
combo["values"] = profile_names
|
|
if active_name in profile_names:
|
|
var.set(active_name)
|
|
elif profile_names:
|
|
var.set(profile_names[0])
|
|
else:
|
|
var.set("")
|
|
|
|
def start_processing_ui(self):
|
|
self.update_ui_for_processing_state(True)
|
|
self.progress_text_var.set("Starting...")
|
|
self.batches_found_var.set("N/A")
|
|
self.file_batch_counter_var.set("N/A")
|
|
self.timetag_var.set("N/A")
|
|
self.progress_bar_var.set(0)
|
|
self.after(100, self.poll_result_queue)
|
|
|
|
def update_ui_for_processing_state(self, is_processing: bool):
|
|
state = tk.DISABLED if is_processing else tk.NORMAL
|
|
|
|
# Disable all major controls on both tabs
|
|
for widget in [
|
|
self.out_browse_button,
|
|
self.out_process_button,
|
|
self.rec_process_button,
|
|
]:
|
|
if widget.winfo_exists():
|
|
widget.config(state=state)
|
|
|
|
# Specifically manage stop buttons, assuming one shared stop button for now
|
|
self.out_stop_button.config(state=tk.NORMAL if is_processing else tk.DISABLED)
|
|
|
|
if is_processing:
|
|
self.status_bar_var.set("Processing... Please wait.")
|
|
self.master.config(cursor="watch")
|
|
else:
|
|
self.status_bar_var.set("Ready")
|
|
self.progress_bar_var.set(0)
|
|
self.progress_text_var.set("Done")
|
|
self.master.config(cursor="")
|
|
|
|
def poll_result_queue(self):
|
|
try:
|
|
while True:
|
|
msg = self.gui_update_queue.get_nowait()
|
|
msg_type = msg.get("type")
|
|
|
|
if msg_type == "start":
|
|
self.total_blocks_for_progress = msg.get("total", 0)
|
|
|
|
elif msg_type == "progress":
|
|
batch_id = msg.get("batch_id", "N/A")
|
|
file_batch_counter = msg.get("file_batch_counter", "N/A")
|
|
timetag = msg.get("timetag", "N/A")
|
|
blocks_done = msg.get("blocks_done", 0)
|
|
|
|
self.batches_found_var.set(str(batch_id))
|
|
self.progress_text_var.set(
|
|
f"Block {blocks_done} / {self.total_blocks_for_progress}"
|
|
)
|
|
self.file_batch_counter_var.set(str(file_batch_counter))
|
|
self.timetag_var.set(f"{timetag} ({_format_timetag(timetag)})")
|
|
|
|
if self.total_blocks_for_progress > 0:
|
|
progress = (blocks_done / self.total_blocks_for_progress) * 100
|
|
self.progress_bar_var.set(progress)
|
|
|
|
elif msg_type == "data_batch":
|
|
self.controller.handle_data_batch(msg.get("data"))
|
|
|
|
elif msg_type == "complete":
|
|
self.controller.handle_worker_completion(msg)
|
|
self.update_ui_for_processing_state(False)
|
|
return
|
|
|
|
elif msg_type == "error":
|
|
log.error(f"Received error from worker: {msg.get('message')}")
|
|
self.controller.handle_worker_completion(msg)
|
|
self.update_ui_for_processing_state(False)
|
|
self.progress_text_var.set("Error!")
|
|
return
|
|
|
|
except queue.Empty:
|
|
pass
|
|
except Exception as e:
|
|
log.error(f"Error in GUI polling loop: {e}")
|
|
|
|
if self.controller.is_processing:
|
|
self.after(100, self.poll_result_queue)
|
|
|
|
def on_stop_click(self):
|
|
self.controller.stop_processing()
|
|
|
|
def on_close(self):
|
|
self.controller.shutdown()
|
|
self.master.destroy()
|