281 lines
8.9 KiB
Python
281 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
HDR10 信号输出验证脚本(UCD-323 HDMI Source + PG + sdpg DRM)。
|
||
|
||
验证 PG(BT.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 峰值亮度。
|
||
若仍约 100–200 nits(SDR 水平),说明信令或 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 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 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 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(" 仍为 ~100–200 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())
|