Files
pqAutomationApp/app/views/panels/single_step_panel.py
2026-05-24 11:02:37 +08:00

549 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""单步调试面板。"""
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
_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(
"<<ListboxSelect>>", 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"])
self.signal_service.send_image(image_path)
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}")