优化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

View File

@@ -6,9 +6,8 @@
"把模块级函数当类方法装到 PQAutomationApp 上" 的写法。
- 拆掉 ``check_port_connection(is_ucd)`` 的布尔旗参数反模式,
分离为 ``connect_ucd`` / ``connect_ca`` 两条独立路径。
- UCD 这一侧不再直接调用旧 ``UCDController``,而是通过
:class:`UCD323Device` + :class:`EventBus`;指示器更新由订阅
:class:`ConnectionChanged` 事件触发,与 GUI 解耦。
- UCD 经由 :class:`UCD323Device` + :class:`EventBus` 管理;
指示灯由 GUI 订阅带 :class:`DeviceKind` :class:`ConnectionChanged` 事件更新。
模块底部仍保留 7 个旧名字函数作为薄兼容层(``check_com_connections``
等),供 ``pqAutomationApp.PQAutomationApp`` 类体继续以同名属性挂接,
@@ -22,9 +21,8 @@ import time
from tkinter import messagebox
from typing import TYPE_CHECKING
from app.ucd_domain import ConnectionChanged, UcdError
from app.ucd import ConnectionChanged, DeviceKind, DeviceInfo, UCD323Device, UcdError
from drivers.caSerail import CASerail
from drivers.ucd_driver import DeviceInfo
from app.views.modern_styles import get_theme_palette
from typing import TYPE_CHECKING
@@ -34,8 +32,7 @@ if TYPE_CHECKING:
if TYPE_CHECKING:
from app.ucd_domain import EventBus
from drivers.ucd_driver import UCD323Device
from app.ucd import EventBus
# ─── ConnectionController ────────────────────────────────────────
@@ -133,7 +130,7 @@ class ConnectionController:
channel_value = self._app.ca_channel_var.get()
ca.setChannel(f"{int(channel_value):02d}")
self._app.ca = ca
self._bus.publish(ConnectionChanged(True, None))
self._bus.publish(ConnectionChanged(DeviceKind.CA, True, None))
return True
except Exception as exc: # noqa: BLE001
self._log(f"CA410 连接失败: {exc}", level="error")
@@ -147,7 +144,7 @@ class ConnectionController:
except Exception: # noqa: BLE001
pass
self._app.ca = None
self._bus.publish(ConnectionChanged(False, None))
self._bus.publish(ConnectionChanged(DeviceKind.CA, False, None))
self._log("CA连接已断开", level="info")
# -- 一次性入口 ----------------------------------------------

View File

@@ -1 +1,3 @@
from app.services.pattern_service import PatternService, PatternSession
from app.ucd import PatternService, PatternSession
__all__ = ["PatternService", "PatternSession"]

View File

@@ -1,219 +0,0 @@
"""UCD 信号 / 图案应用服务层。
服务层是 GUI ↔ Driver 的唯一通道,负责:
- 将 UI 字符串("BT.709""10bit""YCbCr 4:4:4" 等)翻译成 :class:`SignalFormat`
- 将各 panel 的 timing 字符串翻译成 :class:`TimingSpec`
- 协调 :meth:`IUcdDevice.configure` / ``set_pattern`` / ``apply`` 的调用顺序;
- 通过 :class:`EventBus` 让 GUI 订阅状态变化,而非主动轮询。
本层不直接 import UniTAP也不读取 :mod:`tkinter` 变量;
所有输入都是显式参数,便于单测。
线程安全由 :class:`UCD323Device` 的设备锁统一保证,本层不再重复加锁。
"""
from __future__ import annotations
import logging
from app.ucd_domain import (
Colorimetry,
DynamicRange,
EventBus,
PatternKind,
PatternSpec,
SignalFormat,
TimingSpec,
UcdError,
UcdState,
bit_depth_str_to_bpc,
color_space_to_colorimetry,
data_range_to_dynamic_range,
output_format_to_color_format,
parse_timing_str,
)
from drivers.ucd_driver import IUcdDevice
log = logging.getLogger(__name__)
# ─── 视图字符串 → 值对象 转换工具 ────────────────────────────────
def build_signal_format(
*,
color_space: str,
output_format: str,
bit_depth: str,
data_range: str = "Full",
) -> SignalFormat:
"""根据下拉框字符串组装 :class:`SignalFormat`。
各参数解析失败抛 :class:`UcdConfigError`。
"""
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:
"""从 PQConfig test_type 条目组装 :class:`SignalFormat`。"""
bit_depth = f"{int(bpc)}bit"
return build_signal_format(
color_space=color_space,
output_format=color_format,
bit_depth=bit_depth,
data_range=data_range,
)
def build_timing(timing_str: str) -> TimingSpec:
"""``"DMT 3840x2160@60Hz"`` → :class:`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)
# ─── 服务 ────────────────────────────────────────────────────────
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))
__all__ = [
"SignalService",
"build_signal_format",
"build_signal_format_from_profile",
"build_timing",
"solid_rgb_pattern",
"image_pattern",
"SignalFormat",
"TimingSpec",
"PatternSpec",
"PatternKind",
"Colorimetry",
"DynamicRange",
"UcdError",
]

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

View File

@@ -1,17 +1,4 @@
"""UCD 控制 Domain 层。
纯数据 + 纯函数枚举值对象状态机错误体系事件总线
业务字符串解析与映射本模块****依赖 UniTAP / 任何硬件
可用纯单测覆盖
文件分区
§1 枚举与值对象
§2 状态机
§3 错误体系
§4 事件总线
§5 业务字符串解析 / 映射
"""
"""UCD 域层:类型、状态机、事件、字符串与 PQConfig 映射。"""
from __future__ import annotations
import logging
@@ -25,6 +12,14 @@ log = logging.getLogger(__name__)
# ─── §1 枚举与值对象 ──────────────────────────────────────────────
class DeviceKind(str, Enum):
"""连接状态事件所指的设备类型。"""
UCD = "ucd"
CA = "ca"
class Interface(str, Enum):
"""UCD 物理输出接口。"""
@@ -192,6 +187,7 @@ class UcdEvent:
@dataclass(frozen=True)
class ConnectionChanged(UcdEvent):
device: DeviceKind
connected: bool
serial: str | None = None
@@ -353,6 +349,7 @@ def parse_timing_str(timing_str: str) -> TimingSpec:
__all__ = [
# §1
"DeviceKind",
"Interface",
"ColorFormat",
"Colorimetry",
@@ -386,3 +383,104 @@ __all__ = [
"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")

View File

@@ -1,5 +1,179 @@
"""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
@@ -29,6 +203,42 @@ class PatternService:
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"):
@@ -62,46 +272,26 @@ class PatternService:
else screen_cfg.get("color_format", "RGB")
)
if log_details:
self._log("=" * 50, "separator")
self._log("设置屏模组信号格式:", "info")
self._log("=" * 50, "separator")
for label, value in [
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"]),
]:
self._log(f" {label}: {value}", "info")
if not self.app.signal_service.apply_config(active_config):
raise RuntimeError(self._build_apply_config_error(test_type))
success = self.app.signal_service.update_signal_format(
color_space=color_space,
data_range=data_range,
bit_depth=bit_depth,
output_format=output_format,
],
)
if log_details:
self._log(
f"屏模组信号格式设置{'成功' if success else '失败'}",
"success" if success else "error",
)
elif test_type == "sdr_movie":
data_range = self.app.sdr_data_range_var.get()
if log_details:
self._log("=" * 50, "separator")
self._log("设置 SDR 信号格式:", "info")
self._log("=" * 50, "separator")
for label, value in [
("色彩空间", 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()),
]:
self._log(f" {label}: {value}", "info")
converted_params = convert_pattern_params(
source_params, data_range=data_range, verbose=False
)
@@ -110,33 +300,29 @@ class PatternService:
)
if hasattr(active_config, "set_current_test_type"):
active_config.set_current_test_type(test_type)
if not self.app.signal_service.apply_config(active_config):
raise RuntimeError(self._build_apply_config_error(test_type))
success = self.app.signal_service.update_signal_format(
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"SDR 信号格式设置{'成功' if success else '失败'}", "success" if success else "error")
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
elif test_type == "hdr_movie":
data_range = self.app.hdr_data_range_var.get()
if log_details:
self._log("=" * 50, "separator")
self._log("设置 HDR 信号格式:", "info")
self._log("=" * 50, "separator")
for label, value in [
("色彩空间", 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()),
]:
self._log(f" {label}: {value}", "info")
converted_params = convert_pattern_params(
source_params, data_range=data_range, verbose=False
)
@@ -145,18 +331,28 @@ class PatternService:
)
if hasattr(active_config, "set_current_test_type"):
active_config.set_current_test_type(test_type)
if not self.app.signal_service.apply_config(active_config):
raise RuntimeError(self._build_apply_config_error(test_type))
success = self.app.signal_service.update_signal_format(
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"HDR 信号格式设置{'成功' if success else '失败'}", "success" if success else "error")
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
else:
@@ -213,4 +409,4 @@ class PatternService:
def _log(self, message, level):
if hasattr(self.app, "log_gui"):
self.app.log_gui.log(message, level=level)
self.app.log_gui.log(message, level=level)

View File

@@ -4,7 +4,7 @@ import re
import tkinter as tk
import ttkbootstrap as ttk
from drivers.UCD323_Enum import UCDEnum
from app.ucd import UCDEnum
from app.views.collapsing_frame import CollapsingFrame
from app.resources import load_icon