Files
pqAutomationApp/test.py
2026-07-02 17:16:18 +08:00

281 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
HDR10 信号输出验证脚本UCD-323 HDMI Source + PG + sdpg DRM
验证 PGBT.2020 / 10bit / Full RGB与 sdpg 自定义 DRM(0x87) 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 --save-bin drm_hdr10.bin
验收:脚本发出 100% PQ 白场后,用 CA-410 测 DUT 峰值亮度。
若仍约 100200 nitsSDR 水平),说明信令或 DUT 设置未生效。
"""
from __future__ import annotations
import argparse
import sys
import time
from pathlib import Path
import UniTAP
# CTA-861 VIC 16 ≈ 3840×2160 @ 60Hz设备需支持该 timing
DEFAULT_VIC = 16
DEFAULT_MAX_CLL = 1000
DEFAULT_MAX_FALL = 400
DEFAULT_HOLD_SECONDS = 60
# 10bit PQ 白场码值(与 work_with_solid_color.py 一致)
PQ_WHITE_10BIT = 1023
PACKET_PAD_BYTES = 36
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_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 InfoFrameType 0x87, Static Metadata Type 1"""
body = bytearray(26)
body[0] = 0x04 # SMPTE ST 2084 (PQ)
body[1] = 0x01 # Static Metadata Type 1
primaries = [
(0.708, 0.292), # R BT.2020
(0.170, 0.797), # G
(0.131, 0.046), # B
(0.3127, 0.3290), # W D65
]
for i, (x, y) in enumerate(primaries):
o = 2 + i * 4
body[o], body[o + 1] = xy_to_be16(x)
body[o + 2], body[o + 3] = xy_to_be16(y)
body[18:20] = [(max_master >> 8) & 0xFF, max_master & 0xFF]
min_code = int(round(min_master_nits / 0.0001))
body[20:22] = [(min_code >> 8) & 0xFF, min_code & 0xFF]
body[22:24] = [(max_cll >> 8) & 0xFF, max_cll & 0xFF]
body[24:26] = [(max_fall >> 8) & 0xFF, max_fall & 0xFF]
header = bytearray([0x87, 0x01, 0x1A, 0x00])
header[3] = (256 - (sum(header[:3]) + sum(body)) % 256) & 0xFF
packet = header + body
if len(packet) < pad_to:
packet.extend(b"\x00" * (pad_to - len(packet)))
return packet
def verify_drm_checksum(packet: bytearray) -> bool:
"""校验 InfoFrame checksumtype + 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 build_hdr10_video_mode(role, vic: int) -> UniTAP.VideoMode:
timing_mgr = role.hdtx.pg.timing_manager
timing = timing_mgr.get_cta(vic)
if timing is None:
available = timing_mgr.get_all()
raise RuntimeError(
f"设备不支持 CTA VIC={vic}"
f"可用 timing 数量: {len(available)},请尝试 --vic 其它值。"
)
color = UniTAP.ColorInfo()
color.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB
color.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA # Full
color.bpc = 10
return UniTAP.VideoMode(timing=timing, color_info=color)
def apply_pg_white(role, video_mode: UniTAP.VideoMode, white_code: int) -> None:
pg = role.hdtx.pg
pg.set_pattern(pattern=UniTAP.VideoPattern.SolidColor)
pg.set_vm(vm=video_mode)
if not pg.apply():
raise RuntimeError("Pattern Generator apply() 失败")
pg.set_pattern_params(
pattern_params=UniTAP.SolidColorParams(white_code, white_code, white_code)
)
def start_sdpg(role, drm_packet: bytearray):
sdpg = role.hdtx.sdpg
sdpg.reset()
sdpg.add_packet(drm_packet)
sdpg.start()
return sdpg.status()
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 信号输出验证PG + sdpg DRM InfoFrame"
)
parser.add_argument(
"--device",
default="0",
help="设备序号(整数)或 8 位序列号,默认 0",
)
parser.add_argument("--vic", type=int, default=DEFAULT_VIC, help=f"CTA VIC默认 {DEFAULT_VIC}")
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=PQ_WHITE_10BIT,
help=f"PQ 10bit 白场码值,默认 {PQ_WHITE_10BIT}",
)
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="列出设备后退出")
args = parser.parse_args()
tsi = UniTAP.TsiLib()
if args.list_devices:
list_devices(tsi)
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
if args.save_bin:
out = Path(args.save_bin)
out.write_bytes(drm)
print(f"已保存 DRM 包: {out.resolve()} ({len(drm)} bytes)")
print("=== HDR10 信号输出测试 ===")
print(f"DRM MaxCLL={args.max_cll}, MaxFALL={args.max_fall}")
print(f"CTA VIC={args.vic}, bpc=10, BT.2020 RGB Full, PQ 白场码值={args.white_code}")
print("DRM 包内容:")
print(format_packet_hex(drm))
print()
dev = None
role = None
try:
device_id = parse_device_arg(args.device)
print(f"正在打开设备: {device_id!r} ...")
dev = tsi.open(device_id)
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
print("配置 Pattern Generator ...")
vm = build_hdr10_video_mode(role, args.vic)
t = vm.timing
refresh_hz = t.frame_rate / 1000.0 if t.frame_rate else 0.0
print(f" Timing: {t.hactive}x{t.vactive} @ {refresh_hz:g} Hz")
print(
f" Color: {vm.color_info.colorimetry.name}, "
f"bpc={vm.color_info.bpc}, range={vm.color_info.dynamic_range.name}"
)
apply_pg_white(role, vm, args.white_code)
print(" PG apply() 成功,已输出 PQ 白场。")
print("启动 sdpg (DRM InfoFrame) ...")
status = start_sdpg(role, drm)
print(f" sdpg status: {status}")
if status.error:
print(" 警告: sdpg 报告 error=True请检查固件或包格式。", file=sys.stderr)
if not status.active and not status.enabled:
print(" 警告: sdpg 未 active/enabled。", file=sys.stderr)
print()
print("--- 请用 CA-410 测量 DUT 100% 白场峰值亮度 ---")
print(" HDR 生效: 通常显著高于 SDR具体取决于电视峰值")
print(" 仍为 ~100200 nits: 检查 DUT HDR 开关 / HDMI 口 / 信令")
print(f"--- 保持输出 {args.hold:.0f}Ctrl+C 可提前结束 ---")
print()
deadline = time.monotonic() + args.hold
while time.monotonic() < deadline:
time.sleep(1.0)
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:
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())