591 lines
20 KiB
Python
591 lines
20 KiB
Python
"""Pantone 认证摸底测试面板。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import datetime
|
||
import os
|
||
import sys
|
||
import threading
|
||
import tkinter as tk
|
||
from tkinter import filedialog, messagebox
|
||
|
||
import ttkbootstrap as ttk
|
||
|
||
from typing import TYPE_CHECKING
|
||
|
||
if TYPE_CHECKING:
|
||
from pqAutomationApp import PQAutomationApp
|
||
|
||
|
||
|
||
_TEMPLATE_FILE = "pantone_2670_colors.xlsx"
|
||
|
||
|
||
def create_pantone_baseline_panel(self: "PQAutomationApp"):
|
||
"""创建 Pantone 认证摸底测试面板。"""
|
||
frame = ttk.Frame(self.content_frame)
|
||
self.pantone_baseline_frame = frame
|
||
self.pantone_baseline_visible = False
|
||
self.pantone_patterns = []
|
||
self.pantone_results = []
|
||
self._pantone_control_event = None
|
||
self._pantone_running = False
|
||
self._pantone_paused = False
|
||
self._pantone_pause_requested = False
|
||
self._pantone_stop_requested = False
|
||
self._pantone_next_index = 0
|
||
self._pantone_target_count = 0
|
||
|
||
root = ttk.Frame(frame, padding=10)
|
||
root.pack(fill=tk.BOTH, expand=True)
|
||
|
||
title_row = ttk.Frame(root)
|
||
title_row.pack(fill=tk.X, pady=(0, 8))
|
||
ttk.Label(
|
||
title_row,
|
||
text="Pantone认证摸底测试",
|
||
font=("微软雅黑", 14, "bold"),
|
||
).pack(side=tk.LEFT)
|
||
|
||
self.pantone_status_var = tk.StringVar(value="未开始")
|
||
self.pantone_progress_var = tk.StringVar(value="0 / 0")
|
||
self.pantone_settle_var = tk.StringVar(value="0.3")
|
||
|
||
config_row = ttk.LabelFrame(root, text="任务配置", padding=8)
|
||
config_row.pack(fill=tk.X)
|
||
ttk.Label(config_row, text=f"Pattern来源: settings/{_TEMPLATE_FILE}").pack(
|
||
side=tk.LEFT
|
||
)
|
||
ttk.Label(config_row, text="稳定等待(s):").pack(side=tk.LEFT, padx=(14, 4))
|
||
ttk.Entry(config_row, textvariable=self.pantone_settle_var, width=8).pack(
|
||
side=tk.LEFT
|
||
)
|
||
ttk.Label(config_row, textvariable=self.pantone_progress_var).pack(
|
||
side=tk.RIGHT, padx=(8, 0)
|
||
)
|
||
ttk.Label(config_row, textvariable=self.pantone_status_var, style="Muted.TLabel").pack(
|
||
side=tk.RIGHT
|
||
)
|
||
|
||
btn_row = ttk.Frame(root)
|
||
btn_row.pack(fill=tk.X, pady=(8, 8))
|
||
self.pantone_start_btn = ttk.Button(
|
||
btn_row,
|
||
text="开始",
|
||
bootstyle="primary",
|
||
command=lambda: _start_pantone_baseline(self),
|
||
)
|
||
self.pantone_start_btn.pack(side=tk.LEFT, padx=(0, 6))
|
||
self.pantone_pause_btn = ttk.Button(
|
||
btn_row,
|
||
text="暂停",
|
||
bootstyle="warning-outline",
|
||
command=lambda: _pause_pantone_baseline(self),
|
||
state=tk.DISABLED,
|
||
)
|
||
self.pantone_pause_btn.pack(side=tk.LEFT, padx=(0, 6))
|
||
self.pantone_resume_btn = ttk.Button(
|
||
btn_row,
|
||
text="继续",
|
||
bootstyle="info-outline",
|
||
command=lambda: _resume_pantone_baseline(self),
|
||
state=tk.DISABLED,
|
||
)
|
||
self.pantone_resume_btn.pack(side=tk.LEFT, padx=(0, 6))
|
||
self.pantone_end_btn = ttk.Button(
|
||
btn_row,
|
||
text="结束",
|
||
bootstyle="danger-outline",
|
||
command=lambda: _end_pantone_baseline(self),
|
||
state=tk.DISABLED,
|
||
)
|
||
self.pantone_end_btn.pack(side=tk.LEFT, padx=(0, 6))
|
||
ttk.Button(
|
||
btn_row,
|
||
text="清空结果",
|
||
bootstyle="secondary-outline",
|
||
command=lambda: _clear_results(self),
|
||
).pack(side=tk.LEFT, padx=(0, 6))
|
||
ttk.Button(
|
||
btn_row,
|
||
text="另存为模板",
|
||
bootstyle="success",
|
||
command=lambda: _save_as_template(self),
|
||
).pack(side=tk.LEFT)
|
||
|
||
table_frame = ttk.LabelFrame(root, text="测试结果(R,G,B,L,x,y)", padding=8)
|
||
table_frame.pack(fill=tk.BOTH, expand=True)
|
||
columns = ("idx", "r", "g", "b", "l", "x", "y", "time")
|
||
self.pantone_tree = ttk.Treeview(
|
||
table_frame,
|
||
columns=columns,
|
||
show="headings",
|
||
height=18,
|
||
)
|
||
headings = {
|
||
"idx": "序号",
|
||
"r": "R",
|
||
"g": "G",
|
||
"b": "B",
|
||
"l": "L",
|
||
"x": "x",
|
||
"y": "y",
|
||
"time": "时间",
|
||
}
|
||
widths = {
|
||
"idx": 70,
|
||
"r": 70,
|
||
"g": 70,
|
||
"b": 70,
|
||
"l": 90,
|
||
"x": 90,
|
||
"y": 90,
|
||
"time": 110,
|
||
}
|
||
for col in columns:
|
||
self.pantone_tree.heading(col, text=headings[col])
|
||
self.pantone_tree.column(col, width=widths[col], anchor=tk.CENTER)
|
||
self.pantone_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.pantone_tree.yview)
|
||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.pantone_tree.configure(yscrollcommand=scrollbar.set)
|
||
|
||
self.register_panel("pantone_baseline", frame, None, "pantone_baseline_visible")
|
||
_set_button_states(self)
|
||
|
||
|
||
def toggle_pantone_baseline_panel(self: "PQAutomationApp"):
|
||
"""切换 Pantone 认证摸底测试面板。"""
|
||
self.show_panel("pantone_baseline")
|
||
|
||
|
||
def _get_settings_dir(self: "PQAutomationApp"):
|
||
"""返回 settings 绝对目录,避免依赖当前工作目录。"""
|
||
if getattr(self, "config_file", None):
|
||
return os.path.dirname(self.config_file)
|
||
|
||
if getattr(sys, "frozen", False):
|
||
base_dir = os.path.dirname(sys.executable)
|
||
else:
|
||
base_dir = os.path.dirname(
|
||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
)
|
||
return os.path.join(base_dir, "settings")
|
||
|
||
|
||
def _load_patterns(self: "PQAutomationApp"):
|
||
path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
|
||
if not os.path.isfile(path):
|
||
raise FileNotFoundError(f"未找到模板文件: {path}")
|
||
|
||
from openpyxl import load_workbook
|
||
|
||
patterns = []
|
||
wb = load_workbook(path, read_only=True, data_only=True)
|
||
ws = wb.active
|
||
try:
|
||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||
if not row:
|
||
continue
|
||
try:
|
||
r = int(row[0]) if row[0] is not None else None
|
||
g = int(row[1]) if len(row) > 1 and row[1] is not None else None
|
||
b = int(row[2]) if len(row) > 2 and row[2] is not None else None
|
||
except Exception:
|
||
continue
|
||
if r is None or g is None or b is None:
|
||
continue
|
||
if min(r, g, b) < 0 or max(r, g, b) > 255:
|
||
continue
|
||
patterns.append((r, g, b))
|
||
finally:
|
||
wb.close()
|
||
|
||
if not patterns:
|
||
raise RuntimeError("模板中未找到有效 RGB 列表(需包含 R/G/B 三列)")
|
||
return patterns
|
||
|
||
|
||
def _start_pantone_baseline(self: "PQAutomationApp"):
|
||
if self._pantone_running:
|
||
messagebox.showinfo("提示", "Pantone 任务正在执行")
|
||
return
|
||
if not self.signal_service.is_connected:
|
||
messagebox.showwarning("警告", "请先连接 UCD323")
|
||
return
|
||
if not getattr(self, "ca", None):
|
||
messagebox.showwarning("警告", "请先连接 CA410")
|
||
return
|
||
|
||
try:
|
||
settle = float(self.pantone_settle_var.get().strip())
|
||
if settle < 0:
|
||
raise ValueError()
|
||
except Exception:
|
||
messagebox.showerror("参数错误", "稳定等待时间必须是非负数字")
|
||
return
|
||
|
||
try:
|
||
self.pantone_patterns = _load_patterns(self)
|
||
self._pantone_target_count = len(self.pantone_patterns)
|
||
except Exception as exc:
|
||
messagebox.showerror("读取失败", str(exc))
|
||
return
|
||
|
||
if self.pantone_results:
|
||
if not messagebox.askyesno("确认开始", "开始将清空当前内存中的测试结果,是否继续?"):
|
||
return
|
||
|
||
self._pantone_running = True
|
||
self._pantone_paused = False
|
||
self._pantone_pause_requested = False
|
||
self._pantone_stop_requested = False
|
||
self._pantone_control_event = threading.Event()
|
||
self._pantone_next_index = 0
|
||
self.pantone_status_var.set("执行中")
|
||
self.pantone_progress_var.set(f"0 / {self._pantone_target_count}")
|
||
self.pantone_results = []
|
||
for item in self.pantone_tree.get_children():
|
||
self.pantone_tree.delete(item)
|
||
_set_button_states(self)
|
||
|
||
_launch_worker(self, start_index=0, settle=settle)
|
||
|
||
|
||
def _resume_pantone_baseline(self: "PQAutomationApp"):
|
||
if self._pantone_running:
|
||
messagebox.showinfo("提示", "Pantone 任务正在执行")
|
||
return
|
||
if not self._pantone_paused:
|
||
messagebox.showinfo("提示", "当前没有可继续的暂停任务")
|
||
return
|
||
if not self.signal_service.is_connected:
|
||
messagebox.showwarning("警告", "请先连接 UCD323")
|
||
return
|
||
if not getattr(self, "ca", None):
|
||
messagebox.showwarning("警告", "请先连接 CA410")
|
||
return
|
||
|
||
try:
|
||
settle = float(self.pantone_settle_var.get().strip())
|
||
if settle < 0:
|
||
raise ValueError()
|
||
except Exception:
|
||
messagebox.showerror("参数错误", "稳定等待时间必须是非负数字")
|
||
return
|
||
|
||
try:
|
||
self.pantone_patterns = _load_patterns(self)
|
||
self._pantone_target_count = len(self.pantone_patterns)
|
||
except Exception as exc:
|
||
messagebox.showerror("读取失败", str(exc))
|
||
return
|
||
|
||
if self._pantone_next_index >= self._pantone_target_count:
|
||
messagebox.showinfo("提示", "任务已完成,无需继续")
|
||
return
|
||
|
||
self._pantone_running = True
|
||
self._pantone_paused = False
|
||
self._pantone_pause_requested = False
|
||
self._pantone_stop_requested = False
|
||
self._pantone_control_event = threading.Event()
|
||
self.pantone_status_var.set("执行中")
|
||
_set_button_states(self)
|
||
|
||
_launch_worker(self, start_index=self._pantone_next_index, settle=settle)
|
||
|
||
|
||
def _launch_worker(self: "PQAutomationApp", start_index, settle):
|
||
total = self._pantone_target_count or len(self.pantone_patterns)
|
||
|
||
def worker():
|
||
end_state = "completed"
|
||
try:
|
||
src = self.pantone_patterns
|
||
src_count = len(src)
|
||
rgb_session = self.pattern_service.prepare_session("rgb", log_details=False)
|
||
self._dispatch_ui(
|
||
self.log_gui.log,
|
||
f"Pantone 认证摸底启动: source={src_count}, target={total}, start={start_index + 1}",
|
||
"info",
|
||
)
|
||
for i in range(start_index, total):
|
||
if self._pantone_stop_requested:
|
||
end_state = "stopped"
|
||
break
|
||
if self._pantone_pause_requested:
|
||
end_state = "paused"
|
||
break
|
||
|
||
r, g, b = src[i % src_count]
|
||
try:
|
||
self.pattern_service.send_rgb((r, g, b), session=rgb_session)
|
||
except Exception as exc:
|
||
raise RuntimeError(f"第 {i + 1} 组发送失败: {exc}") from exc
|
||
|
||
if settle > 0 and self._pantone_control_event is not None:
|
||
self._pantone_control_event.clear()
|
||
self._pantone_control_event.wait(timeout=settle)
|
||
|
||
if self._pantone_stop_requested:
|
||
end_state = "stopped"
|
||
break
|
||
if self._pantone_pause_requested:
|
||
end_state = "paused"
|
||
break
|
||
|
||
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
|
||
if lv is None:
|
||
raise RuntimeError(f"第 {i + 1} 组 CA410 采集失败")
|
||
|
||
record = {
|
||
"idx": i + 1,
|
||
"r": r,
|
||
"g": g,
|
||
"b": b,
|
||
"l": float(lv),
|
||
"x": float(x),
|
||
"y": float(y),
|
||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||
}
|
||
self.pantone_results.append(record)
|
||
self._pantone_next_index = i + 1
|
||
self._dispatch_ui(_append_result_row, self, record, total)
|
||
|
||
if end_state == "paused":
|
||
self._pantone_paused = True
|
||
self._dispatch_ui(self.pantone_status_var.set, "已暂停")
|
||
self._dispatch_ui(
|
||
self.log_gui.log,
|
||
f"Pantone 任务已暂停,断点 {self._pantone_next_index} / {total}",
|
||
"warning",
|
||
)
|
||
elif end_state == "stopped":
|
||
self._pantone_paused = False
|
||
self._pantone_next_index = 0
|
||
self._dispatch_ui(self.pantone_status_var.set, "已结束")
|
||
self._dispatch_ui(
|
||
self.log_gui.log,
|
||
f"Pantone 任务已结束,保留 {len(self.pantone_results)} 条内存结果",
|
||
"warning",
|
||
)
|
||
else:
|
||
self._pantone_paused = False
|
||
self._pantone_next_index = total
|
||
self._dispatch_ui(self.pantone_status_var.set, "已完成")
|
||
self._dispatch_ui(
|
||
self.log_gui.log,
|
||
f"Pantone 任务完成,共 {len(self.pantone_results)} 条数据",
|
||
"success",
|
||
)
|
||
try:
|
||
auto_path = _auto_save_template(self)
|
||
self._dispatch_ui(
|
||
self.log_gui.log,
|
||
f"Pantone 模板已自动保存: {auto_path}",
|
||
"success",
|
||
)
|
||
except Exception as exc:
|
||
self._dispatch_ui(
|
||
self.log_gui.log,
|
||
f"Pantone 自动保存模板失败: {exc}",
|
||
"error",
|
||
)
|
||
except Exception as exc:
|
||
self._pantone_paused = False
|
||
self._dispatch_ui(self.pantone_status_var.set, "执行失败")
|
||
self._dispatch_ui(self.log_gui.log, f"Pantone 任务失败: {exc}", "error")
|
||
self._dispatch_ui(messagebox.showerror, "执行失败", str(exc))
|
||
finally:
|
||
self._pantone_running = False
|
||
self._pantone_pause_requested = False
|
||
self._pantone_stop_requested = False
|
||
self._dispatch_ui(_set_button_states, self)
|
||
|
||
threading.Thread(target=worker, daemon=True).start()
|
||
|
||
|
||
def _append_result_row(self: "PQAutomationApp", record, total):
|
||
self.pantone_tree.insert(
|
||
"",
|
||
tk.END,
|
||
values=(
|
||
record["idx"],
|
||
record["r"],
|
||
record["g"],
|
||
record["b"],
|
||
f"{record['l']:.2f}",
|
||
f"{record['x']:.4f}",
|
||
f"{record['y']:.4f}",
|
||
record["time"],
|
||
),
|
||
)
|
||
self.pantone_progress_var.set(f"{record['idx']} / {total}")
|
||
# 每次插入都滚到末尾,便于观察采集进度。
|
||
children = self.pantone_tree.get_children()
|
||
if children:
|
||
self.pantone_tree.see(children[-1])
|
||
|
||
|
||
def _pause_pantone_baseline(self: "PQAutomationApp"):
|
||
if not self._pantone_running:
|
||
messagebox.showinfo("提示", "当前没有运行中的任务")
|
||
return
|
||
self._pantone_pause_requested = True
|
||
self.pantone_status_var.set("暂停中...")
|
||
if self._pantone_control_event is not None:
|
||
self._pantone_control_event.set()
|
||
|
||
|
||
def _end_pantone_baseline(self: "PQAutomationApp"):
|
||
if self._pantone_running:
|
||
self._pantone_stop_requested = True
|
||
self.pantone_status_var.set("结束中...")
|
||
if self._pantone_control_event is not None:
|
||
self._pantone_control_event.set()
|
||
return
|
||
|
||
# 非运行态下点击“结束”:清除断点,保留当前内存结果。
|
||
self._pantone_paused = False
|
||
self._pantone_next_index = 0
|
||
self.pantone_status_var.set("已结束")
|
||
_set_button_states(self)
|
||
|
||
|
||
def _clear_results(self: "PQAutomationApp"):
|
||
if self._pantone_running:
|
||
messagebox.showinfo("提示", "任务执行中,无法清空")
|
||
return
|
||
self.pantone_results = []
|
||
self._pantone_paused = False
|
||
self._pantone_next_index = 0
|
||
for item in self.pantone_tree.get_children():
|
||
self.pantone_tree.delete(item)
|
||
self._pantone_target_count = 0
|
||
self.pantone_progress_var.set("0 / 0")
|
||
self.pantone_status_var.set("结果已清空")
|
||
_set_button_states(self)
|
||
|
||
|
||
def _set_button_states(self: "PQAutomationApp"):
|
||
if self._pantone_running:
|
||
self.pantone_start_btn.configure(state=tk.DISABLED)
|
||
self.pantone_pause_btn.configure(state=tk.NORMAL)
|
||
self.pantone_resume_btn.configure(state=tk.DISABLED)
|
||
self.pantone_end_btn.configure(state=tk.NORMAL)
|
||
return
|
||
|
||
self.pantone_start_btn.configure(state=tk.NORMAL)
|
||
self.pantone_pause_btn.configure(state=tk.DISABLED)
|
||
self.pantone_end_btn.configure(state=tk.NORMAL if (self._pantone_paused or self.pantone_results) else tk.DISABLED)
|
||
|
||
can_resume = self._pantone_paused and self._pantone_next_index < self._pantone_target_count
|
||
self.pantone_resume_btn.configure(state=tk.NORMAL if can_resume else tk.DISABLED)
|
||
|
||
|
||
def _save_as_template(self: "PQAutomationApp"):
|
||
if not self.pantone_results:
|
||
messagebox.showinfo("提示", "暂无可导出的结果")
|
||
return
|
||
|
||
default_name = _TEMPLATE_FILE.replace("\xa0", " ")
|
||
path = filedialog.asksaveasfilename(
|
||
title="另存为 Pantone 模板",
|
||
defaultextension=".xlsx",
|
||
initialfile=default_name,
|
||
filetypes=[("Excel 文件", "*.xlsx")],
|
||
)
|
||
if not path:
|
||
return
|
||
|
||
try:
|
||
_write_template_xlsx(self, path)
|
||
self.log_gui.log(f"Pantone 模板已保存: {path}", level="success")
|
||
self.pantone_status_var.set(f"已保存: {os.path.basename(path)}")
|
||
except Exception as exc:
|
||
messagebox.showerror("保存失败", f"写入 xlsx 失败: {exc}")
|
||
|
||
|
||
def _resolve_results_dir(self: "PQAutomationApp"):
|
||
if getattr(self, "config_file", None):
|
||
root_dir = os.path.dirname(os.path.dirname(self.config_file))
|
||
else:
|
||
root_dir = os.path.dirname(
|
||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
)
|
||
results_dir = os.path.join(root_dir, "results")
|
||
os.makedirs(results_dir, exist_ok=True)
|
||
return results_dir
|
||
|
||
|
||
def _auto_save_template(self: "PQAutomationApp"):
|
||
results_dir = _resolve_results_dir(self)
|
||
target_count = len(self.pantone_results)
|
||
filename = (
|
||
f"pantone_{target_count}_baseline_"
|
||
f"{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||
)
|
||
path = os.path.join(results_dir, filename)
|
||
_write_template_xlsx(self, path)
|
||
return path
|
||
|
||
|
||
def _write_template_xlsx(self: "PQAutomationApp", path):
|
||
# 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。
|
||
template_path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
|
||
from openpyxl import load_workbook, Workbook
|
||
|
||
if os.path.isfile(template_path):
|
||
wb = load_workbook(template_path)
|
||
ws = wb.active
|
||
else:
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = "Sheet1"
|
||
ws.cell(row=1, column=1, value="R")
|
||
ws.cell(row=1, column=2, value="G")
|
||
ws.cell(row=1, column=3, value="B")
|
||
ws.cell(row=1, column=4, value="L")
|
||
ws.cell(row=1, column=5, value="x")
|
||
ws.cell(row=1, column=6, value="y")
|
||
|
||
# 清空旧数据
|
||
max_row = max(ws.max_row, 2)
|
||
for row in range(2, max_row + 1):
|
||
for col in range(1, 7):
|
||
ws.cell(row=row, column=col, value=None)
|
||
|
||
for idx, item in enumerate(self.pantone_results, start=2):
|
||
ws.cell(row=idx, column=1, value=int(item["r"]))
|
||
ws.cell(row=idx, column=2, value=int(item["g"]))
|
||
ws.cell(row=idx, column=3, value=int(item["b"]))
|
||
ws.cell(row=idx, column=4, value=float(item["l"]))
|
||
ws.cell(row=idx, column=5, value=float(item["x"]))
|
||
ws.cell(row=idx, column=6, value=float(item["y"]))
|
||
|
||
wb.save(path)
|
||
|
||
|
||
class PantoneBaselinePanelMixin:
|
||
"""由 tools/refactor_to_mixins.py 自动生成。
|
||
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||
"""
|
||
create_pantone_baseline_panel = create_pantone_baseline_panel
|
||
toggle_pantone_baseline_panel = toggle_pantone_baseline_panel
|
||
_get_settings_dir = _get_settings_dir
|
||
_load_patterns = _load_patterns
|
||
_start_pantone_baseline = _start_pantone_baseline
|
||
_resume_pantone_baseline = _resume_pantone_baseline
|
||
_launch_worker = _launch_worker
|
||
_append_result_row = _append_result_row
|
||
_pause_pantone_baseline = _pause_pantone_baseline
|
||
_end_pantone_baseline = _end_pantone_baseline
|
||
_clear_results = _clear_results
|
||
_set_button_states = _set_button_states
|
||
_save_as_template = _save_as_template
|
||
_resolve_results_dir = _resolve_results_dir
|
||
_auto_save_template = _auto_save_template
|
||
_write_template_xlsx = _write_template_xlsx
|