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

1465 lines
49 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
合并官方示例 work_with_solid_color.pyPG 白场)与
work_with_packet_generation.pysdpg InfoFrame并补充 HDR10 必需的
DRM (0x87) 与 AVI BT.2020 (0x82) 包。
用法(在项目根目录)::
python hdr10_demo.py
python hdr10_demo.py --device 0 --vic 16 --hold 60
python hdr10_demo.py --sdr # SDR 对照组(亮度不会升高)
python hdr10_demo.py --no-vsif # 仅 AVI + DRM
python hdr10_demo.py --output image # PQ 编码白场图(默认,测峰值亮度推荐)
python hdr10_demo.py --output solid # 旧 SolidColor 路径
python hdr10_demo.py --peak-nits 1000 --max-cll 1000
python hdr10_demo.py --image path/to/pq_white.png
python hdr10_demo.py --verify-link # 打印 TX InfoFrame 回读与信令/亮度分层结论(默认开启)
python hdr10_demo.py --no-verify-link
验收:脚本保持白场输出后,用 CA-410 测 DUT 中心亮度。
HDR10 成功时峰值应显著高于 SDR通常 4001000+ nits视电视能力而定
运行前请关闭 pqAutomationApp避免 UCD 设备被占用。
"""
from __future__ import annotations
import argparse
import math
import sys
import time
from contextlib import contextmanager, nullcontext
from ctypes import c_byte, 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 import ColorInfo
from UniTAP.dev.modules import MemoryManager
def log_step(msg: str) -> None:
print(msg, flush=True)
def get_memory_manager(role) -> MemoryManager:
return role.hdtx.sdpg._PacketGenerator__memory_manager # noqa: SLF001
def ensure_sdpg_memory(role) -> int:
"""
在 PG 出图前预分配 MO_Events 内存块。
sdpg.start() 在 MO_Events 未分配时会调用 make_default()
若此时 PG 已在输出,重置显存布局会导致原生层崩溃、进程静默退出。
"""
mm = get_memory_manager(role)
layout = mm.get_memory_layout()
need_default = (
len(layout) <= MemoryManager.MemoryOwner.MO_Events.value
or layout[MemoryManager.MemoryOwner.MO_Events.value] == 0
)
if need_default:
log_step(" [内存] 预分配默认布局PG 出图前,含 MO_Events...")
mm.make_default()
layout = mm.get_memory_layout()
events_sz = (
layout[MemoryManager.MemoryOwner.MO_Events.value]
if len(layout) > MemoryManager.MemoryOwner.MO_Events.value
else 0
)
log_step(f" [内存] layout={layout}, MO_Events={events_sz} bytes")
return events_sz
def calculate_pg_frame_block_size(width: int, height: int) -> int:
"""对齐 UniTAP PatternGenerator.__calculate_frame_size。"""
width = int(width)
height = int(height)
def aligned(value: int, alignment: int) -> int:
return ((value + alignment - 1) // alignment) * alignment
line_stride_bytes = width * 4
line_stride_aligned_bytes = aligned(line_stride_bytes, 1024)
line_stride_bytes2 = width * 2
line_stride_aligned_bytes2 = aligned(line_stride_bytes2, 1024)
return int((line_stride_aligned_bytes + line_stride_aligned_bytes2) * height)
@contextmanager
def patch_image_memory_layout(role, events_bytes: int):
"""
拦截 ImageFile apply 内的 set_memory_layout([pg_block])。
UniTAP 会把布局收成单块导致 sdpg 无 MO_Events在 apply 当下扩展为
[PG, AG, MO_Events] 可避免事后再次改 layout会黑屏
"""
mm = get_memory_manager(role)
prior = list(mm.get_memory_layout())
original_set = mm.set_memory_layout
def expanded_set(layout: list[int]) -> None:
if len(layout) == 1:
pg_block = layout[0]
ag_block = (
prior[MemoryManager.MemoryOwner.MO_AudioGenerator.value]
if len(prior) > MemoryManager.MemoryOwner.MO_AudioGenerator.value
and prior[MemoryManager.MemoryOwner.MO_AudioGenerator.value] > 0
else MemoryManager.DEFAULT_MEMOTY_ALLOCATION[1]
)
ev_block = events_bytes
if ev_block <= 0 and len(prior) > MemoryManager.MemoryOwner.MO_Events.value:
ev_block = prior[MemoryManager.MemoryOwner.MO_Events.value]
if ev_block <= 0:
ev_block = 12288
layout = [pg_block, ag_block, ev_block]
if len(prior) > 3:
layout.append(prior[3])
log_step(f" [内存] ImageFile apply 内扩展布局: {layout}")
original_set(layout)
mm.set_memory_layout = expanded_set # type: ignore[method-assign]
try:
yield
finally:
mm.set_memory_layout = original_set # type: ignore[method-assign]
def resolve_sdpg_first(args, *, use_sdpg: bool, output_mode: str) -> bool:
"""SolidColor 默认先 sdpgImageFile 必须先 PGapply 内扩展 layout再启 sdpg"""
if not use_sdpg:
return False
if args.pg_before_sdpg:
return False
if output_mode == "image":
return False
return True
from app.solid_color_scale import SOLID_COLOR_MAX
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,
])
ROOT = Path(__file__).resolve().parent
VSIF_PATH = ROOT / "UniTAP/dev/modules/dut_tests/cfg/hdr10/vsif1.bin"
HDR_IMAGE_DIR = ROOT / "temp_hdr10_images"
# 常见 CTA VIC 分辨率(开设备前预生成 PNG 用;连上设备后会按实际 timing 校正)
VIC_PIXEL_SIZE: dict[int, tuple[int, int]] = {
1: (640, 480),
2: (720, 480),
4: (1280, 720),
16: (1920, 1080),
19: (1280, 720),
31: (1920, 1080),
32: (1920, 1080),
33: (1920, 1080),
34: (1920, 1080),
93: (3840, 2160),
94: (3840, 2160),
95: (3840, 2160),
96: (3840, 2160),
97: (3840, 2160),
98: (3840, 2160),
99: (3840, 2160),
100: (3840, 2160),
101: (3840, 2160),
102: (3840, 2160),
}
def guess_vic_pixel_size(vic: int) -> tuple[int, int]:
return VIC_PIXEL_SIZE.get(vic, (1920, 1080))
def is_bt2020_colorimetry(name: str) -> bool:
return name in (
ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB.name,
ColorInfo.Colorimetry.CM_ITUR_BT2020_YCbCr.name,
)
def pq_oetf_from_nits(nits: float) -> float:
"""ST 2084 PQ OETF绝对亮度 (nits) → 非线性信号 V ∈ [0, 1]。"""
if nits <= 0:
return 0.0
m1 = 2610 / 16384
m2 = 78.84375
c1 = 3424 / 4096
c2 = 2413 / 128
c3 = 2392 / 128
y = min(nits / 10000.0, 1.0)
ym1 = pow(y, m1)
return pow((c1 + c2 * ym1) / (1.0 + c3 * ym1), m2)
def generate_pq_white_image(
width: int,
height: int,
*,
peak_nits: float,
) -> tuple[Path, dict]:
"""
生成 PQ 编码全屏白场 PNG。
像素值为 ST 2084 PQ 码8bit 存储UICL 会按 VideoMode 升到 10bit
"""
width = int(width)
height = int(height)
HDR_IMAGE_DIR.mkdir(parents=True, exist_ok=True)
pq_v = pq_oetf_from_nits(peak_nits)
level = max(0, min(255, int(round(pq_v * 255.0))))
path = HDR_IMAGE_DIR / f"pq_white_{width}x{height}_{int(peak_nits)}nits.png"
log_step(f" [图片] 生成 PQ 白场 {width}x{height}{path.name} (RGB={level}) ...")
Image.new("RGB", (width, height), (level, level, level)).save(path, format="PNG")
meta = {
"encoding": "PQ ST2084",
"peak_nits": peak_nits,
"pq_v": pq_v,
"png_rgb": level,
"approx_10bit": int(round(pq_v * 1023)),
"width": width,
"height": height,
}
return path, meta
def generate_sdr_white_image(width: int, height: int, *, level: int = 255) -> Path:
width = int(width)
height = int(height)
HDR_IMAGE_DIR.mkdir(parents=True, exist_ok=True)
lv = max(0, min(255, level))
path = HDR_IMAGE_DIR / f"sdr_white_{width}x{height}_{lv}.png"
log_step(f" [图片] 生成 SDR 白场 {width}x{height}{path.name} ...")
Image.new("RGB", (width, height), (lv, lv, lv)).save(path, format="PNG")
return path
def ensure_output_image(
width: int,
height: int,
*,
hdr: bool,
peak_nits: float | None,
max_cll: int,
image_path: str | None,
solid_white_code: int,
) -> tuple[Path, dict]:
width = int(width)
height = int(height)
if image_path:
path = Path(image_path).expanduser().resolve()
if not path.is_file():
raise FileNotFoundError(f"找不到图片: {path}")
return path, {"encoding": "user", "path": str(path), "width": width, "height": height}
if hdr:
nits = peak_nits if peak_nits is not None else float(max_cll)
path, meta = generate_pq_white_image(width, height, peak_nits=nits)
return path, meta
path = generate_sdr_white_image(width, height, level=min(255, solid_white_code))
return path, {
"encoding": "SDR 8bit",
"png_rgb": min(255, solid_white_code),
"width": width,
"height": height,
}
def prepare_output_assets(
args,
*,
hdr_mode: bool,
output_mode: str,
infoframe_only: bool,
) -> tuple[Path | None, dict]:
"""在打开 UCD 设备之前生成/校验 PNG避免 PIL 与 TSI 驱动同进程争用导致静默崩溃。"""
if output_mode != "image":
return None, {}
white_code = (
args.white_code if hdr_mode or infoframe_only else min(args.white_code, 255)
)
guess_w, guess_h = guess_vic_pixel_size(args.vic)
log_step(f" [图片] 预生成开设备前VIC {args.vic} 预估 {guess_w}x{guess_h} ...")
return ensure_output_image(
guess_w,
guess_h,
hdr=hdr_mode,
peak_nits=args.peak_nits,
max_cll=args.max_cll,
image_path=args.image,
solid_white_code=white_code,
)
def align_output_image_to_timing(
image_path: Path | None,
image_meta: dict,
timing,
*,
hdr_mode: bool,
peak_nits: float | None,
max_cll: int,
image_arg: str | None,
solid_white_code: int,
) -> tuple[Path | None, dict]:
"""设备连上后按真实 timing 校正图片尺寸(用户自定义 --image 时不重生成)。"""
if image_path is None:
return None, image_meta
if image_arg:
return image_path, image_meta
want_w = int(timing.hactive)
want_h = int(timing.vactive)
have_w = int(image_meta.get("width", want_w))
have_h = int(image_meta.get("height", want_h))
if want_w == have_w and want_h == have_h:
return image_path, image_meta
log_step(
f" [图片] timing 为 {want_w}x{want_h},与预生成 {have_w}x{have_h} 不符,重新生成 ..."
)
return ensure_output_image(
want_w,
want_h,
hdr=hdr_mode,
peak_nits=peak_nits,
max_cll=max_cll,
image_path=None,
solid_white_code=solid_white_code,
)
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 = 1000,
max_fall: int = 400,
max_master: int = 1000,
min_master_nits: float = 0.005,
) -> 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) < PACKET_PAD_BYTES:
packet.extend(b"\x00" * (PACKET_PAD_BYTES - len(packet)))
return packet
def verify_drm_checksum(packet: bytearray) -> bool:
"""校验 DRM InfoFrame checksum。"""
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 parse_device_arg(value: str) -> int | str:
try:
return int(value)
except ValueError:
if len(value) != 8:
raise argparse.ArgumentTypeError(
"设备参数应为索引(整数)或 8 位序列号"
) from None
return value
def wait_for_hpd(role, *, timeout: float = 10.0, interval: float = 0.5) -> bool:
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) -> None:
try:
role.hdtx.sdpg.stop()
except Exception:
pass
try:
role.hdtx.pg.reset()
except Exception:
pass
try:
role.hdtx.ag.stop_generate()
except Exception:
pass
def resolve_timing(role, *, vic: int):
timing = role.hdtx.pg.timing_manager.get_cta(vic)
if timing is None:
raise RuntimeError(f"设备不支持 CTA VIC={vic}")
return timing
def build_image_video_mode(timing, *, hdr: bool, bpc: int = 10) -> UniTAP.VideoMode:
"""ImageFile 专用 VideoMode对齐 test.py build_video_mode"""
color = UniTAP.ColorInfo()
color.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA
if hdr:
# UICL 仅将 CM_ITUR_BT2020_YCbCr 映射到 BT.2020RGB 枚举会落到 Unknown 并在 convert 时崩溃
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_YCbCr
color.bpc = max(10, 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, *, hdr: bool, bpc: int = 10) -> UniTAP.VideoMode:
"""
SolidColor 专用 VideoMode。
SDRCM_sRGB + bpc>=10对齐 work_with_solid_color.py
HDRCM_ITUR_BT2020_RGB + bpc>=10
"""
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 hdr:
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 read_pg_diagnostics(pg) -> dict:
st = pg.status()
vm = pg.get_stream_video_mode()
ci = vm.color_info
refresh_hz = vm.timing.frame_rate / 1000.0 if vm.timing.frame_rate else 0.0
return {
"pg_error": st.error.name,
"timing": f"{vm.timing.hactive}x{vm.timing.vactive}@{refresh_hz:g}Hz",
"colorimetry": ci.colorimetry.name,
"bpc": ci.bpc,
"dynamic_range": ci.dynamic_range.name,
"color_format": ci.color_format.name,
}
def print_pg_diagnostics(role, *, label: str = "PG") -> dict:
diag = read_pg_diagnostics(role.hdtx.pg)
print(f" [{label}] pg_error={diag['pg_error']}, timing={diag['timing']}")
print(
f" [{label}] 读回: {diag['color_format']}, "
f"{diag['colorimetry']}, bpc={diag['bpc']}, "
f"range={diag['dynamic_range']}"
)
return diag
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 print_link_diagnostics(role, *, label: str = "链路") -> dict:
link = read_link_diagnostics(role)
print(
f" [{label}] HPD={'High' if link['hpd'] else 'Low'}, "
f"video={'On' if link['video'] else 'Off'}, "
f"hdmi_mode={link['hdmi_mode']}, "
f"TMDS_lock={link['tmds_locked']}"
)
return link
def read_sdpg_caps_raw(sdpg) -> int:
io = sdpg._PacketGenerator__io # noqa: SLF001 — 诊断用
return io.get(tsi_ci.TSI_PACKETGEN_CAPS_R, c_uint32)[1]
def read_tx_infoframe(sdpg, reg: int, expected_type: int, *, count: int = 36) -> bytes | None:
"""读 TX 侧 HDMI InfoFrame 回读寄存器(未完全文档化,仅供诊断)。"""
try:
io = sdpg._PacketGenerator__io # noqa: SLF001
raw = io.get(reg, c_byte, count)[1]
if raw and len(raw) >= 4 and raw[0] == expected_type:
return bytes(raw)
except Exception:
pass
return None
def read_infoframe_update_flags(sdpg) -> int | None:
try:
io = sdpg._PacketGenerator__io # noqa: SLF001
return io.get(tsi_ci.TSI_HDMI_INFOFRAME_UPDATE_FLAGS_R, c_uint32)[1]
except Exception:
return None
def parse_drm_on_wire(data: bytes) -> dict:
"""解析 DRM InfoFrame 回读header 4B + body"""
length = data[2]
body = data[4 : 4 + length]
max_cll = max_fall = None
if len(body) >= 24:
max_cll = (body[22] << 8) | body[23]
max_fall = (body[24] << 8) | body[25] if len(body) >= 26 else None
return {
"eotf": body[0] if body else -1,
"metadata_type": body[1] if len(body) > 1 else -1,
"max_cll": max_cll,
"max_fall": max_fall,
"pq": bool(body) and body[0] == 0x04,
}
def parse_avi_on_wire(data: bytes) -> dict:
"""解析 AVI InfoFrame 回读。"""
body = data[4 : 4 + data[2]] if len(data) >= 4 else b""
ext_color = (body[2] >> 4) & 0x0F if len(body) > 2 else -1
vic = body[3] & 0x7F if len(body) > 3 else -1
full_range = ((body[2] >> 2) & 0x03) == 0x01 if len(body) > 2 else False
return {
"vic": vic,
"extended_colorimetry": ext_color,
"bt2020": ext_color == 0x06,
"full_range": full_range,
}
def verify_tx_link_signaling(
role,
*,
vic: int,
use_sdpg: bool,
sdpg_st,
) -> dict:
"""汇总 TX 侧 InfoFrame 回读,判断信令层 HDR 是否在管线上。"""
sdpg = role.hdtx.sdpg
out: dict = {
"drm_raw": None,
"avi_raw": None,
"vsif_raw": None,
"drm": None,
"avi": None,
"update_flags": None,
"drm_present": False,
"drm_pq": False,
"avi_present": False,
"avi_bt2020": False,
"avi_vic_match": False,
"vsif_present": False,
"sdpg_ok": use_sdpg and not sdpg_st.error and not sdpg_st.overflow,
"signaling_hdr_on_wire": False,
}
if not use_sdpg:
return out
drm_raw = read_tx_infoframe(sdpg, tsi_ci.TSI_HDMI_INFOFRAME_DRM_R, 0x87)
if drm_raw:
out["drm_raw"] = drm_raw
out["drm"] = parse_drm_on_wire(drm_raw)
out["drm_present"] = True
out["drm_pq"] = out["drm"]["pq"]
avi_raw = read_tx_infoframe(sdpg, tsi_ci.TSI_HDMI_INFOFRAME_AVI_R, 0x82)
if avi_raw:
out["avi_raw"] = avi_raw
out["avi"] = parse_avi_on_wire(avi_raw)
out["avi_present"] = True
out["avi_bt2020"] = out["avi"]["bt2020"]
out["avi_vic_match"] = out["avi"]["vic"] == vic
vsif_raw = read_tx_infoframe(sdpg, tsi_ci.TSI_HDMI_INFOFRAME_VSI_R, 0x81)
if vsif_raw:
out["vsif_raw"] = vsif_raw
out["vsif_present"] = True
out["update_flags"] = read_infoframe_update_flags(sdpg)
out["signaling_hdr_on_wire"] = (
out["sdpg_ok"]
and out["drm_present"]
and out["drm_pq"]
and out["avi_present"]
and out["avi_bt2020"]
)
return out
def print_link_verification_report(
verify: dict,
*,
vic: int,
use_sdpg: bool,
hdr_mode: bool,
output_mode: str = "solid",
image_meta: dict | None = None,
) -> None:
"""打印链路信令验证,区分「信令 HDR」与「峰值亮度 HDR」。"""
print("--- 链路 HDR 信令验证 (--verify-link) ---", flush=True)
if not use_sdpg:
print(" 未启用 sdpg无 InfoFrame 可验证。", flush=True)
print("------------------------", flush=True)
return
print(f" sdpg 硬件: error={not verify['sdpg_ok']}", flush=True)
if verify["drm_raw"]:
drm = verify["drm"]
print(f" DRM (0x87) 回读: {verify['drm_raw'][:20].hex(' ')}...", flush=True)
eotf_label = "PQ/HDR10" if drm["pq"] else f"0x{drm['eotf']:02X}"
print(
f" EOTF={eotf_label}, metadata_type={drm['metadata_type']}, "
f"MaxCLL={drm['max_cll']}, MaxFALL={drm['max_fall']}",
flush=True,
)
else:
print(" DRM (0x87) 回读: 无有效数据(寄存器未回读或尚未更新)", flush=True)
if verify["avi_raw"]:
avi = verify["avi"]
print(f" AVI (0x82) 回读: {verify['avi_raw'][:16].hex(' ')}...", flush=True)
print(
f" VIC={avi['vic']} (期望 {vic}), "
f"extended_colorimetry={avi['extended_colorimetry']} "
f"({'BT.2020' if avi['bt2020'] else '非 BT.2020'}), "
f"range={'Full' if avi['full_range'] else 'Limited'}",
flush=True,
)
else:
print(" AVI (0x82) 回读: 无有效数据", flush=True)
if verify["vsif_present"]:
print(f" VSIF (0x81) 回读: 有 ({len(verify['vsif_raw'])} bytes)", flush=True)
else:
print(" VSIF (0x81) 回读: 无(未发送或寄存器无数据)", flush=True)
flags = verify["update_flags"]
if flags is not None:
print(f" InfoFrame update flags: 0x{flags:08X}", flush=True)
else:
print(" InfoFrame update flags: 无法读取", flush=True)
print(flush=True)
sig = verify["signaling_hdr_on_wire"]
print(f" 【信令层 HDR10】 {'' if sig else '否/不完整'}", flush=True)
print(
" 判定: DRM EOTF=PQ + AVI BT.2020 + sdpg 无 error"
+ (" + VIC 匹配" if verify.get("avi_vic_match") else ""),
flush=True,
)
if not sig and verify["sdpg_ok"]:
print(
" 提示: sdpg 正常但寄存器无回读时,仍可用电视 HDR 标志或 --no-sdpg 对照。",
flush=True,
)
print(" 【像素层 PQ 编码】 无法由 InfoFrame 确认", flush=True)
if hdr_mode:
if output_mode == "image" and image_meta:
enc = image_meta.get("encoding", "")
if enc == "PQ ST2084":
pq_v = image_meta.get("pq_v", 0)
nits = image_meta.get("peak_nits", "?")
lv = image_meta.get("png_rgb", "?")
print(
f" ImageFile: ST 2084 PQ 白场,目标峰值 {nits} nits"
f"PQ V≈{pq_v:.4f}PNG RGB={lv}",
flush=True,
)
else:
print(f" ImageFile: 用户图片 ({image_meta.get('path', '?')})", flush=True)
else:
print(
" SolidColor 仅保证高码值/BT.2020 配置;"
"亮度不变常见于像素未按 PQ 曲线编码。"
"请改用 --output image默认",
flush=True,
)
print(" 【Sink 峰值亮度】 需 CA-410 HDR/PQ 模式 + 电视 HDR 开启", flush=True)
print(flush=True)
print(" 建议对照:", flush=True)
print(" 1. 观察电视输入信息是否显示 HDR10 / BT.2020", flush=True)
print(" 2. python hdr10_demo.py --no-sdpg → HDR 标志应消失", flush=True)
print(" 3. python hdr10_demo.py --sdr → 记录 SDR 亮度/色度基准", flush=True)
print(" 4. CA-410 使用 Flicker-Free + HDR10 测量模式", flush=True)
print("------------------------", flush=True)
def read_drm_eotf_from_tx(sdpg) -> int | None:
"""尝试读 TX 侧 DRM InfoFrame 的 EOTF 字节(未文档化,仅供诊断)。"""
try:
io = sdpg._PacketGenerator__io # noqa: SLF001
drm = io.get(tsi_ci.TSI_HDMI_INFOFRAME_DRM_R, c_byte, 32)[1]
if drm and len(drm) >= 5 and drm[0] == 0x87:
return int(drm[4])
except Exception:
pass
return None
def poll_sdpg_status(sdpg, *, seconds: float = 2.0, interval: float = 0.5) -> list:
samples = []
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
samples.append(sdpg.status())
time.sleep(interval)
return samples
def interpret_sdpg_notes(status, link: dict) -> list[str]:
notes: list[str] = []
if not link["hpd"]:
notes.append("HPD=LowInfoFrame 可能未送到 Sink。")
if not link["video"]:
notes.append("video=Off建议 sdpg 后重新 apply PG。")
if status.error:
notes.append("sdpg error=TrueInfoFrame 格式/长度可能不被接受。")
elif not status.enabled and not status.active:
notes.append(
"Enabled/Active 均为 FalseUCD-323 固件常见,包仍可能已在链路上;"
"请以 CA-410 或电视 HDR 标志为准。"
)
elif status.enabled or status.active:
notes.append("sdpg Enabled/Active信令发生器运行中。")
return notes
def assess_hdr_ready(
*,
hdr_mode: bool,
use_sdpg: bool,
pg_diag: dict,
link: dict,
sdpg_st,
configured_hdr_pixels: bool = False,
configured_pq_pixels: bool = False,
) -> dict:
pixels_hdr = configured_hdr_pixels or configured_pq_pixels or (
is_bt2020_colorimetry(pg_diag["colorimetry"]) and pg_diag["bpc"] >= 10
)
pq_pixels = configured_pq_pixels
pg_ok = pg_diag["pg_error"] == "OK"
signaling_ok = (
use_sdpg
and link["hpd"]
and not sdpg_st.error
and not sdpg_st.overflow
)
hdr_ready = hdr_mode and use_sdpg and pg_ok and pixels_hdr and signaling_ok
return {
"pg_ok": pg_ok,
"pixels_hdr": pixels_hdr,
"pq_pixels": pq_pixels,
"signaling_ok": signaling_ok,
"hdr_ready": hdr_ready,
}
def print_hdr_ready_summary(
assessment: dict,
*,
hdr_mode: bool,
use_sdpg: bool,
infoframe_only: bool = False,
sdr: bool = False,
configured_colorimetry: str | None = None,
output_mode: str = "solid",
) -> None:
print("--- HDR 就绪条件摘要 ---", flush=True)
print(f" PG 无错误: {'' if assessment['pg_ok'] else ''}")
if hdr_mode:
note = ""
if output_mode == "image" and assessment.get("pq_pixels"):
note = "PQ ST2084 ImageFile"
elif configured_colorimetry and not is_bt2020_colorimetry(configured_colorimetry):
note = "(配置色域见 PG 读回说明)"
elif configured_colorimetry and output_mode == "solid" and configured_colorimetry:
note = "SolidColor 读回常为 CM_sRGB以配置 BT.2020 为准)"
elif output_mode == "image" and configured_colorimetry:
note = "ImageFile 配置 BT.2020UICL 用 YCbCr 枚举)"
print(
f" PG HDR 像素: {'是 (BT.2020 ≥10bit)' if assessment['pixels_hdr'] else ''}"
f"{note}"
)
if output_mode == "image":
print(
f" PQ 编码像素: {'' if assessment.get('pq_pixels') else '否/未确认'}"
)
if use_sdpg:
print(f" sdpg 信令层 OK: {'' if assessment['signaling_ok'] else ''}")
if hdr_mode and use_sdpg:
ready = assessment["hdr_ready"]
print(
f" 源端 HDR10 就绪: {'' if ready else ''}"
"(仍需 CA-410 确认 Sink 峰值亮度)"
)
if not ready:
print(" → 源端未就绪CA-410 亮度大概率不会升高。")
else:
print(" → 源端配置完整;若亮度仍不变,请查 CA-410 HDR 模式与电视 HDR 设置。")
elif infoframe_only:
print(" 模式: SDR 像素 + sdpg — 电视可能显示 HDR 标志,亮度通常不会升高。")
elif sdr:
print(" 模式: SDR 对照 — 无 sdpg亮度约 100200 nits。")
print("------------------------")
def apply_pg_image(
role,
video_mode: UniTAP.VideoMode,
image_path: Path,
*,
sdpg_events_bytes: int = 0,
) -> dict:
"""ImageFileset_vm → set_pattern(路径) → apply()(对齐 app/ucd device.send_image_pattern"""
pg = role.hdtx.pg
log_step(f" [PG] ImageFile set_vm ({video_mode.color_info.colorimetry.name}, bpc={video_mode.color_info.bpc}) ...")
pg.set_vm(vm=video_mode)
log_step(f" [PG] ImageFile set_pattern → {image_path}")
pg.set_pattern(pattern=str(image_path))
log_step(" [PG] ImageFile apply() ...")
layout_patch = (
patch_image_memory_layout(role, sdpg_events_bytes)
if sdpg_events_bytes > 0
else nullcontext()
)
with layout_patch:
if not pg.apply():
diag = read_pg_diagnostics(pg)
raise RuntimeError(f"ImageFile apply() 失败: {diag['pg_error']}")
try:
role.hdtx.ag.stop_generate()
except Exception:
pass
return read_pg_diagnostics(pg)
def apply_pg_output(
role,
*,
output_mode: str,
video_mode: UniTAP.VideoMode,
image_path: Path | None,
white_code: int,
sdpg_events_bytes: int = 0,
) -> dict:
if output_mode == "image":
if image_path is None:
raise ValueError("image 模式需要 image_path")
return apply_pg_image(
role,
video_mode,
image_path,
sdpg_events_bytes=sdpg_events_bytes,
)
return apply_pg_solid(role, video_mode, white_code)
def resolve_output_mode(args, *, hdr_mode: bool, infoframe_only: bool) -> str:
if args.output != "auto":
return args.output
if hdr_mode and not infoframe_only:
return "image"
return "solid"
def apply_pg_solid(role, video_mode: UniTAP.VideoMode, white_code: int) -> dict:
"""顺序对齐 work_with_solid_color.pyset_pattern → set_vm → apply → params。"""
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']}")
pg.set_pattern_params(
pattern_params=UniTAP.SolidColorParams(code, code, code)
)
try:
role.hdtx.ag.stop_generate()
except Exception:
pass
return read_pg_diagnostics(pg)
def load_vsif_packet() -> bytearray:
if VSIF_PATH.is_file():
return bytearray(VSIF_PATH.read_bytes())
return bytearray(OFFICIAL_HDR10PLUS_VSIF)
def build_sdpg_packets(
*,
vic: int,
max_cll: int,
max_fall: int,
include_vsif: bool,
) -> list[tuple[str, bytearray]]:
packets: list[tuple[str, bytearray]] = []
if include_vsif:
packets.append(("VSIF HDR10+ (0x81)", load_vsif_packet()))
packets.append((f"AVI BT.2020 VIC{vic} (0x82)", build_avi_bt2020_packet(vic)))
drm = build_drm_hdr10_packet(max_cll=max_cll, max_fall=max_fall)
if not verify_drm_checksum(drm):
raise RuntimeError("DRM 包 checksum 校验失败")
packets.append((f"DRM HDR10 PQ (0x87, MaxCLL={max_cll})", drm))
return packets
def start_sdpg(sdpg, packets: list[bytearray], *, delay: float):
"""
启动 sdpg对齐官方 work_with_packet_generation.py
SolidColor 须在 PG 出图前调用ImageFile 须在 PG apply含 layout 扩展)之后调用。
"""
sdpg.reset()
for idx, packet in enumerate(packets):
sdpg.add_packet(packet)
log_step(
f" sdpg add_packet[{idx}]: type=0x{packet[0]:02X}, "
f"len={len(packet)} bytes"
)
sdpg.stop()
log_step(f" sdpg stop 后: {sdpg.status()}")
if delay > 0:
log_step(f" sdpg 等待 {delay:g}s官方示例 5s...")
time.sleep(delay)
log_step(" sdpg 调用 start() ...")
try:
sdpg.start()
except Exception as exc:
raise RuntimeError(f"sdpg.start() 失败: {exc}") from exc
log_step(" sdpg start() 返回 OK")
samples = poll_sdpg_status(sdpg, seconds=2.0, interval=0.5)
final = samples[-1] if samples else sdpg.status()
log_step(f" sdpg start 后 (最终): {final}")
if len(samples) > 1:
log_step(
f" sdpg 轮询 ({len(samples)} 次): "
+ "".join(str(s) for s in samples)
)
caps = read_sdpg_caps_raw(sdpg)
log_step(f" sdpg caps (raw): 0x{caps:08X}")
eotf = read_drm_eotf_from_tx(sdpg)
if eotf is not None:
eotf_name = "PQ (HDR10)" if eotf == 0x04 else f"0x{eotf:02X}"
log_step(f" TX DRM EOTF 回读: {eotf_name}")
if final.error:
raise RuntimeError("sdpg 启动后报错(请检查 InfoFrame 格式)")
return final
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="UCD-323 HDR10 演示PQ ImageFile 白场 + sdpg InfoFrame"
)
parser.add_argument(
"--device",
type=parse_device_arg,
default=0,
help="设备索引或 8 位序列号(默认 0",
)
parser.add_argument(
"--vic",
type=int,
default=16,
help="CTA VIC默认 16 = 1080p60",
)
parser.add_argument(
"--white-code",
type=int,
default=SOLID_COLOR_MAX,
help=f"SolidColor 白场码值 0{SOLID_COLOR_MAX}--output solid 时有效)",
)
parser.add_argument(
"--output",
choices=("auto", "solid", "image"),
default="auto",
help="像素输出auto=HDR 用 PQ ImageFile、其余用 SolidColor默认 auto",
)
parser.add_argument(
"--peak-nits",
type=float,
default=None,
help="PQ 白场目标峰值亮度 nits默认与 --max-cll 相同)",
)
parser.add_argument(
"--image",
type=str,
default=None,
help="自定义 PNG 路径(--output image 时HDR 请使用 PQ 编码图)",
)
parser.add_argument(
"--bpc",
type=int,
default=10,
help="ImageFile VideoMode 位深(默认 10",
)
parser.add_argument(
"--max-cll",
type=int,
default=1000,
help="DRM MaxCLLnits默认 1000",
)
parser.add_argument(
"--max-fall",
type=int,
default=400,
help="DRM MaxFALLnits默认 400",
)
parser.add_argument(
"--hold",
type=float,
default=60.0,
help="保持输出秒数(默认 60",
)
parser.add_argument(
"--settle",
type=float,
default=3.0,
help="PG/sdpg 生效后等待秒数(默认 3",
)
parser.add_argument(
"--sdpg-delay",
type=float,
default=5.0,
help="sdpg stop 与 start 间隔秒数(官方示例 5s默认 5",
)
parser.add_argument(
"--hpd-wait",
type=float,
default=10.0,
help="等待 HPD 秒数(默认 10",
)
parser.add_argument(
"--sdr",
action="store_true",
help="SDR 对照sRGB 8bit 白场,不发 sdpg亮度约 100200 nits",
)
parser.add_argument(
"--infoframe-only",
action="store_true",
help="SDR 白场 + 仅 sdpg电视可能显示 HDR 标志,亮度不会升高)",
)
parser.add_argument(
"--no-vsif",
action="store_true",
help="sdpg 不发 VSIF仅 AVI + DRM",
)
parser.add_argument(
"--no-sdpg",
action="store_true",
help="仅 PG 出图,不发 sdpg InfoFrame",
)
parser.add_argument(
"--pg-before-sdpg",
action="store_true",
help="先 PG 再 sdpgImageFile 默认SolidColor 请保持默认先 sdpg",
)
parser.add_argument(
"--verify-link",
action=argparse.BooleanOptionalAction,
default=True,
help="验证 TX InfoFrame 回读,输出信令 HDR 与峰值亮度 HDR 的分层结论(默认开启)",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
hdr_mode = not args.sdr and not args.infoframe_only
use_sdpg = not args.sdr and not args.no_sdpg
output_mode = resolve_output_mode(
args, hdr_mode=hdr_mode, infoframe_only=args.infoframe_only
)
print("=== HDR10 演示 (hdr10_demo.py) ===")
print("提示: 运行前请关闭 pqAutomationApp。")
if args.sdr:
print("模式: SDR 对照sRGB 白场,无 sdpg")
elif args.infoframe_only:
print("模式: SDR 白场 + sdpg验证电视 HDR 标志,亮度不会升高)")
elif args.no_sdpg:
print("模式: HDR 像素PQ ImageFile + BT.2020 10bit无 sdpg")
else:
print("模式: HDR10 完整PQ ImageFile + VSIF/AVI/DRM")
if output_mode == "image":
print("像素: ImageFileST 2084 PQ 编码白场CA-410 测峰值推荐)")
else:
print("像素: SolidColor信令对照亮度通常不会升高")
print()
image_path: Path | None = None
image_meta: dict = {}
white_code = args.white_code if hdr_mode or args.infoframe_only else min(args.white_code, 255)
if output_mode == "image":
image_path, image_meta = prepare_output_assets(
args,
hdr_mode=hdr_mode,
output_mode=output_mode,
infoframe_only=args.infoframe_only,
)
tsi = UniTAP.TsiLib()
dev = None
try:
try:
tsi.rescan_devices()
except Exception as exc:
print(f" 警告: rescan_devices 失败: {exc}", file=sys.stderr)
print(f"正在打开设备: {args.device!r} ...")
dev = tsi.open(args.device)
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
prepare_device(role)
if use_sdpg:
sdpg_events_bytes = ensure_sdpg_memory(role)
else:
sdpg_events_bytes = 0
if not wait_for_hpd(role, timeout=args.hpd_wait):
print(
f" 警告: {args.hpd_wait:g}s 内 HPD 仍为 Low"
"请确认电视已开机且 HDMI 已连接。",
file=sys.stderr,
)
timing = resolve_timing(role, vic=args.vic)
refresh_hz = timing.frame_rate / 1000.0 if timing.frame_rate else 0.0
print(
f"Timing: CTA VIC {args.vic}"
f"{timing.hactive}x{timing.vactive} @ {refresh_hz:g} Hz"
)
if output_mode == "image":
image_path, image_meta = align_output_image_to_timing(
image_path,
image_meta,
timing,
hdr_mode=hdr_mode,
peak_nits=args.peak_nits,
max_cll=args.max_cll,
image_arg=args.image,
solid_white_code=white_code,
)
vm = build_image_video_mode(timing, hdr=hdr_mode, bpc=args.bpc)
if args.sdr:
vm.color_info.bpc = 8
color_label = (
f"BT.2020 {vm.color_info.bpc}bit"
if hdr_mode
else f"BT.709 {vm.color_info.bpc}bit"
)
print(f"PG 配置: ImageFile {color_label}")
print(f" 图片: {image_path}")
if image_meta.get("encoding") == "PQ ST2084":
print(
f" PQ: 峰值 {image_meta['peak_nits']} nits, "
f"V≈{image_meta['pq_v']:.4f}, PNG RGB={image_meta['png_rgb']}"
)
else:
vm = build_solid_video_mode(timing, hdr=hdr_mode, bpc=args.bpc)
color_label = (
f"BT.2020 {vm.color_info.bpc}bit"
if hdr_mode
else f"sRGB {vm.color_info.bpc}bit"
)
if args.sdr:
vm.color_info.bpc = 8
color_label = "sRGB 8bit"
print(f"PG 配置: SolidColor {color_label}, 码值={white_code}")
if hdr_mode:
print(
" 说明: SolidColor 硬件读回常为 CM_sRGB"
"测峰值亮度请用默认 ImageFile--output image"
)
configured_cm = vm.color_info.colorimetry.name
configured_hdr_pixels = (
hdr_mode
and is_bt2020_colorimetry(configured_cm)
and vm.color_info.bpc >= 10
)
configured_pq_pixels = (
hdr_mode
and output_mode == "image"
and image_meta.get("encoding") == "PQ ST2084"
)
sdpg_st = None
raw_packets: list[bytearray] = []
if use_sdpg:
packet_defs = build_sdpg_packets(
vic=args.vic,
max_cll=args.max_cll,
max_fall=args.max_fall,
include_vsif=not args.no_vsif,
)
print("sdpg 包:")
for label, packet in packet_defs:
print(f" [{label}]")
print(format_packet_hex(packet))
raw_packets.append(packet)
sdpg_first = resolve_sdpg_first(args, use_sdpg=use_sdpg, output_mode=output_mode)
if sdpg_first:
log_step("顺序: 先 sdpg 再 PGSolidColor 推荐)")
log_step("启动 sdpg ...")
sdpg_st = start_sdpg(role.hdtx.sdpg, raw_packets, delay=args.sdpg_delay)
for note in interpret_sdpg_notes(sdpg_st, read_link_diagnostics(role)):
log_step(f"{note}")
log_step("配置 PG ...")
pg_events = sdpg_events_bytes if use_sdpg and output_mode == "image" else 0
pg_diag = apply_pg_output(
role,
output_mode=output_mode,
video_mode=vm,
image_path=image_path,
white_code=white_code,
sdpg_events_bytes=pg_events,
)
print_pg_diagnostics(role, label="PG apply")
time.sleep(args.settle)
link = print_link_diagnostics(role, label="PG 后链路")
if use_sdpg and not sdpg_first:
log_step("顺序: 先 PG 再 sdpgImageFile 必需)")
log_step("启动 sdpg ...")
sdpg_st = start_sdpg(role.hdtx.sdpg, raw_packets, delay=args.sdpg_delay)
link_after_sdpg = print_link_diagnostics(role, label="sdpg 后链路")
for note in interpret_sdpg_notes(sdpg_st, link_after_sdpg):
log_step(f"{note}")
if not link_after_sdpg["video"]:
if output_mode == "image":
log_step(
" video=OffImageFile 不能在 sdpg 后再 apply会重置显存导致崩溃"
"请检查 HDMI 连接或改用 --output solid。"
)
else:
log_step("video=Off重新 apply PG ...")
pg_diag = apply_pg_output(
role,
output_mode=output_mode,
video_mode=vm,
image_path=image_path,
white_code=white_code,
sdpg_events_bytes=pg_events,
)
print_pg_diagnostics(role, label="PG 再次 apply")
time.sleep(args.settle)
link = print_link_diagnostics(role, label="最终链路")
elif use_sdpg:
link = print_link_diagnostics(role, label="sdpg+PG 后链路")
if sdpg_st is None:
sdpg_st = role.hdtx.sdpg.status()
assessment = assess_hdr_ready(
hdr_mode=hdr_mode,
use_sdpg=use_sdpg,
pg_diag=pg_diag,
link=link,
sdpg_st=sdpg_st,
configured_hdr_pixels=configured_hdr_pixels,
configured_pq_pixels=configured_pq_pixels,
)
print()
print_hdr_ready_summary(
assessment,
hdr_mode=hdr_mode,
use_sdpg=use_sdpg,
infoframe_only=args.infoframe_only,
sdr=args.sdr,
configured_colorimetry=configured_cm if hdr_mode else None,
output_mode=output_mode,
)
if args.verify_link:
link_verify = verify_tx_link_signaling(
role,
vic=args.vic,
use_sdpg=use_sdpg,
sdpg_st=sdpg_st,
)
print()
print_link_verification_report(
link_verify,
vic=args.vic,
use_sdpg=use_sdpg,
hdr_mode=hdr_mode,
output_mode=output_mode,
image_meta=image_meta if output_mode == "image" else None,
)
print()
if hdr_mode and use_sdpg:
print(
f"HDR10 PQ 白场已输出,保持 {args.hold:g}s。"
"请用 CA-410 测中心亮度(预期显著高于 SDR"
)
elif args.infoframe_only:
print(
f"SDR 白场 + sdpg 已输出,保持 {args.hold:g}s。"
"观察电视 HDR 标志;亮度不会升高。"
)
elif args.sdr:
print(f"SDR 白场已输出,保持 {args.hold:g}s对照组")
else:
print(f"输出已保持 {args.hold:g}s。")
time.sleep(args.hold)
print()
print("保持期结束,最终状态:")
pg_diag_final = print_pg_diagnostics(role, label="最终 PG")
link_final = print_link_diagnostics(role, label="最终链路")
sdpg_final = role.hdtx.sdpg.status() if use_sdpg else sdpg_st
if use_sdpg:
print(f" [最终 sdpg] {sdpg_final}")
assessment_final = assess_hdr_ready(
hdr_mode=hdr_mode,
use_sdpg=use_sdpg,
pg_diag=pg_diag_final,
link=link_final,
sdpg_st=sdpg_final,
configured_hdr_pixels=configured_hdr_pixels,
configured_pq_pixels=configured_pq_pixels,
)
print_hdr_ready_summary(
assessment_final,
hdr_mode=hdr_mode,
use_sdpg=use_sdpg,
infoframe_only=args.infoframe_only,
sdr=args.sdr,
configured_colorimetry=configured_cm if hdr_mode else None,
output_mode=output_mode,
)
if args.verify_link and use_sdpg:
print()
link_verify_final = verify_tx_link_signaling(
role,
vic=args.vic,
use_sdpg=use_sdpg,
sdpg_st=sdpg_final,
)
print_link_verification_report(
link_verify_final,
vic=args.vic,
use_sdpg=use_sdpg,
hdr_mode=hdr_mode,
output_mode=output_mode,
image_meta=image_meta if output_mode == "image" else None,
)
return 0
except KeyboardInterrupt:
print("\n用户中断。")
return 130
except Exception as exc:
print(f"错误: {exc}", file=sys.stderr)
return 1
finally:
if dev is not None:
try:
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
role.hdtx.sdpg.stop()
except Exception:
pass
tsi.close(dev)
tsi.cleanup()
if __name__ == "__main__":
sys.exit(main())