#!/usr/bin/env python3 """ HDR10 信号输出演示(UCD-323 HDMI Source)。 合并官方示例 work_with_solid_color.py(PG 白场)与 work_with_packet_generation.py(sdpg 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(通常 400–1000+ 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 默认先 sdpg;ImageFile 必须先 PG(apply 内扩展 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 内联 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, ]) 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 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) < 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.2020;RGB 枚举会落到 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。 SDR:CM_sRGB + bpc>=10(对齐 work_with_solid_color.py) HDR:CM_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=Low:InfoFrame 可能未送到 Sink。") if not link["video"]: notes.append("video=Off:建议 sdpg 后重新 apply PG。") if status.error: notes.append("sdpg error=True:InfoFrame 格式/长度可能不被接受。") elif not status.enabled and not status.active: notes.append( "Enabled/Active 均为 False:UCD-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.2020,UICL 用 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,亮度约 100–200 nits。") print("------------------------") def apply_pg_image( role, video_mode: UniTAP.VideoMode, image_path: Path, *, sdpg_events_bytes: int = 0, ) -> dict: """ImageFile:set_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.py:set_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 MaxCLL(nits,默认 1000)", ) parser.add_argument( "--max-fall", type=int, default=400, help="DRM MaxFALL(nits,默认 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(亮度约 100–200 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 再 sdpg(ImageFile 默认;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("像素: ImageFile(ST 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 再 PG(SolidColor 推荐)") 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 再 sdpg(ImageFile 必需)") 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=Off:ImageFile 不能在 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())