Files
pqAutomationApp/hdr10_demo.py

1465 lines
49 KiB
Python
Raw Normal View History

#!/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())