feat(HDMI-Tool): 光机串口参数可选(默认8-N-1) + TEC 控制与温度读取

This commit is contained in:
2026-09-12 02:07:26 +08:00
parent 96df14caea
commit e131646156
7 changed files with 136 additions and 43 deletions
+3 -1
View File
@@ -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
+10 -8
View File
@@ -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. 典型测试场景
Binary file not shown.
+10 -8
View File
@@ -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. 典型测试场景
+79 -18
View File
@@ -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('<Return>', 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('<Return>', 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('<Return>', 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:
+11 -1
View File
@@ -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]):
+23 -7
View File
@@ -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: