feat(HDMI-Tool): 光机串口参数可选(默认8-N-1) + TEC 控制与温度读取
This commit is contained in:
+3
-1
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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]):
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user