Files
pqAutomationApp/test.py
2026-07-07 18:59:44 +08:00

1010 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
HDR10 信号输出验证脚本UCD-323 HDMI Source + SolidColor + sdpg
通过 SolidColor 白场065535默认 65535并配合 sdpg InfoFrame
验证 DUT 能否进入 HDR 模式并输出峰值亮度。
用法(在项目根目录)::
python test.py
python test.py --device 0
python test.py --device ABCD1234 --max-cll 1000 --max-fall 400 --hold 60
python test.py --image path/to/white.png
python test.py --save-bin drm_hdr10.bin
验收:脚本发出 100% 白场后,用 CA-410 测 DUT 峰值亮度。
若仍约 100200 nitsSDR 水平),说明信令或 DUT 设置未生效。
"""
from __future__ import annotations
import argparse
import sys
import tempfile
import time
from ctypes import c_uint32
from pathlib import Path
from PIL import Image
import UniTAP
import UniTAP.libs.lib_tsi.tsi_types as tsi_ci
from UniTAP.common.timing import Timing
from app.ucd.pg_status import read_pattern_generator_status_lines
# 与 pq_config 默认一致DMT 1080p60勿用 CTA VIC16 代替,时序参数不同)
DEFAULT_WIDTH = 1920
DEFAULT_HEIGHT = 1080
DEFAULT_REFRESH_HZ = 60.0
DEFAULT_STANDARD = "dmt"
DEFAULT_MAX_CLL = 1000
DEFAULT_MAX_FALL = 400
DEFAULT_HOLD_SECONDS = 60
from app.solid_color_scale import SOLID_COLOR_MAX, SOLID_COLOR_MIN
SOLID_WHITE_PEAK = 65535
PACKET_PAD_BYTES = 36
# work_with_packet_generation.py 内联 VSIFHDR10+type 0x81
OFFICIAL_HDR10PLUS_VSIF = bytearray([
0x81, 0x01, 0x1B, 0x16, 0x8B, 0x84, 0x90, 0x40, 0x00, 0x00, 0x06, 0x64,
0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
])
def xy_to_be16(value: float) -> tuple[int, int]:
"""CIE xy → 大端 uint16单位 0.00002)。"""
u = int(round(value / 0.00002))
u = max(0, min(u, 0xFFFF))
return (u >> 8) & 0xFF, u & 0xFF
def build_infoframe_packet(
frame_type: int,
version: int,
body: bytearray,
*,
pad_to: int = PACKET_PAD_BYTES,
) -> bytearray:
"""构造 HDMI InfoFrame 包header + body + 可选 padding"""
header = bytearray([frame_type, version, len(body), 0x00])
header[3] = (256 - (sum(header[:3]) + sum(body)) % 256) & 0xFF
packet = header + body
if len(packet) < pad_to:
packet.extend(b"\x00" * (pad_to - len(packet)))
return packet
def build_avi_bt2020_packet(vic: int = 16, *, full_range: bool = True) -> bytearray:
"""AVI InfoFrame(0x82)RGB / BT.2020 / Full / 指定 VIC。"""
body = bytearray(13)
body[0] = 0x00 # RGB, no scan info
body[1] = (0x03 << 6) | (0x02 << 4) # Extended colorimetry, 16:9
q = 0x01 if full_range else 0x00
body[2] = (0x06 << 4) | (q << 2) # BT.2020, full range
body[3] = vic & 0x7F
return build_infoframe_packet(0x82, 0x02, body)
def build_drm_hdr10_packet(
max_cll: int = DEFAULT_MAX_CLL,
max_fall: int = DEFAULT_MAX_FALL,
max_master: int = DEFAULT_MAX_CLL,
min_master_nits: float = 0.005,
pad_to: int = PACKET_PAD_BYTES,
) -> bytearray:
"""构造 HDR10 静态 DRM InfoFrameType 0x87, Static Metadata Type 1"""
body = bytearray(26)
body[0] = 0x04 # SMPTE ST 2084 (PQ)
body[1] = 0x01 # Static Metadata Type 1
primaries = [
(0.708, 0.292), # R BT.2020
(0.170, 0.797), # G
(0.131, 0.046), # B
(0.3127, 0.3290), # W D65
]
for i, (x, y) in enumerate(primaries):
o = 2 + i * 4
body[o], body[o + 1] = xy_to_be16(x)
body[o + 2], body[o + 3] = xy_to_be16(y)
body[18:20] = [(max_master >> 8) & 0xFF, max_master & 0xFF]
min_code = int(round(min_master_nits / 0.0001))
body[20:22] = [(min_code >> 8) & 0xFF, min_code & 0xFF]
body[22:24] = [(max_cll >> 8) & 0xFF, max_cll & 0xFF]
body[24:26] = [(max_fall >> 8) & 0xFF, max_fall & 0xFF]
header = bytearray([0x87, 0x01, 0x1A, 0x00])
header[3] = (256 - (sum(header[:3]) + sum(body)) % 256) & 0xFF
packet = header + body
if len(packet) < pad_to:
packet.extend(b"\x00" * (pad_to - len(packet)))
return packet
def build_sdpg_packet_list(
args,
*,
drm: bytearray,
vsif_path: Path,
vic: int,
) -> tuple[list[bytearray], str]:
"""组装 sdpg 待发 InfoFrame 列表(可多包)。"""
if args.packet in ("official-vsif", "vendor-vsif"):
if args.packet == "official-vsif":
data = bytearray(OFFICIAL_HDR10PLUS_VSIF)
label = "official VSIF HDR10+ (0x81, 同官方示例)"
else:
data = bytearray(vsif_path.read_bytes())
label = f"vendor VSIF (HDR10+) {vsif_path.name}"
return [data], label
packets: list[bytearray] = []
labels: list[str] = []
if args.packet == "full":
packets.append(bytearray(OFFICIAL_HDR10PLUS_VSIF))
labels.append("VSIF HDR10+ (0x81)")
if not args.no_avi:
packets.append(build_avi_bt2020_packet(vic))
labels.append(f"AVI BT.2020 VIC{vic}")
packets.append(drm)
labels.append("DRM HDR10 (0x87)")
return packets, " + ".join(labels)
def verify_drm_checksum(packet: bytearray) -> bool:
"""校验 InfoFrame checksumtype + version + length + payload + checksum ≡ 0 mod 256"""
if len(packet) < 30 or packet[0] != 0x87:
return False
length = packet[2]
end = 4 + length
if end > len(packet):
return False
return sum(packet[0:3] + packet[4:end] + packet[3:4]) % 256 == 0
def format_packet_hex(packet: bytes, bytes_per_line: int = 16) -> str:
lines = []
for i in range(0, len(packet), bytes_per_line):
chunk = packet[i : i + bytes_per_line]
hex_part = " ".join(f"{b:02X}" for b in chunk)
lines.append(f" {i:04X}: {hex_part}")
return "\n".join(lines)
def resolve_timing(
role,
*,
width: int = DEFAULT_WIDTH,
height: int = DEFAULT_HEIGHT,
refresh_hz: float = DEFAULT_REFRESH_HZ,
standard: str = DEFAULT_STANDARD,
vic: int | None = None,
) -> Timing:
"""按分辨率查找 timing默认 DMT 1080p60与主程序 pq_config 一致)。"""
mgr = role.hdtx.pg.timing_manager
f_rate = int(round(refresh_hz * 1000))
if vic is not None:
timing = mgr.get_cta(vic)
if timing is not None:
return timing
raise RuntimeError(f"设备不支持 CTA VIC={vic}")
std_map = {
"dmt": Timing.Standard.SD_DMT,
"cta": Timing.Standard.SD_CTA,
"cvt": Timing.Standard.SD_CVT,
}
std = std_map.get(standard.lower())
if std is None:
raise ValueError(f"未知 standard={standard},可选 dmt/cta/cvt")
timing = mgr.search(
h_active=width,
v_active=height,
f_rate=f_rate,
standard=std,
)
if timing is not None:
return timing
hint = mgr.print_all()
raise RuntimeError(
f"未找到 timing: {standard.upper()} {width}x{height}@{refresh_hz}Hz\n"
f"可用列表:\n{hint}"
)
def build_video_mode(
timing: Timing,
*,
content_mode: str = "sdr",
bpc: int = 10,
) -> UniTAP.VideoMode:
"""ImageFile 等自定义 pattern 用。"""
color = UniTAP.ColorInfo()
color.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA
if content_mode == "hdr-content":
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB
color.bpc = bpc
else:
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
color.bpc = 8
return UniTAP.VideoMode(timing=timing, color_info=color)
def build_solid_video_mode(
timing: Timing,
*,
content_mode: str = "sdr",
bpc: int = 10,
) -> UniTAP.VideoMode:
"""
SolidColor 专用 VideoMode对齐 work_with_solid_color.py。
RGB SolidColor 硬件只接受 CM_sRGB设 BT.709 会 WrongColorimetry
码值 65535 须 bpc=10PG 内部按 16-bpc 左移 16-bpc 位)。
"""
color = UniTAP.ColorInfo()
color.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA
color.bpc = max(10, bpc)
if content_mode == "hdr-content":
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB
else:
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
return UniTAP.VideoMode(timing=timing, color_info=color)
def resolve_content_mode(args) -> str:
"""PG 像素full 包 + 非 pg-only 时默认 10bit否则 SDR 8bit 保出图。"""
if args.safe:
return "sdr"
if args.hdr_content or args.sdr_content:
return "sdr" if args.sdr_content else "hdr-content"
if args.pg_only or args.infoframe_only:
return "sdr"
if args.packet == "full":
return "hdr-content"
return "sdr"
def resolve_output_mode(args, content_mode: str) -> str:
"""默认 SolidColor065535Image 易触发 WrongColorimetry。"""
if args.no_pg:
return "none"
if args.output is not None:
return args.output
return "solid"
def resolve_white_code(args, *, content_mode: str, output_mode: str) -> int:
"""SolidColor 白场码值Image 模式 SDR 仍映射到 8bit RGB。"""
code = max(0, min(args.white_code, SOLID_COLOR_MAX))
if output_mode == "image" and content_mode == "sdr":
return min(code, 255)
return code
def content_mode_label(mode: str, bpc: int, *, output_mode: str = "solid") -> str:
if mode == "hdr-content":
return f"BT.2020 {bpc}bit"
if output_mode == "solid":
return f"sRGB {max(10, bpc)}bit"
return "BT.709 8bit"
def stop_audio(role) -> None:
try:
role.hdtx.ag.stop_generate()
except Exception:
pass
def read_pg_diagnostics(pg) -> dict:
st = pg.status()
return {
"pg_error": st.error.name,
"pg_error_str": str(st.error),
}
def rgb_from_white_code(white_code: int, *, hdr_content: bool) -> tuple[int, int, int]:
"""将码值映射为 8bit RGBImageFile 经 UICL 按 VideoMode 转换)。"""
if not hdr_content:
level = max(0, min(255, white_code if white_code <= 255 else 255))
else:
level = max(0, min(255, int(round(white_code * 255 / SOLID_COLOR_MAX))))
return level, level, level
def ensure_white_image(
width: int,
height: int,
*,
hdr_content: bool,
white_code: int,
image_path: str | None = None,
) -> Path:
"""返回用于发图的 PNG 路径;未指定 --image 时在临时目录生成全屏白场。"""
if image_path:
path = Path(image_path).expanduser().resolve()
if not path.is_file():
raise FileNotFoundError(f"找不到图片: {path}")
return path
rgb = rgb_from_white_code(white_code, hdr_content=hdr_content)
temp_dir = Path(tempfile.gettempdir()) / "pq_hdr_test_images"
temp_dir.mkdir(parents=True, exist_ok=True)
suffix = "hdr" if hdr_content else "sdr"
out = temp_dir / f"white_{width}x{height}_{suffix}_{rgb[0]:03d}.png"
Image.new("RGB", (width, height), rgb).save(out, format="PNG")
return out
def print_pg_status(pg, hint: str | None = None) -> None:
for line in read_pattern_generator_status_lines(pg, pattern_hint=hint):
print(f" {line}")
def wait_for_hpd(role, *, timeout: float = 10.0, interval: float = 0.5) -> bool:
"""等待 Sink HPD 置位。"""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if role.hdtx.link.status.hpd_status:
return True
time.sleep(interval)
return False
def prepare_device(role, *, reset_pg: bool = True) -> None:
"""打开设备后清理 sdpg/PG 残留状态(避免此前测试黑屏)。"""
try:
role.hdtx.sdpg.stop()
except Exception:
pass
if reset_pg:
try:
role.hdtx.pg.reset()
except Exception:
pass
stop_audio(role)
def read_edid_summary(role) -> dict:
edid = role.hdtx.edid
local = edid.read_i2c()
remote = edid.read_sbm(0)
return {"local_len": len(local), "remote_len": len(remote)}
def apply_pg_solid(role, video_mode: UniTAP.VideoMode, white_code: int) -> dict:
"""
发 SolidColor 白场。顺序对齐 work_with_solid_color.py
set_pattern → set_vm → apply() → set_pattern_params(065535)。
"""
pg = role.hdtx.pg
code = max(0, min(white_code, SOLID_COLOR_MAX))
pg.set_pattern(pattern=UniTAP.VideoPattern.SolidColor)
pg.set_vm(vm=video_mode)
if not pg.apply():
diag = read_pg_diagnostics(pg)
raise RuntimeError(
f"SolidColor apply() 失败: {diag['pg_error_str']} ({diag['pg_error']})"
)
pg.set_pattern_params(
pattern_params=UniTAP.SolidColorParams(code, code, code)
)
stop_audio(role)
return read_pg_diagnostics(pg)
def bootstrap_sdr_white(role, timing: Timing, *, settle: float) -> None:
"""先用 sRGB 10bit SolidColor 建立链路,再切目标格式。"""
boot_vm = build_solid_video_mode(timing, content_mode="sdr")
print(f" [bootstrap] sRGB 10bit SolidColor 白场 (码值 {SOLID_WHITE_PEAK}) ...")
apply_pg_solid(role, boot_vm, SOLID_WHITE_PEAK)
time.sleep(settle)
link = read_link_diagnostics(role)
print(
f" [bootstrap] pg_error=OK, video={'On' if link['video'] else 'Off'}, "
f"TMDS_lock={link['tmds_locked']}"
)
print_pg_status(role.hdtx.pg, "Solid Color bootstrap")
def apply_pg_image(role, video_mode: UniTAP.VideoMode, image_path: Path) -> dict:
"""
通过 ImageFile 发图。顺序对齐 device.send_image_pattern
set_vm → set_pattern(图片路径) → apply()。
"""
pg = role.hdtx.pg
pg.set_vm(vm=video_mode)
pg.set_pattern(pattern=str(image_path))
if not pg.apply():
diag = read_pg_diagnostics(pg)
raise RuntimeError(
f"Image apply() 失败: {diag['pg_error_str']} ({diag['pg_error']})"
)
stop_audio(role)
return read_pg_diagnostics(pg)
def read_sdpg_caps_raw(sdpg) -> int:
"""读取 TSI_PACKETGEN_CAPSPython API 未封装)。"""
io = sdpg._PacketGenerator__io # noqa: SLF001 — 诊断用
return io.get(tsi_ci.TSI_PACKETGEN_CAPS_R, c_uint32)[1]
def read_link_diagnostics(role) -> dict:
st = role.hdtx.link.status
locks = st.channel_lock
return {
"hpd": st.hpd_status,
"video": st.video_status,
"hdmi_mode": st.hdmi_mode.name,
"channel_lock": locks,
"tmds_locked": any(locks) if locks else False,
}
def poll_sdpg_status(sdpg, seconds: float = 3.0, interval: float = 0.5):
"""start() 后轮询 status部分固件需数帧后才置位。"""
samples = []
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
samples.append(sdpg.status())
time.sleep(interval)
return samples
def start_sdpg(role, packets: list[bytearray], *, pre_stop_delay: float = 0.5) -> None:
"""
启动 sdpg。流程对齐 work_with_packet_generation.py
reset → add_packet(s) → stop → 等待 → start。
"""
sdpg = role.hdtx.sdpg
sdpg.reset()
for idx, packet in enumerate(packets):
sdpg.add_packet(packet)
print(
f" sdpg add_packet[{idx}]: type=0x{packet[0]:02X}, "
f"len={len(packet)} bytes"
)
sdpg.stop()
print(f" sdpg stop 后: {sdpg.status()}")
if pre_stop_delay > 0:
time.sleep(pre_stop_delay)
sdpg.start()
def interpret_sdpg_status(status, link: dict) -> list[str]:
"""根据 status / 链路状态给出可读结论。"""
notes = []
notes.append(
"sdpg 通过 add_packet/load_packet 发送自定义 InfoFrame见官方 work_with_packet_generation.py"
"caps 寄存器为 0 时仍可发送。"
)
if not link["hpd"]:
notes.append(
"HPD=Low未检测到 Sink 热插拔。InfoFrame 可能不会在链路上送出,"
"请确认 HDMI 已接 DUT 且电视已开机。"
)
if not link["video"]:
notes.append("video_status=FalseTX 视频未 enable请先确认 Image 已有输出。")
if not status.error and not status.enabled and not status.active:
notes.append(
"start() 后 Enabled/Active 仍为 FalseUCD-323 固件常如此,"
"包仍可能已在链路上;请以电视 HDR 状态或 CA-410 nits 为准。"
)
elif status.error:
notes.append("sdpg error=True包格式或长度可能不被接受可尝试 --packet vendor-vsif 对比。")
elif status.enabled or status.active:
notes.append("sdpg 已 Enabled/Active信令发生器运行中。")
return notes
def parse_device_arg(value: str):
if value.isdigit():
return int(value)
return value
def list_devices(tsi: UniTAP.TsiLib) -> None:
try:
devices = tsi.get_list_of_available_devices()
except Exception as exc:
print(f"无法枚举设备: {exc}")
return
if not devices:
print("未发现 UCD 设备。")
return
print("已连接设备:")
for line in devices:
print(f" {line}")
def main() -> int:
parser = argparse.ArgumentParser(
description="UCD-323 HDR10 信号输出验证SolidColor 065535 + sdpg InfoFrame"
)
parser.add_argument(
"--device",
default="0",
help="设备序号(整数)或 8 位序列号,默认 0",
)
parser.add_argument("--vic", type=int, default=None, help="改用 CTA VIC指定后忽略 --standard/--width 等)")
parser.add_argument("--standard", default=DEFAULT_STANDARD, choices=("dmt", "cta", "cvt"))
parser.add_argument("--width", type=int, default=DEFAULT_WIDTH)
parser.add_argument("--height", type=int, default=DEFAULT_HEIGHT)
parser.add_argument("--refresh", type=float, default=DEFAULT_REFRESH_HZ, help="Hz")
parser.add_argument("--settle", type=float, default=2.0, help="apply 后等待秒数,默认 2")
parser.add_argument("--max-cll", type=int, default=DEFAULT_MAX_CLL, help="DRM MaxCLL (nits)")
parser.add_argument("--max-fall", type=int, default=DEFAULT_MAX_FALL, help="DRM MaxFALL (nits)")
parser.add_argument(
"--white-code",
type=int,
default=SOLID_WHITE_PEAK,
help=f"SolidColor 白场 RGB 码值 065535默认 {SOLID_WHITE_PEAK}(峰值白)",
)
parser.add_argument(
"--image",
metavar="PATH",
help="发图文件路径png/bmp/jpeg 等);省略则按分辨率自动生成白场 PNG",
)
parser.add_argument(
"--hold",
type=float,
default=DEFAULT_HOLD_SECONDS,
help=f"保持输出秒数,默认 {DEFAULT_HOLD_SECONDS}",
)
parser.add_argument(
"--save-bin",
metavar="PATH",
help="将 DRM 包保存为 .bin 文件(便于 sdpg.load_packet 调试)",
)
parser.add_argument("--list-devices", action="store_true", help="列出设备后退出")
parser.add_argument("--list-timings", action="store_true", help="列出设备 timing 后退出")
parser.add_argument(
"--packet",
choices=("full", "official-vsif", "drm", "vendor-vsif"),
default="full",
help="sdpgfull=VSIF+AVI+DRM默认official-vsif=仅官方 VSIFdrm=AVI+DRM",
)
parser.add_argument(
"--no-avi",
action="store_true",
help="drm 模式仅发 DRM、不发 AVI对比测试用",
)
parser.add_argument(
"--sdpg-before-pg",
action="store_true",
default=False,
help="先 sdpg 再出图",
)
parser.add_argument(
"--pg-before-sdpg",
action="store_false",
dest="sdpg_before_pg",
help="先出图再 sdpg默认",
)
parser.add_argument(
"--sdpg-reset",
action="store_true",
help="已废弃start_sdpg 每次都会 reset()",
)
parser.add_argument(
"--sdpg-delay",
type=float,
default=5.0,
help="sdpg stop 与 start 等待秒数(官方 5s默认 5",
)
parser.add_argument(
"--pg-only",
action="store_true",
help="仅 PG 出图,不发 sdpg",
)
parser.add_argument(
"--infoframe-only",
action="store_true",
help="SDR 白场 + 仅测 sdpg不切 HDR 像素;默认仍出图,建议加 --vic 16",
)
parser.add_argument(
"--no-pg",
action="store_true",
help="完全不出图,仅 sdpg复刻官方 work_with_packet_generation.py电视通常黑屏",
)
parser.add_argument(
"--force-sdpg",
action="store_true",
help="已废弃sdpg 默认始终发送(除非 --pg-only",
)
parser.add_argument(
"--safe",
action="store_true",
help="同默认 SDR 模式8bit BT.709),显式指定时使用",
)
parser.add_argument(
"--hdr-content",
action="store_true",
help="强制 PG 10bit BT.2020--packet full 已默认)",
)
parser.add_argument(
"--sdr-content",
action="store_true",
help="强制 PG 8bit SDR只测电视 HDR 标志,亮度不会升高)",
)
parser.add_argument(
"--output",
choices=("image", "solid"),
default=None,
help="白场输出:默认 SolidColor推荐image=ImageFile可能 WrongColorimetry",
)
parser.add_argument(
"--bootstrap",
action="store_true",
default=False,
help="solid 输出前先用 SolidColor 建链UCD-323 上易导致闪屏后黑屏,默认关闭)",
)
parser.add_argument(
"--no-bootstrap",
action="store_false",
dest="bootstrap",
help="同默认行为:跳过 bootstrap",
)
parser.add_argument(
"--reset-pg",
action="store_true",
default=True,
help="打开设备后 reset PG 并 stop sdpg默认开启",
)
parser.add_argument(
"--no-reset-pg",
action="store_false",
dest="reset_pg",
help="不 reset PG",
)
parser.add_argument(
"--hpd-wait",
type=float,
default=10.0,
help="等待 HPD 置位的超时秒数,默认 10",
)
parser.add_argument(
"--bpc",
type=int,
choices=(8, 10, 12),
default=10,
help="HDR 内容色深(非 --safe / 非 --pg-only SDR 时),默认 10",
)
args = parser.parse_args()
tsi = UniTAP.TsiLib()
if args.list_devices:
list_devices(tsi)
tsi.cleanup()
return 0
if args.list_timings:
dev = tsi.open(parse_device_arg(args.device))
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
print(role.hdtx.pg.timing_manager.print_all())
tsi.close(dev)
tsi.cleanup()
return 0
drm = build_drm_hdr10_packet(max_cll=args.max_cll, max_fall=args.max_fall)
if not verify_drm_checksum(drm):
print("错误: DRM 包 checksum 校验失败。", file=sys.stderr)
return 1
vsif_path = (
Path(__file__).resolve().parent
/ "UniTAP/dev/modules/dut_tests/cfg/hdr10/vsif1.bin"
)
if args.packet == "vendor-vsif" and not vsif_path.is_file():
print(f"错误: 找不到 {vsif_path}", file=sys.stderr)
return 1
if args.save_bin:
out = Path(args.save_bin)
out.write_bytes(drm)
print(f"已保存 DRM 包: {out.resolve()} ({len(drm)} bytes)")
preview_mode = resolve_content_mode(args)
output_mode = resolve_output_mode(args, preview_mode)
white_code = resolve_white_code(
args, content_mode=preview_mode, output_mode=output_mode
)
color_desc = content_mode_label(preview_mode, args.bpc, output_mode=output_mode)
preview_vic = args.vic if args.vic is not None else 16
preview_packets, preview_label = build_sdpg_packet_list(
args, drm=drm, vsif_path=vsif_path, vic=preview_vic
)
print("=== HDR10 信号输出测试 ===")
print("提示: 运行前请关闭 pqAutomationApp避免 UCD 设备被占用。")
if args.infoframe_only:
print("模式: SDR 白场 + 仅 sdpg InfoFrame观察电视 HDR 标志,亮度不会升高)")
elif args.no_pg:
print("模式: 无 PG 出图,仅 sdpg官方示例电视通常无画面")
elif args.pg_only:
print("模式: 仅出图(跳过 sdpg InfoFrame")
else:
print(f"模式: {color_desc} + sdpg ({preview_label})")
output_desc = {
"solid": "SolidColor",
"image": "ImageFile",
"none": "无(--no-pg",
}[output_mode]
print(f"输出: {output_desc}" + (" + bootstrap SDR" if args.bootstrap else ""))
if args.infoframe_only:
print(
" 说明: 仍会输出 SDR 8bit 白场;只验证 sdpg 是否让电视进 HDR亮度不会升高。"
" 测峰值亮度: python test.py --vic 16 --sdpg-delay 5"
)
if args.vic is None:
print(" 提示: 建议加 --vic 16与 --pg-only 成功出图时序一致)。")
if preview_mode == "sdr" and not args.pg_only and not args.infoframe_only:
print(
" 注意: SDR 8bit 像素 + InfoFrame 通常不会提高亮度CA-410 仍 ~100200 nits"
" 要测峰值亮度请用默认 --packet full10bit SolidColor + VSIF/AVI/DRM"
)
elif preview_mode == "hdr-content":
print(
f" 策略: SolidColor 码值 {white_code}065535 峰值)+ sdpg 三包。"
" 若黑屏可试 --sdr-content 或 --pg-only --vic 16。"
)
if not args.pg_only and not args.no_pg:
print(f"sdpg: {preview_label}")
if args.packet in ("full", "drm"):
print(f"DRM MaxCLL={args.max_cll}, MaxFALL={args.max_fall}")
timing_desc = (
f"CTA VIC {args.vic}" if args.vic is not None
else f"{args.standard.upper()} {args.width}x{args.height}@{args.refresh}Hz"
)
white_desc = (
f"SolidColor 码值 {white_code}"
if output_mode == "solid"
else (args.image or f"白场 PNG (white_code={white_code})")
)
print(f"Timing: {timing_desc}, PG={color_desc} RGB Full, 内容={white_desc}, settle={args.settle}s")
if not args.pg_only:
for idx, pkt in enumerate(preview_packets):
print(f"sdpg 包[{idx}] (type=0x{pkt[0]:02X}):")
print(format_packet_hex(pkt))
print()
dev = None
role = None
sdpg_started = False
try:
device_id = parse_device_arg(args.device)
print(f"正在打开设备: {device_id!r} ...")
try:
tsi.rescan_devices()
except Exception as exc:
print(f" 警告: rescan_devices 失败: {exc}", file=sys.stderr)
dev = tsi.open(device_id)
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
prepare_device(role, reset_pg=args.reset_pg)
if not wait_for_hpd(role, timeout=args.hpd_wait):
print(
f" 警告: {args.hpd_wait:g}s 内 HPD 仍为 Low"
"请确认电视已开机且 HDMI 已连接。",
file=sys.stderr,
)
edid_info = read_edid_summary(role)
print(
f" EDID: local={edid_info['local_len']}B, "
f"remote(DUT)={edid_info['remote_len']}B"
)
print("配置输出 ...")
timing = resolve_timing(
role,
width=args.width,
height=args.height,
refresh_hz=args.refresh,
standard=args.standard,
vic=args.vic,
)
sdpg = role.hdtx.sdpg
caps = read_sdpg_caps_raw(sdpg)
print(f" sdpg caps (raw): 0x{caps:08X}")
content_mode = resolve_content_mode(args)
output_mode = resolve_output_mode(args, content_mode)
white_code = resolve_white_code(
args, content_mode=content_mode, output_mode=output_mode
)
avi_vic = args.vic if args.vic is not None else timing.id
sdpg_packets, packet_label = build_sdpg_packet_list(
args, drm=drm, vsif_path=vsif_path, vic=avi_vic
)
if args.infoframe_only:
print(" 内容格式: sRGB 10bit SolidColor + sdpg像素 SDR信令由 sdpg 声明 HDR")
elif args.no_pg:
print(" 内容格式: 不出图,仅 sdpg。")
elif content_mode == "hdr-content":
print(
f" 内容格式: PG 10bit BT.2020 SolidColor 码值 {white_code}065535"
)
elif not args.pg_only:
print(" 内容格式: PG 8bit BT.709 + sdpg亮度不会升高仅验证信令")
else:
print(" 内容格式: SDR 8bit BT.709。")
vm = (
build_solid_video_mode(timing, content_mode=content_mode, bpc=args.bpc)
if output_mode == "solid"
else build_video_mode(timing, content_mode=content_mode, bpc=args.bpc)
)
hdr_content = content_mode == "hdr-content"
image_path = None
if output_mode == "image":
image_path = ensure_white_image(
timing.hactive,
timing.vactive,
hdr_content=hdr_content,
white_code=white_code,
image_path=args.image,
)
print(f" Image: {image_path}")
elif output_mode == "none":
print(" Image: (跳过,--no-pg")
print(f" 内容格式: {content_mode_label(content_mode, args.bpc, output_mode=output_mode)}")
refresh_hz = timing.frame_rate / 1000.0 if timing.frame_rate else 0.0
std_name = timing.standard.name.replace("SD_", "")
print(
f" Timing: {std_name} {timing.hactive}x{timing.vactive} @ {refresh_hz:g} Hz"
f" (id={timing.id})"
)
print(
f" Color: {vm.color_info.colorimetry.name}, "
f"bpc={vm.color_info.bpc}, range={vm.color_info.dynamic_range.name}"
)
link = read_link_diagnostics(role)
print(
f" 链路: HPD={'High' if link['hpd'] else 'Low'}, "
f"video={'On' if link['video'] else 'Off'}, "
f"TMDS_lock={link['tmds_locked']}, locks={link['channel_lock']}, "
f"mode={link['hdmi_mode']}"
)
if link["hpd"] and not link["tmds_locked"]:
print(
" 提示: UCD-323 TX 侧 TMDS lock 位常不可靠;"
"请以电视是否实际有画面为准video=On 即可继续)。"
)
def run_output():
if output_mode == "none":
return
if args.bootstrap:
if output_mode == "image":
print(
" 跳过 bootstrapImage 模式直接出图即可;"
"SolidColor 建链在本机上会闪屏后黑屏。"
)
else:
bootstrap_sdr_white(role, timing, settle=args.settle)
if output_mode == "solid":
pg_diag = apply_pg_solid(role, vm, white_code)
hint = f"Solid Color {white_code} (0x{white_code:X})"
else:
pg_diag = apply_pg_image(role, vm, image_path)
hint = image_path.name
time.sleep(args.settle)
link_after = read_link_diagnostics(role)
print(
f" {output_mode} apply() OK, pg_error={pg_diag['pg_error']}, "
f"video={'On' if link_after['video'] else 'Off'}, "
f"TMDS_lock={link_after['tmds_locked']}"
)
if pg_diag["pg_error"] != "OK":
print(f" 警告: PG 状态异常 — {pg_diag['pg_error_str']}", file=sys.stderr)
print_pg_status(role.hdtx.pg, hint)
vm_rb = role.hdtx.pg.get_stream_video_mode()
print(
f" PG 读回: {vm_rb.timing.hactive}x{vm_rb.timing.vactive}, "
f"bpc={vm_rb.color_info.bpc}, cm={vm_rb.color_info.colorimetry.name}"
)
def run_sdpg(*, reapply_pg: bool = True) -> bool:
nonlocal sdpg_started
if args.pg_only:
print(" 跳过 sdpg--pg-only")
return False
print(f"启动 sdpg ({packet_label}) ...")
start_sdpg(role, sdpg_packets, pre_stop_delay=args.sdpg_delay)
sdpg_started = True
samples = poll_sdpg_status(sdpg, seconds=1.0, interval=0.25)
status = samples[-1]
print(f" sdpg status (start 后): {status}")
link_mid = read_link_diagnostics(role)
print(
f" sdpg 后链路: video={'On' if link_mid['video'] else 'Off'}"
f"(若为 Off将重新 apply 出图)"
)
for note in interpret_sdpg_status(status, link_mid):
print(f"{note}")
if reapply_pg and output_mode != "none":
print(" sdpg 后再次 apply 出图,保持画面 ...")
run_output()
return True
if args.no_pg:
run_sdpg(reapply_pg=False)
elif args.infoframe_only or args.pg_only:
run_output()
if args.infoframe_only:
run_sdpg(reapply_pg=True)
elif args.sdpg_before_pg:
run_sdpg(reapply_pg=False)
run_output()
else:
run_output()
run_sdpg(reapply_pg=True)
print()
print("--- 请用 CA-410 测量 DUT 100% 白场峰值亮度 ---")
if preview_mode == "hdr-content":
print(" HDR 生效: 应显著高于 SDR电视峰值通常 4001000+ nits")
else:
print(" 当前为 SDR 像素: 亮度约 100200 nits 属正常,不代表 sdpg 无效")
print(" 对比: 先跑 --pg-only --vic 16 记 SDR 基准")
print(f"--- 保持输出 {args.hold:.0f}Ctrl+C 可提前结束 ---")
print()
deadline = time.monotonic() + args.hold
while time.monotonic() < deadline:
time.sleep(2.0)
link_hold = read_link_diagnostics(role)
if not link_hold["video"] and output_mode != "none":
print(" [watchdog] video=Off重新 apply 输出 ...")
if output_mode == "solid":
apply_pg_solid(role, vm, white_code)
else:
apply_pg_image(role, vm, image_path)
print("测试完成。")
return 0
except KeyboardInterrupt:
print("\n用户中断。")
return 130
except Exception as exc:
print(f"错误: {exc}", file=sys.stderr)
list_devices(tsi)
return 1
finally:
if role is not None and sdpg_started:
try:
role.hdtx.sdpg.stop()
except Exception:
pass
if dev is not None:
try:
tsi.close(dev)
except Exception:
pass
tsi.cleanup()
if __name__ == "__main__":
sys.exit(main())