feat(DLPilot): v0.4.0 — PySide6-Fluent-Widgets UI and Light theme
This commit is contained in:
@@ -0,0 +1,869 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""DLPilot Fluent UI.
|
||||
|
||||
The device, EDID and RS485 modules deliberately remain unchanged. This file
|
||||
is the presentation layer for the current desktop application and uses
|
||||
PySide6-Fluent-Widgets for the cards, controls and dialogs.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
# PyInstaller keeps Shiboken's ABI DLLs in a sibling directory. Register both
|
||||
# Qt and Shiboken before importing QtCore; this is required by one-file builds
|
||||
# on Windows/Python 3.14 where the normal package import hook is unavailable.
|
||||
if getattr(sys, "frozen", False) and os.name == "nt":
|
||||
_frozen_dll_dirs = []
|
||||
for _dll_dir in (os.path.join(getattr(sys, "_MEIPASS", ""), "PySide6"),
|
||||
os.path.join(getattr(sys, "_MEIPASS", ""), "shiboken6")):
|
||||
if os.path.isdir(_dll_dir):
|
||||
_frozen_dll_dirs.append(_dll_dir)
|
||||
try:
|
||||
os.add_dll_directory(_dll_dir)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
# Python 3.14 extension loading may ignore AddDllDirectory for a pyd's
|
||||
# transitive dependencies; PATH keeps QtCore.pyd compatible with both
|
||||
# Python 3.11-3.14 and PyInstaller one-file extraction.
|
||||
if _frozen_dll_dirs:
|
||||
os.environ["PATH"] = os.pathsep.join(_frozen_dll_dirs + [os.environ.get("PATH", "")])
|
||||
# Preload the two libraries used by QtCore.pyd. This also covers Windows
|
||||
# loader configurations that do not apply PATH/AddDllDirectory to a pyd's
|
||||
# transitive dependencies after PyInstaller extraction.
|
||||
try:
|
||||
ctypes.WinDLL(os.path.join(getattr(sys, "_MEIPASS", ""), "shiboken6",
|
||||
"shiboken6.abi3.dll"))
|
||||
ctypes.WinDLL(os.path.join(getattr(sys, "_MEIPASS", ""), "PySide6",
|
||||
"Qt6Core.dll"))
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
from PySide6.QtCore import QPoint, QRect, Qt, QTimer
|
||||
from PySide6.QtGui import QColor, QFont, QIcon, QImage, QPainter, QPen, QPalette
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QDialog, QFileDialog, QFormLayout, QGridLayout, QHBoxLayout,
|
||||
QLabel, QMainWindow, QMessageBox, QPlainTextEdit, QScrollArea, QSplitter,
|
||||
QToolButton, QVBoxLayout, QWidget,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
BodyLabel, CardWidget, ComboBox, LineEdit, PlainTextEdit,
|
||||
PrimaryPushButton, PushButton, Slider, StrongBodyLabel, Theme, setTheme,
|
||||
)
|
||||
|
||||
import config
|
||||
import display_win
|
||||
import dlpc_rs485
|
||||
import edid_parse
|
||||
import patterns
|
||||
|
||||
APP_NAME = "DLPilot"
|
||||
APP_VERSION = "0.4.0"
|
||||
REPO_URL = "https://gitea.hulk.wang/hulk/LT8619C_Debug_Tools"
|
||||
MONO = QFont("Consolas", 10)
|
||||
CHIP_IDLE = "#666666"
|
||||
CHIP_OK = "#2FA574"
|
||||
CHIP_ERR = "#D9534F"
|
||||
|
||||
_QT_PATTERNS = set()
|
||||
|
||||
|
||||
def close_qt_patterns():
|
||||
for window in list(_QT_PATTERNS):
|
||||
window.close()
|
||||
_QT_PATTERNS.clear()
|
||||
patterns.close_all()
|
||||
|
||||
|
||||
def _area_box(spec, width, height):
|
||||
if spec == "full":
|
||||
return 0, 0, width, height
|
||||
try:
|
||||
aw, ah = (int(v) for v in spec.lower().split("x", 1))
|
||||
except Exception:
|
||||
aw, ah = config.PAT_W, config.PAT_H
|
||||
return (width - aw) // 2, (height - ah) // 2, aw, ah
|
||||
|
||||
|
||||
class PatternWindow(QWidget):
|
||||
"""Borderless Qt equivalent of the old tkinter test-pattern window."""
|
||||
|
||||
def __init__(self, rect, kind, area, image_path=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.kind = kind
|
||||
self.area = area
|
||||
self.image = QImage(image_path) if image_path else None
|
||||
x, y, width, height = rect
|
||||
self.setGeometry(x, y, width, height)
|
||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint |
|
||||
Qt.Tool)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||
_QT_PATTERNS.add(self)
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
self.setFocus()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Escape:
|
||||
self.close()
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def closeEvent(self, event):
|
||||
_QT_PATTERNS.discard(self)
|
||||
super().closeEvent(event)
|
||||
|
||||
def paintEvent(self, _event):
|
||||
painter = QPainter(self)
|
||||
painter.fillRect(self.rect(), Qt.black)
|
||||
x0, y0, aw, ah = _area_box(self.area, self.width(), self.height())
|
||||
if self.image and not self.image.isNull():
|
||||
painter.drawImage(QPoint(x0 + (aw - self.image.width()) // 2,
|
||||
y0 + (ah - self.image.height()) // 2),
|
||||
self.image)
|
||||
return
|
||||
if self.kind.startswith("#"):
|
||||
painter.fillRect(x0, y0, aw, ah, QColor(self.kind))
|
||||
elif self.kind in {"Red", "Green", "Blue"}:
|
||||
painter.fillRect(x0, y0, aw, ah,
|
||||
QColor({"Red": "#ff0000", "Green": "#00ff00",
|
||||
"Blue": "#0000ff"}[self.kind]))
|
||||
elif self.kind == "Checker":
|
||||
for gy in range((ah + 39) // 40):
|
||||
for gx in range((aw + 39) // 40):
|
||||
if (gx + gy) % 2 == 0:
|
||||
painter.fillRect(x0 + gx * 40, y0 + gy * 40, 40, 40,
|
||||
Qt.white)
|
||||
elif self.kind == "Grid":
|
||||
painter.setPen(QPen(Qt.white, 1))
|
||||
for x in range(0, aw + 1, 20):
|
||||
painter.drawLine(x0 + x, y0, x0 + x, y0 + ah)
|
||||
for y in range(0, ah + 1, 20):
|
||||
painter.drawLine(x0, y0 + y, x0 + aw, y0 + y)
|
||||
|
||||
|
||||
def _single_instance():
|
||||
if os.name != "nt":
|
||||
return True
|
||||
try:
|
||||
ctypes.windll.kernel32.CreateMutexW(None, False, "hulk.DLPilot.single")
|
||||
return ctypes.windll.kernel32.GetLastError() != 183
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
class App(QMainWindow):
|
||||
def __init__(self, auto_close=0):
|
||||
super().__init__()
|
||||
app = QApplication.instance()
|
||||
self._system_palette = app.palette() if app else QPalette()
|
||||
self._system_style = app.styleSheet() if app else ""
|
||||
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION} — 3D 光固化光机调试台")
|
||||
icon_path = self._icon_path()
|
||||
if icon_path:
|
||||
self.setWindowIcon(QIcon(icon_path))
|
||||
self.resize(1440, 960)
|
||||
self.setMinimumSize(1180, 760)
|
||||
self.displays = []
|
||||
self._disp_labels = []
|
||||
self._modes = {}
|
||||
self._orig_modes = {}
|
||||
self._cur_label = ""
|
||||
self.slots, self.area = config.load_slots()
|
||||
self._dlpc_log_q = queue.Queue()
|
||||
self.dlpc = dlpc_rs485.DLPC485(self._dlpc_on_event, self._dlpc_on_status,
|
||||
on_frame=self._dlpc_on_frame)
|
||||
self._dlpc_connected = False
|
||||
self._tl_order = []
|
||||
self._pd_seq = []
|
||||
self._pd_values = {}
|
||||
self._pd_timer = None
|
||||
self._pwr_seq = []
|
||||
self._pwr_values = {}
|
||||
self._pwr_timer = None
|
||||
self._build()
|
||||
self._load_displays()
|
||||
self._timer = QTimer(self)
|
||||
self._timer.timeout.connect(self._poll_dlpc)
|
||||
if dlpc_rs485.HAS_SERIAL:
|
||||
self._timer.start(80)
|
||||
if auto_close > 0:
|
||||
QTimer.singleShot(auto_close * 1000, self.close)
|
||||
|
||||
@staticmethod
|
||||
def _icon_path():
|
||||
base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
|
||||
path = os.path.join(base, "logo_dlpilot.ico")
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"logos", "logo_dlpilot.ico")
|
||||
return path if os.path.exists(path) else None
|
||||
|
||||
def _button(self, text, callback, primary=False):
|
||||
button = PrimaryPushButton(text) if primary else PushButton(text)
|
||||
button.clicked.connect(callback)
|
||||
return button
|
||||
|
||||
def _card(self, title, parent):
|
||||
card = CardWidget(parent)
|
||||
layout = QVBoxLayout(card)
|
||||
layout.setContentsMargins(14, 12, 14, 14)
|
||||
layout.setSpacing(8)
|
||||
row = QHBoxLayout()
|
||||
row.addWidget(StrongBodyLabel(title, card))
|
||||
row.addStretch(1)
|
||||
layout.addLayout(row)
|
||||
return card, layout
|
||||
|
||||
def _collapsible_card(self, title, parent, expanded=False):
|
||||
"""Card whose body starts collapsed to keep the dashboard compact."""
|
||||
card = CardWidget(parent)
|
||||
layout = QVBoxLayout(card)
|
||||
layout.setContentsMargins(8, 6, 8, 8)
|
||||
layout.setSpacing(4)
|
||||
header = QToolButton(card)
|
||||
header.setText(title)
|
||||
header.setCheckable(True)
|
||||
header.setChecked(expanded)
|
||||
header.setAutoRaise(True)
|
||||
header.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
|
||||
header.setArrowType(Qt.DownArrow if expanded else Qt.RightArrow)
|
||||
header.setStyleSheet("QToolButton { text-align: left; font-weight: 700; padding: 6px; }")
|
||||
layout.addWidget(header)
|
||||
body = QWidget(card)
|
||||
body_layout = QVBoxLayout(body)
|
||||
body_layout.setContentsMargins(6, 0, 6, 4)
|
||||
body_layout.setSpacing(8)
|
||||
body.setVisible(expanded)
|
||||
layout.addWidget(body)
|
||||
|
||||
def toggle(checked):
|
||||
body.setVisible(checked)
|
||||
header.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
|
||||
card.adjustSize()
|
||||
|
||||
header.clicked.connect(toggle)
|
||||
return card, body_layout
|
||||
|
||||
def _text(self, read_only=True, mono=True):
|
||||
text = PlainTextEdit(self)
|
||||
text.setReadOnly(read_only)
|
||||
if mono:
|
||||
text.setFont(MONO)
|
||||
text.setLineWrapMode(QPlainTextEdit.NoWrap)
|
||||
return text
|
||||
|
||||
def _build(self):
|
||||
root = QWidget(self)
|
||||
root.setObjectName("root")
|
||||
outer = QVBoxLayout(root)
|
||||
outer.setContentsMargins(18, 14, 18, 18)
|
||||
outer.setSpacing(12)
|
||||
header = QHBoxLayout()
|
||||
title = StrongBodyLabel("DLPilot", root)
|
||||
title.setStyleSheet("font-size: 24px; font-weight: 700;")
|
||||
header.addWidget(title)
|
||||
header.addWidget(BodyLabel("EDID · 测试图案 · RS485 光机控制", root))
|
||||
header.addStretch(1)
|
||||
self.theme = ComboBox(root)
|
||||
self.theme.addItems(["Dark", "Light", "System"])
|
||||
self.theme.setCurrentText("System")
|
||||
self.theme.currentTextChanged.connect(self._set_theme)
|
||||
header.addWidget(self.theme)
|
||||
header.addWidget(self._button("关于", self._show_about))
|
||||
outer.addLayout(header)
|
||||
|
||||
split = QSplitter(Qt.Horizontal, root)
|
||||
split.setChildrenCollapsible(False)
|
||||
split.addWidget(self._scroll_column(self._build_left))
|
||||
split.addWidget(self._scroll_column(self._build_right))
|
||||
split.setStretchFactor(0, 1)
|
||||
split.setStretchFactor(1, 1)
|
||||
outer.addWidget(split, 1)
|
||||
self.setCentralWidget(root)
|
||||
|
||||
def _scroll_column(self, builder):
|
||||
host = QWidget()
|
||||
layout = QVBoxLayout(host)
|
||||
layout.setContentsMargins(2, 2, 8, 2)
|
||||
layout.setSpacing(10)
|
||||
builder(layout)
|
||||
layout.addStretch(1)
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QScrollArea.NoFrame)
|
||||
scroll.setWidget(host)
|
||||
if builder.__name__ == '_build_left':
|
||||
self.left_scroll = scroll
|
||||
else:
|
||||
self.right_scroll = scroll
|
||||
return scroll
|
||||
|
||||
def _build_left(self, layout):
|
||||
card, body = self._card("显示器选择", self)
|
||||
row = QHBoxLayout()
|
||||
self.cbo_display = ComboBox(card)
|
||||
self.cbo_display.currentTextChanged.connect(self._on_select)
|
||||
self.lbl_cur = BodyLabel("当前输出: —", card)
|
||||
row.addWidget(self.cbo_display, 1)
|
||||
row.addWidget(self._button("刷新", self._load_displays))
|
||||
body.addLayout(row)
|
||||
body.addWidget(self.lbl_cur)
|
||||
layout.addWidget(card)
|
||||
|
||||
card, body = self._collapsible_card("支持的模式(EDID 解算)", self)
|
||||
row = QHBoxLayout()
|
||||
self.cbo_mode = ComboBox(card)
|
||||
row.addWidget(BodyLabel("分辨率:", card))
|
||||
row.addWidget(self.cbo_mode, 1)
|
||||
row.addWidget(self._button("应用", self._apply_mode, True))
|
||||
row.addWidget(self._button("恢复", self._restore_mode))
|
||||
body.addLayout(row)
|
||||
self.txt_modes = self._text()
|
||||
self.txt_modes.setMinimumHeight(135)
|
||||
body.addWidget(self.txt_modes)
|
||||
layout.addWidget(card)
|
||||
|
||||
card, body = self._collapsible_card("EDID 信息", self)
|
||||
self.txt_edid = self._text()
|
||||
self.txt_edid.setMinimumHeight(150)
|
||||
body.addWidget(self.txt_edid)
|
||||
layout.addWidget(card)
|
||||
|
||||
card, body = self._card("测试图案", self)
|
||||
row = QHBoxLayout()
|
||||
for key, name in patterns.PATTERN_NAMES.items():
|
||||
row.addWidget(self._button(name, lambda checked=False, k=key: self._show_pattern(k)))
|
||||
row.addWidget(self._button("停止", close_qt_patterns))
|
||||
body.addLayout(row)
|
||||
area_row = QHBoxLayout()
|
||||
area_row.addWidget(BodyLabel("测试卡区域:", card))
|
||||
self.cbo_area = ComboBox(card)
|
||||
self.cbo_area.addItems(list(config.AREA_OPTIONS))
|
||||
self.cbo_area.setCurrentText(self.area)
|
||||
self.cbo_area.currentTextChanged.connect(self._save_area)
|
||||
area_row.addWidget(self.cbo_area)
|
||||
area_row.addStretch(1)
|
||||
body.addLayout(area_row)
|
||||
for channel, items in patterns.SOLID_ROWS:
|
||||
row = QHBoxLayout()
|
||||
row.addWidget(BodyLabel(f"{channel}: 0x", card))
|
||||
for label, color in items:
|
||||
button = PushButton(label, card)
|
||||
button.setStyleSheet(f"PushButton {{ background: {color}; color: {'black' if color == '#00FF00' else 'white'}; }}")
|
||||
button.clicked.connect(lambda checked=False, c=color: self._show_solid(c))
|
||||
row.addWidget(button)
|
||||
row.addStretch(1)
|
||||
body.addLayout(row)
|
||||
self.slot_buttons = []
|
||||
grid = QGridLayout()
|
||||
for i in range(config.SLOT_COUNT):
|
||||
cell = QWidget(card)
|
||||
cell_layout = QHBoxLayout(cell)
|
||||
cell_layout.setContentsMargins(0, 0, 0, 0)
|
||||
button = self._button(self.slots[i]["name"], lambda checked=False, n=i: self._show_custom(n))
|
||||
gear = self._button("⚙", lambda checked=False, n=i: self._configure_slot(n))
|
||||
cell_layout.addWidget(button, 1)
|
||||
cell_layout.addWidget(gear)
|
||||
grid.addWidget(cell, i // 2, i % 2)
|
||||
self.slot_buttons.append(button)
|
||||
body.addLayout(grid)
|
||||
layout.addWidget(card)
|
||||
|
||||
card, body = self._card("EDID 原始数据", self)
|
||||
self.txt_hex = self._text()
|
||||
self.txt_hex.setMinimumHeight(130)
|
||||
body.addWidget(self.txt_hex)
|
||||
body.addWidget(self._button("逐字节解析", self._show_edid_fields))
|
||||
layout.addWidget(card)
|
||||
|
||||
def _build_right(self, layout):
|
||||
if not dlpc_rs485.HAS_SERIAL:
|
||||
card, body = self._card("光机控制不可用", self)
|
||||
body.addWidget(BodyLabel("未安装 pyserial,请执行 py -m pip install pyserial。"))
|
||||
layout.addWidget(card)
|
||||
return
|
||||
card, body = self._card("串口 · RS485 → GD32 网关 → DLPC3421", self)
|
||||
row = QHBoxLayout()
|
||||
self._dlpc_ports = dlpc_rs485.DLPC485.list_ports_info()
|
||||
self.cbo_dlpc = ComboBox(card)
|
||||
self.cbo_dlpc.addItems([label for _device, label in self._dlpc_ports])
|
||||
if config.SERIAL_CFG["port"]:
|
||||
self._dlpc_pick(config.SERIAL_CFG["port"])
|
||||
row.addWidget(self.cbo_dlpc, 1)
|
||||
row.addWidget(self._button("刷新", self._dlpc_refresh))
|
||||
self.btn_dlpc = self._button("连接", self._dlpc_toggle, True)
|
||||
row.addWidget(self.btn_dlpc)
|
||||
body.addLayout(row)
|
||||
params = QHBoxLayout()
|
||||
for caption, attr, values, default in (
|
||||
("波特率", "cbo_baud", ["115200", "57600", "38400", "9600"], str(config.SERIAL_CFG["baud"])),
|
||||
("数据位", "cbo_databits", ["8", "7", "6", "5"], str(config.SERIAL_CFG["bytesize"])),
|
||||
("校验", "cbo_parity", ["N", "E", "O"], str(config.SERIAL_CFG["parity"])),
|
||||
("停止位", "cbo_stopbits", ["1", "2"], str(config.SERIAL_CFG["stopbits"])),
|
||||
):
|
||||
params.addWidget(BodyLabel(caption, card))
|
||||
combo = ComboBox(card)
|
||||
combo.addItems(values)
|
||||
combo.setCurrentText(default)
|
||||
setattr(self, attr, combo)
|
||||
params.addWidget(combo)
|
||||
self.lbl_dlpc_st = BodyLabel("未连接", card)
|
||||
params.addWidget(self.lbl_dlpc_st)
|
||||
params.addStretch(1)
|
||||
body.addLayout(params)
|
||||
layout.addWidget(card)
|
||||
|
||||
card, body = self._card("串口数据 · 收发日志(译码 + 原始 hex)", self)
|
||||
row = QHBoxLayout()
|
||||
row.addStretch(1)
|
||||
row.addWidget(self._button("清空", self._dlpc_clear))
|
||||
body.addLayout(row)
|
||||
self.txt_dlpc = self._text()
|
||||
self.txt_dlpc.setMinimumHeight(230)
|
||||
body.addWidget(self.txt_dlpc)
|
||||
self._dlpc_log("帧格式: D5 01 LEN <命令> CRC / B5 TYPE LEN <payload> CRC")
|
||||
layout.addWidget(card)
|
||||
|
||||
card, body = self._card("电源检查 · PD 自检 / 功率读数", self)
|
||||
self.chip_pd = QLabel("PD", card)
|
||||
self.chip_fpdo = QLabel("FPDO: —", card)
|
||||
self.var_pwr = "电压: — 电流: — 功率: —"
|
||||
self.box_pwr = QLabel(self.var_pwr, card)
|
||||
for widget in (self.chip_pd, self.chip_fpdo, self.box_pwr):
|
||||
widget.setMinimumHeight(30)
|
||||
widget.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
||||
widget.setStyleSheet(f"background: {CHIP_IDLE}; color: white; border-radius: 6px; padding: 4px 8px;")
|
||||
row = QHBoxLayout()
|
||||
row.addWidget(self._button("电源自检", self._pd_selftest))
|
||||
row.addWidget(self.chip_pd)
|
||||
row.addWidget(self.chip_fpdo, 1)
|
||||
body.addLayout(row)
|
||||
row = QHBoxLayout()
|
||||
row.addWidget(self._button("功率检测", self._pwr_query))
|
||||
row.addWidget(self.box_pwr, 1)
|
||||
body.addLayout(row)
|
||||
layout.addWidget(card)
|
||||
|
||||
card, body = self._card("光机命令 · 预设 / 自定义 / 温度", self)
|
||||
row = QHBoxLayout()
|
||||
self.var_current = BodyLabel("电流: —", card)
|
||||
row.addWidget(self.var_current)
|
||||
row.addStretch(1)
|
||||
row.addWidget(self._button("查电流", self._query_current))
|
||||
body.addLayout(row)
|
||||
preset_row = QHBoxLayout()
|
||||
self._dlpc_presets = [(n, cmd) for n, cmd, _ in dlpc_rs485.PRESETS]
|
||||
self.cbo_dlpc_cmd = ComboBox(card)
|
||||
self.cbo_dlpc_cmd.addItems([f"{n} {cmd}" for n, cmd in self._dlpc_presets])
|
||||
preset_row.addWidget(BodyLabel("预设:", card))
|
||||
preset_row.addWidget(self.cbo_dlpc_cmd, 1)
|
||||
preset_row.addWidget(self._button("发送", self._dlpc_send_preset, True))
|
||||
preset_row.addWidget(self._button("查 DMD 温度", self._query_dmd))
|
||||
self.var_dmd = BodyLabel("DMD: —", card)
|
||||
preset_row.addWidget(self.var_dmd)
|
||||
body.addLayout(preset_row)
|
||||
custom_row = QHBoxLayout()
|
||||
self.var_custom_cmd = LineEdit(card)
|
||||
self.var_custom_cmd.setPlaceholderText("如 M730S0T2000")
|
||||
self.var_custom_cmd.returnPressed.connect(self._dlpc_send_custom)
|
||||
custom_row.addWidget(BodyLabel("自定义:", card))
|
||||
custom_row.addWidget(self.var_custom_cmd, 1)
|
||||
custom_row.addWidget(self._button("发送", self._dlpc_send_custom))
|
||||
custom_row.addWidget(self._button("查 LAMP 温度", self._query_lamp))
|
||||
self.var_lamp = BodyLabel("LAMP: —", card)
|
||||
custom_row.addWidget(self.var_lamp)
|
||||
body.addLayout(custom_row)
|
||||
layout.addWidget(card)
|
||||
|
||||
card, body = self._card("快捷滑条 · 光强 / 定时投光 / TEC 温度", self)
|
||||
self.sld_light, self.ent_light = self._slider_row(body, "光强 (54-800)", 54, 800, 100, self._dlpc_send_light, "下发电流值")
|
||||
self.sld_timer, self.ent_timer = self._slider_row(body, "定时投光 (毫秒)", 0, 32700, 2000, self._dlpc_send_timer, "开光机(定时)")
|
||||
self.sld_tec, self.ent_tec = self._slider_row(body, "TEC 凝露点 (0.1°C)", 200, 350, 330, self._dlpc_send_tec, "下发温度值")
|
||||
layout.addWidget(card)
|
||||
|
||||
def _slider_row(self, layout, text, low, high, value, callback, button_text):
|
||||
row = QHBoxLayout()
|
||||
row.addWidget(BodyLabel(text, self))
|
||||
slider = Slider(Qt.Horizontal, self)
|
||||
slider.setRange(low, high)
|
||||
slider.setValue(value)
|
||||
entry = LineEdit(self)
|
||||
entry.setText(str(value))
|
||||
entry.setFixedWidth(65)
|
||||
slider.valueChanged.connect(lambda v: entry.setText(str(v)))
|
||||
entry.returnPressed.connect(lambda: self._sync_slider(slider, entry, low, high))
|
||||
row.addWidget(slider, 1)
|
||||
row.addWidget(entry)
|
||||
row.addWidget(self._button(button_text, callback))
|
||||
layout.addLayout(row)
|
||||
return slider, entry
|
||||
|
||||
@staticmethod
|
||||
def _sync_slider(slider, entry, low, high):
|
||||
try:
|
||||
value = int(entry.text())
|
||||
except ValueError:
|
||||
value = slider.value()
|
||||
value = max(low, min(high, value))
|
||||
slider.setValue(value)
|
||||
entry.setText(str(value))
|
||||
|
||||
def _set_theme(self, value):
|
||||
app = QApplication.instance()
|
||||
setTheme({"Dark": Theme.DARK, "Light": Theme.LIGHT, "System": Theme.AUTO}[value])
|
||||
if value == "System":
|
||||
app.setPalette(self._system_palette)
|
||||
app.setStyleSheet(self._system_style)
|
||||
is_dark = app.palette().color(QPalette.Window).value() < 128
|
||||
else:
|
||||
is_dark = value == "Dark"
|
||||
self._apply_palette(is_dark)
|
||||
self._apply_native_titlebar(is_dark)
|
||||
|
||||
def _apply_palette(self, is_dark):
|
||||
"""Keep native Qt widgets and Fluent cards on the same surface colors."""
|
||||
app = QApplication.instance()
|
||||
palette = QPalette()
|
||||
if is_dark:
|
||||
window, base, text, border = "#202020", "#2b2b2b", "#f5f5f5", "#3a3a3a"
|
||||
accent = "#00c8d7"
|
||||
else:
|
||||
window, base, text, border = "#f4f6f8", "#ffffff", "#1f2328", "#d8dee4"
|
||||
accent = "#0f6cbd"
|
||||
for role in (QPalette.Window, QPalette.Base, QPalette.AlternateBase):
|
||||
palette.setColor(role, QColor(window if role == QPalette.Window else base))
|
||||
palette.setColor(QPalette.Text, QColor(text))
|
||||
palette.setColor(QPalette.WindowText, QColor(text))
|
||||
palette.setColor(QPalette.Button, QColor(base))
|
||||
palette.setColor(QPalette.ButtonText, QColor(text))
|
||||
palette.setColor(QPalette.PlaceholderText, QColor("#8b949e" if not is_dark else "#8a8a8a"))
|
||||
palette.setColor(QPalette.Highlight, QColor(accent))
|
||||
palette.setColor(QPalette.HighlightedText, QColor("#ffffff"))
|
||||
palette.setColor(QPalette.ToolTipBase, QColor(base))
|
||||
palette.setColor(QPalette.ToolTipText, QColor(text))
|
||||
app.setPalette(palette)
|
||||
app.setStyleSheet(f"""
|
||||
QMainWindow, QWidget#root {{ background-color: {window}; color: {text}; }}
|
||||
QScrollArea, QScrollArea > QWidget > QWidget {{ background: transparent; border: 0; }}
|
||||
CardWidget {{ background-color: {base}; border: 1px solid {border}; border-radius: 8px; }}
|
||||
QPlainTextEdit, QTextEdit, QLineEdit, QComboBox {{
|
||||
background-color: {base}; color: {text};
|
||||
border: 1px solid {border}; border-radius: 5px;
|
||||
selection-background-color: {accent};
|
||||
}}
|
||||
QToolButton {{ color: {text}; }}
|
||||
""")
|
||||
|
||||
def _apply_native_titlebar(self, is_dark):
|
||||
if os.name != "nt" or not hasattr(ctypes, "windll"):
|
||||
return
|
||||
try:
|
||||
hwnd = int(self.winId())
|
||||
dwm = ctypes.windll.dwmapi
|
||||
dark = ctypes.c_int(1 if is_dark else 0)
|
||||
dwm.DwmSetWindowAttribute(hwnd, 20, ctypes.byref(dark), ctypes.sizeof(dark))
|
||||
color = QColor("#202020" if is_dark else "#f4f6f8")
|
||||
colorref = ctypes.c_int(color.red() | (color.green() << 8) | (color.blue() << 16))
|
||||
text = QColor("#f5f5f5" if is_dark else "#1f2328")
|
||||
textref = ctypes.c_int(text.red() | (text.green() << 8) | (text.blue() << 16))
|
||||
dwm.DwmSetWindowAttribute(hwnd, 35, ctypes.byref(colorref), ctypes.sizeof(colorref))
|
||||
dwm.DwmSetWindowAttribute(hwnd, 36, ctypes.byref(textref), ctypes.sizeof(textref))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _cur_display(self):
|
||||
try:
|
||||
return self.displays[self._disp_labels.index(self.cbo_display.currentText())]
|
||||
except (ValueError, IndexError, AttributeError):
|
||||
return None
|
||||
|
||||
def _load_displays(self):
|
||||
previous = self._cur_label
|
||||
try:
|
||||
self.displays = display_win.get_displays()
|
||||
except Exception as exc:
|
||||
self.displays = []
|
||||
self.notice("显示器枚举失败", str(exc), error=True)
|
||||
self._disp_labels = [f"{d['device']} {d['adapter']} [{d['mon_name']}]" for d in self.displays]
|
||||
self.cbo_display.blockSignals(True)
|
||||
self.cbo_display.clear()
|
||||
self.cbo_display.addItems(self._disp_labels)
|
||||
if self._disp_labels:
|
||||
self.cbo_display.setCurrentText(previous if previous in self._disp_labels else self._disp_labels[0])
|
||||
self.cbo_display.blockSignals(False)
|
||||
if self._disp_labels:
|
||||
self._cur_label = self.cbo_display.currentText()
|
||||
self._on_select(self._cur_label)
|
||||
self.left_scroll.verticalScrollBar().setValue(0)
|
||||
self.right_scroll.verticalScrollBar().setValue(0)
|
||||
else:
|
||||
self.lbl_cur.setText("未找到活动显示器")
|
||||
self.txt_modes.clear(); self.txt_edid.clear(); self.txt_hex.clear()
|
||||
|
||||
def _on_select(self, _value=None):
|
||||
d = self._cur_display()
|
||||
if d is None:
|
||||
return
|
||||
self._cur_label = self.cbo_display.currentText()
|
||||
self.lbl_cur.setText(f"当前输出: {d['cur_mode']}")
|
||||
keys, self._modes = display_win.enum_modes(d['device'])
|
||||
self.cbo_mode.clear(); self.cbo_mode.addItems(keys)
|
||||
if d['cur_mode'] in self._modes:
|
||||
self.cbo_mode.setCurrentText(d['cur_mode'])
|
||||
if not d['edid']:
|
||||
self.txt_edid.setPlainText("EDID 读取失败(注册表无 EDID 数据)")
|
||||
self.txt_modes.clear(); self.txt_hex.clear(); return
|
||||
parsed = edid_parse.parse_edid(d['edid'])
|
||||
self.txt_edid.setPlainText("\n".join(f"{k:<16}{v}" for k, v in edid_parse.edid_info_lines(parsed)))
|
||||
lines = [f"{t['hact']}x{t['vact']} @{t['refresh']:g}Hz {t['clk_khz'] / 1000:.2f}MHz {'DTD 首选' if i == 0 else 'DTD'}" for i, t in enumerate(parsed['dtds'])]
|
||||
lines += [f"{m} [CEA-VIC]" for m in parsed['cea_video']]
|
||||
lines += [f"{m} [已建立时序]" for m in parsed['est']]
|
||||
lines += [f"{m} [标准时序]" for m in parsed['std']]
|
||||
self.txt_modes.setPlainText("\n".join(lines))
|
||||
self.txt_hex.setPlainText(edid_parse.hex_dump(d['edid']))
|
||||
|
||||
def _apply_mode(self):
|
||||
d = self._cur_display()
|
||||
if d is None or self.cbo_mode.currentText() not in self._modes:
|
||||
return
|
||||
device = d['device']
|
||||
if device not in self._orig_modes:
|
||||
current = display_win.DEVMODEW(); current.dmSize = ctypes.sizeof(current)
|
||||
if display_win.user32.EnumDisplaySettingsW(device, display_win.ENUM_CURRENT_SETTINGS, ctypes.byref(current)):
|
||||
self._orig_modes[device] = current
|
||||
ok, result = display_win.apply_mode(device, self._modes[self.cbo_mode.currentText()])
|
||||
if not ok:
|
||||
self.notice("切换失败", f"ChangeDisplaySettingsEx 返回码 {result}", error=True)
|
||||
return
|
||||
QTimer.singleShot(500, self._load_displays)
|
||||
|
||||
def _restore_mode(self):
|
||||
d = self._cur_display()
|
||||
if d is None: return
|
||||
mode = self._orig_modes.pop(d['device'], None)
|
||||
if mode is None:
|
||||
self.notice("无可恢复", "该显示器没有记录切改前的模式")
|
||||
return
|
||||
display_win.apply_mode(d['device'], mode)
|
||||
QTimer.singleShot(500, self._load_displays)
|
||||
|
||||
def _show_pattern(self, kind):
|
||||
d = self._cur_display()
|
||||
if d and d['rect']:
|
||||
close_qt_patterns(); PatternWindow(d['rect'], kind, self.cbo_area.currentText(), parent=self)
|
||||
|
||||
def _show_solid(self, color):
|
||||
self._show_pattern(color)
|
||||
|
||||
def _save_area(self, value):
|
||||
self.area = value
|
||||
config.save_slots(self.slots, value)
|
||||
|
||||
def _show_custom(self, index):
|
||||
slot = self.slots[index]
|
||||
if not slot['file']:
|
||||
self.notice("未配置图片", f"请先配置“{slot['name']}”的图片")
|
||||
return
|
||||
path = os.path.join(config.BASE_DIR, slot['file'])
|
||||
d = self._cur_display()
|
||||
if not os.path.exists(path):
|
||||
self.notice("图片不存在", path, error=True); return
|
||||
if d and d['rect']:
|
||||
close_qt_patterns(); PatternWindow(d['rect'], "", self.cbo_area.currentText(), path, self)
|
||||
|
||||
def _configure_slot(self, index):
|
||||
dialog = QDialog(self); dialog.setWindowTitle(f"配置自定义栏位 {index + 1}")
|
||||
form = QFormLayout(dialog)
|
||||
name = LineEdit(dialog); name.setText(self.slots[index]['name'])
|
||||
file_label = BodyLabel(self.slots[index]['file'] or "(未设置)", dialog)
|
||||
form.addRow("按钮名称", name); form.addRow("图片文件", file_label)
|
||||
buttons = QHBoxLayout(); choose = PushButton("选择图片...", dialog); clear = PushButton("清除图片", dialog); save = PrimaryPushButton("保存", dialog); cancel = PushButton("取消", dialog)
|
||||
buttons.addWidget(choose); buttons.addWidget(clear); buttons.addStretch(1); buttons.addWidget(save); buttons.addWidget(cancel); form.addRow(buttons)
|
||||
selected = [self.slots[index]['file']]
|
||||
def choose_file():
|
||||
path, _ = QFileDialog.getOpenFileName(dialog, "选择图片", config.BASE_DIR, f"图片文件 ({' '.join('*' + e for e in config.IMG_EXTS)})")
|
||||
if path:
|
||||
try: selected[0] = config.copy_image_into_script_dir(path); file_label.setText(selected[0])
|
||||
except OSError as exc: self.notice("复制图片失败", str(exc), error=True)
|
||||
choose.clicked.connect(choose_file); clear.clicked.connect(lambda: (selected.__setitem__(0, None), file_label.setText("(未设置)")))
|
||||
cancel.clicked.connect(dialog.reject)
|
||||
def save_slot():
|
||||
self.slots[index] = {'name': name.text().strip() or f'自定义{index + 1}', 'file': selected[0]}
|
||||
config.save_slots(self.slots, self.cbo_area.currentText()); self.slot_buttons[index].setText(self.slots[index]['name']); dialog.accept()
|
||||
save.clicked.connect(save_slot); dialog.exec()
|
||||
|
||||
def _show_about(self):
|
||||
QMessageBox.about(self, f"关于 {APP_NAME}", f"{APP_NAME} v{APP_VERSION}\n\nEDID / 测试图案 / RS485 光机控制\nPySide6-Fluent-Widgets UI\n\n{REPO_URL}")
|
||||
|
||||
def _show_edid_fields(self):
|
||||
d = self._cur_display()
|
||||
if d is None or not d['edid']:
|
||||
self.notice("无 EDID", "该显示器没有可解析的 EDID 数据"); return
|
||||
dialog = QDialog(self); dialog.setWindowTitle("EDID 逐字节解析"); dialog.resize(1120, 700)
|
||||
layout = QVBoxLayout(dialog); text = self._text(); text.setLineWrapMode(QPlainTextEdit.WidgetWidth)
|
||||
lines = []
|
||||
for off, hx, meaning, value, note in edid_parse.edid_field_rows(d['edid']):
|
||||
lines.append(f"{off} {meaning} {value}")
|
||||
if hx: lines.append(f" 原始: {hx}")
|
||||
if note: lines.append(f" {note}")
|
||||
text.setPlainText("\n".join(lines)); layout.addWidget(text); layout.addWidget(self._button("关闭", dialog.accept)); dialog.exec()
|
||||
|
||||
# ---------------- RS485 ----------------
|
||||
def _dlpc_on_frame(self, frame):
|
||||
if self._pd_seq: self._dlpc_log_q.put(('pdframe', frame))
|
||||
elif self._pwr_seq: self._dlpc_log_q.put(('pwrframe', frame))
|
||||
|
||||
def _dlpc_on_event(self, line):
|
||||
match = re.search(r'← okTL:(-?\d+)', line)
|
||||
if match and self._tl_order:
|
||||
which = self._tl_order.pop(0); self._dlpc_log_q.put(('temp', f"{which}: {int(match.group(1)) / 10:.1f}°C"))
|
||||
match = re.search(r'← okR(\d+)G(\d+)B(\d+)', line)
|
||||
if match: self._dlpc_log_q.put(('current', f"电流 R:{match.group(1)} G:{match.group(2)} B:{match.group(3)}"))
|
||||
self._dlpc_log_q.put(('event', line))
|
||||
|
||||
def _dlpc_on_status(self, message): self._dlpc_log_q.put(('status', message))
|
||||
def _query_current(self): self._dlpc_send('M732')
|
||||
def _query_dmd(self): self._tl_order.append('DMD'); self._dlpc_send('M201')
|
||||
def _query_lamp(self): self._tl_order.append('LAMP'); self._dlpc_send('M202')
|
||||
|
||||
def _poll_dlpc(self):
|
||||
try:
|
||||
while True:
|
||||
kind, value = self._dlpc_log_q.get_nowait()
|
||||
if kind == 'status':
|
||||
self.lbl_dlpc_st.setText(value)
|
||||
if value.startswith(('已断开', '串口丢失')): self._serial_reset_state()
|
||||
elif kind == 'temp': (self.var_dmd if value.startswith('DMD') else self.var_lamp).setText(value)
|
||||
elif kind == 'current': self.var_current.setText(value)
|
||||
elif kind == 'pdframe': self._pd_step_done(value.type_code == 0xF0, value)
|
||||
elif kind == 'pwrframe': self._pwr_step_done(value)
|
||||
else: self._dlpc_log(value)
|
||||
except queue.Empty: pass
|
||||
connected = self.dlpc.is_connected()
|
||||
if connected != self._dlpc_connected:
|
||||
self._dlpc_connected = connected; self.btn_dlpc.setText("断开" if connected else "连接")
|
||||
|
||||
def _dlpc_log(self, line):
|
||||
self.txt_dlpc.appendPlainText(time.strftime('%H:%M:%S ') + line)
|
||||
if self.txt_dlpc.blockCount() > 500:
|
||||
cursor = self.txt_dlpc.textCursor(); cursor.movePosition(cursor.Start); cursor.select(cursor.BlockUnderCursor); cursor.removeSelectedText(); cursor.deleteChar()
|
||||
self.txt_dlpc.verticalScrollBar().setValue(self.txt_dlpc.verticalScrollBar().maximum())
|
||||
|
||||
def _dlpc_clear(self): self.txt_dlpc.clear()
|
||||
def _dlpc_refresh(self):
|
||||
self._dlpc_ports = dlpc_rs485.DLPC485.list_ports_info(); self.cbo_dlpc.clear(); self.cbo_dlpc.addItems([label for _d, label in self._dlpc_ports])
|
||||
if self._dlpc_ports: self._dlpc_pick(config.SERIAL_CFG['port'] or self._dlpc_ports[0][0])
|
||||
def _dlpc_pick(self, device):
|
||||
for dev, label in self._dlpc_ports:
|
||||
if dev == device: self.cbo_dlpc.setCurrentText(label); return
|
||||
def _dlpc_selected_port(self):
|
||||
label = self.cbo_dlpc.currentText()
|
||||
for device, shown in self._dlpc_ports:
|
||||
if shown == label: return device
|
||||
return label.strip()
|
||||
def _dlpc_toggle(self):
|
||||
if self.dlpc.is_connected(): self.dlpc.close(); self._serial_reset_state(); return
|
||||
port = self._dlpc_selected_port()
|
||||
try: baud, bytesize, stopbits = int(self.cbo_baud.currentText()), int(self.cbo_databits.currentText()), int(self.cbo_stopbits.currentText())
|
||||
except ValueError: self.lbl_dlpc_st.setText("串口参数非法"); return
|
||||
parity = self.cbo_parity.currentText()[:1] or 'N'; config.SERIAL_CFG.update(port=port, baud=baud, bytesize=bytesize, parity=parity, stopbits=stopbits); config.save_slots(self.slots, self.cbo_area.currentText()); self.dlpc.open(port, baud, bytesize=bytesize, parity=parity, stopbits=stopbits)
|
||||
def _serial_reset_state(self):
|
||||
self._pd_seq.clear(); self._pwr_seq.clear(); self._tl_order.clear()
|
||||
for timer in (self._pd_timer, self._pwr_timer):
|
||||
if timer: timer.stop()
|
||||
self._pd_timer = self._pwr_timer = None
|
||||
def _dlpc_send(self, command):
|
||||
if not self.dlpc.is_connected(): self.lbl_dlpc_st.setText('未连接串口'); return
|
||||
if self._pd_seq or self._pwr_seq: self._dlpc_log(f'序列进行中, 命令已忽略: {command}'); return
|
||||
self.dlpc.send(command)
|
||||
def _dlpc_send_preset(self):
|
||||
label = self.cbo_dlpc_cmd.currentText()
|
||||
for name, command in self._dlpc_presets:
|
||||
if label.startswith(name + ' '): self._dlpc_send(command); return
|
||||
def _dlpc_send_custom(self):
|
||||
if self.var_custom_cmd.text().strip(): self._dlpc_send(self.var_custom_cmd.text().strip())
|
||||
|
||||
def _pd_selftest(self):
|
||||
if not self.dlpc.is_connected(): self.lbl_dlpc_st.setText('未连接串口'); return
|
||||
if self._pd_seq: return
|
||||
self._pd_seq = [('M301', 'pd'), ('M302', 'fpdo_ma'), ('M303', 'meas_mv')]; self._pd_values = {}; self._pd_chip_reset(); self._dlpc_log('电源自检: 开始'); self._pd_step_next()
|
||||
def _pd_chip_reset(self):
|
||||
self.chip_pd.setText('PD'); self.chip_pd.setStyleSheet(f'background: {CHIP_IDLE}; color: white; border-radius: 6px; padding: 4px 8px;'); self.chip_fpdo.setText('FPDO: —'); self.chip_fpdo.setStyleSheet(f'background: {CHIP_IDLE}; color: white; border-radius: 6px; padding: 4px 8px;')
|
||||
def _pd_step_next(self):
|
||||
if not self._pd_seq: self._pd_fpdo_verdict(); self._dlpc_log('电源自检: 完成'); return
|
||||
self.dlpc.send(self._pd_seq[0][0]); self._pd_timer = QTimer(self); self._pd_timer.setSingleShot(True); self._pd_timer.timeout.connect(lambda: self._pd_step_done(False, None)); self._pd_timer.start(3000)
|
||||
def _pd_step_done(self, ok, frame):
|
||||
if not self._pd_seq: return
|
||||
if self._pd_timer: self._pd_timer.stop(); self._pd_timer = None
|
||||
_command, kind = self._pd_seq.pop(0); raw = dlpc_rs485.payload_bytes(frame) if frame is not None else b''
|
||||
if ok and kind == 'pd':
|
||||
proto = raw.decode('ascii', 'replace').strip()
|
||||
good = proto in ('PD', 'EPR')
|
||||
self.chip_pd.setText(proto or 'PD')
|
||||
self.chip_pd.setStyleSheet(
|
||||
f'background: {CHIP_OK if good else CHIP_ERR}; color: white; '
|
||||
'border-radius: 6px; padding: 4px 8px;')
|
||||
elif ok and kind == 'fpdo_ma' and len(raw) == 2: self._pd_values['ma'] = dlpc_rs485.payload_u16_be(raw)
|
||||
elif ok and kind == 'meas_mv' and len(raw) == 2: self._pd_values['mv'] = dlpc_rs485.payload_u16_be(raw)
|
||||
if ok: self._pd_step_next()
|
||||
else: self._pd_seq.clear(); self._pd_chip_error(kind); self._dlpc_log('电源自检: 应答错误/超时, 序列中止')
|
||||
def _pd_chip_error(self, kind):
|
||||
widget = self.chip_pd if kind == 'pd' else self.chip_fpdo
|
||||
if kind == 'pd': widget.setText('ERR')
|
||||
else: widget.setText('FPDO: ERR')
|
||||
widget.setStyleSheet(f'background: {CHIP_ERR}; color: white; border-radius: 6px; padding: 4px 8px;')
|
||||
def _pd_fpdo_verdict(self):
|
||||
mv, ma = self._pd_values.get('mv'), self._pd_values.get('ma')
|
||||
if mv is None or ma is None: self._pd_chip_error('fpdo_ma'); return
|
||||
gear = next((g for g in (5, 9, 12, 15, 20, 28) if abs(mv - g * 1000) <= g * 50), None); text = 'FPDO: ERR' if gear is None else f'FPDO: {gear}V@{ma / 1000:g}A'; self.chip_fpdo.setText(text); good = gear == 12 and ma >= 3000; self.chip_fpdo.setStyleSheet(f'background: {CHIP_OK if good else CHIP_ERR}; color: white; border-radius: 6px; padding: 4px 8px;')
|
||||
|
||||
def _pwr_query(self):
|
||||
if not self.dlpc.is_connected(): self.lbl_dlpc_st.setText('未连接串口'); return
|
||||
if self._pwr_seq: return
|
||||
self._pwr_seq = [('M303', 'mv'), ('M304', 'ma'), ('M305', 'mw')]; self._pwr_values = {}; self._dlpc_log('功率检测: 开始'); self._pwr_step_next()
|
||||
def _pwr_step_next(self):
|
||||
if not self._pwr_seq: self._pwr_update(self._pwr_values.get('mv'), self._pwr_values.get('ma'), self._pwr_values.get('mw')); self._dlpc_log('功率检测: 完成'); return
|
||||
self.dlpc.send(self._pwr_seq[0][0]); self._pwr_timer = QTimer(self); self._pwr_timer.setSingleShot(True); self._pwr_timer.timeout.connect(lambda: self._pwr_step_done(None)); self._pwr_timer.start(3000)
|
||||
def _pwr_step_done(self, frame):
|
||||
if not self._pwr_seq: return
|
||||
if self._pwr_timer: self._pwr_timer.stop(); self._pwr_timer = None
|
||||
_command, kind = self._pwr_seq.pop(0); raw = dlpc_rs485.payload_bytes(frame) if frame is not None else b''; want = {'mv': 2, 'ma': 2, 'mw': 4}[kind]
|
||||
if frame is None or frame.type_code != 0xF0 or len(raw) != want: self._pwr_seq.clear(); self.box_pwr.setText('电压: ERR 电流: — 功率: —'); return
|
||||
self._pwr_values[kind] = {'mv': dlpc_rs485.payload_u16_be, 'ma': dlpc_rs485.payload_s16_be, 'mw': dlpc_rs485.payload_u32_be}[kind](raw); self._pwr_step_next()
|
||||
def _pwr_update(self, mv, ma, mw):
|
||||
voltage = f'{mv / 1000:.2f} V' if mv is not None else '—'
|
||||
current = f'{ma} mA' if ma is not None else '—'
|
||||
power = f'{mw / 1000:.2f} W' if mw is not None else '—'
|
||||
self.box_pwr.setText(f'电压: {voltage} 电流: {current} 功率: {power}')
|
||||
def _dlpc_send_light(self): self._sync_slider(self.sld_light, self.ent_light, 54, 800); self._dlpc_send(f'M731S{self.sld_light.value()}')
|
||||
def _dlpc_send_timer(self): self._sync_slider(self.sld_timer, self.ent_timer, 0, 32700); self._dlpc_send('M730S0' if self.sld_timer.value() <= 0 else f'M730S0T{self.sld_timer.value()}')
|
||||
def _dlpc_send_tec(self): self._sync_slider(self.sld_tec, self.ent_tec, 200, 350); self._dlpc_send(f'M8S{self.sld_tec.value()}')
|
||||
|
||||
def notice(self, title, text, error=False):
|
||||
box = QMessageBox(self); box.setWindowTitle(title); box.setText(text); box.setIcon(QMessageBox.Critical if error else QMessageBox.Information); box.exec()
|
||||
|
||||
def closeEvent(self, event):
|
||||
close_qt_patterns()
|
||||
self._serial_reset_state()
|
||||
try: self.dlpc.close()
|
||||
except Exception: pass
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def run_gui(auto_close=0):
|
||||
if not _single_instance():
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
QMessageBox.information(None, "DLPilot 已在运行", "检测到另一个 DLPilot 实例正在运行。")
|
||||
return
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
app.setApplicationName(APP_NAME)
|
||||
setTheme(Theme.AUTO)
|
||||
window = App(auto_close)
|
||||
window.show()
|
||||
app.exec()
|
||||
Reference in New Issue
Block a user