根据新版本调整set_pattern:0~65535

This commit is contained in:
xinzhu.yin
2026-07-07 18:59:44 +08:00
parent 9fa811a9eb
commit 32aaef1c0f
24 changed files with 3016 additions and 279 deletions

View File

@@ -243,6 +243,13 @@ class BaseIO:
self.__ref_count = tsi.TSI_Clean()
logging.info(f"[UniTAP] BaseIO.del: {self}; Ref count: {self.__ref_count}")
def rescan_devices(self):
if self.__ref_count > 0:
tsi.TSIX_DEV_RescanDevices()
return
logging.error("[UniTAP] BaseIO.rescan_devices call without TSI_Init")
def get_device_count(self) -> int:
if self.__ref_count > 0:
return tsi.TSIX_DEV_GetDeviceCount()

View File

@@ -70,6 +70,11 @@ class TsiLib:
self.__io.cleanup()
logging.info(f"[UniTAP] TsiLib.cleanup {self}")
def rescan_devices(self):
"""Rescan USB for newly connected TSI devices without tearing down the library."""
self.__io.rescan_devices()
logging.info(f"[UniTAP] TsiLib.rescan_devices {self}")
def __del__(self):
"""
Automatically call the destructor after the script completes.

View File

@@ -2,6 +2,8 @@ import math
import numpy as np
from shapely.geometry import Polygon
from app.solid_color_scale import SOLID_COLOR_MAX
# ==================
# 色域覆盖率计算公式
@@ -335,7 +337,7 @@ def calculate_gamma(results, max_index_fix, pattern_params=None):
results: [[x, y, lv, X, Y, Z], ...] 测量结果
max_index_fix: 最大灰阶索引
pattern_params: [[R, G, B], ...] 8bit pattern参数用于计算input_level
如果提供,则使用 pattern_value/255 作为底数
如果提供,则使用 pattern_value/SOLID_COLOR_MAX 作为底数
如果不提供,则使用传统的 灰阶百分比 作为底数
Returns:
@@ -372,10 +374,10 @@ def calculate_gamma(results, max_index_fix, pattern_params=None):
# 计算输入灰阶值 input_level
if pattern_params is not None and i < len(pattern_params):
# 使用 8bit pattern 数据 / 255 作为底数22293 Gamma 数据对齐)
# 使用 SolidColor pattern 数据 / SOLID_COLOR_MAX 作为底数22293 Gamma 数据对齐)
# 取 R 通道值(灰阶图案 R=G=B
pattern_value = pattern_params[i][0]
input_level = pattern_value / 255.0
input_level = pattern_value / SOLID_COLOR_MAX
else:
# 传统计算方式:根据数据顺序计算输入灰阶值
if is_descending:

View File

@@ -1,15 +1,17 @@
# -*- coding: UTF-8 -*-
"""
数据范围转换器
将 Full Range (0-255) 转换为 Limited Range (16-235)
使用方法:
from app.data_range_converter import DataRangeConverter
converter = DataRangeConverter()
converted_params = converter.convert(pattern_params, "Limited")
将 Full Range (065535) 转换为 Limited RangeSolidColor 刻度)
"""
from app.solid_color_scale import (
LIMITED_SOLID_COLOR_BLACK,
LIMITED_SOLID_COLOR_WHITE,
SOLID_COLOR_MAX,
SOLID_COLOR_MIN,
clamp_solid_color,
)
class DataRangeConverter:
"""数据范围转换器"""
@@ -25,38 +27,22 @@ class DataRangeConverter:
def convert_value(self, value):
"""
将单个 Full Range 值转换为 Limited Range
将单个 Full Range 值转换为 Limited RangeSolidColor 刻度)。
转换公式:
limited = 16 + (value / 255) × (235 - 16)
Args:
value: Full Range 值 (0-255)
Returns:
int: Limited Range 值 (16-235)
转换公式(归一化后线性映射):
limited = L_black + (value / MAX) × (L_white - L_black)
"""
# 边界值直接映射
if value == 0:
return 16
elif value == 255:
return 235
else:
# 线性映射
limited = 16 + round((value / 255.0) * (235 - 16))
# 限制范围
return max(16, min(235, limited))
value = clamp_solid_color(value)
if value <= SOLID_COLOR_MIN:
return LIMITED_SOLID_COLOR_BLACK
if value >= SOLID_COLOR_MAX:
return LIMITED_SOLID_COLOR_WHITE
span = LIMITED_SOLID_COLOR_WHITE - LIMITED_SOLID_COLOR_BLACK
limited = LIMITED_SOLID_COLOR_BLACK + round((value / SOLID_COLOR_MAX) * span)
return max(LIMITED_SOLID_COLOR_BLACK, min(LIMITED_SOLID_COLOR_WHITE, limited))
def convert_rgb(self, r, g, b):
"""
转换单个 RGB 值
Args:
r, g, b: Full Range RGB 值 (0-255)
Returns:
tuple: Limited Range RGB 值 (16-235)
"""
"""转换单个 RGB 值SolidColor Full → Limited"""
return (self.convert_value(r), self.convert_value(g), self.convert_value(b))
def convert(self, pattern_params, data_range="Full"):
@@ -73,7 +59,7 @@ class DataRangeConverter:
# Full Range 不需要转换
if data_range == "Full":
if self.verbose:
print("使用 Full Range (0-255),无需转换")
print(f"使用 Full Range (0{SOLID_COLOR_MAX}),无需转换")
return pattern_params
# Limited Range 需要转换
@@ -112,8 +98,11 @@ class DataRangeConverter:
def _print_header(self):
"""打印转换头部信息"""
print("=" * 80)
print("【数据范围转换】Limited Range (16-235)")
print(" 转换公式: 16 + (value / 255) × (235 - 16)")
print("【数据范围转换】Limited Range (SolidColor 刻度)")
print(
f" 映射: {SOLID_COLOR_MIN}{SOLID_COLOR_MAX}"
f"{LIMITED_SOLID_COLOR_BLACK}{LIMITED_SOLID_COLOR_WHITE}"
)
print("=" * 80)
def _print_conversion(self, index, r_orig, g_orig, b_orig, r_new, g_new, b_new):
@@ -126,25 +115,23 @@ class DataRangeConverter:
"""
# 判断是否需要打印
should_print = False
label = ""
mid = SOLID_COLOR_MAX // 2
# 第一个和最后一个
if index == 0:
should_print = True
label = "黑色"
elif index == len([]) - 1: # 需要在外部判断
should_print = True
label = "白色"
# 关键 RGB 值
elif (
r_orig in [0, 128, 255]
or g_orig in [0, 128, 255]
or b_orig in [0, 128, 255]
r_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
or g_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
or b_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
):
should_print = True
if r_orig == 128:
if r_orig == mid:
label = "50%"
else:
label = ""
if should_print:
diff = abs(r_new - r_orig)
@@ -165,9 +152,9 @@ class DataRangeConverter:
return {
"name": "Data Range Converter",
"version": "1.0.0",
"full_range": "0-255",
"limited_range": "16-235",
"formula": "16 + (value / 255) × 219",
"full_range": f"0-{SOLID_COLOR_MAX}",
"limited_range": f"{LIMITED_SOLID_COLOR_BLACK}-{LIMITED_SOLID_COLOR_WHITE}",
"formula": "L_black + (value / MAX) × (L_white - L_black)",
}
@@ -188,9 +175,9 @@ def convert_pattern_params(pattern_params, data_range="Full", verbose=True):
示例:
>>> from app.data_range_converter import convert_pattern_params
>>> params = [[0,0,0], [255,255,255]]
>>> params = [[0,0,0], [65535,65535,65535]]
>>> converted = convert_pattern_params(params, "Limited")
[[16,16,16], [235,235,235]]
[[4112,4112,4112], [60492,60492,60492]]
"""
converter = DataRangeConverter(verbose=verbose)
return converter.convert(pattern_params, data_range)
@@ -201,7 +188,7 @@ def convert_single_rgb(r, g, b, data_range="Full"):
便捷函数:转换单个 RGB 值
Args:
r, g, b: RGB 值 (0-255)
r, g, b: RGB 值 (065535)
data_range: "Full""Limited"
Returns:
@@ -232,7 +219,8 @@ if __name__ == "__main__":
print("\n[测试 1] 基本转换...")
converter = DataRangeConverter(verbose=False)
test_values = [0, 16, 64, 128, 192, 235, 255]
test_values = [0, LIMITED_SOLID_COLOR_BLACK, SOLID_COLOR_MAX // 4, SOLID_COLOR_MAX // 2,
SOLID_COLOR_MAX * 3 // 4, LIMITED_SOLID_COLOR_WHITE, SOLID_COLOR_MAX]
print(" Full Range → Limited Range:")
for v in test_values:
limited = converter.convert_value(v)
@@ -243,8 +231,8 @@ if __name__ == "__main__":
print("\n[测试 2] RGB 转换...")
test_rgb = [
(0, 0, 0),
(128, 128, 128),
(255, 255, 255),
(SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2),
(SOLID_COLOR_MAX, SOLID_COLOR_MAX, SOLID_COLOR_MAX),
]
for r, g, b in test_rgb:
@@ -253,12 +241,14 @@ if __name__ == "__main__":
# 测试 3: 完整转换流程
print("\n[测试 3] 完整转换流程...")
from app.ucd.solid_color_scale import byte_to_solid_color as _sc
pattern_params = [
[255, 255, 255], # 100% 白
[230, 230, 230], # 90%
[204, 204, 204], # 80%
[128, 128, 128], # 50%
[0, 0, 0], # 0% 黑
[_sc(255)] * 3,
[_sc(230)] * 3,
[_sc(204)] * 3,
[_sc(128)] * 3,
[0, 0, 0],
]
converted = converter.convert(pattern_params, "Limited")
@@ -270,11 +260,11 @@ if __name__ == "__main__":
# 测试 4: 便捷函数
print("\n[测试 4] 便捷函数...")
result = convert_pattern_params(
[[0, 0, 0], [255, 255, 255]], "Limited", verbose=False
[[0, 0, 0], [SOLID_COLOR_MAX, SOLID_COLOR_MAX, SOLID_COLOR_MAX]], "Limited", verbose=False
)
print(f" 结果: {result}")
r, g, b = convert_single_rgb(128, 128, 128, "Limited")
r, g, b = convert_single_rgb(SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2, "Limited")
print(f" RGB(128,128,128) → RGB({r},{g},{b})")
# 测试 5: 获取信息

View File

@@ -4,6 +4,7 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
"""
import numpy as np
from app.solid_color_scale import solid_color_to_pct
from app.views.modern_styles import get_theme_palette
from typing import TYPE_CHECKING
@@ -25,7 +26,14 @@ def _is_dark_palette(palette: dict[str, str]) -> bool:
def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_gamma, test_type):
def plot_gamma(
self: "PQAutomationApp",
L_bar,
results_with_gamma_list,
target_gamma,
test_type,
pattern_params=None,
):
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
palette = get_theme_palette()
dark_mode = _is_dark_palette(palette)
@@ -51,13 +59,19 @@ def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_g
for spine in self.gamma_ax.spines.values():
spine.set_color(palette["border"])
# 生成横坐标(灰阶百分比)
# 横坐标:优先用 pattern 码值换算的灰阶百分比,否则等分
if pattern_params and len(pattern_params) == len(L_bar):
x_values = np.array([
solid_color_to_pct(row[0]) for row in pattern_params
])
else:
x_values = np.linspace(0, 100, len(L_bar))
# 反转 L_bar确保从左到右是 0% → 100%
if len(L_bar) > 1 and L_bar[0] > L_bar[-1]:
L_bar = L_bar[::-1]
results_with_gamma_list = results_with_gamma_list[::-1]
x_values = x_values[::-1]
# 计算平均Gamma
gamma_values = []

View File

@@ -3,6 +3,13 @@ import json
import copy
from pathlib import Path
from app.solid_color_scale import (
SOLID_COLOR_MAX,
ensure_solid_color_pattern,
pct_to_solid_color,
solid_color_rgb as _rgb,
)
# =============================================================================
# Pattern 文件读写工具统一存储格式settings/patterns/{name}.json
@@ -25,7 +32,7 @@ def load_pattern_file(filepath) -> dict:
}
"""
with open(filepath, encoding="utf-8") as f:
return json.load(f)
return ensure_solid_color_pattern(json.load(f))
def save_pattern_file(filepath, pattern: dict) -> None:
@@ -137,9 +144,9 @@ _PATTERN_RGB = {
"measurement_bit_depth": 8,
"measurement_max_value": 2,
"pattern_params": [
[255, 0, 0], # 红色
[0, 255, 0], # 绿色
[0, 0, 255], # 蓝色
_rgb(255, 0, 0), # 红色
_rgb(0, 255, 0), # 绿色
_rgb(0, 0, 255), # 蓝色
],
}
@@ -149,17 +156,17 @@ _PATTERN_GRAY_FALLBACK = {
"measurement_bit_depth": 8,
"measurement_max_value": 10,
"pattern_params": [
[255, 255, 255], # 100% 白色
[230, 230, 230], # 90%
[205, 205, 205], # 80%
[179, 179, 179], # 70%
[154, 154, 154], # 60%
[128, 128, 128], # 50%
[102, 102, 102], # 40%
[78, 78, 78], # 30%
[52, 52, 52], # 20%
[26, 26, 26], # 10%
[0, 0, 0], # 0% 黑色
_rgb(255, 255, 255), # 100% 白色
_rgb(230, 230, 230), # 90%
_rgb(205, 205, 205), # 80%
_rgb(179, 179, 179), # 70%
_rgb(154, 154, 154), # 60%
_rgb(128, 128, 128), # 50%
_rgb(102, 102, 102), # 40%
_rgb(78, 78, 78), # 30%
_rgb(52, 52, 52), # 20%
_rgb(26, 26, 26), # 10%
_rgb(0, 0, 0), # 0% 黑色
],
}
# 灰阶 pattern 从文件加载,支持用户编辑;文件缺失时回退到硬编码兜底
@@ -171,37 +178,37 @@ _PATTERN_ACCURACY = {
"measurement_max_value": 28, # 29个颜色最大索引是28
"pattern_params": [
# ========== 灰阶 (5个) ==========
[255, 255, 255], # 0: White
[230, 230, 230], # 1: Gray 80
[209, 209, 209], # 2: Gray 65
[186, 186, 186], # 3: Gray 50
[158, 158, 158], # 4: Gray 35
_rgb(255, 255, 255), # 0: White
_rgb(230, 230, 230), # 1: Gray 80
_rgb(209, 209, 209), # 2: Gray 65
_rgb(186, 186, 186), # 3: Gray 50
_rgb(158, 158, 158), # 4: Gray 35
# ========== ColorChecker 24色 (18个) ==========
[115, 82, 66], # 5: Dark Skin
[194, 150, 130], # 6: Light Skin
[94, 122, 156], # 7: Blue Sky
[89, 107, 66], # 8: Foliage
[130, 128, 176], # 9: Blue Flower
[99, 189, 168], # 10: Bluish Green
[217, 120, 41], # 11: Orange
[74, 92, 163], # 12: Purplish Blue
[194, 84, 97], # 13: Moderate Red
[92, 61, 107], # 14: Purple
[158, 186, 64], # 15: Yellow Green
[230, 161, 46], # 16: Orange Yellow
[51, 61, 150], # 17: Blue (Legacy)
[71, 148, 71], # 18: Green (Legacy)
[176, 48, 59], # 19: Red (Legacy)
[237, 199, 33], # 20: Yellow (Legacy)
[186, 84, 145], # 21: Magenta (Legacy)
[0, 133, 163], # 22: Cyan (Legacy)
_rgb(115, 82, 66), # 5: Dark Skin
_rgb(194, 150, 130), # 6: Light Skin
_rgb(94, 122, 156), # 7: Blue Sky
_rgb(89, 107, 66), # 8: Foliage
_rgb(130, 128, 176), # 9: Blue Flower
_rgb(99, 189, 168), # 10: Bluish Green
_rgb(217, 120, 41), # 11: Orange
_rgb(74, 92, 163), # 12: Purplish Blue
_rgb(194, 84, 97), # 13: Moderate Red
_rgb(92, 61, 107), # 14: Purple
_rgb(158, 186, 64), # 15: Yellow Green
_rgb(230, 161, 46), # 16: Orange Yellow
_rgb(51, 61, 150), # 17: Blue (Legacy)
_rgb(71, 148, 71), # 18: Green (Legacy)
_rgb(176, 48, 59), # 19: Red (Legacy)
_rgb(237, 199, 33), # 20: Yellow (Legacy)
_rgb(186, 84, 145), # 21: Magenta (Legacy)
_rgb(0, 133, 163), # 22: Cyan (Legacy)
# ========== 100% 饱和色 (6个) ==========
[255, 0, 0], # 23: 100% Red
[0, 255, 0], # 24: 100% Green
[0, 0, 255], # 25: 100% Blue
[0, 255, 255], # 26: 100% Cyan
[255, 0, 255], # 27: 100% Magenta
[255, 255, 0], # 28: 100% Yellow
_rgb(255, 0, 0), # 23: 100% Red
_rgb(0, 255, 0), # 24: 100% Green
_rgb(0, 0, 255), # 25: 100% Blue
_rgb(0, 255, 255), # 26: 100% Cyan
_rgb(255, 0, 255), # 27: 100% Magenta
_rgb(255, 255, 0), # 28: 100% Yellow
],
}
@@ -505,7 +512,7 @@ def _gen_even_gray(n: int) -> list[list[int]]:
out = []
for i in range(n):
pct = 100.0 - (100.0 / (n - 1)) * i
v = int(round(pct / 100.0 * 255))
v = pct_to_solid_color(pct)
out.append([v, v, v])
return out
@@ -521,7 +528,7 @@ def _gen_pq_gray(n: int) -> list[list[int]]:
out = []
for i in range(n):
v_pq = 1.0 - i / (n - 1)
v = int(round(v_pq * 255))
v = int(round(v_pq * SOLID_COLOR_MAX))
out.append([v, v, v])
return out
@@ -534,7 +541,7 @@ def _gen_gamma_gray(n: int, gamma: float = 2.2) -> list[list[int]]:
for i in range(n):
lin = 1.0 - i / (n - 1) # 线性光 1→0
code = lin ** (1.0 / gamma) # gamma 编码
v = int(round(code * 255))
v = int(round(code * SOLID_COLOR_MAX))
out.append([v, v, v])
return out

View File

@@ -51,12 +51,13 @@ def run_test(self: "PQAutomationApp", test_type, test_items):
, level="info")
# 根据测试类型执行不同的测试流程
ok = True
if test_type == "screen_module":
self.run_screen_module_test(test_items)
ok = self.run_screen_module_test(test_items)
elif test_type == "sdr_movie":
self.run_sdr_movie_test(test_items)
ok = self.run_sdr_movie_test(test_items)
elif test_type == "hdr_movie":
self.run_hdr_movie_test(test_items)
ok = self.run_hdr_movie_test(test_items)
elif test_type == "local_dimming":
self.log_gui.log(
"Local Dimming 为手动模式,请在 Local Dimming 面板发送图案并采集亮度",
@@ -65,7 +66,10 @@ def run_test(self: "PQAutomationApp", test_type, test_items):
# 测试完成后更新UI状态
if self.testing: # 如果没有被中途停止
if ok:
self._dispatch_ui(self.on_test_completed)
else:
self._dispatch_ui(self.on_test_error)
except Exception as e:
self.log_gui.log(f"测试过程中发生错误: {str(e)}", level="info")
import traceback
@@ -82,7 +86,7 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
self.new_pq_results("screen_module", "屏模组性能测试")
else:
self.log_gui.log("未选择任何测试项目", level="info")
return
return False
# 判断是否需要灰阶数据
needs_gray_data = any(
@@ -96,7 +100,7 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
for item in test_items:
if not self.testing: # 检查是否被停止
return
return False
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
@@ -120,7 +124,7 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足,跳过相关测试", level="error")
return
return False
self.log_gui.log(
f"灰阶数据采集完成,共 {len(shared_gray_data)} 个数据点"
@@ -148,6 +152,8 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
elif item == "contrast":
self.test_contrast("screen_module", shared_gray_data)
return True
def run_custom_sdr_test(self: "PQAutomationApp", test_items):
"""执行客户定制 SDR 测试 - 升级版"""
@@ -173,7 +179,7 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
self.new_pq_results("sdr_movie", "SDR Movie测试")
else:
self.log_gui.log("未选择任何测试项目", level="info")
return
return False
# 获取信号格式设置
color_space = self.sdr_color_space_var.get() # BT.709/BT.601/BT.2020
@@ -194,7 +200,7 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
for item in test_items:
if not self.testing:
return
return False
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
@@ -212,7 +218,7 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足", level="error")
return
return False
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
@@ -235,6 +241,8 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
elif item == "accuracy":
self.test_color_accuracy("sdr_movie")
return True
def run_hdr_movie_test(self: "PQAutomationApp", test_items):
"""执行HDR Movie测试"""
@@ -244,7 +252,7 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
self.new_pq_results("hdr_movie", "HDR Movie测试")
else:
self.log_gui.log("未选择任何测试项目", level="info")
return
return False
# 获取信号格式设置
color_space = self.hdr_color_space_var.get()
@@ -269,7 +277,7 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
for item in test_items:
if not self.testing:
return
return False
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
@@ -287,7 +295,7 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足", level="error")
return
return False
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
@@ -310,6 +318,8 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
elif item == "accuracy":
self.test_color_accuracy("hdr_movie")
return True
def send_fix_pattern(self: "PQAutomationApp", mode):
"""发送固定图案并采集数据 - 支持不同测试类型的信号格式"""
@@ -693,9 +703,9 @@ def test_gamma(self: "PQAutomationApp", test_type, gray_data=None):
# ========================================================
# 获取灰阶 pattern 参数用于22293 Gamma数据对齐
pattern_params = self.config.default_pattern_gray.get(
pattern_params = self.config.current_pattern.get(
"pattern_params", None
)
) or self.config.default_pattern_gray.get("pattern_params", None)
# 计算Gamma值使用修正后的 max_index_fix 和 8bit pattern参数
results_with_gamma_list, L_bar = self.calculate_gamma(
@@ -713,8 +723,12 @@ def test_gamma(self: "PQAutomationApp", test_type, gray_data=None):
target_gamma = 2.2
else:
target_gamma = 2.2
self.plot_gamma(L_bar, results_with_gamma_list, target_gamma, test_type)
self._save_chart_snapshot(test_type, "gamma", (L_bar, results_with_gamma_list, target_gamma, test_type))
self.plot_gamma(
L_bar, results_with_gamma_list, target_gamma, test_type, pattern_params
)
self._save_chart_snapshot(
test_type, "gamma", (L_bar, results_with_gamma_list, target_gamma, test_type)
)
self.log_gui.log("Gamma测试完成", level="success")
@@ -780,9 +794,9 @@ def test_eotf(self: "PQAutomationApp", test_type, gray_data=None):
self.log_gui.log(f"最终使用的 max_index_fix = {max_index_fix}", level="info")
# 获取灰阶 pattern 参数用于22293 Gamma数据对齐
pattern_params = self.config.default_pattern_gray.get(
pattern_params = self.config.current_pattern.get(
"pattern_params", None
)
) or self.config.default_pattern_gray.get("pattern_params", None)
# ========== 计算 EOTF复用 Gamma 计算逻辑使用8bit pattern参数==========
results_with_eotf_list, L_bar = self.calculate_gamma(

118
app/solid_color_scale.py Normal file
View File

@@ -0,0 +1,118 @@
"""UCD SolidColor 码值刻度(与 UniTAP ``SolidColorParams`` 一致065535"""
from __future__ import annotations
SOLID_COLOR_MIN = 0
SOLID_COLOR_MAX = 65535
# 官方 work_with_solid_color.py65535 刻度需 bpc>=10
SOLID_COLOR_MIN_BPC = 10
# Limited Range 在 SolidColor 刻度下的端点(等价于 8bit 的 16 / 235
_LIMITED_8BIT_BLACK = 16
_LIMITED_8BIT_WHITE = 235
LIMITED_SOLID_COLOR_BLACK = round(_LIMITED_8BIT_BLACK / 255 * SOLID_COLOR_MAX)
LIMITED_SOLID_COLOR_WHITE = round(_LIMITED_8BIT_WHITE / 255 * SOLID_COLOR_MAX)
def clamp_solid_color(value: int | float) -> int:
"""将码值限制在 SolidColor 合法范围内。"""
return max(SOLID_COLOR_MIN, min(SOLID_COLOR_MAX, int(round(value))))
def pct_to_solid_color(pct: float) -> int:
"""灰阶/亮度百分比 (0100) → SolidColor 码值。"""
pct = max(0.0, min(100.0, float(pct)))
return int(round(pct / 100.0 * SOLID_COLOR_MAX))
def solid_color_to_pct(value: int | float) -> float:
"""SolidColor 码值 → 百分比 (0100)。"""
if SOLID_COLOR_MAX <= 0:
return 0.0
return clamp_solid_color(value) / SOLID_COLOR_MAX * 100.0
def solid_color_to_byte(value: int | float) -> int:
"""SolidColor 码值 → 8bitUI 色块 / HEX 显示用)。"""
return int(round(clamp_solid_color(value) / SOLID_COLOR_MAX * 255))
def byte_to_solid_color(value: int | float) -> int:
"""旧 8bit 码值 (0255) → SolidColor 刻度。"""
byte = max(0, min(255, int(round(value))))
return int(round(byte / 255.0 * SOLID_COLOR_MAX))
def solid_color_rgb(r: int, g: int, b: int) -> list[int]:
"""将旧 8bit RGB 三元组转为 SolidColor 刻度。"""
return [byte_to_solid_color(r), byte_to_solid_color(g), byte_to_solid_color(b)]
def is_legacy_8bit_pattern_params(params: list) -> bool:
"""判断 ``pattern_params`` 是否仍为旧 0255 刻度。"""
if not params:
return False
max_val = 0
for row in params:
if not row:
continue
for channel in row[:3]:
try:
max_val = max(max_val, int(channel))
except (TypeError, ValueError):
pass
return max_val <= 255
def migrate_pattern_params(params: list) -> list:
"""将 0255 刻度的 pattern 参数迁移到 SolidColor 刻度。"""
if not is_legacy_8bit_pattern_params(params):
return params
migrated: list = []
for row in params:
head = [byte_to_solid_color(v) for v in row[:3]]
migrated.append(head + list(row[3:]))
return migrated
def ensure_solid_color_pattern(pattern: dict) -> dict:
"""确保 pattern 字典中的 ``pattern_params`` 使用 SolidColor 刻度。"""
params = pattern.get("pattern_params")
if params:
pattern["pattern_params"] = migrate_pattern_params(params)
return pattern
def pack_solid_color_hw_registers(
rgb: tuple[int, int, int] | list[int],
) -> tuple[int, int]:
"""将 65535 刻度 RGB 打包为 PG 硬件寄存器值(``shifter=0``,新版 UCD API
旧版 UniTAP SDK 在 ``set_pattern_params`` 内还会按 ``16-bpc`` 左移;
65535 刻度下左移会导致 32 位溢出30%/50%/80% 灰阶发暗)。
新版 UCD 直接接受 065535须绕过 SDK 移位。
"""
r = clamp_solid_color(rgb[0])
g = clamp_solid_color(rgb[1])
b = clamp_solid_color(rgb[2])
param0 = ((r << 16) | g) & 0xFFFFFFFF
param1 = b & 0xFFFFFFFF
return param0, param1
__all__ = [
"SOLID_COLOR_MIN",
"SOLID_COLOR_MAX",
"SOLID_COLOR_MIN_BPC",
"LIMITED_SOLID_COLOR_BLACK",
"LIMITED_SOLID_COLOR_WHITE",
"clamp_solid_color",
"pct_to_solid_color",
"solid_color_to_pct",
"solid_color_to_byte",
"byte_to_solid_color",
"solid_color_rgb",
"is_legacy_8bit_pattern_params",
"migrate_pattern_params",
"ensure_solid_color_pattern",
"pack_solid_color_hw_registers",
]

View File

@@ -18,6 +18,8 @@
import math
import numpy as np
from app.solid_color_scale import solid_color_rgb as _rgb
D65_X = 0.3127
D65_Y = 0.3290
@@ -150,35 +152,35 @@ _GRAYSCALE_SIGNAL = {
_SDR_COLOR_PATTERNS = [
("White", 255, 255, 255),
("Gray 80", 230, 230, 230),
("Gray 65", 209, 209, 209),
("Gray 50", 186, 186, 186),
("Gray 35", 158, 158, 158),
("Dark Skin", 115, 82, 66),
("Light Skin", 194, 150, 130),
("Blue Sky", 94, 122, 156),
("Foliage", 89, 107, 66),
("Blue Flower", 130, 128, 176),
("Bluish Green", 99, 189, 168),
("Orange", 217, 120, 41),
("Purplish Blue", 74, 92, 163),
("Moderate Red", 194, 84, 97),
("Purple", 92, 61, 107),
("Yellow Green", 158, 186, 64),
("Orange Yellow", 230, 161, 46),
("Blue (Legacy)", 51, 61, 150),
("Green (Legacy)", 71, 148, 71),
("Red (Legacy)", 176, 48, 59),
("Yellow (Legacy)", 237, 199, 33),
("Magenta (Legacy)", 186, 84, 145),
("Cyan (Legacy)", 0, 133, 163),
("100% Red", 255, 0, 0),
("100% Green", 0, 255, 0),
("100% Blue", 0, 0, 255),
("100% Cyan", 0, 255, 255),
("100% Magenta", 255, 0, 255),
("100% Yellow", 255, 255, 0),
("White", *_rgb(255, 255, 255)),
("Gray 80", *_rgb(230, 230, 230)),
("Gray 65", *_rgb(209, 209, 209)),
("Gray 50", *_rgb(186, 186, 186)),
("Gray 35", *_rgb(158, 158, 158)),
("Dark Skin", *_rgb(115, 82, 66)),
("Light Skin", *_rgb(194, 150, 130)),
("Blue Sky", *_rgb(94, 122, 156)),
("Foliage", *_rgb(89, 107, 66)),
("Blue Flower", *_rgb(130, 128, 176)),
("Bluish Green", *_rgb(99, 189, 168)),
("Orange", *_rgb(217, 120, 41)),
("Purplish Blue", *_rgb(74, 92, 163)),
("Moderate Red", *_rgb(194, 84, 97)),
("Purple", *_rgb(92, 61, 107)),
("Yellow Green", *_rgb(158, 186, 64)),
("Orange Yellow", *_rgb(230, 161, 46)),
("Blue (Legacy)", *_rgb(51, 61, 150)),
("Green (Legacy)", *_rgb(71, 148, 71)),
("Red (Legacy)", *_rgb(176, 48, 59)),
("Yellow (Legacy)", *_rgb(237, 199, 33)),
("Magenta (Legacy)", *_rgb(186, 84, 145)),
("Cyan (Legacy)", *_rgb(0, 133, 163)),
("100% Red", *_rgb(255, 0, 0)),
("100% Green", *_rgb(0, 255, 0)),
("100% Blue", *_rgb(0, 0, 255)),
("100% Cyan", *_rgb(0, 255, 255)),
("100% Magenta", *_rgb(255, 0, 255)),
("100% Yellow", *_rgb(255, 255, 0)),
]

View File

@@ -13,6 +13,20 @@ from app.ucd.device import (
list_devices,
)
from app.ucd.service import PatternService, PatternSession, SignalService
from app.solid_color_scale import (
SOLID_COLOR_MAX,
SOLID_COLOR_MIN,
SOLID_COLOR_MIN_BPC,
byte_to_solid_color,
clamp_solid_color,
ensure_solid_color_pattern,
migrate_pattern_params,
pack_solid_color_hw_registers,
pct_to_solid_color,
solid_color_rgb,
solid_color_to_byte,
solid_color_to_pct,
)
__all__ = [
"SignalService",
@@ -25,4 +39,15 @@ __all__ = [
"EventBus",
"ConnectionChanged",
"DeviceKind",
"SOLID_COLOR_MIN",
"SOLID_COLOR_MAX",
"SOLID_COLOR_MIN_BPC",
"clamp_solid_color",
"pct_to_solid_color",
"solid_color_to_pct",
"solid_color_to_byte",
"byte_to_solid_color",
"solid_color_rgb",
"migrate_pattern_params",
"ensure_solid_color_pattern",
]

View File

@@ -43,6 +43,8 @@ from app.ucd.domain import (
is_ycbcr,
)
from app.ucd.enum import UCDEnum
from app.ucd.pg_status import read_pattern_generator_status_lines
from app.solid_color_scale import SOLID_COLOR_MIN_BPC, clamp_solid_color, pack_solid_color_hw_registers
log = logging.getLogger(__name__)
_DEVICE_LOCK_TIMEOUT_SECONDS = 8.0
@@ -108,6 +110,7 @@ class _UcdSdkBackend:
self.current_pattern_params = None
self.current_pattern_index = 0
self.last_error = None
self._solid_color_stream_ready = False
def search_device(self):
"""搜索可用设备"""
@@ -213,6 +216,10 @@ class _UcdSdkBackend:
self.current_pattern_params = None
self.current_pattern_index = 0
self.current_interface = "HDMI"
self._solid_color_stream_ready = False
def _invalidate_solid_color_stream(self) -> None:
self._solid_color_stream_ready = False
def _force_cleanup(self):
"""强制清理所有状态"""
@@ -440,6 +447,7 @@ class _UcdSdkBackend:
pg, _ = self.get_tx_modules()
log.info("UcdSdk.set_video_mode calling pg.set_vm()")
pg.set_vm(vm=video_mode)
self._invalidate_solid_color_stream()
self._stop_audio_output()
log.info("UcdSdk.set_video_mode done")
self._last_sent_config = current_config
@@ -681,6 +689,104 @@ class _UcdSdkBackend:
log.warning("UcdSdk.set_pattern_params unsupported pattern=%s", getattr(pattern, "name", pattern))
return False
def _solid_color_pattern_ids(self):
return {
UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor,
UCDEnum.VideoPatternInfo.VideoPattern.SolidColor,
}
def _is_solid_color_pattern(self, pattern) -> bool:
return pattern in self._solid_color_pattern_ids()
def _write_pg_solid_color_registers(self, pg, rgb_list) -> None:
"""直接写入 SolidColor 硬件寄存器65535 刻度,无 SDK 左移)。"""
from ctypes import c_uint
from UniTAP.libs.lib_tsi import ci
param0, param1 = pack_solid_color_hw_registers(rgb_list)
pg._become_active()
pg._PatternGenerator__io.set(ci.TSI_PG_PREDEF_PATTERN_PARAM_1, param0, c_uint)
pg._PatternGenerator__io.set(ci.TSI_PG_PREDEF_PATTERN_PARAM_2, param1, c_uint)
log.info(
"UcdSdk._write_pg_solid_color_registers rgb=%s hw=(0x%08X, 0x%08X)",
rgb_list,
param0,
param1,
)
def apply_solid_color(self, rgb, *, force_reapply: bool = False) -> bool:
"""发送 SolidColor顺序对齐 ``work_with_solid_color.py``。
首次(或信号格式变化后):``set_pattern → set_vm → apply() → set_pattern_params``。
同一会话内后续仅 ``set_pattern_params``,避免重复 ``apply()`` 导致 HDMI 重锁、
灰阶测量出现锯齿形 Gamma 曲线。
"""
if not self.status or not self.role:
return False
try:
pg, _ = self.get_tx_modules()
pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
if pattern is None:
log.error("UcdSdk.apply_solid_color failed: solidcolor pattern not found")
return False
rgb_list = [clamp_solid_color(v) for v in list(rgb)[:3]]
if len(rgb_list) < 3:
log.error("UcdSdk.apply_solid_color invalid rgb=%s", rgb)
return False
if self.current_timing is None:
self.current_timing = self._resolve_timing(pg)
if self.current_timing is None:
log.error("UcdSdk.apply_solid_color current_timing is None")
self.last_error = "current_timing is None"
return False
bpc_bumped = False
if self.color_info.bpc < SOLID_COLOR_MIN_BPC:
log.info(
"UcdSdk.apply_solid_color bump bpc %s -> %s for SolidColor video mode",
self.color_info.bpc,
SOLID_COLOR_MIN_BPC,
)
self.color_info.bpc = SOLID_COLOR_MIN_BPC
bpc_bumped = True
self.current_pattern = pattern
self.current_pattern_param = UniTAP.SolidColorParams(
first=rgb_list[0],
second=rgb_list[1],
third=rgb_list[2],
)
need_full_setup = (
force_reapply
or bpc_bumped
or not self._solid_color_stream_ready
)
if need_full_setup:
log.info("UcdSdk.apply_solid_color full setup rgb=%s", rgb_list)
pg.set_pattern(pattern=pattern)
self.set_video_mode()
if not self._apply_pg_output(pg):
log.error("UcdSdk.apply_solid_color pg.apply failed")
self._invalidate_solid_color_stream()
return False
self._solid_color_stream_ready = True
else:
log.info("UcdSdk.apply_solid_color params-only rgb=%s", rgb_list)
self._write_pg_solid_color_registers(pg, rgb_list)
log.info("UcdSdk.apply_solid_color done")
return True
except Exception:
log.exception("UcdSdk.apply_solid_color exception")
self._invalidate_solid_color_stream()
return False
def apply_pattern(self):
"""应用当前pattern"""
if self.current_pattern is not None:
@@ -715,6 +821,7 @@ class _UcdSdkBackend:
def set_ucd_params(self, config):
"""设置UCD323参数"""
self._invalidate_solid_color_stream()
self.last_error = None
self.config = config
test_type = self.config.current_test_type
@@ -799,6 +906,10 @@ class _UcdSdkBackend:
log.error("UcdSdk.send_current_pattern_params failed: set_pattern returned False")
return False
if self._is_solid_color_pattern(self.current_pattern):
log.info("UcdSdk.send_current_pattern_params using apply_solid_color")
return self.apply_solid_color(pattern_params)
log.info("UcdSdk.send_current_pattern_params calling run()")
self.run()
log.info("UcdSdk.send_current_pattern_params done")
@@ -814,12 +925,7 @@ class _UcdSdkBackend:
try:
log.info("UcdSdk.send_solid_rgb_pattern rgb=%s", rgb)
self.current_pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
if self.current_pattern is None:
log.error("UcdSdk.send_solid_rgb_pattern failed: solidcolor pattern not found")
return False
return self.send_current_pattern_params(list(rgb))
return self.apply_solid_color(list(rgb))
except Exception:
log.exception("UcdSdk.send_solid_rgb_pattern exception")
return False
@@ -839,6 +945,20 @@ class _UcdSdkBackend:
except Exception:
return False
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
"""读取 PG 当前状态,格式对齐 UCD Console 的 Pattern Generator Status。"""
if not self.status or not self.role:
return []
try:
pg, _ = self.get_tx_modules()
return read_pattern_generator_status_lines(pg, pattern_hint=pattern_hint)
except Exception:
log.exception("UcdSdk.get_pattern_generator_status_lines failed")
return []
# ─── §3 DeviceInfo / list_devices ────────────────────────────────
@@ -1244,6 +1364,13 @@ class UCD323Device(IUcdDevice):
self._state = UcdState.APPLIED
return ok
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
with self._acquire_device_lock("get_pattern_generator_status_lines"):
return self._sdk.get_pattern_generator_status_lines(pattern_hint=pattern_hint)
# --- 内部 ---
def _apply_dynamic_range(self, signal: SignalFormat) -> None:
@@ -1262,6 +1389,13 @@ class UCD323Device(IUcdDevice):
raise UcdConfigError("IMAGE pattern 必须提供 image_path")
return bool(self._sdk.send_image_pattern(pattern.image_path))
if pattern.kind is PatternKind.SOLID:
if pattern.solid_rgb is None:
raise UcdConfigError("SOLID pattern 必须提供 solid_rgb")
if not self._sdk.apply_solid_color(list(pattern.solid_rgb)):
raise UcdApplyFailed("apply_solid_color 返回 False")
return True
# 预定义图案路径:复用 controller.set_pattern + run()
video_pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern.kind.value)
@@ -1270,11 +1404,7 @@ class UCD323Device(IUcdDevice):
self._sdk.current_pattern = video_pattern
params: list[int] | None = None
if pattern.kind is PatternKind.SOLID:
if pattern.solid_rgb is None:
raise UcdConfigError("SOLID pattern 必须提供 solid_rgb")
params = list(pattern.solid_rgb)
elif pattern.extras:
if pattern.extras:
params = list(pattern.extras)
if not self._sdk.set_pattern(video_pattern, params):

111
app/ucd/pg_status.py Normal file
View File

@@ -0,0 +1,111 @@
"""从 UniTAP Pattern Generator 读取状态并格式化为日志行。"""
from __future__ import annotations
import logging
import os
import re
from typing import Any
import UniTAP
from UniTAP.common import ColorInfo
log = logging.getLogger(__name__)
def _enum_display_name(name: str) -> str:
spaced = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
return spaced.replace("_", " ")
def _format_pattern_name(pg, pattern_hint: str | None = None) -> str:
if pattern_hint:
return pattern_hint
pattern = getattr(pg, "_pattern", None)
if isinstance(pattern, UniTAP.VideoPattern):
return _enum_display_name(pattern.name)
if isinstance(pattern, str):
return os.path.basename(pattern) or "Image File"
pattern_id = getattr(pg, "_pattern_id", None)
if pattern_id is not None:
return _enum_display_name(getattr(pattern_id, "name", str(pattern_id)))
return ""
def _format_cef(ci: ColorInfo) -> str:
fmt_map = {
ColorInfo.ColorFormat.CF_RGB: "RGB",
ColorInfo.ColorFormat.CF_YCbCr_422: "YCbCr 4:2:2",
ColorInfo.ColorFormat.CF_YCbCr_444: "YCbCr 4:4:4",
ColorInfo.ColorFormat.CF_YCbCr_420: "YCbCr 4:2:0",
ColorInfo.ColorFormat.CF_Y_ONLY: "Y only",
ColorInfo.ColorFormat.CF_RAW: "RAW",
ColorInfo.ColorFormat.CF_DSC: "DSC",
}
fmt = fmt_map.get(
ci.color_format,
ci.color_format.name.replace("CF_", "").replace("_", " "),
)
if ci.color_format == ColorInfo.ColorFormat.CF_RGB:
range_str = (
"Legacy RGB mode"
if ci.dynamic_range == ColorInfo.DynamicRange.DR_VESA
else "Limited range"
)
elif ci.dynamic_range == ColorInfo.DynamicRange.DR_VESA:
range_str = "Full range"
else:
range_str = "Limited range"
return f"{fmt}/{range_str}"
def _max_bitrate_gbps(pg, bpp: int) -> float:
caps = pg._caps()
effective_bpp = bpp if bpp > 0 else 24
return round(caps.max_pix_rate * effective_bpp / 1_000_000_000.0, 3)
def collect_pattern_generator_status(
pg,
*,
pattern_hint: str | None = None,
) -> dict[str, Any]:
vm = pg.get_stream_video_mode()
pg_status = pg.status()
ci = vm.color_info
refresh_hz = vm.timing.frame_rate / 1000.0 if vm.timing.frame_rate else 0.0
timing = f"{vm.timing.hactive} x {vm.timing.vactive} @ {refresh_hz:g}Hz"
current_br = vm.bit_rate
max_br = _max_bitrate_gbps(pg, ci.bpp)
return {
"total_bitrate": f"{current_br:.3f} / {max_br:.3f} Gbps",
"timing": timing,
"pattern": _format_pattern_name(pg, pattern_hint),
"cef": _format_cef(ci),
"bpc": str(ci.bpc),
"status": str(pg_status.error),
}
def format_pattern_generator_status_log(status: dict[str, Any]) -> list[str]:
return [
f"Total bitrate: {status['total_bitrate']}",
f" Timing: {status['timing']}",
f" Pattern: {status['pattern']}",
f" CEF: {status['cef']}",
f" BPC: {status['bpc']}",
f" Status: {status['status']}",
]
def read_pattern_generator_status_lines(
pg,
*,
pattern_hint: str | None = None,
) -> list[str]:
status = collect_pattern_generator_status(pg, pattern_hint=pattern_hint)
return format_pattern_generator_status_log(status)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
import os
from app.ucd.domain import (
Colorimetry,
@@ -33,6 +34,21 @@ class SignalService:
def __init__(self, device: IUcdDevice, bus: EventBus):
self._dev = device
self._bus = bus
self._pg_status_log_callback = None
def set_pg_status_log_callback(self, callback) -> None:
"""设置 PG 状态日志回调:``callback(lines: list[str]) -> None``。"""
self._pg_status_log_callback = callback
def _notify_pattern_sent(self, pattern_hint: str | None = None) -> None:
if not self._pg_status_log_callback:
return
try:
lines = self.get_pattern_generator_status_lines(pattern_hint)
if lines:
self._pg_status_log_callback(lines)
except Exception:
log.exception("SignalService._notify_pattern_sent failed")
# -- 高层接口 ------------------------------------------------
@@ -59,17 +75,30 @@ class SignalService:
self._dev.apply()
return changed
def send_pattern(self, pattern: PatternSpec) -> None:
def send_pattern(
self,
pattern: PatternSpec,
*,
pattern_hint: str | None = None,
) -> None:
"""在已 configure 的信号上仅更新图案后 apply。"""
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
self._dev.set_pattern(pattern)
self._dev.apply()
self._notify_pattern_sent(pattern_hint)
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
self.send_pattern(solid_rgb_pattern(rgb))
rgb_list = list(rgb)
self.send_pattern(
solid_rgb_pattern(rgb_list),
pattern_hint=f"Solid Color {rgb_list}",
)
def send_image(self, path: str) -> None:
self.send_pattern(image_pattern(path))
self.send_pattern(
image_pattern(path),
pattern_hint=os.path.basename(path) or "Image File",
)
def update_signal_format(
self,
@@ -127,9 +156,26 @@ class SignalService:
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern不 apply 输出)。"""
return bool(self._dev.set_ucd_params(config))
def send_pattern_params(self, params) -> bool:
def send_pattern_params(
self,
params,
*,
pattern_hint: str | None = None,
) -> bool:
"""以 ``params`` 更新当前 pattern 的参数并 apply。"""
return bool(self._dev.send_current_pattern_params(params))
ok = bool(self._dev.send_current_pattern_params(params))
if ok:
self._notify_pattern_sent(pattern_hint)
return ok
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
getter = getattr(self._dev, "get_pattern_generator_status_lines", None)
if not callable(getter):
return []
return list(getter(pattern_hint) or [])
def apply_and_run(self, config, pattern_params) -> bool:
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
@@ -380,7 +426,13 @@ class PatternService:
raise IndexError(f"pattern 索引越界: {index}")
pattern_param = session.pattern_params[index]
if not self.app.signal_service.send_pattern_params(pattern_param):
pattern_hint = None
if session.display_names and index < len(session.display_names):
pattern_hint = session.display_names[index]
if not self.app.signal_service.send_pattern_params(
pattern_param,
pattern_hint=pattern_hint,
):
raise RuntimeError(f"发送 pattern 失败: {index}")
return pattern_param

View File

@@ -0,0 +1,2 @@
"""兼容层:请改用 ``app.solid_color_scale``。"""
from app.solid_color_scale import * # noqa: F403

View File

@@ -22,6 +22,12 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from app.tests.color_accuracy import calculate_delta_e_2000
from app.solid_color_scale import (
SOLID_COLOR_MAX,
pct_to_solid_color,
solid_color_to_byte,
solid_color_to_pct,
)
from app.views.modern_styles import get_theme_palette
if TYPE_CHECKING:
@@ -41,10 +47,15 @@ DE_FORMULAS = ["2000", "94", "76"]
def _pct_to_gray_rgb(pct: int) -> tuple[int, int, int]:
value = int(round(pct * 255 / 100))
value = solid_color_to_byte(pct_to_solid_color(pct))
return value, value, value
def _pct_to_pattern_rgb(pct: int) -> int:
"""灰阶百分比 → SolidColor 码值065535"""
return pct_to_solid_color(pct)
def _rgb_to_hex(rgb: tuple[int, int, int]) -> str:
r, g, b = rgb
return f"#{r:02x}{g:02x}{b:02x}"
@@ -325,6 +336,8 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
self.calman_stop_event = threading.Event()
self.calman_running = False
self.calman_patch_send_busy = False
self.calman_rgb_session = None
self.calman_rgb_session_test_type = None
self.calman_current_level = None
self.calman_last_record = None
self.calman_last_step_seconds = None
@@ -728,6 +741,29 @@ def toggle_calman_panel(self: "PQAutomationApp") -> None:
# ---------------------------------------------------------------------------
def _get_calman_rgb_session(self: "PQAutomationApp", test_type: str):
"""复用 Calman 发图 session避免每次点击都重新 stage profile。"""
cached = getattr(self, "calman_rgb_session", None)
if cached is not None and getattr(self, "calman_rgb_session_test_type", None) == test_type:
return cached
if not hasattr(self, "pattern_service") or self.pattern_service is None:
return None
session = self.pattern_service.prepare_session(
"rgb",
test_type=test_type,
log_details=False,
)
self.calman_rgb_session = session
self.calman_rgb_session_test_type = test_type
_calman_log(self, f"calman rgb session prepared test_type={test_type}")
return session
def _clear_calman_rgb_session(self: "PQAutomationApp") -> None:
self.calman_rgb_session = None
self.calman_rgb_session_test_type = None
def send_patch(self: "PQAutomationApp", pct: int) -> None:
"""点击色块时,发送对应灰阶图案到信号发生器。"""
if not self.signal_service.is_connected:
@@ -738,7 +774,7 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
self.calman_status_var.set("发送进行中,请稍候...")
return
rgb_val = int(round(pct * 255 / 100))
rgb_val = _pct_to_pattern_rgb(pct)
self.calman_current_level = pct
self.calman_status_var.set(f"发送 {pct}%RGB={rgb_val}...")
_highlight_patch(self, pct)
@@ -751,8 +787,10 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
_calman_log(self, f"send_solid_rgb start pct={pct}")
test_type = getattr(self.config, "current_test_type", "screen_module")
if hasattr(self, "pattern_service") and self.pattern_service is not None:
rgb_session = _get_calman_rgb_session(self, test_type)
self.pattern_service.send_rgb(
(rgb_val, rgb_val, rgb_val),
session=rgb_session,
test_type=test_type,
)
else:
@@ -902,11 +940,7 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
test_type = getattr(self.config, "current_test_type", "screen_module")
rgb_session = None
if hasattr(self, "pattern_service") and self.pattern_service is not None:
rgb_session = self.pattern_service.prepare_session(
"rgb",
test_type=test_type,
log_details=False,
)
rgb_session = _get_calman_rgb_session(self, test_type)
_calman_log(self, f"sequence ucd profile applied test_type={test_type}")
order = sorted(self.calman_levels, reverse=True)
@@ -918,7 +952,7 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
self._dispatch_ui(self.log_gui.log, "已停止连续测试", "warning")
break
step_t0 = time.perf_counter()
rgb_val = int(round(pct * 255 / 100))
rgb_val = _pct_to_pattern_rgb(pct)
self._dispatch_ui(
self.calman_status_var.set, f"[{i}/{total}] 发送 {pct}%"
)

View File

@@ -24,6 +24,12 @@ from typing import TYPE_CHECKING
import ttkbootstrap as ttk
from app.pq import pq_config
from app.solid_color_scale import (
SOLID_COLOR_MAX,
pct_to_solid_color,
solid_color_to_byte,
solid_color_to_pct,
)
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
@@ -56,19 +62,21 @@ def _gray_pct_of(rgb) -> str:
r = int(rgb[0])
except Exception:
return ""
return str(int(round(r / 255 * 100)))
return str(int(round(solid_color_to_pct(r))))
def _hex_of(rgb) -> str:
try:
r, g, b = int(rgb[0]), int(rgb[1]), int(rgb[2])
r = solid_color_to_byte(rgb[0])
g = solid_color_to_byte(rgb[1])
b = solid_color_to_byte(rgb[2])
except Exception:
return ""
return f"#{r:02X}{g:02X}{b:02X}"
def _luminance(rgb) -> float:
r, g, b = rgb
r, g, b = solid_color_to_byte(rgb[0]), solid_color_to_byte(rgb[1]), solid_color_to_byte(rgb[2])
return 0.2126 * r + 0.7152 * g + 0.0722 * b
@@ -99,7 +107,7 @@ def _gen_even(n: int) -> list[list[int]]:
out = []
for i in range(n):
pct = 100.0 - (100.0 / (n - 1)) * i
v = int(round(pct / 100.0 * 255))
v = pct_to_solid_color(pct)
out.append([v, v, v])
return out
@@ -118,7 +126,7 @@ def _gen_pq(n: int) -> list[list[int]]:
else:
Lm1 = L ** m1
v_pq = ((c1 + c2 * Lm1) / (1.0 + c3 * Lm1)) ** m2
v = int(round(max(0.0, min(1.0, v_pq)) * 255))
v = int(round(max(0.0, min(1.0, v_pq)) * SOLID_COLOR_MAX))
out.append([v, v, v])
return out
@@ -129,7 +137,7 @@ def _gen_gamma(n: int, gamma: float = 2.2) -> list[list[int]]:
out = []
for i in range(n):
lin = 1.0 - i / (n - 1)
v = int(round((lin ** (1.0 / gamma)) * 255))
v = int(round((lin ** (1.0 / gamma)) * SOLID_COLOR_MAX))
out.append([v, v, v])
return out
@@ -761,8 +769,8 @@ def _parse_rgb_from_form(self: "PQAutomationApp"):
except ValueError:
messagebox.showerror("输入错误", "R/G/B 必须为整数")
return None
if not all(0 <= v <= 255 for v in (r, g, b)):
messagebox.showerror("输入错误", "R/G/B 必须在 0~255 范围内")
if not all(0 <= v <= SOLID_COLOR_MAX for v in (r, g, b)):
messagebox.showerror("输入错误", f"R/G/B 必须在 0~{SOLID_COLOR_MAX} 范围内")
return None
return [r, g, b]
@@ -775,7 +783,7 @@ def _fill_rgb_from_pct(self: "PQAutomationApp"):
messagebox.showerror("输入错误", "灰度 % 必须为数字")
return
pct = max(0.0, min(100.0, pct))
v = int(round(pct / 100.0 * 255))
v = pct_to_solid_color(pct)
self._gamma_edit_r_var.set(str(v))
self._gamma_edit_g_var.set(str(v))
self._gamma_edit_b_var.set(str(v))
@@ -904,7 +912,7 @@ def _paste_from_clipboard(self: "PQAutomationApp"):
if s.endswith("%"):
try:
pct = float(s.rstrip("%"))
v = int(round(max(0, min(100, pct)) / 100.0 * 255))
v = pct_to_solid_color(max(0, min(100, pct)))
rows.append([v, v, v])
continue
except ValueError:
@@ -914,7 +922,7 @@ def _paste_from_clipboard(self: "PQAutomationApp"):
if len(parts) == 1:
try:
v = int(parts[0])
if 0 <= v <= 255:
if 0 <= v <= SOLID_COLOR_MAX:
rows.append([v, v, v])
continue
except ValueError:
@@ -929,7 +937,7 @@ def _paste_from_clipboard(self: "PQAutomationApp"):
except ValueError:
errors.append(f"{ln_no} 行非整数:{s}")
continue
if not all(0 <= v <= 255 for v in (r, g, b)):
if not all(0 <= v <= SOLID_COLOR_MAX for v in (r, g, b)):
errors.append(f"{ln_no} 行越界:{s}")
continue
rows.append([r, g, b])
@@ -980,7 +988,7 @@ def _run_validation(self: "PQAutomationApp"):
bad = [
i for i, rgb in enumerate(params)
if not all(0 <= int(v) <= 255 for v in rgb)
if not all(0 <= int(v) <= SOLID_COLOR_MAX for v in rgb)
]
if bad:
msgs.append(f"⚠ 越界点 (索引): {bad}")

View File

@@ -9,9 +9,14 @@ import threading
import tkinter as tk
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING
import ttkbootstrap as ttk
from typing import TYPE_CHECKING
from app.solid_color_scale import (
SOLID_COLOR_MAX,
byte_to_solid_color,
)
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
@@ -196,8 +201,12 @@ def _load_patterns(self: "PQAutomationApp"):
continue
if r is None or g is None or b is None:
continue
if min(r, g, b) < 0 or max(r, g, b) > 255:
if min(r, g, b) < 0 or max(r, g, b) > SOLID_COLOR_MAX:
continue
if max(r, g, b) <= 255:
r = byte_to_solid_color(r)
g = byte_to_solid_color(g)
b = byte_to_solid_color(b)
patterns.append((r, g, b))
finally:
wb.close()

View File

@@ -13,15 +13,15 @@ lUniTAP = UniTAP.TsiLib()
#
# For opening device, please, put serial number of the device as 8 symbol str or put index of device.
#
dev = lUniTAP.open("NNNNNNNN")
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)
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("PATH_TO_PACKET_FILE")
# role = dev.select_role(UniTAP.dev.UCD422.HDMISourceHDMISink)
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
# 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.stop()

View File

@@ -13,8 +13,8 @@ lUniTAP = UniTAP.TsiLib()
#
# For opening device, please, put serial number of the device as 8 symbol str or put index of device.
#
# dev = lUniTAP.open(0) # Open first device
dev = lUniTAP.open("NNNNNNNN") # Put your device serial number here
dev = lUniTAP.open(0) # Open first device
# dev = lUniTAP.open("NNNNNNNN") # Put your device serial number here
# After opening device as in UCD Console device role should be selected.
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
@@ -26,10 +26,12 @@ role.hdtx.pg.set_pattern(pattern=UniTAP.VideoPattern.SolidColor)
role.hdtx.pg.set_vm(vm=video_mode)
role.hdtx.pg.apply()
for i in range(1024):
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)
# for i in range(1024):
# 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))
#
# Since the 3.5 version, TsiLib and TSIDevice objects have the option to be closed earlier.

1464
hdr10_demo.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -208,6 +208,7 @@ class PQAutomationApp(
# 创建日志显示区域
self.create_log_panel()
self.signal_service.set_pg_status_log_callback(self._on_pg_status_log)
# 创建 Local Dimming 面板
self.create_local_dimming_panel()
# 创建 AI 图片对话面板
@@ -271,6 +272,9 @@ class PQAutomationApp(
def on_connection_changed(evt: ConnectionChanged) -> None:
if evt.device is DeviceKind.UCD:
indicator = getattr(self, "ucd_status_indicator", None)
if not evt.connected:
from app.views.panels.calman_panel import _clear_calman_rgb_session
_clear_calman_rgb_session(self)
elif evt.device is DeviceKind.CA:
indicator = getattr(self, "ca_status_indicator", None)
else:
@@ -282,6 +286,14 @@ class PQAutomationApp(
self.event_bus.subscribe(ConnectionChanged, on_connection_changed)
def _on_pg_status_log(self, lines: list[str]) -> None:
"""每次 UCD 发图后,在 GUI 日志中打印 Pattern Generator 状态。"""
if not hasattr(self, "log_gui"):
return
self.log_gui.log("─ Pattern Generator Status ─", level="separator")
for line in lines:
self.log_gui.log(line, level="info")
def _dispatch_ui(self, fn, *args, **kwargs):
"""把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。

View File

@@ -9,8 +9,8 @@
"cct",
"contrast"
],
"timing": "DMT 1920x 1080 @ 60Hz",
"data_range": "Limited",
"timing": "CTA 1920x 1080 @ 60Hz",
"data_range": "Full",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "DCI-P3",
@@ -97,7 +97,7 @@
},
"device_config": {
"ca_com": "COM3",
"ucd_list": "0: UCD-323 [2422C809]",
"ucd_list": "0: UCD-323 [2128C209]",
"ca_channel": "0"
},
"custom_pattern": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

829
test.py
View File

@@ -1,17 +1,19 @@
#!/usr/bin/env python3
"""
HDR10 信号输出验证脚本UCD-323 HDMI Source + PG + sdpg DRM)。
HDR10 信号输出验证脚本UCD-323 HDMI Source + SolidColor + sdpg
验证 PGBT.2020 / 10bit / Full RGB与 sdpg 自定义 DRM(0x87) InfoFrame 能否让 DUT 进入 HDR 模式。
通过 SolidColor 白场065535默认 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% PQ 白场后,用 CA-410 测 DUT 峰值亮度。
验收:脚本发出 100% 白场后,用 CA-410 测 DUT 峰值亮度。
若仍约 100200 nitsSDR 水平),说明信令或 DUT 设置未生效。
"""
@@ -19,20 +21,39 @@ from __future__ import annotations
import argparse
import sys
import tempfile
import time
from ctypes import c_uint32
from pathlib import Path
import UniTAP
from PIL import Image
# CTA-861 VIC 16 ≈ 3840×2160 @ 60Hz设备需支持该 timing
DEFAULT_VIC = 16
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
# 10bit PQ 白场码值(与 work_with_solid_color.py 一致)
PQ_WHITE_10BIT = 1023
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 内联 VSIFHDR10+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)。"""
@@ -41,6 +62,33 @@ def xy_to_be16(value: float) -> tuple[int, int]:
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,
@@ -79,6 +127,39 @@ def build_drm_hdr10_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 checksumtype + version + length + payload + checksum ≡ 0 mod 256"""
if len(packet) < 30 or packet[0] != 0x87:
@@ -99,42 +180,338 @@ def format_packet_hex(packet: bytes, bytes_per_line: int = 16) -> str:
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()
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"设备不支持 CTA VIC={vic}"
f"可用 timing 数量: {len(available)},请尝试 --vic 其它值。"
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.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA # Full
color.bpc = 10
color.bpc = bpc
else:
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
color.bpc = 8
return UniTAP.VideoMode(timing=timing, color_info=color)
def apply_pg_white(role, video_mode: UniTAP.VideoMode, white_code: int) -> None:
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=10PG 内部按 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:
"""默认 SolidColor065535Image 易触发 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 RGBImageFile 经 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(065535)。
"""
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():
raise RuntimeError("Pattern Generator apply() 失败")
pg.set_pattern_params(
pattern_params=UniTAP.SolidColorParams(white_code, white_code, white_code)
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 start_sdpg(role, drm_packet: bytearray):
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_CAPSPython 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()
sdpg.add_packet(drm_packet)
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()
return sdpg.status()
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=FalseTX 视频未 enable请先确认 Image 已有输出。")
if not status.error and not status.enabled and not status.active:
notes.append(
"start() 后 Enabled/Active 仍为 FalseUCD-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):
@@ -159,21 +536,31 @@ def list_devices(tsi: UniTAP.TsiLib) -> None:
def main() -> int:
parser = argparse.ArgumentParser(
description="UCD-323 HDR10 信号输出验证(PG + sdpg DRM InfoFrame"
description="UCD-323 HDR10 信号输出验证(SolidColor 065535 + sdpg 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("--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=PQ_WHITE_10BIT,
help=f"PQ 10bit 白场码值,默认 {PQ_WHITE_10BIT}",
default=SOLID_WHITE_PEAK,
help=f"SolidColor 白场 RGB 码值 065535,默认 {SOLID_WHITE_PEAK}(峰值白)",
)
parser.add_argument(
"--image",
metavar="PATH",
help="发图文件路径png/bmp/jpeg 等);省略则按分辨率自动生成白场 PNG",
)
parser.add_argument(
"--hold",
@@ -187,6 +574,119 @@ def main() -> int:
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="sdpgfull=VSIF+AVI+DRM默认official-vsif=仅官方 VSIFdrm=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()
@@ -195,62 +695,291 @@ def main() -> int:
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 仍 ~100200 nits"
" 要测峰值亮度请用默认 --packet full10bit SolidColor + VSIF/AVI/DRM"
)
elif preview_mode == "hdr-content":
print(
f" 策略: SolidColor 码值 {white_code}065535 峰值)+ 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}")
print(f"CTA VIC={args.vic}, bpc=10, BT.2020 RGB Full, PQ 白场码值={args.white_code}")
print("DRM 包内容:")
print(format_packet_hex(drm))
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)
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")
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}065535"
)
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}"
)
apply_pg_white(role, vm, args.white_code)
print(" PG apply() 成功,已输出 PQ 白场。")
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 即可继续)。"
)
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)
def run_output():
if output_mode == "none":
return
if args.bootstrap:
if output_mode == "image":
print(
" 跳过 bootstrapImage 模式直接出图即可;"
"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% 白场峰值亮度 ---")
print(" HDR 生效: 通常显著高于 SDR具体取决于电视峰值")
print(" 仍为 ~100200 nits: 检查 DUT HDR 开关 / HDMI 口 / 信令")
if preview_mode == "hdr-content":
print(" HDR 生效: 应显著高于 SDR电视峰值通常 4001000+ nits")
else:
print(" 当前为 SDR 像素: 亮度约 100200 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(1.0)
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
@@ -263,7 +992,7 @@ def main() -> int:
list_devices(tsi)
return 1
finally:
if role is not None:
if role is not None and sdpg_started:
try:
role.hdtx.sdpg.stop()
except Exception: