Files
pqAutomationApp/pqAutomationApp.py

909 lines
37 KiB
Python
Raw Normal View History

2026-04-16 16:51:05 +08:00
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
2026-04-17 11:17:29 +08:00
import matplotlib.pyplot as plt
2026-04-16 16:51:05 +08:00
from app_version import APP_NAME, APP_VERSION, get_app_title
2026-04-20 11:48:38 +08:00
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
2026-04-21 11:50:57 +08:00
from app.views import panel_manager as PM
2026-04-16 16:51:05 +08:00
2026-04-20 09:41:24 +08:00
# 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
2026-04-20 10:00:44 +08:00
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
2026-04-20 10:16:31 +08:00
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,
)
2026-04-20 10:54:47 +08:00
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,
)
2026-04-20 09:41:24 +08:00
2026-04-17 11:17:29 +08:00
plt.rcParams["font.family"] = ["sans-serif"]
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
2026-04-16 16:51:05 +08:00
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(
2026-04-20 11:48:38 +08:00
side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=5
2026-04-16 16:51:05 +08:00
)
2026-04-20 11:48:38 +08:00
# 创建日志显示区域
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)
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 状态栏
self.status_var = tk.StringVar(value="就绪")
self.status_bar = ttk.Label(
root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W
2026-04-16 16:51:05 +08:00
)
2026-04-20 11:48:38 +08:00
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
2026-04-16 16:51:05 +08:00
def _dispatch_ui(self, fn, *args, **kwargs):
"""把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。
2026-04-16 16:51:05 +08:00
统一替代散落各处的 ``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
2026-04-20 16:57:30 +08:00
try:
self.root.after(0, _runner)
except Exception:
pass
2026-04-16 16:51:05 +08:00
def initialize_default_test_type(self):
try:
2026-04-21 11:50:57 +08:00
# 初始设置为屏模组
self.change_test_type("screen_module")
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"初始化默认测试类型失败: {str(e)}")
2026-04-16 16:51:05 +08:00
get_config_path = _cfg_get_config_path
load_pq_config = _cfg_load_pq_config
save_pq_config = _cfg_save_pq_config
2026-04-21 11:50:57 +08:00
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
2026-04-16 16:51:05 +08:00
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
2026-04-21 11:50:57 +08:00
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
2026-04-16 16:51:05 +08:00
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
2026-04-16 16:51:05 +08:00
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
2026-04-20 16:57:30 +08:00
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
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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)}")
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
def _save_cct_params_before_test_type_switch(self):
"""切换测试类型前,按当前类型保存色度参数。"""
if not hasattr(self, "cct_x_ideal_var"):
2026-04-20 16:57:30 +08:00
return
2026-04-21 11:50:57 +08:00
self._save_current_cct_params()
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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)}")
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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}")
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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")
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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)}")
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
def _switch_chart_tabs_by_test_type(self, test_type):
"""按测试类型切换 Gamma/EOTF 与客户模板结果 Tab。"""
if not hasattr(self, "chart_notebook"):
return
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
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")
2026-04-20 16:57:30 +08:00
2026-04-21 11:50:57 +08:00
self.chart_notebook.update_idletasks()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}")
2026-04-20 16:57:30 +08:00
2026-04-16 16:51:05 +08:00
def change_test_type(self, test_type):
"""切换测试类型"""
# 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板
if self.current_panel in ("log", "local_dimming"):
self.hide_all_panels()
2026-04-21 11:50:57 +08:00
self._save_cct_params_before_test_type_switch()
self._apply_current_test_type(test_type)
2026-04-16 16:51:05 +08:00
# 更新测试项目和侧边栏
self.update_test_items()
self.update_sidebar_selection()
self.on_test_type_change()
2026-04-21 11:50:57 +08:00
self._switch_signal_format_tabs(test_type)
self._switch_chart_tabs_by_test_type(test_type)
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
def _check_start_preconditions(self):
"""检查开始测试前置条件:设备连接 & 未在测试中。"""
2026-04-16 16:51:05 +08:00
if self.ca is None or self.ucd is None:
messagebox.showerror("错误", "请先连接CA410和信号发生器")
2026-04-21 11:50:57 +08:00
return False
2026-04-16 16:51:05 +08:00
if self.testing:
messagebox.showinfo("提示", "测试已在进行中")
2026-04-21 11:50:57 +08:00
return False
return True
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
def _collapse_config_panel_for_test(self):
"""收起配置项并禁用其按钮。"""
if not hasattr(self, "config_panel_frame"):
2026-04-16 16:51:05 +08:00
return
try:
2026-04-21 11:50:57 +08:00
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")
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
def _set_ui_testing_state(self):
"""切换主 UI 到测试中状态(按钮禁用、状态栏、清空日志/图表)。"""
2026-04-16 16:51:05 +08:00
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()
2026-04-21 11:50:57 +08:00
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()
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
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")
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
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):
"""在新线程中执行测试。"""
2026-04-16 16:51:05 +08:00
self.test_thread = threading.Thread(
target=self.run_test, args=(test_type, test_items)
)
self.test_thread.daemon = True
self.test_thread.start()
2026-04-21 11:50:57 +08:00
def start_test(self):
"""开始测试"""
if not self._check_start_preconditions():
2026-04-16 16:51:05 +08:00
return
2026-04-21 11:50:57 +08:00
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()
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
message = self._build_test_start_message(test_type)
if not messagebox.askyesno("确认测试", message):
self._rollback_test_start()
2026-04-16 16:51:05 +08:00
return
2026-04-21 11:50:57 +08:00
self._launch_test_thread(test_type, test_items)
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
def _confirm_stop_test(self):
"""弹出确认停止测试对话框。"""
return messagebox.askyesno(
"确认停止测试",
"测试正在进行中,确定要停止吗?\n\n 停止后将放弃本次测试的所有数据,无法保存。",
icon="warning",
)
def _signal_stop_and_update_ui(self):
"""设置停止标志并立即更新 UI 以反馈给用户。"""
self.testing = False
2026-04-16 16:51:05 +08:00
self.log_gui.log("=" * 50)
2026-04-21 11:50:57 +08:00
self.log_gui.log("正在停止测试...")
2026-04-16 16:51:05 +08:00
self.log_gui.log("=" * 50)
self.stop_btn.config(state=tk.DISABLED)
self.status_var.set("正在停止测试,请稍候...")
2026-04-21 11:50:57 +08:00
self.root.update()
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
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("✓ 测试线程已结束")
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
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)}")
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
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)}")
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
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可以重新开始新的测试。",
)
2026-04-16 16:51:05 +08:00
2026-04-21 11:50:57 +08:00
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)
2026-04-16 16:51:05 +08:00
# ==================== 保存测试结果 ====================
2026-04-16 16:51:05 +08:00
def save_results(self):
"""保存测试结果(图片 + Excel。实现委派给 app.export。"""
2026-04-16 16:51:05 +08:00
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
2026-04-16 16:51:05 +08:00
log(f"保存测试类型: {current_test_type}")
log(f"已选测试项: {selected_items}")
2026-04-16 16:51:05 +08:00
2026-04-20 17:18:03 +08:00
# 1) 图片
_save_result_images_impl(
result_dir, current_test_type, selected_items,
lambda attr: getattr(self, attr, None),
log,
)
2026-04-20 17:18:03 +08:00
# 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,
)
2026-04-16 16:51:05 +08:00
2026-04-20 17:18:03 +08:00
# 3) 成功提示
log("=" * 50)
2026-04-21 11:50:57 +08:00
log(f"测试结果已保存到目录: {result_dir}")
log("=" * 50)
2026-04-20 17:18:03 +08:00
messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}")
2026-04-16 16:51:05 +08:00
2026-04-20 17:18:03 +08:00
except Exception as e:
2026-04-21 11:50:57 +08:00
self.log_gui.log(f"[Error] 保存测试结果失败: {str(e)}")
2026-04-20 17:18:03 +08:00
import traceback
2026-04-16 16:51:05 +08:00
2026-04-20 17:18:03 +08:00
self.log_gui.log(traceback.format_exc())
messagebox.showerror("错误", f"保存测试结果失败: {str(e)}")
2026-04-16 16:51:05 +08:00
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
2026-04-20 17:18:03 +08:00
update_chart_tabs_state = _cf_update_chart_tabs_state
2026-04-20 16:57:30 +08:00
2026-04-16 16:51:05 +08:00
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:
2026-04-21 11:50:57 +08:00
self.log_gui.log(f"[Error] 更新配置失败: {str(e)}")
2026-04-16 16:51:05 +08:00
def update_config_and_tabs(self):
"""更新配置并同步Tab状态"""
self.update_config()
self.update_chart_tabs_state()
# 根据当前测试类型保存对应参数
2026-04-21 11:50:57 +08:00
if "cct" in self.get_selected_test_items():
self._save_current_cct_params()
2026-04-16 16:51:05 +08:00
# 控制参数框的显示
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()