From cca7cbc9621e81ca7a9a9d5a89aa08024a101aa8 Mon Sep 17 00:00:00 2001 From: hulk Date: Sat, 12 Sep 2026 02:07:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(HDMI-Tool):=20=E5=85=89=E6=9C=BA=E4=B8=B2?= =?UTF-8?q?=E5=8F=A3=E5=8F=82=E6=95=B0=E5=8F=AF=E9=80=89(=E9=BB=98?= =?UTF-8?q?=E8=AE=A48-N-1)=20+=20TEC=20=E6=8E=A7=E5=88=B6=E4=B8=8E?= =?UTF-8?q?=E6=B8=A9=E5=BA=A6=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HDMI-Tool/AGENTS.md | 4 +- HDMI-Tool/README.md | 18 ++++--- HDMI-Tool/release/README.md | 18 ++++--- HDMI-Tool/src/app_gui.py | 97 ++++++++++++++++++++++++++++++------- HDMI-Tool/src/config.py | 12 ++++- HDMI-Tool/src/dlpc_rs485.py | 30 +++++++++--- 6 files changed, 136 insertions(+), 43 deletions(-) diff --git a/HDMI-Tool/AGENTS.md b/HDMI-Tool/AGENTS.md index f5bb745..a0f535f 100644 --- a/HDMI-Tool/AGENTS.md +++ b/HDMI-Tool/AGENTS.md @@ -84,7 +84,9 @@ HDMI-Tool/ 与 I2C-Inject 的 Injector 同源);退出路径(关窗与 `--seconds` 自动关)都会走到 `App.destroy()`,在那里 `dlpc.close()` 收线程。 11. **pattern_slots.json 格式**:`{'area', 'slots', 'serial'}` 三键共存;load 兼容旧版 - 纯数组(无 area)与无 serial 键两种历史格式,save 永远写全三键。 + 纯数组(无 area)与无 serial 键两种历史格式,save 永远写全三键。`serial` 子键为 + `{port, baud, bytesize(8/7/6/5), parity(N/E/O), stopbits(1/2)}`,旧配置缺子键按 + 默认 8-N-1 补齐。 12. **GUI = app_gui.py 双栏仪表盘**(规格:`HDMI-Tool/UI参考布局.svg`):左列 HDMI 源端 五卡(显示器/模式/EDID 信息/测试图案/hex),右列光机四卡(串口/数据/命令/滑条), 顶栏 Dark/Light/System 切换(ttk.Treeview 颜色随动)。打包必须 `--collect-all diff --git a/HDMI-Tool/README.md b/HDMI-Tool/README.md index 602ea1a..1e0f5e4 100644 --- a/HDMI-Tool/README.md +++ b/HDMI-Tool/README.md @@ -50,21 +50,23 @@ HDMI-Tool/ ``` 左列(HDMI 源端) 右列(光机控制 RS485) -┌ 显示器选择区域 [刷新] ┐ ┌ 串口选择 + 波特率 / 连接状态 ┐ +┌ 显示器选择区域 [刷新] ┐ ┌ 串口选择 + 波特率/8-N-1 参数/连接 ┐ ├ 支持的模式: 分辨率[应用][恢复] + 明细 ┐ ├ 串口数据 — 收发日志(译码+hex) ┤ -├ EDID 信息(表格) ┘ ├ 光机命令 — 快捷按钮 │ -├ 测试图案 / 纯色 / 自定义 / 测试卡区域 │ 预设下拉 / 自定义命令输入 │ -├ EDID 原始数据 [逐字节解析] ┘ └ 快捷滑条 — 光强 / 定时投光(秒) ┘ +├ EDID 信息(表格) ┘ ├ 光机命令 — 快捷按钮+TEC │ +├ 测试图案 / 纯色 / 自定义 / 测试卡区域 │ 预设下拉 / 自定义 / 温度读取 │ +├ EDID 原始数据 [逐字节解析] ┘ └ 快捷滑条 — 光强/定时/TEC温度 ┘ ``` 1. 左上显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举; 2. **切分辨率**:"支持的模式"卡片里选模式 → "应用";测完点"恢复"; 3. **投测试图**:"测试图案"卡片先选**测试卡区域**(640x360 / 800x600 / full)→ 点图案按钮全屏投放,按 **ESC** 或点击退出;R/G/B 纯色按钮(55/AA/FF,按通道着色) - 用于 RGB 位交换类测试;自定义图片点"⚙"配置(图片自动复制到 exe 旁); -4. **光机控制**:右列选串口 → 连接 → 快捷按钮/预设下拉/自定义输入(回车即发); - 快捷滑条直接拖光强后"下发 M731S",拖秒数后"开光机(定时)"(0 秒=不限时 M730S0); -5. 测试卡区域、自定义栏位与串口记忆自动保存在 exe 旁的 `pattern_slots.json`。 + 用于 RGB 位交换类测试;自定义图片点齿轮按钮配置(图片自动复制到 exe 旁); +4. **光机控制**:右列选串口与参数(波特率/数据位/校验/停止位,默认 115200 8-N-1)→ 连接 → + 快捷按钮/预设下拉/自定义输入(回车即发);"查DMD温度""查LAMP温度"读温度; + 快捷滑条:光强拖动后"下发 M731S",定时秒数后"开光机(定时)"(0 秒=不限时 M730S0), + TEC 凝露点滑条/输入框(200-350 即 20.0-35.0°C)后"下发 M8S";数值框输入后回车自动限幅同步; +5. 测试卡区域、自定义栏位与串口记忆(含参数)自动保存在 exe 旁的 `pattern_slots.json`。 ## 4. 典型测试场景 diff --git a/HDMI-Tool/release/README.md b/HDMI-Tool/release/README.md index 602ea1a..1e0f5e4 100644 --- a/HDMI-Tool/release/README.md +++ b/HDMI-Tool/release/README.md @@ -50,21 +50,23 @@ HDMI-Tool/ ``` 左列(HDMI 源端) 右列(光机控制 RS485) -┌ 显示器选择区域 [刷新] ┐ ┌ 串口选择 + 波特率 / 连接状态 ┐ +┌ 显示器选择区域 [刷新] ┐ ┌ 串口选择 + 波特率/8-N-1 参数/连接 ┐ ├ 支持的模式: 分辨率[应用][恢复] + 明细 ┐ ├ 串口数据 — 收发日志(译码+hex) ┤ -├ EDID 信息(表格) ┘ ├ 光机命令 — 快捷按钮 │ -├ 测试图案 / 纯色 / 自定义 / 测试卡区域 │ 预设下拉 / 自定义命令输入 │ -├ EDID 原始数据 [逐字节解析] ┘ └ 快捷滑条 — 光强 / 定时投光(秒) ┘ +├ EDID 信息(表格) ┘ ├ 光机命令 — 快捷按钮+TEC │ +├ 测试图案 / 纯色 / 自定义 / 测试卡区域 │ 预设下拉 / 自定义 / 温度读取 │ +├ EDID 原始数据 [逐字节解析] ┘ └ 快捷滑条 — 光强/定时/TEC温度 ┘ ``` 1. 左上显示器下拉选择目标屏(多屏时注意别选成主屏),"刷新"重新枚举; 2. **切分辨率**:"支持的模式"卡片里选模式 → "应用";测完点"恢复"; 3. **投测试图**:"测试图案"卡片先选**测试卡区域**(640x360 / 800x600 / full)→ 点图案按钮全屏投放,按 **ESC** 或点击退出;R/G/B 纯色按钮(55/AA/FF,按通道着色) - 用于 RGB 位交换类测试;自定义图片点"⚙"配置(图片自动复制到 exe 旁); -4. **光机控制**:右列选串口 → 连接 → 快捷按钮/预设下拉/自定义输入(回车即发); - 快捷滑条直接拖光强后"下发 M731S",拖秒数后"开光机(定时)"(0 秒=不限时 M730S0); -5. 测试卡区域、自定义栏位与串口记忆自动保存在 exe 旁的 `pattern_slots.json`。 + 用于 RGB 位交换类测试;自定义图片点齿轮按钮配置(图片自动复制到 exe 旁); +4. **光机控制**:右列选串口与参数(波特率/数据位/校验/停止位,默认 115200 8-N-1)→ 连接 → + 快捷按钮/预设下拉/自定义输入(回车即发);"查DMD温度""查LAMP温度"读温度; + 快捷滑条:光强拖动后"下发 M731S",定时秒数后"开光机(定时)"(0 秒=不限时 M730S0), + TEC 凝露点滑条/输入框(200-350 即 20.0-35.0°C)后"下发 M8S";数值框输入后回车自动限幅同步; +5. 测试卡区域、自定义栏位与串口记忆(含参数)自动保存在 exe 旁的 `pattern_slots.json`。 ## 4. 典型测试场景 diff --git a/HDMI-Tool/src/app_gui.py b/HDMI-Tool/src/app_gui.py index 3503d91..5c0e216 100644 --- a/HDMI-Tool/src/app_gui.py +++ b/HDMI-Tool/src/app_gui.py @@ -289,8 +289,18 @@ class App(ctk.CTk): values=('115200', '57600', '38400', '9600')) self.cbo_baud.set(str(config.SERIAL_CFG['baud'])) self.cbo_baud.pack(side='left', padx=6) - ctk.CTkLabel(row2, text='数据位 8 · 校验 N · 停止位 1(板卡固件固定)', - text_color='gray50').pack(side='left', padx=(0, 6)) + ctk.CTkLabel(row2, text='数据位:').pack(side='left') + self.cbo_databits = ctk.CTkComboBox(row2, width=62, values=('8', '7', '6', '5')) + self.cbo_databits.set(str(config.SERIAL_CFG['bytesize'])) + self.cbo_databits.pack(side='left', padx=(6, 0)) + ctk.CTkLabel(row2, text='校验:').pack(side='left', padx=(10, 0)) + self.cbo_parity = ctk.CTkComboBox(row2, width=54, values=('N', 'E', 'O')) + self.cbo_parity.set(str(config.SERIAL_CFG['parity'])) + self.cbo_parity.pack(side='left', padx=(6, 0)) + ctk.CTkLabel(row2, text='停止位:').pack(side='left', padx=(10, 0)) + self.cbo_stopbits = ctk.CTkComboBox(row2, width=54, values=('1', '2')) + self.cbo_stopbits.set(str(config.SERIAL_CFG['stopbits'])) + self.cbo_stopbits.pack(side='left', padx=(6, 0)) self.lbl_dlpc_st = ctk.CTkLabel(row2, text='未连接', text_color='gray60') self.lbl_dlpc_st.pack(side='left', padx=6) @@ -305,39 +315,48 @@ class App(ctk.CTk): '累加和校验; 全部命令见 README"光机控制"') # -- 光机命令 -- - card, c = self._card(col,'光机命令 — 预设与自定义') + card, c = self._card(col,'光机命令 — 预设 / 自定义 / TEC') card.grid(row=2, column=0, sticky='ew', pady=(10, 0)) row1 = ctk.CTkFrame(c, fg_color='transparent') row1.pack(fill='x') for name, cmd in (('开光机', 'M730S0'), ('关光机', 'M730S1'), - ('查电流', 'M732'), ('查版本', 'M999'), ('软件重启', 'M888')): - ctk.CTkButton(row1, text=name, width=88, + ('查电流', 'M732'), ('查版本', 'M999'), ('软件重启', 'M888'), + ('开TEC', 'M7S0'), ('关TEC', 'M7S1'), ('TEC33.0°C', 'M8S330')): + ctk.CTkButton(row1, text=name, width=68, command=lambda cc=cmd: self._dlpc_send(cc) - ).pack(side='left', padx=(0, 6)) + ).pack(side='left', padx=(0, 4)) row2 = ctk.CTkFrame(c, fg_color='transparent') row2.pack(fill='x', pady=(6, 0)) - ctk.CTkLabel(row2, text='预设:').pack(side='left') + lf2 = ctk.CTkFrame(row2, fg_color='transparent') + lf2.pack(side='left', fill='x', expand=True) + ctk.CTkLabel(lf2, text='预设:', width=52, anchor='w').pack(side='left') self._dlpc_presets = [(n, cmd) for n, cmd, _ in dlpc_rs485.PRESETS] self.cbo_dlpc_cmd = ctk.CTkComboBox( - row2, width=240, state='readonly', + lf2, state='readonly', values=[f'{n} {cmd}' for n, cmd in self._dlpc_presets]) self.cbo_dlpc_cmd.set(self._dlpc_presets[0][0] + ' ' + self._dlpc_presets[0][1]) - self.cbo_dlpc_cmd.pack(side='left', padx=6) - ctk.CTkButton(row2, text='发送', width=64, + self.cbo_dlpc_cmd.pack(side='left', fill='x', expand=True, padx=6) + ctk.CTkButton(lf2, text='发送', width=64, command=self._dlpc_send_preset).pack(side='left') + ctk.CTkButton(row2, text='查DMD温度', width=96, + command=lambda: self._dlpc_send('M201')).pack(side='right') row3 = ctk.CTkFrame(c, fg_color='transparent') row3.pack(fill='x', pady=(6, 0)) - ctk.CTkLabel(row3, text='自定义:').pack(side='left') + lf3 = ctk.CTkFrame(row3, fg_color='transparent') + lf3.pack(side='left', fill='x', expand=True) + ctk.CTkLabel(lf3, text='自定义:', width=52, anchor='w').pack(side='left') self.var_custom_cmd = tk.StringVar() - ent = ctk.CTkEntry(row3, width=180, textvariable=self.var_custom_cmd, + ent = ctk.CTkEntry(lf3, textvariable=self.var_custom_cmd, placeholder_text='如 M730S0T2000') - ent.pack(side='left', padx=6) + ent.pack(side='left', fill='x', expand=True, padx=6) ent.bind('', lambda _e: self._dlpc_send_custom()) - ctk.CTkButton(row3, text='发送', width=64, + ctk.CTkButton(lf3, text='发送', width=64, command=self._dlpc_send_custom).pack(side='left') + ctk.CTkButton(row3, text='查LAMP温度', width=96, + command=lambda: self._dlpc_send('M202')).pack(side='right') # -- 快捷滑条 -- - card, c = self._card(col,'快捷滑条 — 光强 / 定时投光') + card, c = self._card(col,'快捷滑条 — 光强 / 定时投光 / TEC 温度') card.grid(row=3, column=0, sticky='ew', pady=(10, 0)) rowa = ctk.CTkFrame(c, fg_color='transparent') rowa.pack(fill='x') @@ -369,6 +388,21 @@ class App(ctk.CTk): self.ent_timer.bind('', self._timer_entry_apply) ctk.CTkButton(rowb, text='开光机(定时)', width=110, command=self._dlpc_send_timer).pack(side='left', padx=(6, 0)) + rowc = ctk.CTkFrame(c, fg_color='transparent') + rowc.pack(fill='x', pady=(8, 0)) + ctk.CTkLabel(rowc, text='TEC凝露点(0.1°C):').pack(side='left') + self.var_tec = tk.StringVar(value='330') + sld_c = ctk.CTkSlider(rowc, from_=200, to=350, number_of_steps=150, + command=lambda v: self.var_tec.set(str(int(round(float(v)))))) + sld_c.set(330) + sld_c.pack(side='left', fill='x', expand=True, padx=8) + self.sld_tec = sld_c + self.ent_tec = ctk.CTkEntry(rowc, width=56, justify='center', + textvariable=self.var_tec) + self.ent_tec.pack(side='left') + self.ent_tec.bind('', self._tec_entry_apply) + ctk.CTkButton(rowc, text='下发 M8S', width=110, + command=self._dlpc_send_tec).pack(side='left', padx=(6, 0)) # ============================================================ # 显示器 / 分辨率 / EDID @@ -658,12 +692,16 @@ class App(ctk.CTk): return try: baud = int(self.cbo_baud.get()) + bytesize = int(self.cbo_databits.get()) + stopbits = int(self.cbo_stopbits.get()) except ValueError: - self.lbl_dlpc_st.configure(text='波特率非法', text_color='#E07A3F') + self.lbl_dlpc_st.configure(text='串口参数非法', text_color='#E07A3F') return - config.SERIAL_CFG['port'], config.SERIAL_CFG['baud'] = port, baud + parity = self.cbo_parity.get().strip().upper()[:1] or 'N' + config.SERIAL_CFG.update(port=port, baud=baud, bytesize=bytesize, + parity=parity, stopbits=stopbits) config.save_slots(self.slots, self.area_var.get()) # 记住本次串口 - self.dlpc.open(port, baud) + self.dlpc.open(port, baud, bytesize=bytesize, parity=parity, stopbits=stopbits) def _dlpc_send(self, cmd): if not self.dlpc.is_connected(): @@ -724,6 +762,29 @@ class App(ctk.CTk): return self._dlpc_send('M730S0' if v <= 0 else f'M730S0T{v * 1000}') + def _tec_entry_apply(self, _evt=None): + """TEC 温度输入框回车: 校验并限幅 200-350(0.1°C), 同步回滑条""" + try: + v = int(self.var_tec.get()) + except ValueError: + self.var_tec.set(str(int(float(self.sld_tec.get())))) + return + v = max(200, min(350, v)) + self.sld_tec.set(v) + self.var_tec.set(str(v)) + + def _dlpc_send_tec(self): + try: + v = int(self.var_tec.get()) + except ValueError: + messagebox.showinfo('TEC 温度非法', 'TEC 凝露点需为 200-350 的整数(0.1°C)') + return + if not 200 <= v <= 350: + messagebox.showinfo('TEC 温度越界', + 'TEC 凝露点范围 200-350(0.1°C), 越界固件会回 err:OL') + return + self._dlpc_send(f'M8S{v}') + def destroy(self): try: # 取消待触发的轮询回调, 避免销毁后 Tcl 报错 if self._poll_job: diff --git a/HDMI-Tool/src/config.py b/HDMI-Tool/src/config.py index 63d80e6..f7a2d9f 100644 --- a/HDMI-Tool/src/config.py +++ b/HDMI-Tool/src/config.py @@ -44,7 +44,8 @@ SLOT_CONFIG = os.path.join(BASE_DIR, 'pattern_slots.json') IMG_EXTS = ['.png', '.gif'] + (['.jpg', '.jpeg', '.bmp', '.webp'] if HAS_PIL else []) # 光机控制串口记忆(与栏位配置同存 pattern_slots.json 的 "serial" 键) -SERIAL_CFG = {'port': '', 'baud': dlpc_rs485.DEFAULT_BAUD} +SERIAL_CFG = {'port': '', 'baud': dlpc_rs485.DEFAULT_BAUD, + 'bytesize': 8, 'parity': 'N', 'stopbits': 1} def load_slots(): @@ -69,6 +70,15 @@ def load_slots(): SERIAL_CFG['baud'] = int(ser.get('baud') or dlpc_rs485.DEFAULT_BAUD) except (TypeError, ValueError): pass + try: + SERIAL_CFG['bytesize'] = int(ser.get('bytesize') or 8) + except (TypeError, ValueError): + pass + SERIAL_CFG['parity'] = str(ser.get('parity') or 'N').upper()[:1] + try: + SERIAL_CFG['stopbits'] = int(ser.get('stopbits') or 1) + except (TypeError, ValueError): + pass else: area, items = DEFAULT_AREA, data for i, item in enumerate(items[:SLOT_COUNT]): diff --git a/HDMI-Tool/src/dlpc_rs485.py b/HDMI-Tool/src/dlpc_rs485.py index dbae74b..5b7d90f 100644 --- a/HDMI-Tool/src/dlpc_rs485.py +++ b/HDMI-Tool/src/dlpc_rs485.py @@ -161,7 +161,8 @@ class DLPC485: def __init__(self, on_event, on_status): self.on_event, self.on_status = on_event, on_status self.q = queue.Queue() - self._target = None # 想态 (port, baud); None=断开 + self._target = None # 想态 (port, baud, bytesize, parity, stopbits) + self._open_key = None # 当前已打开串口的参数元组 self.ser = None self.parser = FrameParser() self._lock = threading.Lock() @@ -194,12 +195,13 @@ class DLPC485: out.append((dev, label)) return out - def open(self, port, baud=DEFAULT_BAUD): + def open(self, port, baud=DEFAULT_BAUD, bytesize=8, parity='N', stopbits=1): if not HAS_SERIAL: self.on_status('未安装 pyserial: py -m pip install pyserial') return with self._lock: - self._target = (port, int(baud)) + self._target = (port, int(baud), int(bytesize), + str(parity).upper(), int(stopbits)) def close(self): with self._lock: @@ -224,23 +226,37 @@ class DLPC485: self.q.put((cmd, frame)) def _run(self): + # 串口参数映射(pyserial 常量): 数据位/校验/停止位 + bytesizes = {8: serial.EIGHTBITS, 7: serial.SEVENBITS, + 6: serial.SIXBITS, 5: serial.FIVEBITS} + parities = {'N': serial.PARITY_NONE, 'E': serial.PARITY_EVEN, + 'O': serial.PARITY_ODD} + stopbits = {1: serial.STOPBITS_ONE, 2: serial.STOPBITS_TWO} while True: with self._lock: target = self._target - cur = (self.ser.port, self.ser.baudrate) if self.ser else None - if target != cur: # 端口/波特率变化或断开: 统一在此切换 + cur = self._open_key + if target != cur: if self.ser is not None: try: self.ser.close() except Exception: pass self.ser = None + self._open_key = None self.on_status('已断开') if target: + port, baud, dbsize, parity, sbits = target try: - self.ser = serial.Serial(target[0], target[1], timeout=0.15) + self.ser = serial.Serial( + port, baud, bytesize=bytesizes.get(dbsize, serial.EIGHTBITS), + parity=parities.get(parity, serial.PARITY_NONE), + stopbits=stopbits.get(sbits, serial.STOPBITS_ONE), + timeout=0.15) self.parser = FrameParser() - self.on_status(f'已连接 {target[0]} @ {target[1]}') + self._open_key = target + self.on_status(f'已连接 {port} @ {baud} ' + f'{dbsize}{parity}{sbits}') except Exception as e: self.ser = None with self._lock: