1465 lines
49 KiB
Python
1465 lines
49 KiB
Python
|
|
#!/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())
|