2026-07-02 17:16:18 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
2026-07-07 18:59:44 +08:00
|
|
|
|
HDR10 信号输出验证脚本(UCD-323 HDMI Source + SolidColor + sdpg)。
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
通过 SolidColor 白场(0–65535,默认 65535)并配合 sdpg InfoFrame,
|
|
|
|
|
|
验证 DUT 能否进入 HDR 模式并输出峰值亮度。
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
|
|
|
|
|
用法(在项目根目录)::
|
|
|
|
|
|
|
|
|
|
|
|
python test.py
|
|
|
|
|
|
python test.py --device 0
|
|
|
|
|
|
python test.py --device ABCD1234 --max-cll 1000 --max-fall 400 --hold 60
|
2026-07-07 18:59:44 +08:00
|
|
|
|
python test.py --image path/to/white.png
|
2026-07-02 17:16:18 +08:00
|
|
|
|
python test.py --save-bin drm_hdr10.bin
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
验收:脚本发出 100% 白场后,用 CA-410 测 DUT 峰值亮度。
|
2026-07-02 17:16:18 +08:00
|
|
|
|
若仍约 100–200 nits(SDR 水平),说明信令或 DUT 设置未生效。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
import sys
|
2026-07-07 18:59:44 +08:00
|
|
|
|
import tempfile
|
2026-07-02 17:16:18 +08:00
|
|
|
|
import time
|
2026-07-07 18:59:44 +08:00
|
|
|
|
from ctypes import c_uint32
|
2026-07-02 17:16:18 +08:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
import UniTAP
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
# 与 pq_config 默认一致:DMT 1080p60(勿用 CTA VIC16 代替,时序参数不同)
|
|
|
|
|
|
DEFAULT_WIDTH = 1920
|
|
|
|
|
|
DEFAULT_HEIGHT = 1080
|
|
|
|
|
|
DEFAULT_REFRESH_HZ = 60.0
|
|
|
|
|
|
DEFAULT_STANDARD = "dmt"
|
2026-07-02 17:16:18 +08:00
|
|
|
|
DEFAULT_MAX_CLL = 1000
|
|
|
|
|
|
DEFAULT_MAX_FALL = 400
|
|
|
|
|
|
DEFAULT_HOLD_SECONDS = 60
|
2026-07-07 18:59:44 +08:00
|
|
|
|
from app.solid_color_scale import SOLID_COLOR_MAX, SOLID_COLOR_MIN
|
|
|
|
|
|
SOLID_WHITE_PEAK = 65535
|
2026-07-02 17:16:18 +08:00
|
|
|
|
PACKET_PAD_BYTES = 36
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
# work_with_packet_generation.py 内联 VSIF(HDR10+,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,
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
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 InfoFrame(Type 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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
def verify_drm_checksum(packet: bytearray) -> bool:
|
|
|
|
|
|
"""校验 InfoFrame checksum(type + 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
def build_video_mode(
|
|
|
|
|
|
timing: Timing,
|
|
|
|
|
|
*,
|
|
|
|
|
|
content_mode: str = "sdr",
|
|
|
|
|
|
bpc: int = 10,
|
|
|
|
|
|
) -> UniTAP.VideoMode:
|
|
|
|
|
|
"""ImageFile 等自定义 pattern 用。"""
|
2026-07-02 17:16:18 +08:00
|
|
|
|
color = UniTAP.ColorInfo()
|
|
|
|
|
|
color.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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=10(PG 内部按 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
|
2026-07-02 17:16:18 +08:00
|
|
|
|
return UniTAP.VideoMode(timing=timing, color_info=color)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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:
|
|
|
|
|
|
"""默认 SolidColor(0–65535);Image 易触发 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 RGB(ImageFile 经 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(0–65535)。
|
|
|
|
|
|
"""
|
2026-07-02 17:16:18 +08:00
|
|
|
|
pg = role.hdtx.pg
|
2026-07-07 18:59:44 +08:00
|
|
|
|
code = max(0, min(white_code, SOLID_COLOR_MAX))
|
2026-07-02 17:16:18 +08:00
|
|
|
|
pg.set_pattern(pattern=UniTAP.VideoPattern.SolidColor)
|
|
|
|
|
|
pg.set_vm(vm=video_mode)
|
|
|
|
|
|
if not pg.apply():
|
2026-07-07 18:59:44 +08:00
|
|
|
|
diag = read_pg_diagnostics(pg)
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
f"SolidColor apply() 失败: {diag['pg_error_str']} ({diag['pg_error']})"
|
|
|
|
|
|
)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
pg.set_pattern_params(
|
2026-07-07 18:59:44 +08:00
|
|
|
|
pattern_params=UniTAP.SolidColorParams(code, code, code)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
)
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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")
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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_CAPS(Python 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。
|
|
|
|
|
|
"""
|
2026-07-02 17:16:18 +08:00
|
|
|
|
sdpg = role.hdtx.sdpg
|
|
|
|
|
|
sdpg.reset()
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
sdpg.start()
|
2026-07-07 18:59:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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=False:TX 视频未 enable,请先确认 Image 已有输出。")
|
|
|
|
|
|
if not status.error and not status.enabled and not status.active:
|
|
|
|
|
|
notes.append(
|
|
|
|
|
|
"start() 后 Enabled/Active 仍为 False:UCD-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
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
2026-07-07 18:59:44 +08:00
|
|
|
|
description="UCD-323 HDR10 信号输出验证(SolidColor 0–65535 + sdpg InfoFrame)"
|
2026-07-02 17:16:18 +08:00
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--device",
|
|
|
|
|
|
default="0",
|
|
|
|
|
|
help="设备序号(整数)或 8 位序列号,默认 0",
|
|
|
|
|
|
)
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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")
|
2026-07-02 17:16:18 +08:00
|
|
|
|
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,
|
2026-07-07 18:59:44 +08:00
|
|
|
|
default=SOLID_WHITE_PEAK,
|
|
|
|
|
|
help=f"SolidColor 白场 RGB 码值 0–65535,默认 {SOLID_WHITE_PEAK}(峰值白)",
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--image",
|
|
|
|
|
|
metavar="PATH",
|
|
|
|
|
|
help="发图文件路径(png/bmp/jpeg 等);省略则按分辨率自动生成白场 PNG",
|
2026-07-02 17:16:18 +08:00
|
|
|
|
)
|
|
|
|
|
|
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="列出设备后退出")
|
2026-07-07 18:59:44 +08:00
|
|
|
|
parser.add_argument("--list-timings", action="store_true", help="列出设备 timing 后退出")
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--packet",
|
|
|
|
|
|
choices=("full", "official-vsif", "drm", "vendor-vsif"),
|
|
|
|
|
|
default="full",
|
|
|
|
|
|
help="sdpg:full=VSIF+AVI+DRM(默认);official-vsif=仅官方 VSIF;drm=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",
|
|
|
|
|
|
)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
tsi = UniTAP.TsiLib()
|
|
|
|
|
|
if args.list_devices:
|
|
|
|
|
|
list_devices(tsi)
|
|
|
|
|
|
tsi.cleanup()
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
if args.save_bin:
|
|
|
|
|
|
out = Path(args.save_bin)
|
|
|
|
|
|
out.write_bytes(drm)
|
|
|
|
|
|
print(f"已保存 DRM 包: {out.resolve()} ({len(drm)} bytes)")
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-02 17:16:18 +08:00
|
|
|
|
print("=== HDR10 信号输出测试 ===")
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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 仍 ~100–200 nits)。"
|
|
|
|
|
|
" 要测峰值亮度请用默认 --packet full(10bit SolidColor + VSIF/AVI/DRM)。"
|
|
|
|
|
|
)
|
|
|
|
|
|
elif preview_mode == "hdr-content":
|
|
|
|
|
|
print(
|
|
|
|
|
|
f" 策略: SolidColor 码值 {white_code}(0–65535 峰值)+ 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))
|
2026-07-02 17:16:18 +08:00
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
dev = None
|
|
|
|
|
|
role = None
|
2026-07-07 18:59:44 +08:00
|
|
|
|
sdpg_started = False
|
2026-07-02 17:16:18 +08:00
|
|
|
|
try:
|
|
|
|
|
|
device_id = parse_device_arg(args.device)
|
|
|
|
|
|
print(f"正在打开设备: {device_id!r} ...")
|
2026-07-07 18:59:44 +08:00
|
|
|
|
try:
|
|
|
|
|
|
tsi.rescan_devices()
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f" 警告: rescan_devices 失败: {exc}", file=sys.stderr)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
dev = tsi.open(device_id)
|
|
|
|
|
|
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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"
|
|
|
|
|
|
)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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}(0–65535)。"
|
|
|
|
|
|
)
|
|
|
|
|
|
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})"
|
|
|
|
|
|
)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
print(
|
|
|
|
|
|
f" Color: {vm.color_info.colorimetry.name}, "
|
|
|
|
|
|
f"bpc={vm.color_info.bpc}, range={vm.color_info.dynamic_range.name}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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(
|
|
|
|
|
|
" 跳过 bootstrap:Image 模式直接出图即可;"
|
|
|
|
|
|
"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)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print("--- 请用 CA-410 测量 DUT 100% 白场峰值亮度 ---")
|
2026-07-07 18:59:44 +08:00
|
|
|
|
if preview_mode == "hdr-content":
|
|
|
|
|
|
print(" HDR 生效: 应显著高于 SDR(电视峰值通常 400–1000+ nits)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(" 当前为 SDR 像素: 亮度约 100–200 nits 属正常,不代表 sdpg 无效")
|
|
|
|
|
|
print(" 对比: 先跑 --pg-only --vic 16 记 SDR 基准")
|
2026-07-02 17:16:18 +08:00
|
|
|
|
print(f"--- 保持输出 {args.hold:.0f} 秒,Ctrl+C 可提前结束 ---")
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
deadline = time.monotonic() + args.hold
|
|
|
|
|
|
while time.monotonic() < deadline:
|
2026-07-07 18:59:44 +08:00
|
|
|
|
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)
|
2026-07-02 17:16:18 +08:00
|
|
|
|
|
|
|
|
|
|
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:
|
2026-07-07 18:59:44 +08:00
|
|
|
|
if role is not None and sdpg_started:
|
2026-07-02 17:16:18 +08:00
|
|
|
|
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())
|