修改UCD逻辑、添加关闭界面的x

This commit is contained in:
xinzhu.yin
2026-06-22 10:31:19 +08:00
parent 9c15751dd0
commit 22f8b12690
5 changed files with 294 additions and 52 deletions

View File

@@ -78,12 +78,21 @@ class ConnectionController:
# -- UCD 连接 ------------------------------------------------
def _pick_ucd_device(self, ucd_list: list[str], display: str) -> str | None:
"""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()
if display in ucd_list:
return ucd_list, display
return ucd_list, (ucd_list[0] if ucd_list else None)
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
@@ -91,6 +100,19 @@ class ConnectionController:
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":
try:
self._device.close()
return
except Exception as exc: # noqa: BLE001
self._log(f"关闭旧 UCD 会话失败,将强制重置: {exc}", level="info")
try:
self._device.force_reset()
except Exception as exc: # noqa: BLE001
self._log(f"强制重置 UCD SDK 失败: {exc}", level="error")
def connect_ucd(self, display: str, *, rescan: bool = True) -> bool:
"""打开指定 UCD 设备。成功返回 True。"""
if rescan:
@@ -99,24 +121,46 @@ class ConnectionController:
self._log("未检测到 UCD 设备,请确认电源已打开后重试", level="error")
return False
if self._device.state.name != "CLOSED":
try:
self._device.close()
except Exception: # noqa: BLE001
pass
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:
self._log(f"设备 {display} 异常UCD323 连接失败: {exc}", level="error")
return False
last_error = exc
self._log(
f"设备 {display} 连接失败 ({attempt + 1}/2): {exc}",
level="error",
)
except Exception as exc: # noqa: BLE001
self._log(f"UCD323 连接异常: {exc}", level="error")
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:
try:
self._device.close()
except Exception: # noqa: BLE001
try:
self._device.force_reset()
except Exception: # noqa: BLE001
pass
self._log("UCD连接已断开", level="info")
@@ -297,9 +341,15 @@ class ConnectionController:
app = self._app
com_ports = self.list_com_ports()
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:
app.ucd_list_var.set(ucd_list[0] if ucd_list else "")
selected = self._pick_ucd_device(ucd_list, app.ucd_list_var.get())
app.ucd_list_var.set(selected or "")
app.ucd_list_combo.config(values=ucd_list)
if app.ca_com_var.get() not in com_ports:

View File

@@ -156,8 +156,7 @@ class _UcdSdkBackend:
return True
except Exception as e:
self._force_cleanup()
self._reinit_tsi_lib()
self.hard_reset()
return False
def close(self):
@@ -168,19 +167,24 @@ class _UcdSdkBackend:
self._stop_audio_output()
except Exception:
pass
try:
self._close_device_object(self.dev)
self._reset_state()
self._reinit_tsi_lib()
return True
except Exception as e:
except Exception:
log.exception("UcdSdk.close device object failed")
finally:
self._reset_state()
try:
self._reinit_tsi_lib()
except Exception:
pass
return False
log.exception("UcdSdk.close reinit failed")
return True
def hard_reset(self) -> None:
"""丢弃失效句柄并重建 TSI意外断连后重连前调用"""
self.dev = None
self.role = None
self._reset_state()
self._reinit_tsi_lib()
def _ensure_tsi_lib(self) -> None:
if self.lUniTAP is None:
@@ -188,7 +192,13 @@ class _UcdSdkBackend:
def _reinit_tsi_lib(self) -> None:
"""Destroy and recreate TsiLib so the native driver rescans USB devices."""
old = self.lUniTAP
self.lUniTAP = None
if old is not None:
try:
old.cleanup()
except Exception:
log.exception("UcdSdk._reinit_tsi_lib cleanup failed")
for _ in range(3):
gc.collect()
time.sleep(2.0)
@@ -1068,6 +1078,26 @@ class UCD323Device(IUcdDevice):
self._info = None
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:

View File

@@ -3,19 +3,174 @@
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:
from pqAutomationApp import PQAutomationApp
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):
"""注册一个面板到管理系统"""
self.panels[panel_name] = {
"frame": frame,
"button": button,
"button_label": None,
"sidebar_item": None,
"visible_attr": visible_attr,
}
@@ -66,9 +221,8 @@ def show_panel(self: "PQAutomationApp", panel_name):
# 显示目标面板
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")
# 更新侧栏选中样式
_set_tool_panel_sidebar_selected(panel_info, True)
# 更新状态
setattr(self, panel_info["visible_attr"], True)
@@ -80,8 +234,7 @@ def hide_all_panels(self: "PQAutomationApp"):
# 隐藏所有注册的面板
for panel_name, panel_info in self.panels.items():
panel_info["frame"].pack_forget()
if panel_info["button"]:
panel_info["button"].configure(style="Sidebar.TButton")
_set_tool_panel_sidebar_selected(panel_info, False)
setattr(self, panel_info["visible_attr"], False)
# 显示主内容区域
@@ -110,6 +263,8 @@ class PanelManagerMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
create_tool_panel_sidebar_item = create_tool_panel_sidebar_item
register_panel = register_panel
show_panel = show_panel
hide_all_panels = hide_all_panels
refresh_tool_panel_sidebar_theme = refresh_tool_panel_sidebar_theme

View File

@@ -774,21 +774,19 @@ def create_test_type_frame(self: "PQAutomationApp"):
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
panel_buttons = [
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel),
("ai_image_btn", "ai_image", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "pantone_baseline", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "gamma_pattern", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
("calman_btn", "calman", "CALMAN 灰阶", self.toggle_calman_panel),
]
for attr, text, cmd in panel_buttons:
btn = ttk.Button(
self.sidebar_frame,
text=text,
style="Sidebar.TButton",
command=cmd,
takefocus=False,
)
btn.pack(fill=tk.X, padx=0, pady=1)
setattr(self, attr, btn)
for attr, panel_name, text, cmd in panel_buttons:
item = self.create_tool_panel_sidebar_item(self.sidebar_frame, text, cmd)
item["pack"]()
setattr(self, attr, item["row"])
if hasattr(self, "panels") and panel_name in self.panels:
self.panels[panel_name]["button_label"] = text
self.panels[panel_name]["sidebar_item"] = item
self.panels[panel_name]["button"] = item["row"]
# 测试版水印标签(版本 x.x.0.0 时显示)
from app_version import is_beta_version, APP_VERSION
@@ -822,7 +820,7 @@ def create_test_type_frame(self: "PQAutomationApp"):
self.theme_toggle_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM)
_refresh_theme_toggle_label(self)
# 注册面板按钮
# 注册面板侧栏按钮
if hasattr(self, "panels"):
if "ai_image" in self.panels:
self.panels["ai_image"]["button"] = self.ai_image_btn
@@ -862,6 +860,11 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
toggle_theme()
# apply_modern_styles()
_refresh_theme_toggle_label(self)
if hasattr(self, "refresh_tool_panel_sidebar_theme"):
try:
self.refresh_tool_panel_sidebar_theme()
except Exception:
pass
if hasattr(self, "refresh_connection_indicators"):
try:
self.refresh_connection_indicators()

View File

@@ -105,5 +105,9 @@
"measurement_bit_depth": 8,
"measurement_max_value": 0,
"pattern_params": []
},
"acquisition_timing": {
"pattern_settle_time": 0.4,
"pattern_progress_log_step": 5
}
}