From 3c519dde20140cb15c25707db472ca44f6080567 Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Thu, 7 May 2026 11:21:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Pantone=20=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E6=91=B8=E5=BA=95=E6=B5=8B=E8=AF=95=E9=9D=A2=E6=9D=BFUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/panels/main_layout.py | 22 + app/views/panels/pantone_baseline_panel.py | 506 +++++++++++++++++++ app/views/panels/single_step_panel.py | 551 +++++++++++++++++++++ pqAutomationApp.py | 22 +- 4 files changed, 1100 insertions(+), 1 deletion(-) create mode 100644 app/views/panels/pantone_baseline_panel.py create mode 100644 app/views/panels/single_step_panel.py diff --git a/app/views/panels/main_layout.py b/app/views/panels/main_layout.py index ed6dbbe..cd862a4 100644 --- a/app/views/panels/main_layout.py +++ b/app/views/panels/main_layout.py @@ -435,6 +435,24 @@ def create_test_type_frame(self): ) self.ai_image_btn.pack(fill=tk.X, padx=0, pady=1) + self.single_step_btn = ttk.Button( + self.sidebar_frame, + text="单步调试", + style="Sidebar.TButton", + command=self.toggle_single_step_panel, + takefocus=False, + ) + self.single_step_btn.pack(fill=tk.X, padx=0, pady=1) + + self.pantone_baseline_btn = ttk.Button( + self.sidebar_frame, + text="Pantone认证摸底测试", + style="Sidebar.TButton", + command=self.toggle_pantone_baseline_panel, + takefocus=False, + ) + self.pantone_baseline_btn.pack(fill=tk.X, padx=0, pady=1) + # 注册面板按钮 if hasattr(self, "panels"): if "log" in self.panels: @@ -443,6 +461,10 @@ def create_test_type_frame(self): self.panels["local_dimming"]["button"] = self.local_dimming_btn if "ai_image" in self.panels: self.panels["ai_image"]["button"] = self.ai_image_btn + if "single_step" in self.panels: + self.panels["single_step"]["button"] = self.single_step_btn + if "pantone_baseline" in self.panels: + self.panels["pantone_baseline"]["button"] = self.pantone_baseline_btn def update_config_info_display(self): diff --git a/app/views/panels/pantone_baseline_panel.py b/app/views/panels/pantone_baseline_panel.py new file mode 100644 index 0000000..e8bcbe5 --- /dev/null +++ b/app/views/panels/pantone_baseline_panel.py @@ -0,0 +1,506 @@ +"""Pantone 认证摸底测试面板。""" + +from __future__ import annotations + +import csv +import datetime +import os +import tempfile +import threading +import tkinter as tk +from tkinter import filedialog, messagebox + +import ttkbootstrap as ttk +from PIL import Image + +from drivers.ucd_helpers import get_current_resolution, send_image_pattern + + +_PATTERN_FILE = "pantone_patterns_2670.csv" +_TEMPLATE_FILE = "pantone\xa02670\xa0colors.xlsx" +_TARGET_RESULT_COUNT = 2760 + + +def create_pantone_baseline_panel(self): + """创建 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 + + 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 / 2670") + self.pantone_settle_var = tk.StringVar(value="0.35") + + config_row = ttk.LabelFrame(root, text="任务配置", padding=8) + config_row.pack(fill=tk.X) + ttk.Label(config_row, text=f"Pattern来源: settings/{_PATTERN_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, foreground="#666").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): + """切换 Pantone 认证摸底测试面板。""" + self.show_panel("pantone_baseline") + + +def _load_patterns(self): + path = os.path.join("settings", _PATTERN_FILE) + if not os.path.isfile(path): + raise FileNotFoundError(f"未找到 pattern 文件: {path}") + + patterns = [] + with open(path, "r", encoding="utf-8-sig", newline="") as fp: + reader = csv.DictReader(fp) + for row in reader: + try: + r = int(row.get("R", "").strip()) + g = int(row.get("G", "").strip()) + b = int(row.get("B", "").strip()) + except Exception: + continue + if min(r, g, b) < 0 or max(r, g, b) > 255: + continue + patterns.append((r, g, b)) + + if not patterns: + raise RuntimeError("pattern 文件为空或格式不正确,需包含列 R,G,B") + return patterns + + +def _build_temp_patch(self, rgb): + width, height = get_current_resolution(self.ucd) + temp_dir = os.path.join(tempfile.gettempdir(), "pq_pantone_baseline") + os.makedirs(temp_dir, exist_ok=True) + file_path = os.path.join(temp_dir, "pantone_current_patch.png") + Image.new("RGB", (width, height), rgb).save(file_path, format="PNG") + return file_path + + +def _start_pantone_baseline(self): + if self._pantone_running: + messagebox.showinfo("提示", "Pantone 任务正在执行") + return + if not getattr(self, "ucd", None) or not self.ucd.status: + 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) + 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 / {_TARGET_RESULT_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): + if self._pantone_running: + messagebox.showinfo("提示", "Pantone 任务正在执行") + return + if not self._pantone_paused: + messagebox.showinfo("提示", "当前没有可继续的暂停任务") + return + if self._pantone_next_index >= _TARGET_RESULT_COUNT: + messagebox.showinfo("提示", "任务已完成,无需继续") + return + if not getattr(self, "ucd", None) or not self.ucd.status: + 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) + except Exception as exc: + messagebox.showerror("读取失败", str(exc)) + 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, start_index, settle): + total = _TARGET_RESULT_COUNT + + def worker(): + end_state = "completed" + try: + src = self.pantone_patterns + src_count = len(src) + 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] + image_path = _build_temp_patch(self, (r, g, b)) + if not send_image_pattern(self.ucd, image_path): + raise RuntimeError(f"第 {i + 1} 组发送失败") + + 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.ca.readAllDisplay() + 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", + ) + 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, 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): + 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): + 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): + 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_progress_var.set(f"0 / {_TARGET_RESULT_COUNT}") + self.pantone_status_var.set("结果已清空") + _set_button_states(self) + + +def _set_button_states(self): + 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 < _TARGET_RESULT_COUNT + self.pantone_resume_btn.configure(state=tk.NORMAL if can_resume else tk.DISABLED) + + +def _save_as_template(self): + if not self.pantone_results: + messagebox.showinfo("提示", "暂无可导出的结果") + return + + default_name = "pantone 2670 colors.xlsx" + path = filedialog.asksaveasfilename( + title="另存为 Pantone 模板", + defaultextension=".xlsx", + initialfile=default_name, + filetypes=[("Excel 文件", "*.xlsx")], + ) + if not path: + return + + # 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。 + template_path = os.path.join("settings", _TEMPLATE_FILE) + try: + 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) + 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}") diff --git a/app/views/panels/single_step_panel.py b/app/views/panels/single_step_panel.py new file mode 100644 index 0000000..848bcf8 --- /dev/null +++ b/app/views/panels/single_step_panel.py @@ -0,0 +1,551 @@ +"""单步调试面板。""" + +from __future__ import annotations + +import csv +import datetime +import os +import tempfile +import threading +import tkinter as tk +from tkinter import filedialog, messagebox + +import ttkbootstrap as ttk +from PIL import Image + +from drivers.ucd_helpers import get_current_resolution, send_image_pattern + + +_DEFAULT_SAMPLES = [ + {"name": "Red Sample", "hex": "#D22630", "x": "0.6400", "y": "0.3300"}, + {"name": "Green Sample", "hex": "#00A651", "x": "0.3000", "y": "0.6000"}, + {"name": "Blue Sample", "hex": "#21409A", "x": "0.1500", "y": "0.0600"}, + {"name": "Orange Sample", "hex": "#FF6A13", "x": "0.5500", "y": "0.4000"}, + {"name": "Violet Sample", "hex": "#6A0DAD", "x": "0.2700", "y": "0.1400"}, + {"name": "Gray Sample", "hex": "#8A8D8F", "x": "0.3127", "y": "0.3290"}, +] + + +def create_single_step_panel(self): + """创建单步调试面板。""" + frame = ttk.Frame(self.content_frame) + self.single_step_frame = frame + self.single_step_visible = False + self.single_step_samples = [] + self.single_step_results = [] + self.single_step_current_index = None + self.single_step_current_image_path = None + + root = ttk.Frame(frame, padding=10) + root.pack(fill=tk.BOTH, expand=True) + root.columnconfigure(0, weight=0) + root.columnconfigure(1, weight=1) + root.rowconfigure(1, weight=1) + + title_row = ttk.Frame(root) + title_row.grid(row=0, column=0, columnspan=2, sticky=tk.EW, pady=(0, 10)) + ttk.Label( + title_row, + text="单步调试", + font=("微软雅黑", 14, "bold"), + ).pack(side=tk.LEFT) + ttk.Label( + title_row, + text="录入目标色块,发送纯色色块并采集 xyY / ΔE2000。", + foreground="#666", + ).pack(side=tk.LEFT, padx=(12, 0)) + + left = ttk.LabelFrame(root, text="样本列表", padding=8) + left.grid(row=1, column=0, sticky=tk.NS, padx=(0, 10)) + left.grid_propagate(False) + left.configure(width=340) + + self.single_step_listbox = tk.Listbox( + left, + width=34, + activestyle="none", + font=("微软雅黑", 9), + highlightthickness=1, + highlightbackground="#d8d8d8", + highlightcolor="#4a90e2", + selectbackground="#2b6cb0", + selectforeground="#ffffff", + ) + self.single_step_listbox.pack(fill=tk.BOTH, expand=True) + self.single_step_listbox.bind( + "<>", lambda e: _on_sample_select(self) + ) + + list_btn_row = ttk.Frame(left) + list_btn_row.pack(fill=tk.X, pady=(8, 0)) + ttk.Button( + list_btn_row, + text="导入 CSV", + bootstyle="secondary-outline", + command=lambda: _import_samples_csv(self), + ).pack(side=tk.LEFT, padx=(0, 4)) + ttk.Button( + list_btn_row, + text="载入示例", + bootstyle="secondary-outline", + command=lambda: _load_default_samples(self), + ).pack(side=tk.LEFT, padx=(0, 4)) + ttk.Button( + list_btn_row, + text="删除", + bootstyle="danger-outline", + command=lambda: _delete_current_sample(self), + ).pack(side=tk.LEFT) + + right = ttk.Frame(root) + right.grid(row=1, column=1, sticky=tk.NSEW) + + form_frame = ttk.LabelFrame(right, text="样本配置", padding=8) + form_frame.pack(fill=tk.X) + for column in range(6): + form_frame.columnconfigure(column, weight=1 if column in (1, 3, 5) else 0) + + self.single_step_name_var = tk.StringVar() + self.single_step_hex_var = tk.StringVar(value="#FFFFFF") + self.single_step_target_x_var = tk.StringVar() + self.single_step_target_y_var = tk.StringVar() + self.single_step_measured_x_var = tk.StringVar() + self.single_step_measured_y_var = tk.StringVar() + self.single_step_measured_lv_var = tk.StringVar() + self.single_step_status_var = tk.StringVar(value="未选择样本") + + ttk.Label(form_frame, text="名称").grid(row=0, column=0, sticky=tk.W, pady=4) + ttk.Entry(form_frame, textvariable=self.single_step_name_var).grid( + row=0, column=1, sticky=tk.EW, padx=(0, 8), pady=4 + ) + ttk.Label(form_frame, text="HEX").grid(row=0, column=2, sticky=tk.W, pady=4) + ttk.Entry(form_frame, textvariable=self.single_step_hex_var, width=12).grid( + row=0, column=3, sticky=tk.EW, padx=(0, 8), pady=4 + ) + ttk.Label(form_frame, text="目标 x").grid(row=0, column=4, sticky=tk.W, pady=4) + ttk.Entry(form_frame, textvariable=self.single_step_target_x_var, width=10).grid( + row=0, column=5, sticky=tk.EW, pady=4 + ) + + ttk.Label(form_frame, text="目标 y").grid(row=1, column=0, sticky=tk.W, pady=4) + ttk.Entry(form_frame, textvariable=self.single_step_target_y_var, width=10).grid( + row=1, column=1, sticky=tk.EW, padx=(0, 8), pady=4 + ) + ttk.Label(form_frame, text="实测 x").grid(row=1, column=2, sticky=tk.W, pady=4) + ttk.Entry(form_frame, textvariable=self.single_step_measured_x_var, width=10).grid( + row=1, column=3, sticky=tk.EW, padx=(0, 8), pady=4 + ) + ttk.Label(form_frame, text="实测 y").grid(row=1, column=4, sticky=tk.W, pady=4) + ttk.Entry(form_frame, textvariable=self.single_step_measured_y_var, width=10).grid( + row=1, column=5, sticky=tk.EW, pady=4 + ) + + ttk.Label(form_frame, text="实测 Lv").grid(row=2, column=0, sticky=tk.W, pady=4) + ttk.Entry(form_frame, textvariable=self.single_step_measured_lv_var, width=10).grid( + row=2, column=1, sticky=tk.EW, padx=(0, 8), pady=4 + ) + ttk.Label( + form_frame, + textvariable=self.single_step_status_var, + foreground="#666", + ).grid(row=2, column=2, columnspan=4, sticky=tk.W, pady=4) + + action_row = ttk.Frame(form_frame) + action_row.grid(row=3, column=0, columnspan=6, sticky=tk.EW, pady=(6, 0)) + ttk.Button( + action_row, + text="新增 / 更新样本", + bootstyle="primary", + command=lambda: _upsert_sample(self), + ).pack(side=tk.LEFT, padx=(0, 6)) + ttk.Button( + action_row, + text="发送当前色块", + bootstyle="info-outline", + command=lambda: _send_current_patch(self), + ).pack(side=tk.LEFT, padx=(0, 6)) + ttk.Button( + action_row, + text="CA410 采集", + bootstyle="success-outline", + command=lambda: _measure_current_sample(self), + ).pack(side=tk.LEFT, padx=(0, 6)) + ttk.Button( + action_row, + text="记录结果", + bootstyle="warning-outline", + command=lambda: _commit_result(self), + ).pack(side=tk.LEFT) + + result_frame = ttk.LabelFrame(right, text="调试结果", padding=8) + result_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0)) + + columns = ( + "name", + "hex", + "target_x", + "target_y", + "measured_x", + "measured_y", + "lv", + "delta_e", + "time", + ) + self.single_step_result_tree = ttk.Treeview( + result_frame, + columns=columns, + show="headings", + height=12, + ) + headings = { + "name": "名称", + "hex": "HEX", + "target_x": "目标 x", + "target_y": "目标 y", + "measured_x": "实测 x", + "measured_y": "实测 y", + "lv": "Lv", + "delta_e": "ΔE2000", + "time": "时间", + } + widths = { + "name": 130, + "hex": 90, + "target_x": 80, + "target_y": 80, + "measured_x": 80, + "measured_y": 80, + "lv": 80, + "delta_e": 90, + "time": 120, + } + for column in columns: + self.single_step_result_tree.heading(column, text=headings[column]) + self.single_step_result_tree.column( + column, width=widths[column], anchor=tk.CENTER + ) + self.single_step_result_tree.pack(fill=tk.BOTH, expand=True) + + bottom_row = ttk.Frame(right) + bottom_row.pack(fill=tk.X, pady=(8, 0)) + ttk.Button( + bottom_row, + text="导出结果 CSV", + bootstyle="success", + command=lambda: _export_results_csv(self), + ).pack(side=tk.LEFT, padx=(0, 6)) + ttk.Button( + bottom_row, + text="清空结果", + bootstyle="danger-outline", + command=lambda: _clear_results(self), + ).pack(side=tk.LEFT) + + self.register_panel("single_step", frame, None, "single_step_visible") + _load_default_samples(self) + + +def toggle_single_step_panel(self): + """切换单步调试面板。""" + self.show_panel("single_step") + + +def _load_default_samples(self): + self.single_step_samples = [dict(item) for item in _DEFAULT_SAMPLES] + _refresh_sample_list(self, select_index=0 if self.single_step_samples else None) + self.single_step_status_var.set( + f"已载入 {len(self.single_step_samples)} 个示例样本" + ) + + +def _refresh_sample_list(self, select_index=None): + self.single_step_listbox.delete(0, tk.END) + for sample in self.single_step_samples: + self.single_step_listbox.insert( + tk.END, + f"{sample['name']} {sample['hex']} ({sample['x']}, {sample['y']})", + ) + if select_index is not None and 0 <= select_index < len(self.single_step_samples): + self.single_step_listbox.selection_clear(0, tk.END) + self.single_step_listbox.selection_set(select_index) + self.single_step_listbox.activate(select_index) + _select_sample(self, select_index) + elif not self.single_step_samples: + self.single_step_current_index = None + self.single_step_name_var.set("") + self.single_step_hex_var.set("#FFFFFF") + self.single_step_target_x_var.set("") + self.single_step_target_y_var.set("") + self.single_step_status_var.set("样本列表为空") + + +def _on_sample_select(self): + selection = self.single_step_listbox.curselection() + if not selection: + return + _select_sample(self, selection[0]) + + +def _select_sample(self, index): + sample = self.single_step_samples[index] + self.single_step_current_index = index + self.single_step_name_var.set(sample["name"]) + self.single_step_hex_var.set(sample["hex"]) + self.single_step_target_x_var.set(sample["x"]) + self.single_step_target_y_var.set(sample["y"]) + self.single_step_status_var.set(f"当前样本: {sample['name']}") + + +def _import_samples_csv(self): + path = filedialog.askopenfilename( + title="选择单步调试样本 CSV", + filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")], + ) + if not path: + return + samples = [] + try: + with open(path, "r", encoding="utf-8-sig", newline="") as fp: + reader = csv.DictReader(fp) + for row in reader: + name = (row.get("name") or row.get("sample") or "").strip() + hex_value = (row.get("hex") or row.get("rgb") or "").strip() + target_x = (row.get("x") or "").strip() + target_y = (row.get("y") or "").strip() + if not name or not hex_value or not target_x or not target_y: + continue + samples.append( + { + "name": name, + "hex": _normalize_hex(hex_value), + "x": target_x, + "y": target_y, + } + ) + except Exception as exc: + messagebox.showerror("导入失败", f"读取 CSV 失败: {exc}") + return + if not samples: + messagebox.showwarning("导入失败", "CSV 中没有有效样本,要求列包含 name/hex/x/y") + return + self.single_step_samples = samples + _refresh_sample_list(self, select_index=0) + self.log_gui.log(f"单步调试样本已导入: {len(samples)} 条", level="success") + + +def _delete_current_sample(self): + if self.single_step_current_index is None: + return + removed = self.single_step_samples.pop(self.single_step_current_index) + next_index = min(self.single_step_current_index, len(self.single_step_samples) - 1) + _refresh_sample_list(self, select_index=next_index if next_index >= 0 else None) + self.single_step_status_var.set(f"已删除样本: {removed['name']}") + + +def _upsert_sample(self): + try: + sample = { + "name": self.single_step_name_var.get().strip(), + "hex": _normalize_hex(self.single_step_hex_var.get()), + "x": _format_float(self.single_step_target_x_var.get()), + "y": _format_float(self.single_step_target_y_var.get()), + } + except ValueError as exc: + messagebox.showerror("参数错误", str(exc)) + return + if not sample["name"]: + messagebox.showwarning("提示", "请输入样本名称") + return + if self.single_step_current_index is None: + self.single_step_samples.append(sample) + select_index = len(self.single_step_samples) - 1 + self.single_step_status_var.set(f"已新增样本: {sample['name']}") + else: + self.single_step_samples[self.single_step_current_index] = sample + select_index = self.single_step_current_index + self.single_step_status_var.set(f"已更新样本: {sample['name']}") + _refresh_sample_list(self, select_index=select_index) + + +def _normalize_hex(value): + text = (value or "").strip().upper() + if not text: + raise ValueError("HEX 不能为空") + if not text.startswith("#"): + text = "#" + text + if len(text) != 7 or any(ch not in "#0123456789ABCDEF" for ch in text): + raise ValueError("HEX 格式应为 #RRGGBB") + return text + + +def _format_float(value): + try: + number = float(str(value).strip()) + except Exception as exc: + raise ValueError("xy 值必须是数字") from exc + return f"{number:.4f}" + + +def _build_color_patch(self, hex_value): + if not getattr(self, "ucd", None) or not self.ucd.status: + raise RuntimeError("请先连接 UCD323 设备") + width, height = get_current_resolution(self.ucd) + rgb = tuple(int(hex_value[i:i + 2], 16) for i in (1, 3, 5)) + temp_dir = os.path.join(tempfile.gettempdir(), "pq_single_step_patches") + os.makedirs(temp_dir, exist_ok=True) + file_path = os.path.join( + temp_dir, f"single_step_{hex_value[1:]}_{width}x{height}.png" + ) + Image.new("RGB", (width, height), rgb).save(file_path, format="PNG") + return file_path + + +def _send_current_patch(self): + if self.single_step_current_index is None: + messagebox.showinfo("提示", "请先选择一个样本") + return + sample = self.single_step_samples[self.single_step_current_index] + + def worker(): + try: + image_path = _build_color_patch(self, sample["hex"]) + ok = send_image_pattern(self.ucd, image_path) + if not ok: + raise RuntimeError("UCD323 发送失败") + self.single_step_current_image_path = image_path + self._dispatch_ui( + self.single_step_status_var.set, + f"已发送色块: {sample['name']} {sample['hex']}", + ) + self._dispatch_ui( + self.log_gui.log, + f"单步调试色块已发送: {sample['name']} {sample['hex']}", + "success", + ) + except Exception as exc: + self._dispatch_ui(self.single_step_status_var.set, f"发送失败: {exc}") + self._dispatch_ui(self.log_gui.log, f"单步调试色块发送失败: {exc}", "error") + + threading.Thread(target=worker, daemon=True).start() + + +def _measure_current_sample(self): + if self.single_step_current_index is None: + messagebox.showinfo("提示", "请先选择一个样本") + return + if not getattr(self, "ca", None): + messagebox.showwarning("警告", "请先连接 CA410 色度计") + return + + def worker(): + try: + x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() + if lv is None: + raise RuntimeError("CA410 未返回有效亮度") + self._dispatch_ui(self.single_step_measured_x_var.set, f"{x:.4f}") + self._dispatch_ui(self.single_step_measured_y_var.set, f"{y:.4f}") + self._dispatch_ui(self.single_step_measured_lv_var.set, f"{lv:.2f}") + self._dispatch_ui(self.single_step_status_var.set, "采集完成,可记录结果") + self._dispatch_ui( + self.log_gui.log, + f"单步调试采集完成: x={x:.4f}, y={y:.4f}, Lv={lv:.2f}", + "success", + ) + except Exception as exc: + self._dispatch_ui(self.single_step_status_var.set, f"采集失败: {exc}") + self._dispatch_ui(self.log_gui.log, f"单步调试采集失败: {exc}", "error") + + threading.Thread(target=worker, daemon=True).start() + + +def _commit_result(self): + if self.single_step_current_index is None: + messagebox.showinfo("提示", "请先选择一个样本") + return + sample = self.single_step_samples[self.single_step_current_index] + try: + measured_x = float(self.single_step_measured_x_var.get().strip()) + measured_y = float(self.single_step_measured_y_var.get().strip()) + measured_lv = float(self.single_step_measured_lv_var.get().strip()) + target_x = float(sample["x"]) + target_y = float(sample["y"]) + delta_e = self.calculate_delta_e_2000( + measured_x, measured_y, measured_lv, target_x, target_y + ) + except Exception as exc: + messagebox.showerror("记录失败", f"请先准备完整的目标值与实测值: {exc}") + return + + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + record = { + "name": sample["name"], + "hex": sample["hex"], + "target_x": f"{target_x:.4f}", + "target_y": f"{target_y:.4f}", + "measured_x": f"{measured_x:.4f}", + "measured_y": f"{measured_y:.4f}", + "lv": f"{measured_lv:.2f}", + "delta_e": f"{delta_e:.3f}", + "time": timestamp, + } + self.single_step_results.append(record) + self.single_step_result_tree.insert( + "", + tk.END, + values=tuple( + record[key] + for key in ( + "name", + "hex", + "target_x", + "target_y", + "measured_x", + "measured_y", + "lv", + "delta_e", + "time", + ) + ), + ) + self.single_step_status_var.set(f"已记录结果,ΔE2000={record['delta_e']}") + + +def _clear_results(self): + self.single_step_results = [] + for item in self.single_step_result_tree.get_children(): + self.single_step_result_tree.delete(item) + self.single_step_status_var.set("结果已清空") + + +def _export_results_csv(self): + if not self.single_step_results: + messagebox.showinfo("提示", "暂无可导出的调试结果") + return + path = filedialog.asksaveasfilename( + title="导出单步调试结果", + defaultextension=".csv", + initialfile=f"single_step_debug_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", + filetypes=[("CSV 文件", "*.csv")], + ) + if not path: + return + fieldnames = [ + "name", + "hex", + "target_x", + "target_y", + "measured_x", + "measured_y", + "lv", + "delta_e", + "time", + ] + try: + with open(path, "w", encoding="utf-8-sig", newline="") as fp: + writer = csv.DictWriter(fp, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(self.single_step_results) + self.log_gui.log(f"单步调试结果已导出: {path}", level="success") + self.single_step_status_var.set(f"结果已导出: {os.path.basename(path)}") + except Exception as exc: + messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}") \ No newline at end of file diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 3a12b1a..4cfa5ba 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -21,6 +21,8 @@ from app.views.panels import side_panels as _sp from app.views.panels import cct_panel as _ccp from app.views.panels import main_layout as _main from app.views.panels import ai_image_panel as _aip +from app.views.panels import single_step_panel as _ssp +from app.views.panels import pantone_baseline_panel as _pbp from app.views import panel_manager as PM from app.logging_setup import setup_logging, attach_gui_handler @@ -200,6 +202,10 @@ class PQAutomationApp: self.create_local_dimming_panel() # 创建 AI 图片对话面板 self.create_ai_image_panel() + # # 创建单步调试面板 + # self.create_single_step_panel() + # 创建 Pantone 认证摸底测试面板 + self.create_pantone_baseline_panel() # 创建测试类型选择区域 self.create_test_type_frame() # 创建操作按钮区域 @@ -324,6 +330,14 @@ class PQAutomationApp: toggle_ai_image_panel = _aip.toggle_ai_image_panel reload_ai_image_list = _aip.reload_ai_image_list + # ---- 单步调试面板 ---- + create_single_step_panel = _ssp.create_single_step_panel + toggle_single_step_panel = _ssp.toggle_single_step_panel + + # ---- Pantone 认证摸底测试面板 ---- + create_pantone_baseline_panel = _pbp.create_pantone_baseline_panel + toggle_pantone_baseline_panel = _pbp.toggle_pantone_baseline_panel + # ---- 单步调试面板(统一实现,委托到 side_panels 模块) ---- _toggle_debug_panel = _sp._toggle_debug_panel toggle_screen_debug_panel = _sp.toggle_screen_debug_panel @@ -503,7 +517,13 @@ class PQAutomationApp: def change_test_type(self, test_type): """切换测试类型""" # 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板 - if self.current_panel in ("log", "local_dimming", "ai_image"): + if self.current_panel in ( + "log", + "local_dimming", + "ai_image", + "single_step", + "pantone_baseline", + ): self.hide_all_panels() self._save_cct_params_before_test_type_switch() self._apply_current_test_type(test_type)