#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ HDMI EDID 查看器 + 测试图案投屏 + 光机控制 — 入口与命令行模式 ================================================ 配合 LT8619C(HDMI 转 RGB888)→ DLPC3421 转接板调试使用。 模块划分(重构自原单文件): app_fluent.py PySide6-Fluent-Widgets 主界面(双栏仪表盘) display_win.py Win32 显示器枚举/分辨率/拓扑 edid_parse.py EDID 1.4 / CEA-861 解析 patterns.py 测试图案全屏投屏窗口 config.py 运行目录与 pattern_slots.json 配置 dlpc_rs485.py 光机 RS485 协议与串口链路 依赖: CLI/EDID 纯标准库; GUI 需 customtkinter(py -m pip install customtkinter); 可选 pyserial(光机控制)与 Pillow(自定义图片 JPG/BMP/WebP)。 命令行模式(自动化测试用): python hdmi_gui.py 启动 GUI python hdmi_gui.py --dump 控制台输出 EDID 解算结果 python hdmi_gui.py --edid-fields 控制台输出 EDID 逐字节字段解析表 python hdmi_gui.py --test-pattern Checker --seconds 3 直接出图案, 3 秒自动退出 python hdmi_gui.py --test-pattern Blue --area 800x600 指定测试卡区域(full=铺满所选屏) python hdmi_gui.py --port COM9 --dlpc-cmd M730S0 --dlpc-cmd M999 RS485 逐条发光机命令并打印译码响应 python hdmi_gui.py --seconds 5 GUI 5 秒自动关闭(冒烟测试) """ import argparse import os import sys import time import dlpc_rs485 import display_win import edid_parse import patterns from config import (AREA_OPTIONS, BASE_DIR, DEFAULT_AREA, SLOT_COUNT, load_slots) from display_win import ensure_display_extend, get_displays from edid_parse import edid_field_text, edid_info_lines, hex_dump, parse_edid from patterns import PATTERN_NAMES, show_pattern, show_pattern_image def run_gui(auto_close=0): """启动 PySide6-Fluent-Widgets 主界面(实现在 app_fluent.py)""" import app_fluent app_fluent.run_gui(auto_close) def run_dump(edid_fields=False): displays = get_displays() if not displays: print('未找到活动显示器') return 1 for i, d in enumerate(displays, 1): print() print(f'== 显示器 {i} {d["device"]} 显卡[{d["adapter"]}] ' f'监视器[{d["mon_name"]}] 当前输出 {d["cur_mode"]}') if not d['edid']: print(' EDID 读取失败(注册表无 EDID 数据)') continue if edid_fields: print(edid_field_text(d['edid'])) continue e = parse_edid(d['edid']) for k, v in edid_info_lines(e): print(f' {k:<14}: {v}') print(' ---- 支持的模式 ----') for k, t in enumerate(e['dtds']): src = 'DTD1(首选)' if k == 0 else f'DTD{k + 1}' print(f" [{src}] {t['hact']}x{t['vact']}@{t['refresh']} " f"{t['clk_khz'] / 1000:.3f}MHz 总{t['htotal']}x{t['vtotal']}") for m in e['cea_video']: print(f' [CEA-VIC] {m}') for m in e['cea_blocks']: print(f' [CEA-块] {m}') for m in e['est']: print(f' [已建立] {m}') for m in e['std']: print(f' [标准] {m}') print(' ---- EDID 原始数据 ----') print(hex_dump(d['edid'])) return 0 def run_dlpc(port, baud, cmds): """RS485 直连模式: 逐条发送光机命令, 等响应打印译码结果(自动化脚本用)。 返回码: 任一命令非法/超时/响应非 0xF0 成功 -> 1, 全部成功 -> 0。 """ if not dlpc_rs485.HAS_SERIAL: print('错误: 未安装 pyserial, 请先执行 py -m pip install pyserial') return 2 try: ser = dlpc_rs485.serial.Serial(port, baud, timeout=0.1) except Exception as e: print(f'打开串口失败: {port} @ {baud}: {e}') return 1 print(f'已连接 {port} @ {baud} (RS485 半双工, 单发单收)') parser = dlpc_rs485.FrameParser() rc = 0 for cmd in cmds: try: frame = dlpc_rs485.build_frame(cmd) except ValueError as e: print(f'→ {cmd} 命令非法: {e}') rc = 1 continue ser.write(frame) print(f'→ {cmd} [{frame.hex(" ").upper()}]') deadline = time.time() + 35.0 # M737 定时投光中延迟应答(定时上限 32.7s) got = None while got is None and time.time() < deadline: data = ser.read(256) if data: got = next(iter(parser.feed(data)), None) if got is None: print('← (35s 超时无响应)') rc = 1 else: nice = dlpc_rs485.format_power(cmd, got) # 电源命令二进制载荷译码 print(f'← {nice or dlpc_rs485.describe(got)} [{got.raw.hex(" ").upper()}]') if got.type_code != 0xF0: rc = 1 ser.close() return rc def main(): ap = argparse.ArgumentParser(description='HDMI EDID 查看器 + 测试图案 + 光机控制') ap.add_argument('--dump', action='store_true', help='控制台输出 EDID 解算结果, 不开 GUI') ap.add_argument('--edid-fields', action='store_true', help='控制台输出 EDID 逐字节字段解析表(配合 --dump 风格, 不开 GUI); exe 版不支持') ap.add_argument('--test-pattern', choices=list(PATTERN_NAMES) + [f'Custom{i + 1}' for i in range(SLOT_COUNT)], default=None, help='直接全屏出测试图案(不开主窗口; CustomN 为自定义栏位)') ap.add_argument('--area', choices=list(AREA_OPTIONS), default=DEFAULT_AREA, help='测试卡区域: WxH 黑底居中 或 full 铺满所选显示器(默认 640x360)') ap.add_argument('--monitor', type=int, default=1, help='--test-pattern 用的显示器序号(默认 1)') ap.add_argument('--seconds', type=int, default=0, help='自动退出秒数(GUI/图案冒烟测试)') ap.add_argument('--port', default=None, help='光机 RS485 串口(如 COM9), 配合 --dlpc-cmd') ap.add_argument('--baud', type=int, default=dlpc_rs485.DEFAULT_BAUD, help='光机串口波特率(默认 115200)') ap.add_argument('--dlpc-cmd', action='append', default=None, dest='dlpc_cmds', metavar='CMD', help='RS485 发送光机命令(可多次), 如 --dlpc-cmd M730S0 --dlpc-cmd M999') args = ap.parse_args() # LT8619C 板卡热插拔后可能停留在"仅电脑屏幕"拓扑: 仅当检测到转接板 # (EDID 厂商 XLS)接入但未进桌面时才强制扩展, 其余拓扑不动(失败静默) ensure_display_extend() if args.dump or args.edid_fields: if getattr(sys, 'frozen', False): # --noconsole 打包的 exe 没有控制台可写, 引导用户用源码方式 import tkinter as tk from tkinter import messagebox r = tk.Tk() r.withdraw() messagebox.showinfo('不支持', 'exe 版本无控制台输出。\n请用源码方式运行:\n' 'python hdmi_gui.py --edid-fields') sys.exit(1) sys.exit(run_dump(edid_fields=args.edid_fields)) if args.dlpc_cmds: if not args.port: print('错误: --dlpc-cmd 需要配合 --port 指定串口(如 --port COM9)') sys.exit(2) sys.exit(run_dlpc(args.port, args.baud, args.dlpc_cmds)) if args.test_pattern: displays = get_displays() if not displays: print('未找到活动显示器') sys.exit(1) d = displays[min(args.monitor, len(displays)) - 1] if not d['rect']: print('无法获取显示器矩形') sys.exit(1) import tkinter as tk root = tk.Tk() root.withdraw() if args.test_pattern.startswith('Custom'): slots, _ = load_slots() slot = slots[int(args.test_pattern[6:]) - 1] if not slot['file']: print(f"错误: {args.test_pattern}({slot['name']})未配置图片") sys.exit(1) show_pattern_image(root, d['rect'], os.path.join(BASE_DIR, slot['file']), args.seconds, area=args.area) else: show_pattern(root, d['rect'], args.test_pattern, args.seconds, area=args.area) sys.exit(0) run_gui(args.seconds) if __name__ == '__main__': main()