Files
pqAutomationApp/pqAutomationApp.py
2026-04-21 11:50:57 +08:00

909 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ttkbootstrap as ttk
import tkinter as tk
from tkinter import messagebox, filedialog
import sys
import threading
import time
import os
import datetime
import traceback
import matplotlib.pyplot as plt
from app_version import APP_NAME, APP_VERSION, get_app_title
from drivers.UCD323_Function import UCDController
from app.pq.pq_config import PQConfig
from app.pq.pq_result import PQResult
from app.views.pq_debug_panel import PQDebugPanel
from app.export import (
save_result_images as _save_result_images_impl,
export_excel_report as _export_excel_report_impl,
EXCEL_EXPORT_CONFIG as _EXCEL_EXPORT_CONFIG,
)
from app.views.panels import custom_template_panel as _ctp
from app.views.panels import side_panels as _sp
from app.views.panels import cct_panel as _ccp
from app.views.panels import main_layout as _main
from app.views import panel_manager as PM
# Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持
# 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。
from app.resources import (
backgroud_style_set,
get_resource_path,
load_icon,
)
from app.tests.color_accuracy import (
calculate_color_accuracy as _calc_color_accuracy,
calculate_delta_e_2000 as _calc_delta_e_2000,
get_accuracy_color_standards as _get_accuracy_color_standards,
)
from app.tests.eotf import calculate_pq_curve as _calc_pq_curve
from app.tests.gamma import calculate_gamma as _calc_gamma
from app.tests.gamut import calculate_gamut_coverage as _calc_gamut_coverage
from app.plots.plot_accuracy import plot_accuracy as _plot_accuracy
from app.plots.plot_cct import plot_cct as _plot_cct
from app.plots.plot_contrast import plot_contrast as _plot_contrast
from app.plots.plot_eotf import plot_eotf as _plot_eotf
from app.plots.plot_gamma import plot_gamma as _plot_gamma
from app.plots.plot_gamut import plot_gamut as _plot_gamut
from app.views.chart_frame import (
clear_chart as _cf_clear_chart,
create_result_chart_frame as _cf_create_result_chart_frame,
init_accuracy_chart as _cf_init_accuracy_chart,
init_cct_chart as _cf_init_cct_chart,
init_contrast_chart as _cf_init_contrast_chart,
init_eotf_chart as _cf_init_eotf_chart,
init_gamma_chart as _cf_init_gamma_chart,
init_gamut_chart as _cf_init_gamut_chart,
on_chart_tab_changed as _cf_on_chart_tab_changed,
update_chart_tabs_state as _cf_update_chart_tabs_state,
)
from app.config_io import (
clear_config_file as _cfg_clear_config_file,
get_config_path as _cfg_get_config_path,
load_pq_config as _cfg_load_pq_config,
save_pq_config as _cfg_save_pq_config,
)
from app.tests.local_dimming import (
clear_ld_records as _ld_clear_ld_records,
measure_ld_luminance as _ld_measure_ld_luminance,
save_local_dimming_results as _ld_save_local_dimming_results,
send_ld_window as _ld_send_ld_window,
start_local_dimming_test as _ld_start_local_dimming_test,
stop_local_dimming_test as _ld_stop_local_dimming_test,
update_ld_results as _ld_update_ld_results,
)
from app.device.connection import (
check_com_connections as _dev_check_com_connections,
check_port_connection as _dev_check_port_connection,
disconnect_com_connections as _dev_disconnect_com_connections,
enable_com_widgets as _dev_enable_com_widgets,
get_available_com_ports as _dev_get_available_com_ports,
get_available_ucd_ports as _dev_get_available_ucd_ports,
refresh_com_ports as _dev_refresh_com_ports,
update_connection_indicator as _dev_update_connection_indicator,
)
from app.runner.test_runner import (
get_current_test_result as _run_get_current_test_result,
new_pq_results as _run_new_pq_results,
on_custom_template_test_completed as _run_on_custom_template_test_completed,
on_test_completed as _run_on_test_completed,
on_test_error as _run_on_test_error,
run_custom_sdr_test as _run_run_custom_sdr_test,
run_hdr_movie_test as _run_run_hdr_movie_test,
run_screen_module_test as _run_run_screen_module_test,
run_sdr_movie_test as _run_run_sdr_movie_test,
run_test as _run_run_test,
send_fix_pattern as _run_send_fix_pattern,
test_cct as _run_test_cct,
test_color_accuracy as _run_test_color_accuracy,
test_contrast as _run_test_contrast,
test_custom_sdr as _run_test_custom_sdr,
test_eotf as _run_test_eotf,
test_gamma as _run_test_gamma,
test_gamut as _run_test_gamut,
)
plt.rcParams["font.family"] = ["sans-serif"]
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
class PQAutomationApp:
def __init__(self, root):
self.root = root
self.root.title(get_app_title())
self.root.geometry("900x650")
self.root.minsize(900, 650)
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.app_name = APP_NAME
self.app_version = APP_VERSION
self.config_cleared = False
# 初始化设备连接状态
self.ca = None # CA410色度计
self.ucd = UCDController() # 信号发生器
# 初始化测试状态
self.testing = False
self.test_thread = None
# 采集节奏参数:默认在稳定性与速度之间取平衡,可按现场情况再微调。
self.pattern_settle_time = 0.4
self.pattern_progress_log_step = 5
# 创建主框架
self.main_frame = ttk.Frame(root)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
backgroud_style_set()
# 创建配置对象
self.config = PQConfig()
self.results = None
# 加载上次保存的设置
self.config_file = self.get_config_path()
self.load_pq_config()
# 如果加载的配置不是屏模组,强制切换为屏模组
if self.config.current_test_type != "screen_module":
self.config.set_current_test_type("screen_module")
# 初始化侧边栏功能显示状态 - 使用统一的页面管理
self.current_panel = None # 当前显示的面板名称
self.panels = {} # 存储所有面板的信息
self.log_visible = False
# 创建左侧面板
self.left_frame = ttk.Frame(self.main_frame, width=180)
self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=0, pady=5)
self.left_frame.pack_propagate(False)
# 创建左侧导航栏
self.sidebar_frame = ttk.Frame(self.left_frame, bootstyle="primary")
self.sidebar_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=5)
# self.sidebar_frame.pack_propagate(False)
# 创建右侧内容区域
self.content_frame = ttk.Frame(self.main_frame)
self.content_frame.pack(
side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 创建右侧内容区域的上中下三个分区
self.control_frame_top = ttk.Frame(self.content_frame)
self.control_frame_top.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.control_frame_middle = ttk.Frame(self.content_frame)
self.control_frame_middle.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.control_frame_bottom = ttk.Frame(self.content_frame)
self.control_frame_bottom.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
# 创建右上角悬浮配置框
self.create_floating_config_panel()
# 创建右侧结果显示区域
self.result_frame = ttk.LabelFrame(self.control_frame_middle, text="测试结果")
self.result_frame.pack(
side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=5
)
# 创建日志显示区域
self.create_log_panel()
# 创建 Local Dimming 面板
self.create_local_dimming_panel()
# 创建测试类型选择区域
self.create_test_type_frame()
# 创建操作按钮区域
self.create_operation_frame()
# 创建结果图表区域
self.create_result_chart_frame()
# 创建客户模板结果显示区域(黑底表格)
self.create_custom_template_result_panel()
# 在所有控件创建完成后,统一初始化测试类型
self.root.after(100, self.initialize_default_test_type)
# 状态栏
self.status_var = tk.StringVar(value="就绪")
self.status_bar = ttk.Label(
root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W
)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def _dispatch_ui(self, fn, *args, **kwargs):
"""把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。
统一替代散落各处的 ``self.root.after(0, lambda: ...)`` 写法:
- 自动捕获异常并记录日志,避免工作线程静默丢失 UI 更新失败;
- 参数用位置/关键字传入,绕开 ``lambda`` 闭包捕获变量的常见坑;
- 允许在 UI 销毁(如关闭窗口)后安全失败。
"""
def _runner():
try:
fn(*args, **kwargs)
except Exception as exc:
log = getattr(self, "log_gui", None)
if log is not None:
try:
log.log(f"UI 调度异常: {exc}")
except Exception:
pass
try:
self.root.after(0, _runner)
except Exception:
pass
def initialize_default_test_type(self):
try:
# 初始设置为屏模组
self.change_test_type("screen_module")
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"初始化默认测试类型失败: {str(e)}")
get_config_path = _cfg_get_config_path
load_pq_config = _cfg_load_pq_config
save_pq_config = _cfg_save_pq_config
register_panel = PM.register_panel
show_panel = PM.show_panel
hide_all_panels = PM.hide_all_panels
init_gamut_chart = _cf_init_gamut_chart
init_gamma_chart = _cf_init_gamma_chart
init_eotf_chart = _cf_init_eotf_chart
init_cct_chart = _cf_init_cct_chart
init_contrast_chart = _cf_init_contrast_chart
init_accuracy_chart = _cf_init_accuracy_chart
clear_chart = _cf_clear_chart
create_result_chart_frame = _cf_create_result_chart_frame
on_chart_tab_changed = _cf_on_chart_tab_changed
create_floating_config_panel = _main.create_floating_config_panel
create_test_items_content = _main.create_test_items_content
create_signal_format_content = _main.create_signal_format_content
create_connection_content = _main.create_connection_content
create_operation_frame = _main.create_operation_frame
create_test_type_frame = _main.create_test_type_frame
update_config_info_display = _main.update_config_info_display
on_screen_module_timing_changed = _main.on_screen_module_timing_changed
update_test_items = _main.update_test_items
on_test_type_change = _main.on_test_type_change
create_cct_params_frame = _ccp.create_cct_params_frame
_get_cct_var_dict = _ccp._get_cct_var_dict
_parse_cct_float = _ccp._parse_cct_float
_save_cct_params_for = _ccp._save_cct_params_for
_handle_cct_focus_out = _ccp._handle_cct_focus_out
on_sdr_cct_param_focus_out = _ccp.on_sdr_cct_param_focus_out
save_sdr_cct_params = _ccp.save_sdr_cct_params
on_hdr_cct_param_focus_out = _ccp.on_hdr_cct_param_focus_out
save_hdr_cct_params = _ccp.save_hdr_cct_params
recalculate_cct = _ccp.recalculate_cct
recalculate_gamut = _ccp.recalculate_gamut
on_cct_param_focus_out = _ccp.on_cct_param_focus_out
save_cct_params = _ccp.save_cct_params
reload_cct_params = _ccp.reload_cct_params
toggle_cct_params_frame = _ccp.toggle_cct_params_frame
on_screen_gamut_ref_changed = _ccp.on_screen_gamut_ref_changed
on_sdr_gamut_ref_changed = _ccp.on_sdr_gamut_ref_changed
on_hdr_gamut_ref_changed = _ccp.on_hdr_gamut_ref_changed
get_available_ucd_ports = _dev_get_available_ucd_ports
get_available_com_ports = _dev_get_available_com_ports
refresh_com_ports = _dev_refresh_com_ports
check_com_connections = _dev_check_com_connections
update_connection_indicator = _dev_update_connection_indicator
check_port_connection = _dev_check_port_connection
enable_com_widgets = _dev_enable_com_widgets
disconnect_com_connections = _dev_disconnect_com_connections
create_custom_template_result_panel = _ctp.create_custom_template_result_panel
show_custom_result_context_menu = _ctp.show_custom_result_context_menu
set_custom_result_table_locked = _ctp.set_custom_result_table_locked
start_custom_row_single_step = _ctp.start_custom_row_single_step
_clear_custom_result_row = _ctp._clear_custom_result_row
_run_custom_row_single_step = _ctp._run_custom_row_single_step
_update_custom_result_row = _ctp._update_custom_result_row
copy_custom_result_table = _ctp.copy_custom_result_table
fill_custom_result_test_data = _ctp.fill_custom_result_test_data
clear_custom_template_results = _ctp.clear_custom_template_results
auto_expand_custom_result_view = _ctp.auto_expand_custom_result_view
append_custom_template_result = _ctp.append_custom_template_result
start_custom_template_test = _ctp.start_custom_template_test
update_custom_button_visibility = _ctp.update_custom_button_visibility
create_log_panel = _sp.create_log_panel
create_local_dimming_panel = _sp.create_local_dimming_panel
toggle_local_dimming_panel = _sp.toggle_local_dimming_panel
toggle_log_panel = _sp.toggle_log_panel
update_sidebar_selection = _sp.update_sidebar_selection
# ---- 单步调试面板(统一实现,委托到 side_panels 模块) ----
_toggle_debug_panel = _sp._toggle_debug_panel
toggle_screen_debug_panel = _sp.toggle_screen_debug_panel
toggle_sdr_debug_panel = _sp.toggle_sdr_debug_panel
toggle_hdr_debug_panel = _sp.toggle_hdr_debug_panel
clear_config_file = _cfg_clear_config_file
start_local_dimming_test = _ld_start_local_dimming_test
update_ld_results = _ld_update_ld_results
stop_local_dimming_test = _ld_stop_local_dimming_test
send_ld_window = _ld_send_ld_window
measure_ld_luminance = _ld_measure_ld_luminance
clear_ld_records = _ld_clear_ld_records
save_local_dimming_results = _ld_save_local_dimming_results
def _save_current_cct_params(self, swallow_errors=True):
"""按当前测试类型分发保存对应的 CCT 参数。"""
try:
current_type = self.config.current_test_type
if current_type == "screen_module":
self.save_cct_params()
elif current_type == "sdr_movie":
self.save_sdr_cct_params()
elif current_type == "hdr_movie" and hasattr(self, "save_hdr_cct_params"):
self.save_hdr_cct_params()
except Exception as e:
if not swallow_errors:
raise
if hasattr(self, "log_gui"):
self.log_gui.log(f"保存参数失败: {str(e)}")
def _save_cct_params_before_test_type_switch(self):
"""切换测试类型前,按当前类型保存色度参数。"""
if not hasattr(self, "cct_x_ideal_var"):
return
self._save_current_cct_params()
def _set_gamut_combos_state(self, state, success_msg=None, error_prefix="色域参考标准状态切换失败"):
"""统一切换三个色域参考下拉框的状态。"""
try:
for attr in ("screen_gamut_combo", "sdr_gamut_combo", "hdr_gamut_combo"):
combo = getattr(self, attr, None)
if combo is not None:
combo.configure(state=state)
if success_msg and hasattr(self, "log_gui"):
self.log_gui.log(success_msg)
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"{error_prefix}: {str(e)}")
def _hide_recalc_buttons(self, include_gamut=False):
"""隐藏重新计算按钮。include_gamut=True 时同时隐藏色域重算按钮。"""
attrs = ["recalc_cct_btn", "sdr_recalc_cct_btn", "hdr_recalc_cct_btn"]
if include_gamut:
attrs += ["recalc_gamut_btn", "sdr_recalc_gamut_btn", "hdr_recalc_gamut_btn"]
hidden = 0
for attr in attrs:
btn = getattr(self, attr, None)
if btn is None:
continue
try:
btn.grid_remove()
hidden += 1
except Exception:
pass
return hidden
def _disable_debug_panel(self):
"""禁用并隐藏单步调试面板(统一实现)。"""
if hasattr(self, "debug_panel"):
try:
self.debug_panel.disable_all_debug()
self.log_gui.log("✓ 单步调试已禁用")
except Exception as e:
self.log_gui.log(f"[Error] 禁用单步调试失败: {str(e)}")
if hasattr(self, "debug_container"):
try:
self.debug_container.pack_forget()
self.log_gui.log("✓ 单步调试面板已隐藏")
except Exception as e:
self.log_gui.log(f"[Error] 隐藏调试面板失败: {str(e)}")
def _set_config_panel_btn_state(self, state):
"""统一设置配置面板按钮状态disabled/normal"""
if not hasattr(self, "config_panel_frame"):
return
try:
self.config_panel_frame.btn.configure(state=state)
except Exception:
pass
def _apply_current_test_type(self, test_type):
"""更新 UI 变量与配置中的当前测试类型。"""
self.test_type_var.set(test_type)
if hasattr(self, "config") and hasattr(self.config, "set_current_test_type"):
success = self.config.set_current_test_type(test_type)
if not success and hasattr(self, "log_gui"):
self.log_gui.log(f"切换测试类型失败: {test_type}")
def _switch_signal_format_tabs(self, test_type):
"""切换信号格式 Tab 到目标测试类型。"""
if not hasattr(self, "signal_tabs"):
if hasattr(self, "log_gui"):
self.log_gui.log("[Error] signal_tabs 尚未创建")
return
try:
tab_mapping = {
"screen_module": 0,
"sdr_movie": 1,
"hdr_movie": 2,
}
target_tab = tab_mapping.get(test_type, 0)
for i in range(3):
self.signal_tabs.tab(i, state="normal")
self.signal_tabs.select(target_tab)
self.signal_tabs.update()
self.root.update_idletasks()
if target_tab == 0:
self.screen_module_signal_frame.tkraise()
elif target_tab == 1:
self.sdr_signal_frame.tkraise()
elif target_tab == 2:
self.hdr_signal_frame.tkraise()
for i in range(3):
if i != target_tab:
self.signal_tabs.tab(i, state="disabled")
if hasattr(self, "log_gui"):
tab_names = ["屏模组测试", "SDR测试", "HDR"]
self.log_gui.log(f"✓ 已切换到 {tab_names[target_tab]} 信号格式")
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"切换信号格式失败: {str(e)}")
def _switch_chart_tabs_by_test_type(self, test_type):
"""按测试类型切换 Gamma/EOTF 与客户模板结果 Tab。"""
if not hasattr(self, "chart_notebook"):
return
try:
current_tabs = list(self.chart_notebook.tabs())
gamma_tab_id = str(self.gamma_chart_frame)
eotf_tab_id = str(self.eotf_chart_frame)
if test_type == "hdr_movie":
if gamma_tab_id in current_tabs:
gamma_index = current_tabs.index(gamma_tab_id)
self.chart_notebook.forget(gamma_index)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已隐藏 Gamma 曲线 Tab")
if eotf_tab_id not in current_tabs:
self.chart_notebook.insert(1, self.eotf_chart_frame, text="EOTF 曲线")
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已显示 EOTF 曲线 Tab")
else:
if eotf_tab_id in current_tabs:
eotf_index = current_tabs.index(eotf_tab_id)
self.chart_notebook.forget(eotf_index)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已隐藏 EOTF 曲线 Tab")
if gamma_tab_id not in current_tabs:
self.chart_notebook.insert(1, self.gamma_chart_frame, text="Gamma 曲线")
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已显示 Gamma 曲线 Tab")
custom_tab_id = str(self.custom_template_tab_frame)
current_tabs = list(self.chart_notebook.tabs())
if test_type == "sdr_movie":
if custom_tab_id not in current_tabs:
self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示")
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已显示客户模板结果 Tab")
else:
if custom_tab_id in current_tabs:
self.chart_notebook.forget(self.custom_template_tab_frame)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已隐藏客户模板结果 Tab")
self.chart_notebook.update_idletasks()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}")
def change_test_type(self, test_type):
"""切换测试类型"""
# 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板
if self.current_panel in ("log", "local_dimming"):
self.hide_all_panels()
self._save_cct_params_before_test_type_switch()
self._apply_current_test_type(test_type)
# 更新测试项目和侧边栏
self.update_test_items()
self.update_sidebar_selection()
self.on_test_type_change()
self._switch_signal_format_tabs(test_type)
self._switch_chart_tabs_by_test_type(test_type)
def _check_start_preconditions(self):
"""检查开始测试前置条件:设备连接 & 未在测试中。"""
if self.ca is None or self.ucd is None:
messagebox.showerror("错误", "请先连接CA410和信号发生器")
return False
if self.testing:
messagebox.showinfo("提示", "测试已在进行中")
return False
return True
def _collapse_config_panel_for_test(self):
"""收起配置项并禁用其按钮。"""
if not hasattr(self, "config_panel_frame"):
return
try:
if self.config_panel_frame.winfo_viewable():
self.config_panel_frame.btn.invoke()
self.root.update_idletasks()
time.sleep(0.2)
except Exception:
pass
self._set_config_panel_btn_state("disabled")
def _set_ui_testing_state(self):
"""切换主 UI 到测试中状态(按钮禁用、状态栏、清空日志/图表)。"""
self.testing = True
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.save_btn.config(state=tk.DISABLED)
self.clear_config_btn.config(state=tk.DISABLED)
self.status_var.set("测试进行中...")
self.log_gui.clear_log()
self.clear_chart()
def _prepare_ui_for_test_start(self):
"""为开始测试准备 UI禁用调试、收起配置、禁用色域、隐藏重算按钮、切换状态"""
self._disable_debug_panel()
self._collapse_config_panel_for_test()
self._set_gamut_combos_state("disabled", error_prefix="禁用色域参考标准失败")
self._hide_recalc_buttons(include_gamut=False)
self._set_ui_testing_state()
def _rollback_test_start(self):
"""用户取消测试时恢复 UI 状态。"""
self.testing = False
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.clear_config_btn.config(state=tk.NORMAL)
self.status_var.set("测试已取消")
self._set_config_panel_btn_state("normal")
def _build_test_start_message(self, test_type):
"""根据测试类型生成确认弹框提示文案。"""
if test_type == "screen_module":
return "开始屏模组性能测试,请 byPass All PQ"
if test_type == "sdr_movie":
return "开始 SDR Movie 测试,请设置正确的图像模式"
if test_type == "hdr_movie":
return "开始 HDR Movie 测试,请设置正确的图像模式"
return f"开始{self.get_test_type_name(test_type)}测试"
def _launch_test_thread(self, test_type, test_items):
"""在新线程中执行测试。"""
self.test_thread = threading.Thread(
target=self.run_test, args=(test_type, test_items)
)
self.test_thread.daemon = True
self.test_thread.start()
def start_test(self):
"""开始测试"""
if not self._check_start_preconditions():
return
test_type = self.test_type_var.get()
test_items = self.get_selected_test_items()
if not test_items:
messagebox.showinfo("提示", "请至少选择一个测试项目")
return
self._prepare_ui_for_test_start()
message = self._build_test_start_message(test_type)
if not messagebox.askyesno("确认测试", message):
self._rollback_test_start()
return
self._launch_test_thread(test_type, test_items)
def _confirm_stop_test(self):
"""弹出确认停止测试对话框。"""
return messagebox.askyesno(
"确认停止测试",
"测试正在进行中,确定要停止吗?\n\n 停止后将放弃本次测试的所有数据,无法保存。",
icon="warning",
)
def _signal_stop_and_update_ui(self):
"""设置停止标志并立即更新 UI 以反馈给用户。"""
self.testing = False
self.log_gui.log("=" * 50)
self.log_gui.log("正在停止测试...")
self.log_gui.log("=" * 50)
self.stop_btn.config(state=tk.DISABLED)
self.status_var.set("正在停止测试,请稍候...")
self.root.update()
def _wait_for_test_thread(self, timeout_seconds=5):
"""等待测试线程结束,最多 timeout_seconds 秒,同时保持 UI 响应。"""
if not (self.test_thread and self.test_thread.is_alive()):
return
self.log_gui.log("等待测试线程结束...")
for _ in range(int(timeout_seconds * 10)):
if not self.test_thread.is_alive():
break
time.sleep(0.1)
self.root.update()
if self.test_thread.is_alive():
self.log_gui.log("[Error] 测试线程未能正常结束,将在后台继续等待")
else:
self.log_gui.log("✓ 测试线程已结束")
def _clear_test_data(self):
"""清空测试结果对象与中间数据缓存。"""
try:
self.log_gui.log("清理测试数据...")
if hasattr(self, "results"):
self.results = None
self.log_gui.log(" ✓ 测试结果对象已清空")
for attr in [
"gamut_results",
"gamma_results",
"cct_results",
"contrast_results",
"accuracy_results",
]:
if hasattr(self, attr):
setattr(self, attr, None)
self.log_gui.log(" ✓ 所有中间数据已清空")
except Exception as e:
self.log_gui.log(f"[Error] 清理数据时出错: {str(e)}")
def _clear_charts_and_tables(self):
"""清空图表与客户模板结果表格,并跳转到色域图 Tab。"""
try:
self.clear_chart()
self.log_gui.log("✓ 图表已清空")
except Exception as e:
self.log_gui.log(f"[Error] 清空图表时出错: {str(e)}")
try:
self.clear_custom_template_results()
self.log_gui.log("✓ 客户模板结果表格已清空")
except Exception as e:
self.log_gui.log(f"[Error] 清空客户模板结果表格失败: {str(e)}")
try:
if hasattr(self, "chart_notebook"):
self.chart_notebook.select(self.gamut_chart_frame)
self.root.update_idletasks()
except Exception as e:
self.log_gui.log(f"[Error] 跳转到色域图失败: {str(e)}")
def _restore_ui_after_stop(self):
"""恢复主按钮与状态栏到非测试态。"""
self.set_custom_result_table_locked(False)
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.save_btn.config(state=tk.DISABLED)
self.clear_config_btn.config(state=tk.NORMAL)
if hasattr(self, "custom_btn"):
self.custom_btn.config(state=tk.NORMAL)
self.status_var.set("测试已停止 - 数据已清空")
if hasattr(self, "config_panel_frame"):
self._set_config_panel_btn_state("normal")
def _finalize_stop_test(self):
"""延迟执行的清理流程总入口(由 root.after 调用)。"""
self._clear_test_data()
self._clear_charts_and_tables()
self._restore_ui_after_stop()
self._set_gamut_combos_state(
"disabled",
success_msg="✓ 色域参考标准已禁用",
error_prefix="禁用色域参考标准失败",
)
hidden = self._hide_recalc_buttons(include_gamut=True)
if hidden > 0:
self.log_gui.log(f"✓ 已隐藏 {hidden} 个重新计算按钮")
self._disable_debug_panel()
self.log_gui.log("=" * 50)
self.log_gui.log("✓ 测试已停止,所有数据已清空")
self.log_gui.log("=" * 50)
messagebox.showinfo(
"测试已停止",
"测试已停止,本次测试数据已清空。\n\n可以重新开始新的测试。",
)
def stop_test(self):
"""停止测试 - 放弃本次所有数据(完全集成版)"""
if not self.testing:
return
if not self._confirm_stop_test():
return
self._signal_stop_and_update_ui()
self._wait_for_test_thread(timeout_seconds=5)
self.root.after(1000, self._finalize_stop_test)
# ==================== 保存测试结果 ====================
def save_results(self):
"""保存测试结果(图片 + Excel。实现委派给 app.export。"""
save_dir = filedialog.askdirectory(title="选择保存测试结果的目录")
if not save_dir:
return
try:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
test_type = self.get_test_type_name(self.test_type_var.get())
result_dir = os.path.join(save_dir, f"{test_type}_{timestamp}")
os.makedirs(result_dir, exist_ok=True)
current_test_type = self.test_type_var.get()
selected_items = self.get_selected_test_items()
log = self.log_gui.log
log(f"保存测试类型: {current_test_type}")
log(f"已选测试项: {selected_items}")
# 1) 图片
_save_result_images_impl(
result_dir, current_test_type, selected_items,
lambda attr: getattr(self, attr, None),
log,
)
# 2) Excel
if (current_test_type in _EXCEL_EXPORT_CONFIG
and hasattr(self, "results") and self.results):
_export_excel_report_impl(
result_dir, current_test_type, selected_items,
self.results, log,
)
# 3) 成功提示
log("=" * 50)
log(f"测试结果已保存到目录: {result_dir}")
log("=" * 50)
messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}")
except Exception as e:
self.log_gui.log(f"[Error] 保存测试结果失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
messagebox.showerror("错误", f"保存测试结果失败: {str(e)}")
new_pq_results = _run_new_pq_results
run_test = _run_run_test
run_screen_module_test = _run_run_screen_module_test
run_custom_sdr_test = _run_run_custom_sdr_test
run_sdr_movie_test = _run_run_sdr_movie_test
run_hdr_movie_test = _run_run_hdr_movie_test
send_fix_pattern = _run_send_fix_pattern
test_custom_sdr = _run_test_custom_sdr
test_gamut = _run_test_gamut
test_gamma = _run_test_gamma
test_eotf = _run_test_eotf
test_cct = _run_test_cct
test_contrast = _run_test_contrast
test_color_accuracy = _run_test_color_accuracy
calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000)
get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards)
calculate_gamut_coverage = staticmethod(_calc_gamut_coverage)
calculate_gamma = staticmethod(_calc_gamma)
calculate_color_accuracy = staticmethod(_calc_color_accuracy)
calculate_pq_curve = staticmethod(_calc_pq_curve)
plot_gamut = _plot_gamut
plot_gamma = _plot_gamma
plot_eotf = _plot_eotf
plot_cct = _plot_cct
plot_contrast = _plot_contrast
plot_accuracy = _plot_accuracy
on_test_completed = _run_on_test_completed
on_custom_template_test_completed = _run_on_custom_template_test_completed
get_current_test_result = _run_get_current_test_result
on_test_error = _run_on_test_error
update_chart_tabs_state = _cf_update_chart_tabs_state
def get_test_type_name(self, test_type):
"""获取测试类型的显示名称"""
if test_type == "screen_module":
return "屏模组性能测试"
elif test_type == "sdr_movie":
return "SDR Movie测试"
elif test_type == "hdr_movie":
return "HDR Movie测试"
return test_type
def get_selected_test_items(self):
"""获取当前选中的测试项"""
selected_items = []
for var_name, var in self.test_vars.items():
if var.get():
selected_items.append(var_name.split("_")[-1])
return selected_items
def update_config(self, event=None):
"""更新配置"""
try:
self.config.set_device_config(
self.ca_com_var.get(),
self.ucd_list_var.get(),
self.ca_channel_var.get(),
)
# 保存当前选中的测试项到配置
self.config.set_current_test_items(self.get_selected_test_items())
# 待修改为三种测试类型的timing值
self.config.set_current_timing(self.screen_module_timing_var.get())
# 自动保存配置到文件
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"[Error] 更新配置失败: {str(e)}")
def update_config_and_tabs(self):
"""更新配置并同步Tab状态"""
self.update_config()
self.update_chart_tabs_state()
# 根据当前测试类型保存对应参数
if "cct" in self.get_selected_test_items():
self._save_current_cct_params()
# 控制参数框的显示
self.toggle_cct_params_frame()
def on_closing(self):
"""窗口关闭时的处理"""
try:
# ✅ 检查是否清理了配置
if not self.config_cleared:
# 保存配置
self.save_pq_config()
else:
print("配置已清理,不再保存")
# 断开设备连接
if self.ucd.status:
self.ucd.close()
if self.ca is not None:
self.ca.close()
# 关闭窗口
self.root.destroy()
except Exception as e:
print(f"关闭窗口时出错: {str(e)}")
self.root.destroy()
def main():
try:
# root = tk.Tk()
root = ttk.Window(themename="yeti")
app = PQAutomationApp(root)
root.mainloop()
except Exception as e:
print("程序发生错误:", e)
traceback.print_exc()
finally:
pass
if __name__ == "__main__":
main()