Compare commits

...

5 Commits

Author SHA1 Message Date
xinzhu.yin
2d8d4119e3 继续优化UI加载速度 2026-06-22 10:52:51 +08:00
xinzhu.yin
92cba114a9 优化UI加载速度 2026-06-22 10:40:39 +08:00
xinzhu.yin
22f8b12690 修改UCD逻辑、添加关闭界面的x 2026-06-22 10:31:19 +08:00
xinzhu.yin
9c15751dd0 将 pattern 切换后的等待时间设置添加到pq_config 2026-06-16 15:56:49 +08:00
xinzhu.yin
3dd383b982 修改UCD连接需要重启错误 2026-06-16 14:31:00 +08:00
19 changed files with 1032 additions and 258 deletions

View File

@@ -231,6 +231,12 @@ class BaseIO:
self.__ref_count = TSI_Clean() self.__ref_count = TSI_Clean()
logging.info(f"[UniTAP] BaseIO.del: {self}; Ref count: {self.__ref_count}") logging.info(f"[UniTAP] BaseIO.del: {self}; Ref count: {self.__ref_count}")
def rescan_devices(self):
if self.__ref_count > 0:
TSIX_DEV_RescanDevices()
return
logging.error("[UniTAP] BaseIO.rescan_devices call without TSI_Init")
def get_device_count(self) -> int: def get_device_count(self) -> int:
if self.__ref_count > 0: if self.__ref_count > 0:
return TSIX_DEV_GetDeviceCount() return TSIX_DEV_GetDeviceCount()

View File

@@ -70,6 +70,11 @@ class TsiLib:
self.__io.cleanup() self.__io.cleanup()
logging.info(f"[UniTAP] TsiLib.cleanup {self}") logging.info(f"[UniTAP] TsiLib.cleanup {self}")
def rescan_devices(self):
"""Rescan USB for newly connected TSI devices without tearing down the library."""
self.__io.rescan_devices()
logging.info(f"[UniTAP] TsiLib.rescan_devices {self}")
def __del__(self): def __del__(self):
""" """
Automatically call the destructor after the script completes. Automatically call the destructor after the script completes.

View File

@@ -36,6 +36,15 @@ def get_config_path(self: "PQAutomationApp"):
return config_file return config_file
def _apply_acquisition_timing(self: "PQAutomationApp"):
"""将 PQConfig 中的采集节奏同步到 app 运行时属性。"""
timing = getattr(self.config, "acquisition_timing", {})
self.pattern_settle_time = max(0.2, float(timing.get("pattern_settle_time", 0.4)))
self.pattern_progress_log_step = max(
1, int(timing.get("pattern_progress_log_step", 5))
)
def load_pq_config(self: "PQAutomationApp"): def load_pq_config(self: "PQAutomationApp"):
"""加载PQ配置兼容打包后的程序""" """加载PQ配置兼容打包后的程序"""
try: try:
@@ -44,12 +53,15 @@ def load_pq_config(self: "PQAutomationApp"):
with open(self.config_file, "r", encoding="utf-8") as f: with open(self.config_file, "r", encoding="utf-8") as f:
config_dict = json.load(f) config_dict = json.load(f)
self.config.from_dict(config_dict) self.config.from_dict(config_dict)
_apply_acquisition_timing(self)
if hasattr(self, "log_gui"): if hasattr(self, "log_gui"):
self.log_gui.log("配置文件加载成功", level="success") self.log_gui.log("配置文件加载成功", level="success")
else: else:
_apply_acquisition_timing(self)
if hasattr(self, "log_gui"): if hasattr(self, "log_gui"):
self.log_gui.log("配置文件不存在,使用默认配置", level="error") self.log_gui.log("配置文件不存在,使用默认配置", level="error")
except Exception as e: except Exception as e:
_apply_acquisition_timing(self)
if hasattr(self, "log_gui"): if hasattr(self, "log_gui"):
self.log_gui.log(f"加载配置文件失败: {str(e)},使用默认配置", level="error") self.log_gui.log(f"加载配置文件失败: {str(e)},使用默认配置", level="error")

View File

@@ -78,28 +78,91 @@ class ConnectionController:
# -- UCD 连接 ------------------------------------------------ # -- UCD 连接 ------------------------------------------------
def connect_ucd(self, display: str) -> bool: def _pick_ucd_device(self, ucd_list: list[str], display: str) -> str | None:
"""打开指定 UCD 设备。成功返回 True。""" """Prefer devices not marked in-use; fall back to full list."""
usable = [d for d in ucd_list if "(in use)" not in d.lower()]
if display in usable:
return display
if usable:
return usable[0]
if display in ucd_list:
return display
return ucd_list[0] if ucd_list else None
def _refresh_ucd_selection(self, display: str) -> tuple[list[str], str | None]:
"""Rescan devices and return (device list, valid selection or None)."""
ucd_list = self.list_ucd_devices()
return ucd_list, self._pick_ucd_device(ucd_list, display)
def _sync_ucd_combo(self, ucd_list: list[str], selected: str) -> None:
app = self._app
app.ucd_list_combo.config(values=ucd_list)
app.ucd_list_var.set(selected)
app.update_config()
def _prepare_ucd_reconnect(self) -> None:
"""Release stale SDK/native handles before a new open attempt."""
if self._device.state.name != "CLOSED": if self._device.state.name != "CLOSED":
try: try:
self._device.close() self._device.close()
except Exception: # noqa: BLE001 return
pass except Exception as exc: # noqa: BLE001
self._log(f"关闭旧 UCD 会话失败,将强制重置: {exc}", level="info")
try: try:
self._device.open(DeviceInfo.parse(display)) self._device.force_reset()
return True
except UcdError as exc:
self._log(f"设备 {display} 异常UCD323 连接失败: {exc}", level="error")
return False
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
self._log(f"UCD323 连接异常: {exc}", level="error") self._log(f"强制重置 UCD SDK 失败: {exc}", level="error")
def connect_ucd(self, display: str, *, rescan: bool = True) -> bool:
"""打开指定 UCD 设备。成功返回 True。"""
if rescan:
_, display = self._refresh_ucd_selection(display)
if not display:
self._log("未检测到 UCD 设备,请确认电源已打开后重试", level="error")
return False return False
self._prepare_ucd_reconnect()
last_error: Exception | None = None
for attempt in range(2):
try:
self._device.open(DeviceInfo.parse(display))
return True
except UcdError as exc:
last_error = exc
self._log(
f"设备 {display} 连接失败 ({attempt + 1}/2): {exc}",
level="error",
)
except Exception as exc: # noqa: BLE001
last_error = exc
self._log(
f"UCD323 连接异常 ({attempt + 1}/2): {exc}",
level="error",
)
if attempt == 0:
try:
self._device.force_reset()
except Exception as exc: # noqa: BLE001
self._log(f"重连前强制重置 UCD 失败: {exc}", level="error")
if rescan:
_, display = self._refresh_ucd_selection(display)
if not display:
return False
if last_error is not None:
self._log(f"UCD323 重连失败: {last_error}", level="error")
return False
def disconnect_ucd(self) -> None: def disconnect_ucd(self) -> None:
try: try:
self._device.close() self._device.close()
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
pass try:
self._device.force_reset()
except Exception: # noqa: BLE001
pass
self._log("UCD连接已断开", level="info") self._log("UCD连接已断开", level="info")
# -- CA 连接 ------------------------------------------------- # -- CA 连接 -------------------------------------------------
@@ -158,7 +221,11 @@ class ConnectionController:
def worker(): def worker():
try: try:
ucd_ok = self.connect_ucd(app.ucd_list_var.get()) ucd_list, selected = self._refresh_ucd_selection(app.ucd_list_var.get())
app._dispatch_ui(self._sync_ucd_combo, ucd_list, selected or "")
ucd_ok = False
if selected:
ucd_ok = self.connect_ucd(selected, rescan=False)
app._dispatch_ui( app._dispatch_ui(
app.update_connection_indicator, app.update_connection_indicator,
app.ucd_status_indicator, ucd_ok, app.ucd_status_indicator, ucd_ok,
@@ -185,7 +252,19 @@ class ConnectionController:
def worker(): def worker():
try: try:
ucd_ok = self.connect_ucd(app.ucd_list_var.get()) ucd_list, selected = self._refresh_ucd_selection(app.ucd_list_var.get())
app._dispatch_ui(self._sync_ucd_combo, ucd_list, selected or "")
if not selected:
app._dispatch_ui(
app.update_connection_indicator,
app.ucd_status_indicator,
False,
)
app._dispatch_ui(app.status_var.set, "未检测到 UCD 设备")
app._dispatch_ui(self._enable_widgets)
return
ucd_ok = self.connect_ucd(selected, rescan=False)
app._dispatch_ui( app._dispatch_ui(
app.update_connection_indicator, app.update_connection_indicator,
app.ucd_status_indicator, app.ucd_status_indicator,
@@ -262,9 +341,15 @@ class ConnectionController:
app = self._app app = self._app
com_ports = self.list_com_ports() com_ports = self.list_com_ports()
ucd_list = self.list_ucd_devices() ucd_list = self.list_ucd_devices()
if not ucd_list or all("(in use)" in d.lower() for d in ucd_list):
try:
self._device.force_reset()
except Exception as exc: # noqa: BLE001
self._log(f"刷新时重置 UCD SDK: {exc}", level="info")
ucd_list = self.list_ucd_devices()
if app.ucd_list_var.get() not in ucd_list: selected = self._pick_ucd_device(ucd_list, app.ucd_list_var.get())
app.ucd_list_var.set(ucd_list[0] if ucd_list else "") app.ucd_list_var.set(selected or "")
app.ucd_list_combo.config(values=ucd_list) app.ucd_list_combo.config(values=ucd_list)
if app.ca_com_var.get() not in com_ports: if app.ca_com_var.get() not in com_ports:

View File

@@ -655,6 +655,12 @@ class PQConfig:
"ca_channel": "0", "ca_channel": "0",
} }
# ---- 采集节奏settings/pq_config.json → acquisition_timing----
self.acquisition_timing = {
"pattern_settle_time": 0.4,
"pattern_progress_log_step": 5,
}
# ---- 自定义图案(用户可变)---- # ---- 自定义图案(用户可变)----
self.custom_pattern = { self.custom_pattern = {
"pattern_mode": "SolidColor", "pattern_mode": "SolidColor",
@@ -710,6 +716,7 @@ class PQConfig:
"test_types": self.current_test_types, "test_types": self.current_test_types,
"device_config": self.device_config, "device_config": self.device_config,
"custom_pattern": self.custom_pattern, "custom_pattern": self.custom_pattern,
"acquisition_timing": self.acquisition_timing,
} }
def from_dict(self, config_dict): def from_dict(self, config_dict):
@@ -728,6 +735,17 @@ class PQConfig:
self.device_config = config_dict.get("device_config", self.device_config) self.device_config = config_dict.get("device_config", self.device_config)
self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern) self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern)
loaded_timing = config_dict.get("acquisition_timing")
if isinstance(loaded_timing, dict):
if "pattern_settle_time" in loaded_timing:
self.acquisition_timing["pattern_settle_time"] = max(
0.2, float(loaded_timing["pattern_settle_time"])
)
if "pattern_progress_log_step" in loaded_timing:
self.acquisition_timing["pattern_progress_log_step"] = max(
1, int(loaded_timing["pattern_progress_log_step"])
)
def save_to_file(self, filename): def save_to_file(self, filename):
"""将配置保存到文件""" """将配置保存到文件"""
with open(filename, "w", encoding="utf-8") as f: with open(filename, "w", encoding="utf-8") as f:

View File

@@ -356,7 +356,8 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
1, int(getattr(self, "pattern_progress_log_step", 5)) 1, int(getattr(self, "pattern_progress_log_step", 5))
) )
self.log_gui.log( self.log_gui.log(
f"采集等待时间: {settle_time:.2f}s(可通过 pattern_settle_time 调整)" f"采集等待时间: {settle_time:.2f}s"
f"(可在 settings/pq_config.json 的 acquisition_timing.pattern_settle_time 调整)"
, level="info") , level="info")
display_names = session.display_names display_names = session.display_names

View File

@@ -111,6 +111,12 @@ class _UcdSdkBackend:
def search_device(self): def search_device(self):
"""搜索可用设备""" """搜索可用设备"""
self._ensure_tsi_lib()
try:
self.lUniTAP.rescan_devices()
except Exception:
log.exception("UcdSdk.search_device rescan failed; reinitializing TsiLib")
self._reinit_tsi_lib()
available_devices = self.lUniTAP.get_list_of_available_devices() available_devices = self.lUniTAP.get_list_of_available_devices()
return available_devices if available_devices else [] return available_devices if available_devices else []
@@ -122,6 +128,13 @@ class _UcdSdkBackend:
if self.dev is not None or self.status: if self.dev is not None or self.status:
self._force_cleanup() self._force_cleanup()
self._ensure_tsi_lib()
try:
self.lUniTAP.rescan_devices()
except Exception:
log.exception("UcdSdk.open rescan failed; reinitializing TsiLib")
self._reinit_tsi_lib()
device_id = int(device_name.split(":")[0]) device_id = int(device_name.split(":")[0])
temp_dev = self.lUniTAP.open(device_id) temp_dev = self.lUniTAP.open(device_id)
@@ -143,7 +156,7 @@ class _UcdSdkBackend:
return True return True
except Exception as e: except Exception as e:
self._force_cleanup() self.hard_reset()
return False return False
def close(self): def close(self):
@@ -154,32 +167,42 @@ class _UcdSdkBackend:
self._stop_audio_output() self._stop_audio_output()
except Exception: except Exception:
pass pass
self._close_device_object(self.dev) try:
self._close_device_object(self.dev)
except Exception:
log.exception("UcdSdk.close device object failed")
finally:
self._reset_state() self._reset_state()
self.lUniTAP = None try:
self._reinit_tsi_lib()
except Exception:
log.exception("UcdSdk.close reinit failed")
return True
for i in range(3): def hard_reset(self) -> None:
gc.collect() """丢弃失效句柄并重建 TSI意外断连后重连前调用"""
self.dev = None
time.sleep(2.0) self.role = None
self._reset_state()
self._reinit_tsi_lib()
def _ensure_tsi_lib(self) -> None:
if self.lUniTAP is None:
self.lUniTAP = UniTAP.TsiLib() self.lUniTAP = UniTAP.TsiLib()
return True def _reinit_tsi_lib(self) -> None:
"""Destroy and recreate TsiLib so the native driver rescans USB devices."""
except Exception as e: old = self.lUniTAP
self._reset_state() self.lUniTAP = None
if old is not None:
try: try:
self.lUniTAP = None old.cleanup()
gc.collect() except Exception:
time.sleep(2.0) log.exception("UcdSdk._reinit_tsi_lib cleanup failed")
self.lUniTAP = UniTAP.TsiLib() for _ in range(3):
except Exception as init_error: gc.collect()
pass time.sleep(2.0)
self.lUniTAP = UniTAP.TsiLib()
return False
def _reset_state(self): def _reset_state(self):
"""重置所有运行时状态(不关闭设备句柄)""" """重置所有运行时状态(不关闭设备句柄)"""
@@ -1055,6 +1078,26 @@ class UCD323Device(IUcdDevice):
self._info = None self._info = None
self._bus.publish(ConnectionChanged(DeviceKind.UCD, False, prev_serial)) self._bus.publish(ConnectionChanged(DeviceKind.UCD, False, prev_serial))
def force_reset(self) -> None:
"""释放失效的 SDK 句柄,软件状态复位为 CLOSED意外断连后重连前调用"""
with self._acquire_device_lock("force_reset"):
was_open = self._state != UcdState.CLOSED
prev_serial = self._info.serial if self._info else None
try:
self._sdk.hard_reset()
except Exception: # noqa: BLE001
log.exception("UCD force_reset SDK failed")
self._state = UcdState.CLOSED
self._curr_signal = None
self._curr_timing = None
self._curr_pattern = None
self._last_applied = None
self._info = None
if was_open:
self._bus.publish(
ConnectionChanged(DeviceKind.UCD, False, prev_serial)
)
# --- 信号 / 图案配置 --- # --- 信号 / 图案配置 ---
def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool: def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool:

View File

@@ -25,7 +25,138 @@ def _result_bg_color() -> str:
return "#FFFFFF" return "#FFFFFF"
def apply_result_chart_theme(self: "PQAutomationApp"): def _refresh_ax_theme_in_place(ax, palette) -> None:
"""就地更新已有坐标轴的主题色,无需清空重绘。"""
ax.set_facecolor(palette["card_bg"])
for spine in ax.spines.values():
spine.set_color(palette["border"])
ax.tick_params(axis="both", colors=palette["fg"])
try:
ax.xaxis.label.set_color(palette["fg"])
ax.yaxis.label.set_color(palette["fg"])
except Exception:
pass
try:
if ax.title.get_text():
ax.title.set_color(palette["fg"])
except Exception:
pass
for artist in getattr(ax, "texts", []):
try:
artist.set_color(palette["fg"])
except Exception:
pass
for line in ax.get_lines():
# 保留曲线本身颜色,仅刷新网格等辅助线
pass
for gridline in ax.get_xgridlines() + ax.get_ygridlines():
try:
gridline.set_color(palette["border"])
gridline.set_alpha(0.35)
except Exception:
pass
_CHART_THEME_SPECS = {
"gamut": ("gamut_chart_frame", "gamut_fig", "gamut_canvas"),
"gamma": ("gamma_chart_frame", "gamma_fig", "gamma_canvas"),
"eotf": ("eotf_chart_frame", "eotf_fig", "eotf_canvas"),
"cct": ("cct_chart_frame", "cct_fig", "cct_canvas"),
"contrast": ("contrast_chart_frame", "contrast_fig", "contrast_canvas"),
"accuracy": ("accuracy_chart_frame", "accuracy_fig", "accuracy_canvas"),
}
def _get_active_chart_key(self: "PQAutomationApp") -> str | None:
"""返回当前选中的结果图 Tab 对应的 chart key。"""
notebook = getattr(self, "chart_notebook", None)
if notebook is None:
return None
try:
selected = notebook.select()
if not selected:
return "gamut"
for key, (frame_attr, _, _) in _CHART_THEME_SPECS.items():
frame = getattr(self, frame_attr, None)
if frame is not None and str(frame) == selected:
return key
except Exception:
pass
return "gamut"
def _apply_fig_theme_colors(fig, canvas, palette, bg) -> None:
"""更新 Figure/Axes 主题色,不触发绘制。"""
fig.patch.set_facecolor(bg)
suptitle = getattr(fig, "_suptitle", None)
if suptitle is not None:
try:
suptitle.set_color(palette["fg"])
except Exception:
pass
for ax in fig.axes:
_refresh_ax_theme_in_place(ax, palette)
if canvas is not None:
try:
widget = canvas.get_tk_widget()
widget.configure(bg=bg, highlightthickness=0)
except Exception:
pass
def flush_chart_theme_if_dirty(self: "PQAutomationApp", chart_key: str | None = None) -> None:
"""绘制主题已更新但尚未 draw 的图表(通常在切换到该 Tab 时调用)。"""
dirty = getattr(self, "_charts_theme_dirty", None)
if not dirty:
return
keys = [chart_key] if chart_key and chart_key in dirty else list(dirty)
for key in keys:
spec = _CHART_THEME_SPECS.get(key)
if spec is None:
continue
canvas = getattr(self, spec[2], None)
if canvas is None:
continue
try:
canvas.draw()
except Exception:
pass
dirty.discard(key)
def refresh_result_charts_theme(
self: "PQAutomationApp", *, sync_draw: bool = True, visible_only: bool = True
) -> None:
"""就地刷新结果区 Matplotlib 图表主题色;默认仅绘制当前可见 Tab。"""
palette = get_theme_palette()
bg = _result_bg_color()
active_key = _get_active_chart_key(self) if visible_only else None
dirty = getattr(self, "_charts_theme_dirty", None)
if dirty is None:
dirty = set()
self._charts_theme_dirty = dirty
for key, (_, fig_attr, canvas_attr) in _CHART_THEME_SPECS.items():
fig = getattr(self, fig_attr, None)
canvas = getattr(self, canvas_attr, None)
if fig is None or canvas is None:
continue
_apply_fig_theme_colors(fig, canvas, palette, bg)
should_draw = sync_draw and ((not visible_only) or key == active_key)
if should_draw:
try:
canvas.draw()
except Exception:
pass
dirty.discard(key)
else:
dirty.add(key)
def apply_result_chart_theme(self: "PQAutomationApp", *, sync_draw: bool = False):
"""统一刷新结果图画布背景,使其跟随浅/深色主题。""" """统一刷新结果图画布背景,使其跟随浅/深色主题。"""
bg = _result_bg_color() bg = _result_bg_color()
@@ -50,7 +181,10 @@ def apply_result_chart_theme(self: "PQAutomationApp"):
except Exception: except Exception:
pass pass
try: try:
canvas.draw_idle() if sync_draw:
canvas.draw()
else:
canvas.draw_idle()
except Exception: except Exception:
pass pass
@@ -1111,6 +1245,9 @@ def on_chart_tab_changed(self: "PQAutomationApp", event):
if not selected_tab: if not selected_tab:
return return
self._last_tab_index = self.chart_notebook.index(selected_tab) self._last_tab_index = self.chart_notebook.index(selected_tab)
active_key = _get_active_chart_key(self)
if active_key:
flush_chart_theme_if_dirty(self, active_key)
except Exception as e: except Exception as e:
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error") self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error")
@@ -1129,6 +1266,8 @@ class ChartFrameMixin:
init_contrast_chart = init_contrast_chart init_contrast_chart = init_contrast_chart
init_accuracy_chart = init_accuracy_chart init_accuracy_chart = init_accuracy_chart
apply_result_chart_theme = apply_result_chart_theme apply_result_chart_theme = apply_result_chart_theme
refresh_result_charts_theme = refresh_result_charts_theme
flush_chart_theme_if_dirty = flush_chart_theme_if_dirty
_init_accuracy_result_table = _init_accuracy_result_table _init_accuracy_result_table = _init_accuracy_result_table
clear_accuracy_result_table = clear_accuracy_result_table clear_accuracy_result_table = clear_accuracy_result_table
update_accuracy_result_table = update_accuracy_result_table update_accuracy_result_table = update_accuracy_result_table

View File

@@ -3,88 +3,219 @@
register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面板间切换。 register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面板间切换。
""" """
import tkinter as tk from __future__ import annotations
from typing import TYPE_CHECKING import tkinter as tk
import tkinter.font as tkfont
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING: if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp from pqAutomationApp import PQAutomationApp
# 叠层切换lift/lower避免 pack_forget 触发 Matplotlib 等大面板重绘。
_STACK_PANELS = frozenset(
{"log", "ai_image", "pantone_baseline", "gamma_pattern", "calman", "single_step"}
)
def create_tool_panel_sidebar_item(
self: "PQAutomationApp", parent: tk.Misc, label: str, command: Callable[[], None]
) -> dict[str, Any]:
"""创建工具面板侧栏项:名称左对齐,选中时 × 贴右端,整体可点击。"""
from app.views.modern_styles import _contrast_text, _is_dark, get_theme_palette
row = tk.Frame(parent, bd=0, highlightthickness=0, cursor="hand2")
name_lbl = tk.Label(row, text=label, anchor=tk.W, bd=0, padx=18, pady=9)
close_lbl = tk.Label(
row,
text="×",
anchor=tk.CENTER,
bd=0,
padx=0,
pady=9,
width=2,
font=("Segoe UI", 9),
)
name_lbl.pack(side=tk.LEFT, fill=tk.X, expand=True)
item: dict[str, Any] = {
"row": row,
"name_label": name_lbl,
"close_label": close_lbl,
"label": label,
"selected": False,
"hover": False,
}
def _apply_style() -> None:
palette = get_theme_palette()
sidebar_bg = palette["sidebar_bg"]
sidebar_hover = palette["sidebar_hover"]
sidebar_selected = palette["sidebar_selected"]
sidebar_fg = palette["sidebar_fg"]
selected_fg = _contrast_text(
sidebar_selected,
dark_text=palette["badge_fg"],
light_text=sidebar_fg,
)
if item["selected"]:
bg = sidebar_selected
fg = selected_fg
name_font = ("Segoe UI Semibold", 10)
elif item["hover"]:
bg = sidebar_hover
fg = "#ffffff" if _is_dark(sidebar_hover) else sidebar_fg
name_font = ("Segoe UI", 10)
else:
bg = sidebar_bg
fg = sidebar_fg
name_font = ("Segoe UI", 10)
row.configure(bg=bg)
for widget in (name_lbl, close_lbl):
widget.configure(
bg=bg,
fg=fg,
activebackground=bg,
activeforeground=fg,
)
name_lbl.configure(font=name_font)
def _truncate_name() -> None:
full = item["label"]
if not item["selected"]:
name_lbl.configure(text=full)
return
row.update_idletasks()
close_w = close_lbl.winfo_width() if close_lbl.winfo_ismapped() else 20
avail = max(24, row.winfo_width() - close_w - 18 - 14)
font = tkfont.Font(font=name_lbl.cget("font"))
if font.measure(full) <= avail:
name_lbl.configure(text=full)
return
for i in range(len(full), 0, -1):
candidate = full[:i].rstrip() + ""
if font.measure(candidate) <= avail:
name_lbl.configure(text=candidate)
return
name_lbl.configure(text="")
def set_selected(selected: bool) -> None:
item["selected"] = selected
if selected:
close_lbl.pack(side=tk.RIGHT, padx=(0, 14))
else:
close_lbl.pack_forget()
_apply_style()
row.after_idle(_truncate_name)
def _on_click(_event=None) -> None:
command()
def _on_enter(_event=None) -> None:
if not item["selected"]:
item["hover"] = True
_apply_style()
def _on_leave(_event=None) -> None:
px, py = row.winfo_pointerxy()
rx, ry = row.winfo_rootx(), row.winfo_rooty()
rw, rh = row.winfo_width(), row.winfo_height()
if rx <= px < rx + rw and ry <= py < ry + rh:
return
item["hover"] = False
_apply_style()
def _on_configure(_event=None) -> None:
if item["selected"]:
_truncate_name()
for widget in (row, name_lbl, close_lbl):
widget.bind("<Button-1>", _on_click)
widget.bind("<Enter>", _on_enter)
widget.bind("<Leave>", _on_leave)
row.bind("<Configure>", _on_configure)
item["set_selected"] = set_selected
item["apply_theme"] = _apply_style
item["pack"] = lambda **kw: row.pack(fill=tk.X, padx=0, pady=1, **kw)
_apply_style()
return item
def _set_tool_panel_sidebar_selected(
panel_info: dict[str, Any], selected: bool
) -> None:
sidebar_item = panel_info.get("sidebar_item")
if sidebar_item is not None:
sidebar_item["set_selected"](selected)
return
button = panel_info.get("button")
if button is not None:
button.configure(
style="SidebarSelected.TButton" if selected else "Sidebar.TButton"
)
def refresh_tool_panel_sidebar_theme(self: "PQAutomationApp") -> None:
"""主题切换后刷新工具面板侧栏项配色。"""
for panel_info in self.panels.values():
sidebar_item = panel_info.get("sidebar_item")
if sidebar_item is not None:
sidebar_item["apply_theme"]()
def register_panel(self: "PQAutomationApp", panel_name, frame, button, visible_attr): def register_panel(self: "PQAutomationApp", panel_name, frame, button, visible_attr):
"""注册一个面板到管理系统""" """注册一个面板到管理系统"""
self.panels[panel_name] = { self.panels[panel_name] = {
"frame": frame, "frame": frame,
"button": button, "button": button,
"button_label": None,
"sidebar_item": None,
"visible_attr": visible_attr, "visible_attr": visible_attr,
} }
def show_panel(self: "PQAutomationApp", panel_name): def setup_view_stack(self: "PQAutomationApp") -> None:
"""显示指定面板,隐藏其他所有面板""" """将所有工具面板叠放在 view_stack 上,切换时仅调整 Z 序。"""
if panel_name not in self.panels: if getattr(self, "_view_stack_ready", False):
return return
# 如果当前面板就是要显示的面板,则隐藏它 self.main_view.place(relx=0, rely=0, relwidth=1, relheight=1)
if self.current_panel == panel_name:
self.hide_all_panels()
# 如果当前测试类型是 Local Dimming则在关闭日志等面板后自动恢复 Local Dimming 面板
try:
if (
getattr(self, "config", None)
and getattr(self.config, "current_test_type", None) == "local_dimming"
and panel_name != "local_dimming"
):
self.show_panel("local_dimming")
except Exception:
pass
return
# 隐藏所有面板
self.hide_all_panels()
# 显示指定面板
panel_info = self.panels[panel_name]
# 隐藏主内容区域。
# Local Dimming 作为并列测试类型时,需要保留顶部配置区,
# 让用户在面板上方直接看到并修改配置项。
if panel_name == "local_dimming":
# 重新按“自适应高度”布局顶部配置区,避免其占用可扩展空间把
# Local Dimming 主面板整体向下挤出大块空白。
self.control_frame_top.pack_forget()
self.control_frame_top.pack(
side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=5
)
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
else:
self.control_frame_top.pack_forget()
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
# 显示目标面板
panel_info["frame"].pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
# 更新按钮样式
if panel_info["button"]:
panel_info["button"].configure(style="SidebarSelected.TButton")
# 更新状态
setattr(self, panel_info["visible_attr"], True)
self.current_panel = panel_name
def hide_all_panels(self: "PQAutomationApp"):
"""隐藏所有面板,显示主内容区域"""
# 隐藏所有注册的面板
for panel_name, panel_info in self.panels.items(): for panel_name, panel_info in self.panels.items():
panel_info["frame"].pack_forget() if panel_name == "local_dimming":
if panel_info["button"]: continue
panel_info["button"].configure(style="Sidebar.TButton") panel_info["frame"].place(relx=0, rely=0, relwidth=1, relheight=1)
panel_info["frame"].lower()
self._view_stack_ready = True
def _lower_stack_panels(self: "PQAutomationApp") -> None:
for panel_name, panel_info in self.panels.items():
if panel_name in _STACK_PANELS:
panel_info["frame"].lower()
def _clear_panel_selection(self: "PQAutomationApp") -> None:
for panel_info in self.panels.values():
_set_tool_panel_sidebar_selected(panel_info, False)
setattr(self, panel_info["visible_attr"], False) setattr(self, panel_info["visible_attr"], False)
# 显示主内容区域
def _restore_main_view_layout(self: "PQAutomationApp") -> None:
"""恢复主界面三区布局,并收起嵌入的 Local Dimming 面板。"""
if getattr(self, "_ld_embedded", False):
self.local_dimming_frame.pack_forget()
self._ld_embedded = False
self.control_frame_top.pack_forget()
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
self.control_frame_top.pack( self.control_frame_top.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
) )
@@ -95,6 +226,14 @@ def hide_all_panels(self: "PQAutomationApp"):
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
) )
def _show_main_content(self: "PQAutomationApp") -> None:
"""显示主内容区并清除当前面板状态。"""
setup_view_stack(self)
_lower_stack_panels(self)
_restore_main_view_layout(self)
self.main_view.lift()
_clear_panel_selection(self)
self.current_panel = None self.current_panel = None
if hasattr(self, "_sync_save_button_state"): if hasattr(self, "_sync_save_button_state"):
@@ -104,12 +243,128 @@ def hide_all_panels(self: "PQAutomationApp"):
pass pass
def _notify_panel_shown(self: "PQAutomationApp", panel_name: str) -> None:
"""面板显示后的轻量刷新(延迟执行,不阻塞切换)。"""
if getattr(self, "_theme_transition", False):
return
if panel_name == "calman":
refresh = getattr(self, "_on_calman_panel_shown", None)
if refresh is not None:
self.root.after_idle(refresh)
return
if panel_name == "ai_image" and getattr(self, "_ai_image_theme_dirty", False):
self.root.after_idle(lambda: self.refresh_ai_image_theme())
return
if panel_name == "log" and hasattr(self, "log_gui") and hasattr(
self.log_gui, "refresh_log_theme"
):
self.root.after_idle(self.log_gui.refresh_log_theme)
def _show_stack_panel(self: "PQAutomationApp", panel_name: str) -> None:
"""叠层模式显示工具面板。"""
setup_view_stack(self)
panel_info = self.panels[panel_name]
_set_tool_panel_sidebar_selected(panel_info, True)
previous = self.current_panel
if previous == "local_dimming":
self.local_dimming_frame.pack_forget()
self._ld_embedded = False
elif previous is not None and previous in _STACK_PANELS:
prev_info = self.panels[previous]
_set_tool_panel_sidebar_selected(prev_info, False)
setattr(self, prev_info["visible_attr"], False)
prev_info["frame"].lower()
self.main_view.lower()
panel_info["frame"].lift()
setattr(self, panel_info["visible_attr"], True)
self.current_panel = panel_name
_notify_panel_shown(self, panel_name)
def _show_local_dimming_panel(self: "PQAutomationApp") -> None:
"""Local Dimming保留顶部配置区下方嵌入 LD 面板。"""
setup_view_stack(self)
panel_info = self.panels["local_dimming"]
if self.current_panel in _STACK_PANELS:
prev_info = self.panels[self.current_panel]
_set_tool_panel_sidebar_selected(prev_info, False)
setattr(self, prev_info["visible_attr"], False)
prev_info["frame"].lower()
_set_tool_panel_sidebar_selected(panel_info, True)
_lower_stack_panels(self)
self.main_view.lift()
self.control_frame_top.pack_forget()
self.control_frame_top.pack(
side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=5
)
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
self.local_dimming_frame.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5
)
self._ld_embedded = True
setattr(self, panel_info["visible_attr"], True)
self.current_panel = "local_dimming"
def _maybe_restore_local_dimming_panel(
self: "PQAutomationApp", closed_panel: str
) -> None:
"""关闭工具面板后Local Dimming 测试类型下自动恢复 LD 面板。"""
if closed_panel == "local_dimming":
return
try:
if (
getattr(self, "config", None)
and getattr(self.config, "current_test_type", None) == "local_dimming"
):
self.show_panel("local_dimming")
except Exception:
pass
def show_panel(self: "PQAutomationApp", panel_name):
"""显示指定面板,隐藏其他所有面板"""
if panel_name not in self.panels:
return
if self.current_panel == panel_name:
_show_main_content(self)
_maybe_restore_local_dimming_panel(self, panel_name)
return
if panel_name == "local_dimming":
_show_local_dimming_panel(self)
return
if panel_name in _STACK_PANELS:
_show_stack_panel(self, panel_name)
return
# 未知面板类型:回退到叠层显示
_show_stack_panel(self, panel_name)
def hide_all_panels(self: "PQAutomationApp"):
"""隐藏所有面板,显示主内容区域"""
_show_main_content(self)
class PanelManagerMixin: class PanelManagerMixin:
"""由 tools/refactor_to_mixins.py 自动生成。 """由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
""" """
create_tool_panel_sidebar_item = create_tool_panel_sidebar_item
register_panel = register_panel register_panel = register_panel
setup_view_stack = setup_view_stack
show_panel = show_panel show_panel = show_panel
hide_all_panels = hide_all_panels hide_all_panels = hide_all_panels
refresh_tool_panel_sidebar_theme = refresh_tool_panel_sidebar_theme

View File

@@ -163,7 +163,7 @@ def _on_tree_motion(self: "PQAutomationApp", event):
def create_ai_image_panel(self: "PQAutomationApp"): def create_ai_image_panel(self: "PQAutomationApp"):
"""创建 AI 图片对话面板,并注册到面板管理。""" """创建 AI 图片对话面板,并注册到面板管理。"""
frame = ttk.Frame(self.content_frame) frame = ttk.Frame(self.view_stack)
self.ai_image_frame = frame self.ai_image_frame = frame
# 内部状态 # 内部状态
@@ -413,9 +413,9 @@ def create_ai_image_panel(self: "PQAutomationApp"):
def toggle_ai_image_panel(self: "PQAutomationApp"): def toggle_ai_image_panel(self: "PQAutomationApp"):
"""切换 AI 图片面板显隐。""" """切换 AI 图片面板显隐。"""
self.show_panel("ai_image") self.show_panel("ai_image")
_apply_ai_image_list_style(self) self.root.after_idle(lambda: _apply_ai_image_list_style(self))
if not getattr(self, "_ai_image_list_loaded", False): if not getattr(self, "_ai_image_list_loaded", False):
_start_new_session(self) self.root.after_idle(lambda: _start_new_session(self))
def _get_app_base_dir(self: "PQAutomationApp") -> str: def _get_app_base_dir(self: "PQAutomationApp") -> str:
@@ -1220,6 +1220,10 @@ def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> s
def refresh_ai_image_theme(self: "PQAutomationApp"): def refresh_ai_image_theme(self: "PQAutomationApp"):
"""刷新 AI 图片面板中的主题相关控件。""" """刷新 AI 图片面板中的主题相关控件。"""
if getattr(self, "current_panel", None) != "ai_image":
self._ai_image_theme_dirty = True
return
self._ai_image_theme_dirty = False
if hasattr(self, "_apply_ai_image_list_style"): if hasattr(self, "_apply_ai_image_list_style"):
self._apply_ai_image_list_style() self._apply_ai_image_list_style()
tip = getattr(self, "_ai_image_tooltip", None) tip = getattr(self, "_ai_image_tooltip", None)

View File

@@ -317,7 +317,7 @@ def _refresh_calman_config_summary(self: "PQAutomationApp") -> None:
def create_calman_panel(self: "PQAutomationApp") -> None: def create_calman_panel(self: "PQAutomationApp") -> None:
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。""" """创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
palette = _get_calman_palette() palette = _get_calman_palette()
self.calman_frame = ttk.Frame(self.content_frame) self.calman_frame = ttk.Frame(self.view_stack)
self.calman_visible = False self.calman_visible = False
self.calman_levels = list(DEFAULT_LEVELS_PCT) self.calman_levels = list(DEFAULT_LEVELS_PCT)
# level_pct -> dict(pct, x, y, Y, X, Z, cct, gamma, de2000, rgb_r, rgb_g, rgb_b, time) # level_pct -> dict(pct, x, y, Y, X, Z, cct, gamma, de2000, rgb_r, rgb_g, rgb_b, time)
@@ -677,22 +677,50 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
_apply_calman_tree_style(self) _apply_calman_tree_style(self)
right.bind("<Configure>", lambda _e: _adaptive_matrix_columns(self)) right.bind("<Configure>", lambda _e: _schedule_adaptive_matrix_columns(self))
_refresh_metric_table(self) self.root.after_idle(lambda: _finish_calman_panel_init(self))
_refresh_calman_config_summary(self)
_update_target_strip(self)
_update_actual_strip(self)
_redraw_calman_charts(self)
# 注册到统一面板管理(按钮稍后由 main_layout 注入) # 注册到统一面板管理(按钮稍后由 main_layout 注入)
self.register_panel("calman", self.calman_frame, None, "calman_visible") self.register_panel("calman", self.calman_frame, None, "calman_visible")
def _finish_calman_panel_init(self: "PQAutomationApp") -> None:
"""延迟初始化 CALMAN 图表与表格,避免阻塞主窗口首屏。"""
_refresh_metric_table(self)
_refresh_calman_config_summary(self)
_update_target_strip(self)
_update_actual_strip(self)
_redraw_calman_charts(self)
self._calman_panel_initialized = True
def _on_calman_panel_shown(self: "PQAutomationApp") -> None:
"""面板切到前台时只做轻量刷新,不重绘 Matplotlib。"""
_refresh_calman_config_summary(self)
_schedule_adaptive_matrix_columns(self)
if getattr(self, "_calman_theme_dirty", False):
_redraw_calman_charts(self)
self._calman_theme_dirty = False
def _schedule_adaptive_matrix_columns(self: "PQAutomationApp") -> None:
if getattr(self, "_theme_transition", False):
return
job = getattr(self, "_calman_matrix_col_job", None)
if job is not None:
try:
self.root.after_cancel(job)
except Exception:
pass
self._calman_matrix_col_job = self.root.after_idle(
lambda: _adaptive_matrix_columns(self)
)
def toggle_calman_panel(self: "PQAutomationApp") -> None: def toggle_calman_panel(self: "PQAutomationApp") -> None:
"""切换 CALMAN 灰阶面板显示。""" """切换 CALMAN 灰阶面板显示。"""
self.show_panel("calman") self.show_panel("calman")
_refresh_calman_config_summary(self)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1093,7 +1121,7 @@ def _adaptive_matrix_columns(self: "PQAutomationApp") -> None:
self.calman_data_tree.column(str(p), width=width, minwidth=min_w, stretch=False) self.calman_data_tree.column(str(p), width=width, minwidth=min_w, stretch=False)
def _redraw_calman_charts(self: "PQAutomationApp") -> None: def _redraw_calman_charts(self: "PQAutomationApp", *, sync_draw: bool = False) -> None:
"""根据 calman_results 重绘四张图和 xy 散点。""" """根据 calman_results 重绘四张图和 xy 散点。"""
palette = _get_calman_palette() palette = _get_calman_palette()
if hasattr(self, "calman_fig"): if hasattr(self, "calman_fig"):
@@ -1213,12 +1241,15 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
a4.set_ylim(1.8, 2.8) a4.set_ylim(1.8, 2.8)
a4.set_xlabel("", fontsize=8) a4.set_xlabel("", fontsize=8)
self.calman_canvas.draw_idle() if sync_draw or getattr(self, "_theme_transition", False):
self.calman_canvas.draw()
else:
self.calman_canvas.draw_idle()
_redraw_xy_chart(self) _redraw_xy_chart(self, sync_draw=sync_draw)
def _redraw_xy_chart(self: "PQAutomationApp") -> None: def _redraw_xy_chart(self: "PQAutomationApp", *, sync_draw: bool = False) -> None:
palette = _get_calman_palette() palette = _get_calman_palette()
if hasattr(self, "calman_xy_fig"): if hasattr(self, "calman_xy_fig"):
self.calman_xy_fig.patch.set_facecolor(palette["figure_bg"]) self.calman_xy_fig.patch.set_facecolor(palette["figure_bg"])
@@ -1245,7 +1276,10 @@ def _redraw_xy_chart(self: "PQAutomationApp") -> None:
last = recs[-1] last = recs[-1]
ax.plot([last["x"]], [last["y"]], marker="o", color="#ffcc00", markersize=5) ax.plot([last["x"]], [last["y"]], marker="o", color="#ffcc00", markersize=5)
ax.plot([last["x"], D65_X], [last["y"], D65_Y], color=_mix(palette["fg"], palette["axes_bg"], 0.4), linewidth=0.8) ax.plot([last["x"], D65_X], [last["y"], D65_Y], color=_mix(palette["fg"], palette["axes_bg"], 0.4), linewidth=0.8)
self.calman_xy_canvas.draw_idle() if sync_draw or getattr(self, "_theme_transition", False):
self.calman_xy_canvas.draw()
else:
self.calman_xy_canvas.draw_idle()
def _update_actual_strip(self: "PQAutomationApp") -> None: def _update_actual_strip(self: "PQAutomationApp") -> None:
@@ -1337,11 +1371,18 @@ def _refresh_metric_table(self: "PQAutomationApp") -> None:
self.calman_table_ysb.set(first, last) self.calman_table_ysb.set(first, last)
def refresh_calman_theme(self: "PQAutomationApp") -> None: def refresh_calman_theme(self: "PQAutomationApp", *, redraw_charts: bool | None = None) -> None:
"""主题切换后刷新 Calman 灰阶调试面板的表格与图表颜色。""" """主题切换后刷新 Calman 灰阶调试面板的表格与图表颜色。"""
if not hasattr(self, "calman_frame"): if not hasattr(self, "calman_frame"):
return return
if getattr(self, "current_panel", None) != "calman":
self._calman_theme_dirty = True
return
if redraw_charts is None:
redraw_charts = True
palette = _get_calman_palette() palette = _get_calman_palette()
if hasattr(self, "calman_elapsed_label"): if hasattr(self, "calman_elapsed_label"):
@@ -1365,7 +1406,11 @@ def refresh_calman_theme(self: "PQAutomationApp") -> None:
_refresh_metric_table(self) _refresh_metric_table(self)
_refresh_calman_config_summary(self) _refresh_calman_config_summary(self)
_redraw_calman_charts(self) if redraw_charts:
_redraw_calman_charts(self, sync_draw=getattr(self, "_theme_transition", False))
self._calman_theme_dirty = False
else:
self._calman_theme_dirty = True
class CalmanPanelMixin: class CalmanPanelMixin:
@@ -1374,3 +1419,4 @@ class CalmanPanelMixin:
create_calman_panel = create_calman_panel create_calman_panel = create_calman_panel
toggle_calman_panel = toggle_calman_panel toggle_calman_panel = toggle_calman_panel
refresh_calman_theme = refresh_calman_theme refresh_calman_theme = refresh_calman_theme
_on_calman_panel_shown = _on_calman_panel_shown

View File

@@ -172,6 +172,14 @@ def _apply_custom_result_theme(self: "PQAutomationApp"):
def refresh_custom_template_theme(self: "PQAutomationApp"): def refresh_custom_template_theme(self: "PQAutomationApp"):
"""刷新客户模板结果表的主题色。""" """刷新客户模板结果表的主题色。"""
notebook = getattr(self, "chart_notebook", None)
tab_frame = getattr(self, "custom_template_tab_frame", None)
if notebook is not None and tab_frame is not None:
try:
if str(tab_frame) not in notebook.tabs():
return
except Exception:
pass
_apply_custom_result_theme(self) _apply_custom_result_theme(self)

View File

@@ -148,7 +148,7 @@ _GENERATORS = {
def create_gamma_pattern_panel(self: "PQAutomationApp"): def create_gamma_pattern_panel(self: "PQAutomationApp"):
"""创建灰阶 Pattern 配置面板。""" """创建灰阶 Pattern 配置面板。"""
frame = ttk.Frame(self.content_frame) frame = ttk.Frame(self.view_stack)
self.gamma_pattern_frame = frame self.gamma_pattern_frame = frame
self.gamma_pattern_visible = False self.gamma_pattern_visible = False

View File

@@ -171,8 +171,32 @@ def create_test_items_content(self: "PQAutomationApp"):
}, },
} }
# 根据当前测试类型创建复选框 # 预创建各测试类型复选框,切换时只改显隐与勾选状态,避免重复构建控件。
self.test_vars = {} self.test_vars = {}
toggle_bootstyle = "success-round-toggle"
for type_key, config in self.test_items.items():
frame = config["frame"]
type_label = ttk.Label(
frame,
text=self.get_test_type_name(type_key),
style="primary.TLabel",
)
type_label.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=5, pady=3)
config["type_label"] = type_label
checkbox_vars: dict[str, tk.BooleanVar] = {}
for i, (text, var_name) in enumerate(config["items"]):
var = tk.BooleanVar(value=False)
ttk.Checkbutton(
frame,
text=text,
variable=var,
bootstyle=toggle_bootstyle,
command=self.update_config_and_tabs,
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
checkbox_vars[var_name] = var
config["checkbox_vars"] = checkbox_vars
self.update_test_items() self.update_test_items()
# 创建色度参数设置框架 # 创建色度参数设置框架
@@ -774,21 +798,19 @@ def create_test_type_frame(self: "PQAutomationApp"):
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w") ).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
panel_buttons = [ panel_buttons = [
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel), ("ai_image_btn", "ai_image", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel), ("pantone_baseline_btn", "pantone_baseline", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel), ("gamma_pattern_btn", "gamma_pattern", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel), ("calman_btn", "calman", "CALMAN 灰阶", self.toggle_calman_panel),
] ]
for attr, text, cmd in panel_buttons: for attr, panel_name, text, cmd in panel_buttons:
btn = ttk.Button( item = self.create_tool_panel_sidebar_item(self.sidebar_frame, text, cmd)
self.sidebar_frame, item["pack"]()
text=text, setattr(self, attr, item["row"])
style="Sidebar.TButton", if hasattr(self, "panels") and panel_name in self.panels:
command=cmd, self.panels[panel_name]["button_label"] = text
takefocus=False, self.panels[panel_name]["sidebar_item"] = item
) self.panels[panel_name]["button"] = item["row"]
btn.pack(fill=tk.X, padx=0, pady=1)
setattr(self, attr, btn)
# 测试版水印标签(版本 x.x.0.0 时显示) # 测试版水印标签(版本 x.x.0.0 时显示)
from app_version import is_beta_version, APP_VERSION from app_version import is_beta_version, APP_VERSION
@@ -822,7 +844,7 @@ def create_test_type_frame(self: "PQAutomationApp"):
self.theme_toggle_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM) self.theme_toggle_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM)
_refresh_theme_toggle_label(self) _refresh_theme_toggle_label(self)
# 注册面板按钮 # 注册面板侧栏按钮
if hasattr(self, "panels"): if hasattr(self, "panels"):
if "ai_image" in self.panels: if "ai_image" in self.panels:
self.panels["ai_image"]["button"] = self.ai_image_btn self.panels["ai_image"]["button"] = self.ai_image_btn
@@ -847,9 +869,113 @@ def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None:
self.theme_toggle_btn.configure(text="切换深色模式") self.theme_toggle_btn.configure(text="切换深色模式")
def _replot_charts_for_current_theme(self: "PQAutomationApp") -> None:
"""刷新结果区图表配色(仅同步绘制当前可见 Tab"""
if hasattr(self, "refresh_result_charts_theme"):
try:
self.refresh_result_charts_theme(sync_draw=True, visible_only=True)
return
except Exception:
pass
test_type = None
if hasattr(self, "config"):
test_type = getattr(self.config, "current_test_type", None)
snapshots = {}
if test_type and hasattr(self, "_chart_snapshots"):
snapshots = self._chart_snapshots.get(test_type) or {}
if not snapshots:
if hasattr(self, "apply_result_chart_theme"):
self.apply_result_chart_theme(sync_draw=True)
return
for chart_name, args in snapshots.items():
plot_fn = getattr(self, f"plot_{chart_name}", None)
if plot_fn:
try:
plot_fn(*args)
except Exception:
pass
def _cancel_pending_ui_jobs(self: "PQAutomationApp") -> None:
"""取消可能在主题切换后继续触发的延迟 UI 任务。"""
for attr in ("_calman_matrix_col_job", "_chart_restore_job"):
job = getattr(self, attr, None)
if job is None:
continue
try:
self.root.after_cancel(job)
except Exception:
pass
setattr(self, attr, None)
def _refresh_all_theme_widgets(self: "PQAutomationApp") -> None:
"""同步刷新主题相关控件:优先处理可见区域,隐藏面板延后。"""
_refresh_theme_toggle_label(self)
# 侧栏 / 连接指示 / 选中态 —— 始终可见,必须立即更新
for method_name in (
"refresh_tool_panel_sidebar_theme",
"refresh_connection_indicators",
):
method = getattr(self, method_name, None)
if method is not None:
try:
method()
except Exception:
pass
current_panel = getattr(self, "current_panel", None)
if current_panel == "ai_image" and hasattr(self, "refresh_ai_image_theme"):
try:
self.refresh_ai_image_theme()
except Exception:
pass
else:
self._ai_image_theme_dirty = True
if current_panel == "single_step" and hasattr(self, "refresh_single_step_theme"):
try:
self.refresh_single_step_theme()
except Exception:
pass
if current_panel == "log" and hasattr(self, "log_gui") and hasattr(
self.log_gui, "refresh_log_theme"
):
try:
self.log_gui.refresh_log_theme()
except Exception:
pass
if hasattr(self, "refresh_custom_template_theme"):
try:
self.refresh_custom_template_theme()
except Exception:
pass
if hasattr(self, "refresh_calman_theme"):
try:
self.refresh_calman_theme()
except Exception:
pass
if hasattr(self, "update_sidebar_selection"):
try:
self.update_sidebar_selection()
except Exception:
pass
_replot_charts_for_current_theme(self)
def _on_toggle_theme(self: "PQAutomationApp") -> None: def _on_toggle_theme(self: "PQAutomationApp") -> None:
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签""" """切换主题:保持窗口可见,同步批量刷新避免卡顿与分批变色"""
# 在测试进行时禁止切换主题,避免影响测量稳定性
if getattr(self, "testing", False): if getattr(self, "testing", False):
try: try:
if hasattr(self, "log_gui"): if hasattr(self, "log_gui"):
@@ -859,62 +985,14 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
return return
from app.views.theme_manager import toggle_theme from app.views.theme_manager import toggle_theme
toggle_theme()
# apply_modern_styles() _cancel_pending_ui_jobs(self)
_refresh_theme_toggle_label(self) self._theme_transition = True
if hasattr(self, "refresh_connection_indicators"): try:
try: toggle_theme()
self.refresh_connection_indicators() _refresh_all_theme_widgets(self)
except Exception: finally:
pass self._theme_transition = False
if hasattr(self, "apply_result_chart_theme"):
try:
self.apply_result_chart_theme()
except Exception:
pass
if hasattr(self, "log_gui") and hasattr(self.log_gui, "refresh_log_theme"):
try:
self.log_gui.refresh_log_theme()
except Exception:
pass
if hasattr(self, "refresh_ai_image_theme"):
try:
self.refresh_ai_image_theme()
except Exception:
pass
if hasattr(self, "refresh_single_step_theme"):
try:
self.refresh_single_step_theme()
except Exception:
pass
if hasattr(self, "refresh_custom_template_theme"):
try:
self.refresh_custom_template_theme()
except Exception:
pass
if hasattr(self, "refresh_calman_theme"):
try:
self.refresh_calman_theme()
except Exception:
pass
# 同步刷新侧栏选中态(高亮样式跟随新色板)
if hasattr(self, "update_sidebar_selection"):
try:
self.update_sidebar_selection()
except Exception:
pass
# 以新的 dark_mode 值重绘当前测试类型的所有图表
if hasattr(self, "_chart_snapshots") and hasattr(self, "config"):
test_type = getattr(self.config, "current_test_type", None)
if test_type:
snapshots = self._chart_snapshots.get(test_type, {})
for chart_name, args in snapshots.items():
plot_fn = getattr(self, f"plot_{chart_name}", None)
if plot_fn:
try:
plot_fn(*args)
except Exception:
pass
def update_config_info_display(self: "PQAutomationApp"): def update_config_info_display(self: "PQAutomationApp"):
@@ -1240,33 +1318,17 @@ def update_test_items(self: "PQAutomationApp"):
frame = config["frame"] frame = config["frame"]
frame.pack(fill=tk.X, padx=5, pady=5) frame.pack(fill=tk.X, padx=5, pady=5)
# 添加测试类型标签 config["type_label"].configure(
type_label = ttk.Label(
frame,
text=self.get_test_type_name(current_test_type), text=self.get_test_type_name(current_test_type),
style="primary.TLabel",
) )
type_label.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=5, pady=3)
# 从配置中读取保存的选择状态
saved_test_items = self.config.current_test_types[current_test_type].get( saved_test_items = self.config.current_test_types[current_test_type].get(
"test_items", [] "test_items", []
) )
# 添加复选框 for var_name, var in config["checkbox_vars"].items():
toggle_bootstyle = "success-round-toggle" var.set(var_name in saved_test_items)
for i, (text, var_name) in enumerate(config["items"]):
is_checked = var_name in saved_test_items
var = tk.BooleanVar(value=is_checked)
self.test_vars[f"{current_test_type}_{var_name}"] = var self.test_vars[f"{current_test_type}_{var_name}"] = var
ttk.Checkbutton(
frame,
text=text,
variable=var,
bootstyle=toggle_bootstyle,
command=self.update_config_and_tabs,
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
if hasattr(self, "chart_notebook"): if hasattr(self, "chart_notebook"):
self.update_chart_tabs_state() self.update_chart_tabs_state()

View File

@@ -23,7 +23,7 @@ _TEMPLATE_FILE = "pantone_2670_colors.xlsx"
def create_pantone_baseline_panel(self: "PQAutomationApp"): def create_pantone_baseline_panel(self: "PQAutomationApp"):
"""创建 Pantone 认证摸底测试面板。""" """创建 Pantone 认证摸底测试面板。"""
frame = ttk.Frame(self.content_frame) frame = ttk.Frame(self.view_stack)
self.pantone_baseline_frame = frame self.pantone_baseline_frame = frame
self.pantone_baseline_visible = False self.pantone_baseline_visible = False
self.pantone_patterns = [] self.pantone_patterns = []

View File

@@ -15,7 +15,7 @@ if TYPE_CHECKING:
def create_log_panel(self: "PQAutomationApp"): def create_log_panel(self: "PQAutomationApp"):
"""创建日志面板""" """创建日志面板"""
self.log_frame = ttk.Frame(self.content_frame) self.log_frame = ttk.Frame(self.view_stack)
self.log_gui = PQLogGUI(self.log_frame) self.log_gui = PQLogGUI(self.log_frame)
self.log_gui.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) self.log_gui.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
@@ -30,7 +30,7 @@ def create_log_panel(self: "PQAutomationApp"):
def create_local_dimming_panel(self: "PQAutomationApp"): def create_local_dimming_panel(self: "PQAutomationApp"):
"""创建 Local Dimming 测试面板。""" """创建 Local Dimming 测试面板。"""
self.local_dimming_frame = ttk.Frame(self.content_frame) self.local_dimming_frame = ttk.Frame(self.main_view)
# 主容器 # 主容器
main_container = ttk.Frame(self.local_dimming_frame, padding=10) main_container = ttk.Frame(self.local_dimming_frame, padding=10)

View File

@@ -3,6 +3,8 @@ PQ 单步调试面板
支持屏模组、SDR、HDR 三种测试类型的单步调试功能 支持屏模组、SDR、HDR 三种测试类型的单步调试功能
""" """
import copy
import ttkbootstrap as ttk import ttkbootstrap as ttk
import tkinter as tk import tkinter as tk
from tkinter import messagebox from tkinter import messagebox
@@ -840,8 +842,12 @@ class PQDebugPanel:
# ==================== 构建对比数据 ==================== # ==================== 构建对比数据 ====================
comparison = {} comparison = {}
if test_item == "gamma": if test_item in ("gamma", "eotf"):
# ========== Gamma 测试:只对比 x, y, lv ========== result_key = "gamma" if test_item == "gamma" else "eotf"
original_gamma = self._get_gamma_from_results(result_key, index)
new_gamma = self._calculate_gamma_at_index(
test_type, test_item, index, new_data
)
comparison["x"] = ( comparison["x"] = (
original_data[0], original_data[0],
new_data[0], new_data[0],
@@ -857,23 +863,10 @@ class PQDebugPanel:
new_data[2], new_data[2],
new_data[2] - original_data[2], new_data[2] - original_data[2],
) )
comparison["γ"] = (
elif test_item == "eotf": original_gamma,
# ========== EOTF 测试:只对比 x, y, lv ========== new_gamma,
comparison["x"] = ( new_gamma - original_gamma,
original_data[0],
new_data[0],
new_data[0] - original_data[0],
)
comparison["y"] = (
original_data[1],
new_data[1],
new_data[1] - original_data[1],
)
comparison["lv"] = (
original_data[2],
new_data[2],
new_data[2] - original_data[2],
) )
elif test_item == "accuracy": elif test_item == "accuracy":
@@ -983,6 +976,16 @@ class PQDebugPanel:
diff_str = f"{diff:+.2f}" diff_str = f"{diff:+.2f}"
threshold = 0.5 threshold = 0.5
elif key == "γ":
original_str = self._format_gamma_value(original)
new_str = self._format_gamma_value(new)
if original_str == "--" or new_str == "--":
diff_str = "--"
threshold = None
else:
diff_str = f"{diff:+.2f}"
threshold = 0.05
else: # x, y else: # x, y
# x, y 使用 4 位小数 # x, y 使用 4 位小数
original_str = f"{original:.4f}" original_str = f"{original:.4f}"
@@ -991,7 +994,9 @@ class PQDebugPanel:
threshold = 0.001 threshold = 0.001
# 判断差异 # 判断差异
if abs(diff) > threshold: if threshold is None:
tag = "normal"
elif abs(diff) > threshold:
tag = "warning" tag = "warning"
else: else:
tag = "normal" tag = "normal"
@@ -1114,6 +1119,79 @@ class PQDebugPanel:
"100% Yellow", "100% Yellow",
] ]
def _format_gamma_value(self, gamma):
"""格式化 Gamma 值,无效时显示 --"""
if gamma is None or gamma < 0.5 or gamma > 5.0:
return "--"
return f"{gamma:.2f}"
def _get_gamma_calc_params(self, results):
"""获取 Gamma 计算参数(与批量测试保持一致)"""
config_max_value = self.app.config.current_pattern.get(
"measurement_max_value", 10
)
try:
max_index_fix = int(config_max_value)
except (ValueError, TypeError):
max_index_fix = 10
actual_max_index = len(results) - 1
if max_index_fix > actual_max_index:
max_index_fix = actual_max_index
pattern_params = self.app.config.default_pattern_gray.get(
"pattern_params", None
)
return max_index_fix, pattern_params
def _get_gamma_from_results(self, result_key, index):
"""从原始测试结果中读取指定灰阶的 Gamma 值"""
try:
if result_key not in self.app.results.test_items:
return 0.0
result_data = self.app.results.test_items[result_key].final_result
if not result_data:
return 0.0
gamma_list = result_data.get(result_key, [])
if index >= len(gamma_list):
return 0.0
item = gamma_list[index]
return item[3] if len(item) > 3 else 0.0
except Exception as e:
self.app.log_gui.log(f"读取 Gamma 失败: {str(e)}", level="error")
return 0.0
def _calculate_gamma_at_index(self, test_type, test_item, index, new_data):
"""用单步测量更新对应灰阶后,重新计算该点的 Gamma 值"""
try:
key = f"{test_type}_{test_item}"
results = copy.deepcopy(self.original_data[key])
updated = list(results[index])
updated[0] = new_data[0]
updated[1] = new_data[1]
updated[2] = new_data[2]
for i in range(3, min(len(updated), len(new_data))):
updated[i] = new_data[i]
results[index] = updated
max_index_fix, pattern_params = self._get_gamma_calc_params(results)
results_with_gamma, _ = self.app.calculate_gamma(
results, max_index_fix, pattern_params
)
if index >= len(results_with_gamma):
return 0.0
return results_with_gamma[index][3]
except Exception as e:
self.app.log_gui.log(f"计算 Gamma 失败: {str(e)}", level="error")
return 0.0
def _get_delta_e_from_results(self, test_type, color_name): def _get_delta_e_from_results(self, test_type, color_name):
"""从原始测试结果中读取 ΔE 值""" """从原始测试结果中读取 ΔE 值"""
try: try:

View File

@@ -6,7 +6,6 @@ import time
import os import os
import datetime import datetime
import traceback import traceback
import matplotlib
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from app_version import APP_NAME, APP_VERSION, get_app_title from app_version import APP_NAME, APP_VERSION, get_app_title
from app.ucd import ( from app.ucd import (
@@ -61,7 +60,7 @@ from app.plots.plot_gamut import PlotGamutMixin
from app.views.chart_frame import ChartFrameMixin from app.views.chart_frame import ChartFrameMixin
from app.config_io import ConfigIOMixin from app.config_io import ConfigIOMixin
from app.tests.local_dimming import LocalDimmingMixin from app.tests.local_dimming import LocalDimmingMixin
from app.device.connection import DeviceConnectionMixin from app.device.connection import DeviceConnectionMixin, ConnectionController
from app.runner.test_runner import TestRunnerMixin from app.runner.test_runner import TestRunnerMixin
plt.rcParams["font.family"] = ["sans-serif"] plt.rcParams["font.family"] = ["sans-serif"]
@@ -123,13 +122,12 @@ class PQAutomationApp(
# 连接控制器:统一管理 CA/UCD 生命周期。 # 连接控制器:统一管理 CA/UCD 生命周期。
# 旧的 check_com_connections / disconnect_com_connections 等模块级 # 旧的 check_com_connections / disconnect_com_connections 等模块级
# 函数仍以类属性形式挂在 PQAutomationApp 上,内部全部委托给本对象。 # 函数仍以类属性形式挂在 PQAutomationApp 上,内部全部委托给本对象。
from app.device.connection import ConnectionController
self.connection = ConnectionController(self) self.connection = ConnectionController(self)
# 初始化测试状态 # 初始化测试状态
self.testing = False self.testing = False
self.test_thread = None self.test_thread = None
# 采集节奏参数:默认在稳定性与速度之间取平衡,可按现场情况再微调 # 采集节奏参数由 settings/pq_config.json → acquisition_timing 加载(见 load_pq_config
self.pattern_settle_time = 0.4 self.pattern_settle_time = 0.4
self.pattern_progress_log_step = 5 self.pattern_progress_log_step = 5
@@ -158,6 +156,7 @@ class PQAutomationApp(
self.current_panel = None # 当前显示的面板名称 self.current_panel = None # 当前显示的面板名称
self.panels = {} # 存储所有面板的信息 self.panels = {} # 存储所有面板的信息
self.log_visible = False self.log_visible = False
self._theme_transition = False
# 创建左侧面板 # 创建左侧面板
self.left_frame = ttk.Frame(self.main_frame, width=208) self.left_frame = ttk.Frame(self.main_frame, width=208)
@@ -175,18 +174,25 @@ class PQAutomationApp(
side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5 side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5
) )
# 叠层容器:主界面与工具面板同尺寸叠放,切换时用 lift 而非 pack_forget。
self.view_stack = ttk.Frame(self.content_frame)
self.view_stack.pack(fill=tk.BOTH, expand=True)
self.main_view = ttk.Frame(self.view_stack)
self._ld_embedded = False
self._view_stack_ready = False
# 创建右侧内容区域的上中下三个分区 # 创建右侧内容区域的上中下三个分区
self.control_frame_top = ttk.Frame(self.content_frame) self.control_frame_top = ttk.Frame(self.main_view)
self.control_frame_top.pack( self.control_frame_top.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 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 = ttk.Frame(self.main_view)
self.control_frame_middle.pack( self.control_frame_middle.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 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 = ttk.Frame(self.main_view)
self.control_frame_bottom.pack( self.control_frame_bottom.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
) )
@@ -224,6 +230,7 @@ class PQAutomationApp(
self.create_result_chart_frame() self.create_result_chart_frame()
# 创建客户模板结果显示区域(黑底表格) # 创建客户模板结果显示区域(黑底表格)
self.create_custom_template_result_panel() self.create_custom_template_result_panel()
self.setup_view_stack()
# 在所有控件创建完成后,统一初始化测试类型 # 在所有控件创建完成后,统一初始化测试类型
self.root.after(100, self.initialize_default_test_type) self.root.after(100, self.initialize_default_test_type)
@@ -408,17 +415,6 @@ class PQAutomationApp(
self.signal_tabs.tab(i, state="normal") self.signal_tabs.tab(i, state="normal")
self.signal_tabs.select(target_tab) 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()
elif target_tab == 3:
self.local_dimming_signal_frame.tkraise()
for i in range(4): for i in range(4):
if i != target_tab: if i != target_tab:
@@ -478,7 +474,19 @@ class PQAutomationApp(
self._switch_signal_format_tabs(test_type) self._switch_signal_format_tabs(test_type)
self._sync_custom_template_tab_visibility(test_type) self._sync_custom_template_tab_visibility(test_type)
self.sync_gamut_toolbar() self.sync_gamut_toolbar()
self._restore_charts_for_type(test_type) self._schedule_chart_restore(test_type)
def _schedule_chart_restore(self, test_type: str) -> None:
"""延迟恢复图表,避免阻塞测试类型切换时的 UI 响应。"""
job = getattr(self, "_chart_restore_job", None)
if job is not None:
try:
self.root.after_cancel(job)
except Exception:
pass
self._chart_restore_job = self.root.after_idle(
lambda tt=test_type: self._restore_charts_for_type(tt)
)
def _save_chart_snapshot(self, test_type: str, chart_name: str, args: tuple): def _save_chart_snapshot(self, test_type: str, chart_name: str, args: tuple):
"""保存某次绘图的参数,以便切换测试类型时可以重绘。""" """保存某次绘图的参数,以便切换测试类型时可以重绘。"""

View File

@@ -1,5 +1,5 @@
{ {
"current_test_type": "sdr_movie", "current_test_type": "local_dimming",
"test_types": { "test_types": {
"screen_module": { "screen_module": {
"name": "屏模组性能测试", "name": "屏模组性能测试",
@@ -10,10 +10,10 @@
"contrast" "contrast"
], ],
"timing": "DMT 1920x 1080 @ 60Hz", "timing": "DMT 1920x 1080 @ 60Hz",
"data_range": "Full", "data_range": "Limited",
"color_format": "RGB", "color_format": "RGB",
"bpc": 8, "bpc": 8,
"colorimetry": "sRGB", "colorimetry": "DCI-P3",
"patterns": { "patterns": {
"gamut": "rgb", "gamut": "rgb",
"gamma": "gray", "gamma": "gray",
@@ -105,5 +105,9 @@
"measurement_bit_depth": 8, "measurement_bit_depth": 8,
"measurement_max_value": 0, "measurement_max_value": 0,
"pattern_params": [] "pattern_params": []
},
"acquisition_timing": {
"pattern_settle_time": 0.4,
"pattern_progress_log_step": 5
} }
} }