优化ucd调用结构

This commit is contained in:
xinzhu.yin
2026-06-11 16:29:36 +08:00
parent cc7218411c
commit 46a97d6ae7
13 changed files with 1700 additions and 1702 deletions

28
app/ucd/__init__.py Normal file
View File

@@ -0,0 +1,28 @@
"""UCD 信号发生器 — domain / enum / device / service。
GUI 与测试代码通常只需::
from app.ucd import SignalService, UCD323Device, EventBus
"""
from app.ucd.domain import * # noqa: F403
from app.ucd.enum import UCDEnum
from app.ucd.device import (
DeviceInfo,
IUcdDevice,
UCD323Device,
list_devices,
)
from app.ucd.service import PatternService, PatternSession, SignalService
__all__ = [
"SignalService",
"PatternService",
"PatternSession",
"UCD323Device",
"IUcdDevice",
"DeviceInfo",
"UCDEnum",
"EventBus",
"ConnectionChanged",
"DeviceKind",
]

1267
app/ucd/device.py Normal file

File diff suppressed because it is too large Load Diff

486
app/ucd/domain.py Normal file
View File

@@ -0,0 +1,486 @@
"""UCD 域层:类型、状态机、事件、字符串与 PQConfig 映射。"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable
log = logging.getLogger(__name__)
# ─── §1 枚举与值对象 ──────────────────────────────────────────────
class DeviceKind(str, Enum):
"""连接状态事件所指的设备类型。"""
UCD = "ucd"
CA = "ca"
class Interface(str, Enum):
"""UCD 物理输出接口。"""
HDMI = "HDMI"
DP = "DP"
USBC = "Type-C"
class ColorFormat(str, Enum):
"""像素颜色格式(与 UI 显示字符串解耦的内部表示)。"""
RGB = "rgb"
YCBCR_444 = "ycbcr444"
YCBCR_422 = "ycbcr422"
YCBCR_420 = "ycbcr420"
Y_ONLY = "yonly"
IDO_DEFINED = "ido_defined"
RAW = "raw"
DSC = "dsc"
class Colorimetry(str, Enum):
"""色度空间。"""
SRGB = "sRGB"
BT709 = "BT.709"
BT601 = "BT.601"
BT2020 = "BT.2020"
DCI_P3 = "DCI-P3"
ADOBE_RGB = "AdobeRGB"
class DynamicRange(str, Enum):
FULL = "Full"
LIMITED = "Limited"
class TimingStandard(str, Enum):
DMT = "dmt"
CTA = "cta"
CVT = "cvt"
OVT = "ovt"
class PatternKind(str, Enum):
"""高层图案种类。
与 UCD 内部 VideoPattern 枚举区分:仅暴露业务上真正用到的几种。
"""
DISABLED = "disabled"
SOLID = "solidcolor"
SOLID_WHITE = "solidwhite"
SOLID_RED = "solidred"
SOLID_GREEN = "solidgreen"
SOLID_BLUE = "solidblue"
COLOR_BARS = "colorbars"
CHESSBOARD = "chessboard"
WHITE_VSTRIPS = "whitevstrips"
GRADIENT_RGB_STRIPES = "gradientrgbstripes"
COLOR_RAMP = "colorramp"
COLOR_SQUARES = "coloursquares"
MOTION = "motionpattern"
SQUARE_WINDOW = "squarewindow"
IMAGE = "image" # 来自文件路径
@dataclass(frozen=True)
class SignalFormat:
"""信号格式color_info 部分)。"""
color_format: ColorFormat
colorimetry: Colorimetry
bpc: int # 8 / 10 / 12
dynamic_range: DynamicRange = DynamicRange.FULL
@dataclass(frozen=True)
class TimingSpec:
"""显示 Timing 描述。"""
standard: TimingStandard
width: int
height: int
refresh_hz: float
def __str__(self) -> str: # 便于日志
return f"{self.standard.value.upper()} {self.width}x{self.height}@{self.refresh_hz:g}Hz"
@dataclass(frozen=True)
class PatternSpec:
"""图案描述。
联合字段含义:
SOLID → solid_rgb=(r,g,b)
IMAGE → image_path
其它预定义图案 → extras 视具体类型
"""
kind: PatternKind
solid_rgb: tuple[int, int, int] | None = None
image_path: str | None = None
extras: tuple = field(default_factory=tuple)
# ─── §2 状态机 ───────────────────────────────────────────────────
class UcdState(Enum):
CLOSED = 0
OPENED = 1 # 设备已打开,未配置信号
CONFIGURED = 2 # SignalFormat + Timing 已写入,未 apply
APPLIED = 3 # 已 apply硬件正在输出
_ALLOWED: dict[UcdState, set[UcdState]] = {
UcdState.CLOSED: {UcdState.OPENED, UcdState.CLOSED},
UcdState.OPENED: {UcdState.CONFIGURED, UcdState.CLOSED, UcdState.OPENED},
UcdState.CONFIGURED: {UcdState.CONFIGURED, UcdState.APPLIED, UcdState.OPENED, UcdState.CLOSED},
UcdState.APPLIED: {UcdState.CONFIGURED, UcdState.APPLIED, UcdState.OPENED, UcdState.CLOSED},
}
def assert_transition(curr: UcdState, nxt: UcdState) -> None:
"""校验状态转移。非法转移抛 :class:`UcdStateError`。"""
if nxt not in _ALLOWED[curr]:
raise UcdStateError(f"非法状态转移: {curr.name} -> {nxt.name}")
# ─── §3 错误体系 ─────────────────────────────────────────────────
class UcdError(Exception):
"""UCD 控制相关错误的基类。"""
class UcdNotConnected(UcdError):
"""设备未打开/未连接。"""
class UcdStateError(UcdError):
"""状态机非法转移或前置条件不满足。"""
class UcdConfigError(UcdError):
"""业务配置参数不合法(如不支持的 color_format、解析失败"""
class UcdApplyFailed(UcdError):
"""SDK ``apply`` 返回失败或超时。"""
class UcdSdkError(UcdError):
"""UniTAP SDK 内部异常的包装;原始异常保存在 ``__cause__``。"""
# ─── §4 事件总线 ─────────────────────────────────────────────────
@dataclass(frozen=True)
class UcdEvent:
"""事件基类。"""
@dataclass(frozen=True)
class ConnectionChanged(UcdEvent):
device: DeviceKind
connected: bool
serial: str | None = None
@dataclass(frozen=True)
class SignalApplied(UcdEvent):
signal: SignalFormat
timing: TimingSpec
format_changed: bool # 与上一次 apply 相比 (signal,timing) 是否改变
@dataclass(frozen=True)
class PatternApplied(UcdEvent):
pattern: PatternSpec
class EventBus:
"""极简同步事件总线(按事件类型分发)。
单线程使用;如需跨线程派发,由订阅者自行 marshal 到 UI 线程。
"""
def __init__(self) -> None:
self._subs: dict[type, list[Callable[[Any], None]]] = {}
def subscribe(self, evt_type: type, cb: Callable[[Any], None]) -> None:
self._subs.setdefault(evt_type, []).append(cb)
def publish(self, evt: UcdEvent) -> None:
for cb in self._subs.get(type(evt), []):
try:
cb(evt)
except Exception: # noqa: BLE001 - 订阅者错误不应影响发布者
log.exception("UCD 事件处理器抛出异常: %r", evt)
# ─── §5 业务字符串解析 / 映射 ────────────────────────────────────
_OUTPUT_FORMAT_TO_COLOR_FORMAT: dict[str, ColorFormat] = {
"RGB": ColorFormat.RGB,
"YCbCr 4:4:4": ColorFormat.YCBCR_444,
"YCbCr 4:2:2": ColorFormat.YCBCR_422,
"YCbCr 4:2:0": ColorFormat.YCBCR_420,
"Y Only": ColorFormat.Y_ONLY,
"IDO Defined": ColorFormat.IDO_DEFINED,
"RAW": ColorFormat.RAW,
"DSC": ColorFormat.DSC,
}
_COLOR_SPACE_TO_COLORIMETRY: dict[str, Colorimetry] = {
"sRGB": Colorimetry.SRGB,
"BT.709": Colorimetry.BT709,
"BT.601": Colorimetry.BT601,
"BT.2020": Colorimetry.BT2020,
"DCI-P3": Colorimetry.DCI_P3,
"AdobeRGB": Colorimetry.ADOBE_RGB,
}
_BIT_DEPTH_STR_TO_BPC: dict[str, int] = {
"8bit": 8,
"10bit": 10,
"12bit": 12,
}
_DATA_RANGE_TO_DYNAMIC_RANGE: dict[str, DynamicRange] = {
"Full": DynamicRange.FULL,
"Limited": DynamicRange.LIMITED,
}
def output_format_to_color_format(s: str) -> ColorFormat:
"""显示用 ``"YCbCr 4:4:4"`` → :class:`ColorFormat`。未知值视为 RGB。"""
return _OUTPUT_FORMAT_TO_COLOR_FORMAT.get(s, ColorFormat.RGB)
def color_space_to_colorimetry(s: str) -> Colorimetry:
"""显示用 ``"BT.709"`` → :class:`Colorimetry`。未知抛 :class:`UcdConfigError`。"""
if s in _COLOR_SPACE_TO_COLORIMETRY:
return _COLOR_SPACE_TO_COLORIMETRY[s]
raise UcdConfigError(f"未知色彩空间: {s!r}")
def bit_depth_str_to_bpc(s: str) -> int:
"""``"10bit"`` → 10。未知抛 :class:`UcdConfigError`。"""
if s in _BIT_DEPTH_STR_TO_BPC:
return _BIT_DEPTH_STR_TO_BPC[s]
raise UcdConfigError(f"未知位深: {s!r}")
def data_range_to_dynamic_range(s: str) -> DynamicRange:
if s in _DATA_RANGE_TO_DYNAMIC_RANGE:
return _DATA_RANGE_TO_DYNAMIC_RANGE[s]
raise UcdConfigError(f"未知数据范围: {s!r}")
def is_ycbcr(color_format: ColorFormat | str | None) -> bool:
"""判断输出是否为 YCbCr 系列。接受 :class:`ColorFormat` 或 UI 字符串。"""
if color_format is None:
return False
if isinstance(color_format, ColorFormat):
return color_format in {
ColorFormat.YCBCR_444,
ColorFormat.YCBCR_422,
ColorFormat.YCBCR_420,
}
return "YCbCr" in str(color_format)
def parse_timing_str(timing_str: str) -> TimingSpec:
"""解析 ``"DMT 3840x2160@60Hz"`` 风格的字符串为 :class:`TimingSpec`。
宽容处理空格 / 大小写 / ``Hz`` 后缀大小写。
解析失败抛 :class:`UcdConfigError`。
"""
if not isinstance(timing_str, str):
raise UcdConfigError(f"timing_str 必须是字符串: {timing_str!r}")
s = " ".join(timing_str.strip().split())
s = s.replace(" x", "x").replace("x ", "x")
parts = s.split(" ", 1)
if len(parts) < 2:
raise UcdConfigError(f"无法解析 timing: {timing_str!r}")
type_str, rest = parts[0].strip().upper(), parts[1].strip()
if "@" not in rest:
raise UcdConfigError(f"无法解析 timing (缺少 '@'): {timing_str!r}")
left, right = (p.strip() for p in rest.split("@", 1))
if "x" not in left:
raise UcdConfigError(f"无法解析分辨率 (缺少 'x'): {timing_str!r}")
wh = left.split("x")
if len(wh) != 2:
raise UcdConfigError(f"无法解析分辨率: {timing_str!r}")
try:
width, height = int(wh[0]), int(wh[1])
except ValueError as exc:
raise UcdConfigError(f"分辨率数字解析失败: {timing_str!r}") from exc
hz_str = right.replace("Hz", "").replace("HZ", "").strip()
try:
refresh_hz = float(hz_str)
except ValueError as exc:
raise UcdConfigError(f"刷新率解析失败: {timing_str!r}") from exc
try:
standard = TimingStandard(type_str.lower())
except ValueError as exc:
raise UcdConfigError(f"未知的分辨率类型: {type_str!r}") from exc
return TimingSpec(
standard=standard,
width=width,
height=height,
refresh_hz=refresh_hz,
)
__all__ = [
# §1
"DeviceKind",
"Interface",
"ColorFormat",
"Colorimetry",
"DynamicRange",
"TimingStandard",
"PatternKind",
"SignalFormat",
"TimingSpec",
"PatternSpec",
# §2
"UcdState",
"assert_transition",
# §3
"UcdError",
"UcdNotConnected",
"UcdStateError",
"UcdConfigError",
"UcdApplyFailed",
"UcdSdkError",
# §4
"UcdEvent",
"ConnectionChanged",
"SignalApplied",
"PatternApplied",
"EventBus",
# §5
"output_format_to_color_format",
"color_space_to_colorimetry",
"bit_depth_str_to_bpc",
"data_range_to_dynamic_range",
"is_ycbcr",
"parse_timing_str",
]
# --- PQConfig / pattern 映射 ---
# PQ pattern_mode 字符串 → PatternKind大小写不敏感
_PQ_PATTERN_MODE_TO_KIND: dict[str, PatternKind] = {
"disabled": PatternKind.DISABLED,
"solidcolor": PatternKind.SOLID,
"solidwhite": PatternKind.SOLID_WHITE,
"solidred": PatternKind.SOLID_RED,
"solidgreen": PatternKind.SOLID_GREEN,
"solidblue": PatternKind.SOLID_BLUE,
"colorbars": PatternKind.COLOR_BARS,
"chessboard": PatternKind.CHESSBOARD,
"whitevstrips": PatternKind.WHITE_VSTRIPS,
"gradientrgbstripes": PatternKind.GRADIENT_RGB_STRIPES,
"colorramp": PatternKind.COLOR_RAMP,
"coloursquares": PatternKind.COLOR_SQUARES,
"motionpattern": PatternKind.MOTION,
"squarewindow": PatternKind.SQUARE_WINDOW,
}
def pattern_mode_to_kind(pattern_mode: str) -> PatternKind:
key = (pattern_mode or "solidcolor").strip().lower()
kind = _PQ_PATTERN_MODE_TO_KIND.get(key)
if kind is None:
raise UcdConfigError(f"不支持的 pattern_mode: {pattern_mode!r}")
return kind
def build_profile_from_config(config, test_type: str | None = None):
"""从 PQConfig 当前 test_type 条目构建 SignalFormat + TimingSpec。"""
test_type = test_type or config.current_test_type
profile = config.current_test_types[test_type]
signal = build_signal_format_from_profile(
color_space=profile["colorimetry"],
color_format=profile["color_format"],
bpc=int(profile["bpc"]),
data_range=profile.get("data_range", "Full"),
)
timing = build_timing(profile["timing"])
return signal, timing
def build_pattern_spec(config, params: list[int] | None = None) -> PatternSpec:
"""将 PQConfig 当前 pattern 与一组参数转为 :class:`PatternSpec`。"""
pattern_mode = config.current_pattern["pattern_mode"]
kind = pattern_mode_to_kind(pattern_mode)
if params is None:
params = config.current_pattern.get("pattern_params", [[]])[0]
if kind is PatternKind.SOLID and params and len(params) >= 3:
return PatternSpec(
kind=kind,
solid_rgb=(int(params[0]), int(params[1]), int(params[2])),
)
if params:
return PatternSpec(kind=kind, extras=tuple(int(v) for v in params))
return PatternSpec(kind=kind)
def build_signal_format(
*,
color_space: str,
output_format: str,
bit_depth: str,
data_range: str = "Full",
) -> SignalFormat:
return SignalFormat(
color_format=output_format_to_color_format(output_format),
colorimetry=color_space_to_colorimetry(color_space),
bpc=bit_depth_str_to_bpc(bit_depth),
dynamic_range=data_range_to_dynamic_range(data_range),
)
def build_signal_format_from_profile(
*,
color_space: str,
color_format: str,
bpc: int,
data_range: str = "Full",
) -> SignalFormat:
return build_signal_format(
color_space=color_space,
output_format=color_format,
bit_depth=f"{int(bpc)}bit",
data_range=data_range,
)
def build_timing(timing_str: str) -> TimingSpec:
return parse_timing_str(timing_str)
def solid_rgb_pattern(rgb: tuple[int, int, int] | list[int]) -> PatternSpec:
r, g, b = rgb[0], rgb[1], rgb[2]
return PatternSpec(kind=PatternKind.SOLID, solid_rgb=(int(r), int(g), int(b)))
def image_pattern(path: str) -> PatternSpec:
return PatternSpec(kind=PatternKind.IMAGE, image_path=path)

625
app/ucd/enum.py Normal file
View File

@@ -0,0 +1,625 @@
"""UCD SDK 枚举与 UI/SDK 字符串映射。"""
from enum import IntEnum
import UniTAP
class UCDEnum:
class ColorInfo:
"""
Class contains information of frame `ColorFormat`, `DynamicRange`, `Colorimetry`.
"""
class ColorFormat(IntEnum):
"""
Contains values of possible color format.
"""
CF_NONE = 0
CF_UNKNOWN = 1
CF_RGB = 2
CF_YCbCr_422 = 3
CF_YCbCr_444 = 4
CF_YCbCr_420 = 5
CF_IDO_DEFINED = 6
CF_Y_ONLY = 7
CF_RAW = 8
CF_DSC = 9
class DynamicRange(IntEnum):
"""
Contains values of possible dynamic range.
"""
DR_UNKNOWN = -1
DR_VESA = 0
DR_CTA = 1
class Colorimetry(IntEnum):
"""
Contains values of possible colorimetry.
"""
CM_NONE = 0
CM_RESERVED = 1
CM_sRGB = 2
CM_SMPTE_170M = 3
CM_ITUR_BT601 = 4
CM_ITUR_BT709 = 5
CM_xvYCC601 = 6
CM_xvYCC709 = 7
CM_sYCC601 = 8
CM_AdobeYCC601 = 9
CM_AdobeRGB = 10
CM_ITUR_BT2020_YcCbcCrc = 11
CM_ITUR_BT2020_YCbCr = 12
CM_ITUR_BT2020_RGB = 13
CM_RGB_WIDE_GAMUT_FIX = 14
CM_RGB_WIDE_GAMUT_FLT = 15
CM_DCI_P3 = 16
CM_DICOM_1_4_GRAY_SCALE = 17
CM_CUSTOM_COLOR_PROFILE = 18
CM_opYCC601 = CM_AdobeYCC601
CM_opRGB = CM_AdobeRGB
# 颜色格式映射 - 支持不区分大小写的字符串匹配
@staticmethod
def get_color_format(format_str):
format_map = {
"none": UniTAP.ColorInfo.ColorFormat.CF_NONE,
"unknown": UniTAP.ColorInfo.ColorFormat.CF_UNKNOWN,
"rgb": UniTAP.ColorInfo.ColorFormat.CF_RGB,
"ycbcr422": UniTAP.ColorInfo.ColorFormat.CF_YCbCr_422,
"ycbcr444": UniTAP.ColorInfo.ColorFormat.CF_YCbCr_444,
"ycbcr420": UniTAP.ColorInfo.ColorFormat.CF_YCbCr_420,
"ido_defined": UniTAP.ColorInfo.ColorFormat.CF_IDO_DEFINED,
"yonly": UniTAP.ColorInfo.ColorFormat.CF_Y_ONLY,
"raw": UniTAP.ColorInfo.ColorFormat.CF_RAW,
"dsc": UniTAP.ColorInfo.ColorFormat.CF_DSC,
}
if not format_str:
return None
return format_map.get(format_str.lower(), None)
# 色度映射 - 支持不区分大小写的字符串匹配
@staticmethod
def get_colorimetry(colorimetry_str):
colorimetry_map = {
"none": UniTAP.ColorInfo.Colorimetry.CM_NONE,
"reserved": UniTAP.ColorInfo.Colorimetry.CM_RESERVED,
"srgb": UniTAP.ColorInfo.Colorimetry.CM_sRGB,
"smpte170m": UniTAP.ColorInfo.Colorimetry.CM_SMPTE_170M,
"bt601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
"bt709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
"xvycc601": UniTAP.ColorInfo.Colorimetry.CM_xvYCC601,
"xvycc709": UniTAP.ColorInfo.Colorimetry.CM_xvYCC709,
"sycc601": UniTAP.ColorInfo.Colorimetry.CM_sYCC601,
"adobeycc601": UniTAP.ColorInfo.Colorimetry.CM_AdobeYCC601,
"adobergb": UniTAP.ColorInfo.Colorimetry.CM_AdobeRGB,
"bt2020yccbccrc": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_YcCbcCrc,
"bt2020ycbcr": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_YCbCr,
"bt2020rgb": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB,
"rgbwidegamutfix": UniTAP.ColorInfo.Colorimetry.CM_RGB_WIDE_GAMUT_FIX,
"rgbwidegamutflt": UniTAP.ColorInfo.Colorimetry.CM_RGB_WIDE_GAMUT_FLT,
"dcip3": UniTAP.ColorInfo.Colorimetry.CM_DCI_P3,
"dicom14grayscale": UniTAP.ColorInfo.Colorimetry.CM_DICOM_1_4_GRAY_SCALE,
"customcolorprofile": UniTAP.ColorInfo.Colorimetry.CM_CUSTOM_COLOR_PROFILE,
"opycc601": UniTAP.ColorInfo.Colorimetry.CM_opYCC601,
"oprgb": UniTAP.ColorInfo.Colorimetry.CM_opRGB,
}
if not colorimetry_str:
return None
# Normalize: strip hyphens, spaces, dots, underscores so that
# "DCI-P3" → "dcip3", "BT.709" → "bt709", "BT.2020 YCbCr" → "bt2020ycbcr"
normalized = (
colorimetry_str.lower()
.replace("-", "")
.replace(" ", "")
.replace(".", "")
.replace("_", "")
)
return colorimetry_map.get(normalized, colorimetry_map.get(colorimetry_str.lower(), None))
class VideoPatternInfo:
class VideoPattern(IntEnum):
"""
Class `VideoPattern` contains all possible variants of patterns which can be set in the function `set_pattern`.
"""
Disabled = 0
ColorBars = 1
Chessboard = 2
SolidColor = 3
SolidWhite = 4
SolidRed = 5
SolidGreen = 6
SolidBlue = 7
WhiteVStrips = 8
GradientRGBStripes = 9
ColorRamp = 10
ColorSquares = 11
MotionPattern = 12
SquareWindow = 15
class VideoPatternParams(IntEnum):
"""
Class `VideoPatternParams` contains all possible variants of parameters which can be set in the function `set_pattern_params`.
"""
SolidColor = 3
WhiteVStrips = 8
GradientRGBStripes = 9
MotionPattern = 12
SquareWindow = 15
@staticmethod
def get_video_pattern(pattern_str):
pattern_map = {
"disabled": UniTAP.VideoPattern.Disabled,
"colorbars": UniTAP.VideoPattern.ColorBars,
"chessboard": UniTAP.VideoPattern.Chessboard,
"solidcolor": UniTAP.VideoPattern.SolidColor,
"solidwhite": UniTAP.VideoPattern.SolidWhite,
"solidred": UniTAP.VideoPattern.SolidRed,
"solidgreen": UniTAP.VideoPattern.SolidGreen,
"solidblue": UniTAP.VideoPattern.SolidBlue,
"whitevstrips": UniTAP.VideoPattern.WhiteVStrips,
"gradientrgbstripes": UniTAP.VideoPattern.GradientRGBStripes,
"colorramp": UniTAP.VideoPattern.ColorRamp,
"coloursquares": UniTAP.VideoPattern.ColorSquares,
"motionpattern": UniTAP.VideoPattern.MotionPattern,
"squarewindow": UniTAP.VideoPattern.SquareWindow,
}
if not pattern_str:
return None
return pattern_map.get(pattern_str.lower(), None)
class TimingInfo:
class ResolutionType(IntEnum):
"""
分辨率类型枚举包含DMT、CTA、CVT和OVT四种类型
"""
DMT = 0 # VESA Display Monitor Timing
CTA = 1 # Consumer Technology Association
CVT = 2 # Coordinated Video Timing
OVT = 3 # Other Video Timing
# 分辨率类型映射
resolution_type_map = {
"dmt": ResolutionType.DMT,
"cta": ResolutionType.CTA,
"cvt": ResolutionType.CVT,
"ovt": ResolutionType.OVT,
}
# DMT分辨率ID映射
dmt_resolution_map = {
9: {"width": 800, "height": 600, "refresh_rate": 60.317, "id_hex": "9h"},
14: {"width": 848, "height": 480, "refresh_rate": 60.0, "id_hex": "Eh"},
16: {"width": 1024, "height": 768, "refresh_rate": 60.0, "id_hex": "10h"},
23: {"width": 1280, "height": 768, "refresh_rate": 60.0, "id_hex": "17h"},
27: {
"width": 1280,
"height": 800,
"refresh_rate": 60.0,
"id_hex": "1Bh",
"note": "RB1",
},
28: {"width": 1280, "height": 800, "refresh_rate": 60.0, "id_hex": "1Ch"},
32: {"width": 1280, "height": 960, "refresh_rate": 60.0, "id_hex": "20h"},
35: {"width": 1280, "height": 1024, "refresh_rate": 60.0, "id_hex": "23h"},
39: {"width": 1360, "height": 768, "refresh_rate": 60.0, "id_hex": "27h"},
41: {
"width": 1400,
"height": 1050,
"refresh_rate": 60.0,
"id_hex": "29h",
"note": "RB1",
},
42: {"width": 1400, "height": 1050, "refresh_rate": 60.0, "id_hex": "2Ah"},
47: {"width": 1440, "height": 900, "refresh_rate": 59.887, "id_hex": "2Fh"},
51: {"width": 1600, "height": 1200, "refresh_rate": 60.0, "id_hex": "33h"},
57: {
"width": 1680,
"height": 1050,
"refresh_rate": 60.0,
"id_hex": "39h",
"note": "RB1",
},
58: {"width": 1680, "height": 1050, "refresh_rate": 60.0, "id_hex": "3Ah"},
62: {"width": 1792, "height": 1344, "refresh_rate": 60.0, "id_hex": "3Eh"},
65: {"width": 1856, "height": 1392, "refresh_rate": 60.0, "id_hex": "41h"},
69: {"width": 1920, "height": 1200, "refresh_rate": 60.0, "id_hex": "45h"},
73: {"width": 1920, "height": 1440, "refresh_rate": 60.0, "id_hex": "49h"},
76: {
"width": 2560,
"height": 1600,
"refresh_rate": 60.0,
"id_hex": "4Ch",
"note": "RB1",
},
77: {"width": 2560, "height": 1600, "refresh_rate": 60.0, "id_hex": "4Dh"},
82: {"width": 1920, "height": 1080, "refresh_rate": 60.0, "id_hex": "52h"},
}
# CTA分辨率ID映射
cta_resolution_map = {
1: {"width": 640, "height": 480, "refresh_rate": 59.94, "vic": 1},
2: {"width": 720, "height": 480, "refresh_rate": 59.94, "vic": 2},
3: {"width": 720, "height": 480, "refresh_rate": 59.94, "vic": 3},
4: {"width": 1280, "height": 720, "refresh_rate": 60.0, "vic": 4},
8: {"width": 1440, "height": 240, "refresh_rate": 59.826, "vic": 8},
9: {"width": 1440, "height": 240, "refresh_rate": 60.054, "vic": 9},
12: {"width": 2880, "height": 240, "refresh_rate": 59.826, "vic": 12},
13: {"width": 2880, "height": 240, "refresh_rate": 59.826, "vic": 13},
14: {"width": 1440, "height": 480, "refresh_rate": 59.94, "vic": 14},
15: {"width": 1440, "height": 480, "refresh_rate": 59.94, "vic": 15},
16: {"width": 1920, "height": 1080, "refresh_rate": 60.0, "vic": 16},
17: {"width": 720, "height": 576, "refresh_rate": 50.0, "vic": 17},
18: {"width": 720, "height": 576, "refresh_rate": 50.0, "vic": 18},
19: {"width": 1280, "height": 720, "refresh_rate": 50.0, "vic": 19},
23: {"width": 1440, "height": 288, "refresh_rate": 49.761, "vic": 23},
24: {"width": 1440, "height": 288, "refresh_rate": 49.761, "vic": 24},
27: {"width": 2880, "height": 288, "refresh_rate": 49.761, "vic": 27},
28: {"width": 2880, "height": 288, "refresh_rate": 49.761, "vic": 28},
29: {"width": 1440, "height": 576, "refresh_rate": 50.0, "vic": 29},
30: {"width": 1440, "height": 576, "refresh_rate": 50.0, "vic": 30},
31: {"width": 1920, "height": 1080, "refresh_rate": 50.0, "vic": 31},
32: {"width": 1920, "height": 1080, "refresh_rate": 24.0, "vic": 32},
33: {"width": 1920, "height": 1080, "refresh_rate": 25.0, "vic": 33},
34: {"width": 1920, "height": 1080, "refresh_rate": 30.0, "vic": 34},
35: {"width": 2880, "height": 480, "refresh_rate": 59.94, "vic": 35},
36: {"width": 2880, "height": 480, "refresh_rate": 59.94, "vic": 36},
37: {"width": 2880, "height": 576, "refresh_rate": 50.0, "vic": 37},
38: {"width": 2880, "height": 576, "refresh_rate": 50.0, "vic": 38},
41: {"width": 1280, "height": 720, "refresh_rate": 100.0, "vic": 41},
42: {"width": 720, "height": 576, "refresh_rate": 100.0, "vic": 42},
43: {"width": 720, "height": 576, "refresh_rate": 100.0, "vic": 43},
47: {"width": 1280, "height": 720, "refresh_rate": 120.0, "vic": 47},
48: {"width": 720, "height": 480, "refresh_rate": 120.0, "vic": 48},
49: {"width": 720, "height": 480, "refresh_rate": 119.88, "vic": 49},
52: {"width": 720, "height": 576, "refresh_rate": 200.0, "vic": 52},
53: {"width": 720, "height": 576, "refresh_rate": 200.0, "vic": 53},
56: {"width": 720, "height": 480, "refresh_rate": 239.76, "vic": 56},
57: {"width": 720, "height": 480, "refresh_rate": 239.76, "vic": 57},
60: {"width": 1280, "height": 720, "refresh_rate": 24.0, "vic": 60},
61: {"width": 1280, "height": 720, "refresh_rate": 25.0, "vic": 61},
62: {"width": 1280, "height": 720, "refresh_rate": 30.0, "vic": 62},
63: {"width": 1920, "height": 1080, "refresh_rate": 120.0, "vic": 63},
64: {"width": 1920, "height": 1080, "refresh_rate": 100.0, "vic": 64},
65: {"width": 1280, "height": 720, "refresh_rate": 24.0, "vic": 65},
66: {"width": 1280, "height": 720, "refresh_rate": 25.0, "vic": 66},
67: {"width": 1280, "height": 720, "refresh_rate": 30.0, "vic": 67},
68: {"width": 1280, "height": 720, "refresh_rate": 50.0, "vic": 68},
69: {"width": 1280, "height": 720, "refresh_rate": 60.0, "vic": 69},
70: {"width": 1280, "height": 720, "refresh_rate": 100.0, "vic": 70},
71: {"width": 1280, "height": 720, "refresh_rate": 120.0, "vic": 71},
72: {"width": 1920, "height": 1080, "refresh_rate": 24.0, "vic": 72},
73: {"width": 1920, "height": 1080, "refresh_rate": 25.0, "vic": 73},
74: {"width": 1920, "height": 1080, "refresh_rate": 30.0, "vic": 74},
75: {"width": 1920, "height": 1080, "refresh_rate": 50.0, "vic": 75},
76: {"width": 1920, "height": 1080, "refresh_rate": 60.0, "vic": 76},
77: {"width": 1920, "height": 1080, "refresh_rate": 100.0, "vic": 77},
78: {"width": 1920, "height": 1080, "refresh_rate": 120.0, "vic": 78},
79: {"width": 1680, "height": 720, "refresh_rate": 24.0, "vic": 79},
80: {"width": 1680, "height": 720, "refresh_rate": 25.0, "vic": 80},
81: {"width": 1680, "height": 720, "refresh_rate": 30.0, "vic": 81},
82: {"width": 1680, "height": 720, "refresh_rate": 50.0, "vic": 82},
83: {"width": 1680, "height": 720, "refresh_rate": 60.0, "vic": 83},
84: {"width": 1680, "height": 720, "refresh_rate": 100.0, "vic": 84},
85: {"width": 1680, "height": 720, "refresh_rate": 120.0, "vic": 85},
86: {"width": 2560, "height": 1080, "refresh_rate": 24.0, "vic": 86},
87: {"width": 2560, "height": 1080, "refresh_rate": 25.0, "vic": 87},
88: {"width": 2560, "height": 1080, "refresh_rate": 30.0, "vic": 88},
89: {"width": 2560, "height": 1080, "refresh_rate": 50.0, "vic": 89},
90: {"width": 2560, "height": 1080, "refresh_rate": 60.0, "vic": 90},
91: {"width": 2560, "height": 1080, "refresh_rate": 100.0, "vic": 91},
92: {"width": 2560, "height": 1080, "refresh_rate": 120.0, "vic": 92},
93: {"width": 3840, "height": 2160, "refresh_rate": 24.0, "vic": 93},
94: {"width": 3840, "height": 2160, "refresh_rate": 25.0, "vic": 94},
95: {"width": 3840, "height": 2160, "refresh_rate": 30.0, "vic": 95},
96: {"width": 3840, "height": 2160, "refresh_rate": 50.0, "vic": 96},
97: {"width": 3840, "height": 2160, "refresh_rate": 60.0, "vic": 97},
98: {"width": 4096, "height": 2160, "refresh_rate": 24.0, "vic": 98},
99: {"width": 4096, "height": 2160, "refresh_rate": 25.0, "vic": 99},
100: {"width": 4096, "height": 2160, "refresh_rate": 30.0, "vic": 100},
101: {"width": 4096, "height": 2160, "refresh_rate": 50.0, "vic": 101},
102: {"width": 4096, "height": 2160, "refresh_rate": 60.0, "vic": 102},
103: {"width": 3840, "height": 2160, "refresh_rate": 24.0, "vic": 103},
104: {"width": 3840, "height": 2160, "refresh_rate": 25.0, "vic": 104},
105: {"width": 3840, "height": 2160, "refresh_rate": 30.0, "vic": 105},
106: {"width": 3840, "height": 2160, "refresh_rate": 50.0, "vic": 106},
107: {"width": 3840, "height": 2160, "refresh_rate": 60.0, "vic": 107},
108: {"width": 1280, "height": 720, "refresh_rate": 48.0, "vic": 108},
109: {"width": 1280, "height": 720, "refresh_rate": 48.0, "vic": 109},
110: {"width": 1680, "height": 720, "refresh_rate": 48.0, "vic": 110},
111: {"width": 1920, "height": 1080, "refresh_rate": 48.0, "vic": 111},
112: {"width": 1920, "height": 1080, "refresh_rate": 48.0, "vic": 112},
113: {"width": 2560, "height": 1080, "refresh_rate": 48.0, "vic": 113},
114: {"width": 3840, "height": 2160, "refresh_rate": 48.0, "vic": 114},
115: {"width": 4096, "height": 2160, "refresh_rate": 48.0, "vic": 115},
116: {"width": 3840, "height": 2160, "refresh_rate": 48.0, "vic": 116},
117: {"width": 3840, "height": 2160, "refresh_rate": 100.0, "vic": 117},
118: {"width": 3840, "height": 2160, "refresh_rate": 120.0, "vic": 118},
119: {"width": 3840, "height": 2160, "refresh_rate": 100.0, "vic": 119},
120: {"width": 3840, "height": 2160, "refresh_rate": 120.0, "vic": 120},
218: {"width": 4096, "height": 2160, "refresh_rate": 100.0, "vic": 218},
219: {"width": 4096, "height": 2160, "refresh_rate": 120.0, "vic": 219},
}
# CVT分辨率ID映射
cvt_resolution_map = {
0: [
{"width": 640, "height": 480, "refresh_rate": 60.0},
{"width": 768, "height": 480, "refresh_rate": 84.502},
{"width": 1024, "height": 640, "refresh_rate": 59.887},
{"width": 1152, "height": 720, "refresh_rate": 74.721},
{"width": 1280, "height": 768, "refresh_rate": 60.0, "note": "RB1"},
{"width": 1280, "height": 960, "refresh_rate": 59.939},
{"width": 1536, "height": 960, "refresh_rate": 84.884},
{"width": 1600, "height": 1200, "refresh_rate": 60.0, "note": "RB1"},
{"width": 1920, "height": 1080, "refresh_rate": 30.0, "note": "RB1"},
{"width": 1920, "height": 1080, "refresh_rate": 30.0, "note": "RB2"},
{"width": 1920, "height": 1080, "refresh_rate": 60.0, "note": "RB1"},
{"width": 1920, "height": 1080, "refresh_rate": 60.0, "note": "RB2"},
{"width": 1920, "height": 1080, "refresh_rate": 84.884},
{"width": 1920, "height": 1080, "refresh_rate": 120.0, "note": "RB1"},
{"width": 1920, "height": 1080, "refresh_rate": 120.0, "note": "RB2"},
{"width": 1920, "height": 1080, "refresh_rate": 144.05, "note": "RB1"},
{"width": 1920, "height": 1080, "refresh_rate": 144.05, "note": "RB2"},
{"width": 1920, "height": 1080, "refresh_rate": 144.05, "note": "RB3"},
{"width": 1920, "height": 1080, "refresh_rate": 200.07, "note": "RB3"},
{"width": 1920, "height": 1080, "refresh_rate": 239.898, "note": "RB1"},
{"width": 1920, "height": 1080, "refresh_rate": 239.898, "note": "RB2"},
{"width": 1920, "height": 1080, "refresh_rate": 239.898, "note": "RB3"},
{"width": 1920, "height": 1440, "refresh_rate": 59.974, "note": "RB1"},
{"width": 2048, "height": 1280, "refresh_rate": 59.922, "note": "RB1"},
{"width": 2048, "height": 1536, "refresh_rate": 60.0, "note": "RB1"},
{"width": 2128, "height": 1200, "refresh_rate": 59.946},
{"width": 2456, "height": 1536, "refresh_rate": 50.0},
{"width": 2456, "height": 1536, "refresh_rate": 74.935},
{"width": 2560, "height": 1080, "refresh_rate": 60.0},
{"width": 2560, "height": 1080, "refresh_rate": 60.0, "note": "RB1"},
{"width": 2560, "height": 1080, "refresh_rate": 144.051, "note": "RB3"},
{"width": 2560, "height": 1080, "refresh_rate": 200.07, "note": "RB3"},
{"width": 2560, "height": 1440, "refresh_rate": 60.0, "note": "RB1"},
{"width": 2560, "height": 1440, "refresh_rate": 60.0, "note": "RB2"},
{"width": 2560, "height": 1440, "refresh_rate": 144.05, "note": "RB3"},
{"width": 2560, "height": 1440, "refresh_rate": 200.07, "note": "RB3"},
{"width": 2560, "height": 1920, "refresh_rate": 74.979},
{"width": 2728, "height": 1536, "refresh_rate": 59.944},
{"width": 3440, "height": 1440, "refresh_rate": 60.0},
{"width": 3440, "height": 1440, "refresh_rate": 60.0, "note": "RB1"},
{"width": 3440, "height": 1440, "refresh_rate": 60.0, "note": "RB2"},
{"width": 3440, "height": 1440, "refresh_rate": 120.0},
{"width": 3440, "height": 1440, "refresh_rate": 120.0, "note": "RB1"},
{"width": 3440, "height": 1440, "refresh_rate": 120.0, "note": "RB2"},
{"width": 3440, "height": 1440, "refresh_rate": 165.0, "note": "RB1"},
{"width": 3440, "height": 1440, "refresh_rate": 165.0, "note": "RB2"},
{"width": 3440, "height": 1440, "refresh_rate": 200.0, "note": "RB1"},
{"width": 3440, "height": 1440, "refresh_rate": 200.0, "note": "RB2"},
{"width": 3840, "height": 2160, "refresh_rate": 30.0, "note": "RB1"},
{"width": 3840, "height": 2160, "refresh_rate": 30.0, "note": "RB2"},
{"width": 3840, "height": 2160, "refresh_rate": 60.0, "note": "RB1"},
{"width": 3840, "height": 2160, "refresh_rate": 60.0, "note": "RB2"},
{"width": 3840, "height": 2160, "refresh_rate": 60.021, "note": "RB3"},
{"width": 3840, "height": 2160, "refresh_rate": 120.0, "note": "RB1"},
{"width": 3840, "height": 2160, "refresh_rate": 120.0, "note": "RB2"},
{"width": 4096, "height": 2160, "refresh_rate": 60.0, "note": "RB1"},
{"width": 4096, "height": 2160, "refresh_rate": 60.0, "note": "RB2"},
{"width": 4096, "height": 2160, "refresh_rate": 60.021, "note": "RB3"},
]
}
# OVT分辨率ID映射
ovt_resolution_map = {
0: [
{"width": 768, "height": 480, "refresh_rate": 85.0},
{"width": 1024, "height": 640, "refresh_rate": 60.0},
{"width": 1152, "height": 720, "refresh_rate": 75.0},
{"width": 1280, "height": 768, "refresh_rate": 60.0},
{"width": 1280, "height": 960, "refresh_rate": 60.0},
{"width": 1440, "height": 240, "refresh_rate": 60.0},
{"width": 1440, "height": 480, "refresh_rate": 60.0},
{"width": 1440, "height": 900, "refresh_rate": 60.0},
{"width": 1536, "height": 960, "refresh_rate": 85.0},
{"width": 1920, "height": 1080, "refresh_rate": 30.0},
{"width": 1920, "height": 1080, "refresh_rate": 60.0},
{"width": 1920, "height": 1080, "refresh_rate": 85.0},
{"width": 1920, "height": 1080, "refresh_rate": 100.0},
{"width": 1920, "height": 1080, "refresh_rate": 120.0},
{"width": 1920, "height": 1440, "refresh_rate": 60.0},
{"width": 2048, "height": 1280, "refresh_rate": 60.0},
{"width": 2048, "height": 1536, "refresh_rate": 60.0},
{"width": 2128, "height": 1200, "refresh_rate": 60.0},
{"width": 2456, "height": 1536, "refresh_rate": 50.0},
{"width": 2456, "height": 1536, "refresh_rate": 75.0},
{"width": 2560, "height": 1080, "refresh_rate": 30.0},
{"width": 2560, "height": 1080, "refresh_rate": 60.0},
{"width": 2560, "height": 1080, "refresh_rate": 120.0},
{"width": 2560, "height": 1600, "refresh_rate": 60.0},
{"width": 2560, "height": 1920, "refresh_rate": 75.0},
{"width": 2728, "height": 1536, "refresh_rate": 60.0},
{"width": 3840, "height": 2160, "refresh_rate": 30.0},
{"width": 3840, "height": 2160, "refresh_rate": 60.0},
{"width": 3840, "height": 2160, "refresh_rate": 120.0},
{"width": 4096, "height": 2160, "refresh_rate": 30.0},
],
1: [
{"width": 1280, "height": 720, "refresh_rate": 24.0},
{"width": 1280, "height": 720, "refresh_rate": 120.0},
],
4: [
{"width": 1920, "height": 1080, "refresh_rate": 144.0},
{"width": 1920, "height": 1080, "refresh_rate": 240.0},
],
}
# 根据分辨率类型和ID获取分辨率信息的函数
@staticmethod
def get_resolution_info(resolution_type, resolution_id):
"""
根据分辨率类型和ID获取分辨率信息
Args:
resolution_type: 分辨率类型可以是DMT、CTA、CVT或OVT
resolution_id: 分辨率ID
Returns:
包含分辨率信息的字典如果未找到则返回None
"""
if resolution_type == UCDEnum.ResolutionType.DMT:
return UCDEnum.dmt_resolution_map.get(resolution_id)
elif resolution_type == UCDEnum.ResolutionType.CTA:
return UCDEnum.cta_resolution_map.get(resolution_id)
elif resolution_type == UCDEnum.ResolutionType.CVT:
resolutions = UCDEnum.cvt_resolution_map.get(resolution_id, [])
return resolutions[0] if resolutions else None
elif resolution_type == UCDEnum.ResolutionType.OVT:
resolutions = UCDEnum.ovt_resolution_map.get(resolution_id, [])
return resolutions[0] if resolutions else None
return None
@staticmethod
def get_formatted_resolution_list():
"""
从分辨率映射中生成格式化的分辨率字符串列表用于UI显示
格式为: "类型 宽度x 高度 @ 刷新率Hz"
Returns:
包含格式化分辨率字符串的列表
"""
formatted_list = []
# 添加DMT分辨率
for res_id, res_info in UCDEnum.TimingInfo.dmt_resolution_map.items():
formatted_str = f"DMT {res_info['width']}x {res_info['height']} @ {int(res_info['refresh_rate'])}Hz"
formatted_list.append(formatted_str)
# 添加CTA分辨率
for res_id, res_info in UCDEnum.TimingInfo.cta_resolution_map.items():
formatted_str = f"CTA {res_info['width']}x {res_info['height']} @ {int(res_info['refresh_rate'])}Hz"
formatted_list.append(formatted_str)
# 添加CVT分辨率 (只取每个ID的第一个)
for res_id, res_list in UCDEnum.TimingInfo.cvt_resolution_map.items():
for res_info in res_list:
note = f" {res_info.get('note', '')}" if "note" in res_info else ""
formatted_str = f"CVT {res_info['width']}x {res_info['height']} @ {int(res_info['refresh_rate'])}Hz{note}"
formatted_list.append(formatted_str)
# 添加OVT分辨率 (只取每个ID的第一个)
for res_id, res_list in UCDEnum.TimingInfo.ovt_resolution_map.items():
for res_info in res_list:
formatted_str = f"OVT {res_info['width']}x {res_info['height']} @ {int(res_info['refresh_rate'])}Hz"
formatted_list.append(formatted_str)
# 排序并去重
return sorted(list(set(formatted_list)))
class SignalFormat:
"""信号格式相关枚举"""
class GammaType:
"""Gamma 类型枚举"""
GAMMA_22 = "2.2"
GAMMA_24 = "2.4"
GAMMA_26 = "2.6"
@staticmethod
def get_list():
return ["2.2", "2.4", "2.6"]
@staticmethod
def get_gamma_value(gamma_str):
"""将 Gamma 字符串转换为数值"""
gamma_map = {"2.2": 2.2, "2.4": 2.4, "2.6": 2.6}
return gamma_map.get(gamma_str, 2.2)
class DataRange:
"""数据范围枚举"""
FULL = "Full"
LIMITED = "Limited"
@staticmethod
def get_list():
return ["Full", "Limited"]
class BitDepth:
"""编码位深枚举"""
BIT_8 = "8bit"
BIT_10 = "10bit"
BIT_12 = "12bit"
@staticmethod
def get_list():
return ["8bit", "10bit", "12bit"]
@staticmethod
def get_bit_value(bit_str):
"""将位深字符串转换为数值"""
bit_map = {"8bit": 8, "10bit": 10, "12bit": 12}
return bit_map.get(bit_str, 8)
class HDRMetadata:
"""HDR Metadata 参数"""
# MaxCLL (Maximum Content Light Level) - 最大内容亮度级别
MAX_CLL_DEFAULT = 1000
MAX_CLL_OPTIONS = [400, 600, 800, 1000, 1200, 1500, 2000, 4000]
# MaxFALL (Maximum Frame Average Light Level) - 最大帧平均亮度级别
MAX_FALL_DEFAULT = 400
MAX_FALL_OPTIONS = [200, 300, 400, 500, 600, 800, 1000]
@staticmethod
def get_maxcll_list():
return [400, 600, 800, 1000, 1200, 1500, 2000, 4000]
@staticmethod
def get_maxfall_list():
return [200, 300, 400, 500, 600, 800, 1000]
class OutputFormat:
"""输出色彩格式枚举(决定信号是 RGB 还是 YCbCr 格式)"""
RGB = "RGB"
YCBCR_422 = "YCbCr 4:2:2"
YCBCR_444 = "YCbCr 4:4:4"
YCBCR_420 = "YCbCr 4:2:0"
Y_ONLY = "Y Only"
IDO_DEFINED = "IDO Defined"
RAW = "RAW"
DSC = "DSC"
@staticmethod
def get_list():
return ["RGB", "YCbCr 4:4:4", "YCbCr 4:2:2", "YCbCr 4:2:0",
"Y Only", "IDO Defined", "RAW", "DSC"]
@staticmethod
def is_ycbcr(format_str):
return "YCbCr" in (format_str or "")
@staticmethod
def get_format_key(format_str):
"""将显示字符串转换为 UCDEnum.ColorInfo.get_color_format() 的 key"""
fmt_map = {
"RGB": "rgb",
"YCbCr 4:4:4": "ycbcr444",
"YCbCr 4:2:2": "ycbcr422",
"YCbCr 4:2:0": "ycbcr420",
"Y Only": "yonly",
"IDO Defined": "ido_defined",
"RAW": "raw",
"DSC": "dsc",
}
return fmt_map.get(format_str, "rgb")

412
app/ucd/service.py Normal file
View File

@@ -0,0 +1,412 @@
"""UCD 服务层SignalService硬件编排+ PatternService测试发图"""
from __future__ import annotations
import logging
from app.ucd.domain import (
EventBus,
PatternSpec,
SignalFormat,
TimingSpec,
UcdState,
image_pattern,
solid_rgb_pattern,
)
from app.ucd.device import IUcdDevice
log = logging.getLogger(__name__)
# ─── SignalService ────────────────────────────────────────────────────────
class SignalService:
"""协调 SignalFormat / Timing / Pattern 的写入与提交。"""
def __init__(self, device: IUcdDevice, bus: EventBus):
self._dev = device
self._bus = bus
# -- 高层接口 ------------------------------------------------
def apply(
self,
*,
signal: SignalFormat,
timing: TimingSpec,
pattern: PatternSpec,
) -> bool:
"""一次性提交信号格式 + timing + 图案。
Returns:
``format_changed``——本次相对上一次 :meth:`apply` 是否变化。
"""
log.info(
"SignalService.apply signal=%s timing=%s pattern=%s",
signal,
timing,
pattern.kind.value,
)
changed = self._dev.configure(signal, timing)
self._dev.set_pattern(pattern)
self._dev.apply()
return changed
def send_pattern(self, pattern: PatternSpec) -> None:
"""在已 configure 的信号上仅更新图案后 apply。"""
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
self._dev.set_pattern(pattern)
self._dev.apply()
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
self.send_pattern(solid_rgb_pattern(rgb))
def send_image(self, path: str) -> None:
self.send_pattern(image_pattern(path))
def update_signal_format(
self,
*,
color_space: str,
output_format: str,
bit_depth: str,
data_range: str = "Full",
max_cll: int | None = None,
max_fall: int | None = None,
) -> bool:
"""仅将信号格式提交到 SDK沿用上一次的 timing不切换图案。
UI 字符串先经域层解析做参数校验;解析失败抛 :class:`UcdConfigError`。
"""
_ = build_signal_format(
color_space=color_space,
output_format=output_format,
bit_depth=bit_depth,
data_range=data_range,
)
return self._dev.apply_signal_format(
color_space=color_space,
color_format=output_format,
bit_depth=bit_depth,
data_range=data_range,
max_cll=max_cll,
max_fall=max_fall,
)
# -- 透传给上层的查询 ---------------------------------------
@property
def device(self) -> IUcdDevice:
return self._dev
def current_resolution(self) -> tuple[int, int]:
return self._dev.current_resolution()
@property
def is_connected(self) -> bool:
"""UCD 设备是否已打开。供 GUI 做前置校验。"""
return self._dev.state != UcdState.CLOSED
@property
def format_changed(self) -> bool:
"""最近一次视频模式提交是否相对上次发生变化。"""
return self._dev.format_changed
@property
def last_error(self) -> str | None:
return self._dev.last_error
def apply_config(self, config) -> bool:
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern不 apply 输出)。"""
return bool(self._dev.set_ucd_params(config))
def send_pattern_params(self, params) -> bool:
"""以 ``params`` 更新当前 pattern 的参数并 apply。"""
return bool(self._dev.send_current_pattern_params(params))
def apply_and_run(self, config, pattern_params) -> bool:
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
return bool(self._dev.apply_config_and_run(config, pattern_params))
def stage_test_profile(
self,
config,
*,
color_space: str,
output_format: str,
bit_depth: str,
data_range: str = "Full",
max_cll: int | None = None,
max_fall: int | None = None,
) -> bool:
"""按 PQConfig stage 色彩/Timing/Pattern 类型,并提交 UI 覆盖的信号格式。
自动化测试在发图前调用;等价于 ``apply_config`` + ``update_signal_format``。
"""
if not self.apply_config(config):
return False
return self.update_signal_format(
color_space=color_space,
output_format=output_format,
bit_depth=bit_depth,
data_range=data_range,
max_cll=max_cll,
max_fall=max_fall,
)
__all__ = [
"SignalService",
"build_signal_format",
"build_signal_format_from_profile",
"build_timing",
"solid_rgb_pattern",
"image_pattern",
"SignalFormat",
"TimingSpec",
"PatternSpec",
"PatternKind",
"Colorimetry",
"DynamicRange",
"UcdError",
]
# --- PatternService ---
import copy
from dataclasses import dataclass
from app.data_range_converter import convert_pattern_params
from app.pq.pq_config import get_pattern
@dataclass
class PatternSession:
mode: str
test_type: str
active_config: object
pattern_params: list[list[int]]
total_patterns: int
display_names: list[str]
class PatternService:
def __init__(self, app):
self.app = app
def _build_apply_config_error(self, test_type):
timing = self.app.config.current_test_types.get(test_type, {}).get("timing", "-")
detail = ""
err = self.app.signal_service.last_error
if err:
detail = f", detail={err}"
return f"UCD profile apply_config failed for {test_type}, timing={timing}{detail}"
def _stage_profile(
self,
active_config,
*,
color_space: str,
output_format: str,
bit_depth: str,
data_range: str = "Full",
max_cll: int | None = None,
max_fall: int | None = None,
test_type: str,
log_details: bool = False,
log_title: str = "",
log_fields: list[tuple[str, str]] | None = None,
) -> None:
if log_details and log_title:
self._log("=" * 50, "separator")
self._log(log_title, "info")
self._log("=" * 50, "separator")
for label, value in log_fields or []:
self._log(f" {label}: {value}", "info")
if not self.app.signal_service.stage_test_profile(
active_config,
color_space=color_space,
output_format=output_format,
bit_depth=bit_depth,
data_range=data_range,
max_cll=max_cll,
max_fall=max_fall,
):
raise RuntimeError(self._build_apply_config_error(test_type))
if log_details:
self._log("信号格式设置成功", "success")
def prepare_session(self, mode, *, test_type=None, log_details=False):
test_type = test_type or self.app.config.current_test_type
if hasattr(self.app.config, "set_current_test_type"):
self.app.config.set_current_test_type(test_type)
if not self.app.config.set_current_pattern(mode):
raise ValueError(f"未知的图案模式: {mode}")
active_config = self.app.config
source_params = self._get_source_pattern_params(mode)
if test_type == "screen_module":
screen_cfg = self.app.config.current_test_types.get("screen_module", {})
color_space = (
self.app.screen_module_color_space_var.get()
if hasattr(self.app, "screen_module_color_space_var")
else screen_cfg.get("colorimetry", "sRGB")
)
data_range = (
self.app.screen_module_data_range_var.get()
if hasattr(self.app, "screen_module_data_range_var")
else screen_cfg.get("data_range", "Full")
)
bit_depth = (
self.app.screen_module_bit_depth_var.get()
if hasattr(self.app, "screen_module_bit_depth_var")
else f"{int(screen_cfg.get('bpc', 8))}bit"
)
output_format = (
self.app.screen_module_output_format_var.get()
if hasattr(self.app, "screen_module_output_format_var")
else screen_cfg.get("color_format", "RGB")
)
self._stage_profile(
active_config,
color_space=color_space,
data_range=data_range,
bit_depth=bit_depth,
output_format=output_format,
test_type=test_type,
log_details=log_details,
log_title="设置屏模组信号格式:",
log_fields=[
("色彩空间", color_space),
("色彩格式", output_format),
("数据范围", data_range),
("编码位深", bit_depth),
("Timing", self.app.config.current_test_types[test_type]["timing"]),
],
)
elif test_type == "sdr_movie":
data_range = self.app.sdr_data_range_var.get()
converted_params = convert_pattern_params(
source_params, data_range=data_range, verbose=False
)
active_config = self.app.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params
)
if hasattr(active_config, "set_current_test_type"):
active_config.set_current_test_type(test_type)
self._stage_profile(
active_config,
color_space=self.app.sdr_color_space_var.get(),
data_range=data_range,
bit_depth=self.app.sdr_bit_depth_var.get(),
output_format=self.app.sdr_output_format_var.get(),
test_type=test_type,
log_details=log_details,
log_title="设置 SDR 信号格式:",
log_fields=[
("色彩空间", self.app.sdr_color_space_var.get()),
("色彩格式", self.app.sdr_output_format_var.get()),
("Gamma", self.app.sdr_gamma_type_var.get()),
("数据范围", data_range),
("编码位深", self.app.sdr_bit_depth_var.get()),
],
)
if log_details:
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
elif test_type == "hdr_movie":
data_range = self.app.hdr_data_range_var.get()
converted_params = convert_pattern_params(
source_params, data_range=data_range, verbose=False
)
active_config = self.app.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params
)
if hasattr(active_config, "set_current_test_type"):
active_config.set_current_test_type(test_type)
self._stage_profile(
active_config,
color_space=self.app.hdr_color_space_var.get(),
data_range=data_range,
bit_depth=self.app.hdr_bit_depth_var.get(),
output_format=self.app.hdr_output_format_var.get(),
max_cll=self.app.hdr_maxcll_var.get(),
max_fall=self.app.hdr_maxfall_var.get(),
test_type=test_type,
log_details=log_details,
log_title="设置 HDR 信号格式:",
log_fields=[
("色彩空间", self.app.hdr_color_space_var.get()),
("色彩格式", self.app.hdr_output_format_var.get()),
("数据范围", data_range),
("编码位深", self.app.hdr_bit_depth_var.get()),
("MaxCLL", self.app.hdr_maxcll_var.get()),
("MaxFALL", self.app.hdr_maxfall_var.get()),
],
)
if log_details:
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
else:
raise ValueError(f"不支持的测试类型: {test_type}")
pattern_params = copy.deepcopy(active_config.current_pattern["pattern_params"])
return PatternSession(
mode=mode,
test_type=test_type,
active_config=active_config,
pattern_params=pattern_params,
total_patterns=len(pattern_params),
display_names=self._get_display_names(mode, len(pattern_params)),
)
def send_session_pattern(self, session, index):
if index < 0 or index >= session.total_patterns:
raise IndexError(f"pattern 索引越界: {index}")
pattern_param = session.pattern_params[index]
if not self.app.signal_service.send_pattern_params(pattern_param):
raise RuntimeError(f"发送 pattern 失败: {index}")
return pattern_param
def send_rgb(self, rgb, *, session=None, test_type=None):
active_session = session or self.prepare_session(
"rgb",
test_type=test_type,
log_details=False,
)
converted_rgb = self._convert_rgb_for_test_type(rgb, active_session.test_type)
self.app.signal_service.send_solid_rgb(converted_rgb)
return True
def _get_source_pattern_params(self, mode):
return copy.deepcopy(get_pattern(mode)["pattern_params"])
def _get_display_names(self, mode, total_patterns):
if mode == "accuracy":
return self.app.config.get_accuracy_color_names()
if mode == "custom" and hasattr(self.app.config, "get_temp_pattern_names"):
return self.app.config.get_temp_pattern_names()
return [f"P {index + 1}" for index in range(total_patterns)]
def _convert_rgb_for_test_type(self, rgb, test_type):
if test_type == "sdr_movie":
data_range = self.app.sdr_data_range_var.get()
elif test_type == "hdr_movie":
data_range = self.app.hdr_data_range_var.get()
else:
data_range = "Full"
return convert_pattern_params([list(rgb)], data_range=data_range, verbose=False)[0]
def _log(self, message, level):
if hasattr(self.app, "log_gui"):
self.app.log_gui.log(message, level=level)