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