1.1.0版本

This commit is contained in:
xinzhu.yin
2026-04-16 16:51:05 +08:00
commit c157e774e5
333 changed files with 70759 additions and 0 deletions

590
utils/UCD323_Enum.py Normal file
View File

@@ -0,0 +1,590 @@
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
return 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 [
SignalFormat.GammaType.GAMMA_22,
SignalFormat.GammaType.GAMMA_24,
SignalFormat.GammaType.GAMMA_26,
]
@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 [SignalFormat.DataRange.FULL, SignalFormat.DataRange.LIMITED]
@staticmethod
def is_full_range(range_str):
"""判断是否为 Full Range"""
return range_str == SignalFormat.DataRange.FULL
class BitDepth:
"""编码位深枚举"""
BIT_8 = "8bit"
BIT_10 = "10bit"
BIT_12 = "12bit"
@staticmethod
def get_list():
return [
SignalFormat.BitDepth.BIT_8,
SignalFormat.BitDepth.BIT_10,
SignalFormat.BitDepth.BIT_12,
]
@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 SignalFormat.HDRMetadata.MAX_CLL_OPTIONS
@staticmethod
def get_maxfall_list():
return SignalFormat.HDRMetadata.MAX_FALL_OPTIONS

589
utils/UCD323_Function.py Normal file
View File

@@ -0,0 +1,589 @@
# -*- coding: UTF-8 -*-
import UniTAP
import time
import gc
from utils.UCD323_Enum import UCDEnum
class UCDController:
"""UCD323信号发生器控制类"""
def __init__(self):
self.lUniTAP = UniTAP.TsiLib()
self.dev = None
self.role = None
self.timing_manager = None
self.config = None
self.color_mode = None
self.status = False
self.current_timing = None
self.current_pattern = None
self.current_pattern_param = None
self.current_pattern_params = None
self.current_pattern_index = 0
def search_device(self):
"""搜索可用设备"""
available_devices = self.lUniTAP.get_list_of_available_devices()
return available_devices if available_devices else []
def open(self, device_name):
"""打开设备"""
temp_dev = None
try:
if self.dev is not None or self.status:
self._force_cleanup()
device_id = int(device_name.split(":")[0])
temp_dev = self.lUniTAP.open(device_id)
try:
self.role = temp_dev.select_role(UniTAP.dev.UCD323.HDMISource)
self.dev = temp_dev
except Exception as role_error:
self._close_device_object(temp_dev)
raise role_error
self.timing_manager = self.role.hdtx.pg.timing_manager
self.color_mode = UniTAP.ColorInfo()
self.status = True
return True
except Exception as e:
self._force_cleanup()
return False
def close(self):
"""关闭设备"""
try:
if self.dev:
self._close_device_object(self.dev)
self.dev = None
self.role = None
self.status = False
self.timing_manager = None
self.current_timing = None
self.current_pattern = None
self.current_pattern_param = None
self.current_pattern_params = None
self.current_pattern_index = 0
self.lUniTAP = None
for i in range(3):
gc.collect()
time.sleep(2.0)
self.lUniTAP = UniTAP.TsiLib()
return True
except Exception as e:
self.dev = None
self.role = None
self.status = False
self.timing_manager = None
self.current_timing = None
self.current_pattern = None
self.current_pattern_param = None
self.current_pattern_params = None
self.current_pattern_index = 0
try:
self.lUniTAP = None
gc.collect()
time.sleep(2.0)
self.lUniTAP = UniTAP.TsiLib()
except Exception as init_error:
pass
return False
def _close_device_object(self, dev_obj):
"""显式关闭设备对象"""
try:
if dev_obj is None:
return
if self.lUniTAP and hasattr(self.lUniTAP, "close"):
try:
self.lUniTAP.close(dev_obj)
except Exception as e:
pass
dev_obj = None
gc.collect()
time.sleep(1.0)
except Exception as e:
pass
def _force_cleanup(self):
"""强制清理所有状态"""
try:
if self.dev:
self._close_device_object(self.dev)
self.dev = None
self.role = None
self.status = False
self.timing_manager = None
self.current_timing = None
self.current_pattern = None
self.current_pattern_param = None
self.current_pattern_params = None
self.current_pattern_index = 0
except Exception as e:
pass
def _cleanup(self):
"""清理设备资源"""
try:
if self.dev:
self._close_device_object(self.dev)
self.dev = None
if hasattr(self.lUniTAP, "cleanup"):
self.lUniTAP.cleanup()
except Exception as e:
pass
def set_ucd_params(self, config):
"""设置UCD323参数"""
self.config = config
test_type = self.config.current_test_type
if test_type == "hdr_movie":
color_format_str = self.config.current_test_types[test_type]["color_format"]
color_format = UCDEnum.ColorInfo.get_color_format(color_format_str)
if color_format:
self.color_mode.color_format = color_format
else:
return False
else:
color_format = self.config.current_test_types[test_type]["color_format"]
bpc = self.config.current_test_types[test_type]["bpc"]
colorimetry = self.config.current_test_types[test_type]["colorimetry"]
if not self.set_color_mode(color_format, bpc, colorimetry):
return False
timing_str = self.config.current_test_types[test_type]["timing"]
self.set_timing_from_string(timing_str)
self.current_pattern_index = 0
pattern_mode = self.config.current_pattern["pattern_mode"]
pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern_mode)
if pattern is None:
return False
self.current_pattern = pattern
self.current_pattern_params = self.config.current_pattern["pattern_params"]
return True
def run(self):
"""运行设备"""
self.apply_video_mode()
self.apply_pattern()
self.role.hdtx.pg.apply()
return True
def set_color_mode(self, cf, bpc, cm):
"""设置颜色模式"""
current_dynamic_range = None
current_transfer_characteristic = None
if hasattr(self.color_mode, "dynamic_range"):
current_dynamic_range = self.color_mode.dynamic_range
if hasattr(self.color_mode, "transfer_characteristic"):
current_transfer_characteristic = self.color_mode.transfer_characteristic
color_format = UCDEnum.ColorInfo.get_color_format(cf)
if color_format is None:
return False
if not isinstance(bpc, int) or bpc <= 0:
return False
colorimetry = UCDEnum.ColorInfo.get_colorimetry(cm)
if colorimetry is None:
return False
self.color_mode.color_format = color_format
self.color_mode.bpc = bpc
self.color_mode.colorimetry = colorimetry
if current_dynamic_range is not None:
self.color_mode.dynamic_range = current_dynamic_range
if current_transfer_characteristic is not None:
self.color_mode.transfer_characteristic = current_transfer_characteristic
return True
def apply_video_mode(self):
"""应用当前colormode和timing"""
if self.current_timing:
self.set_video_mode()
return True
return False
def set_video_mode(self):
"""设置视频模式"""
video_mode = UniTAP.VideoMode(
timing=self.current_timing, color_info=self.color_mode
)
self.role.hdtx.pg.set_vm(vm=video_mode)
return True
def set_pattern(self, pattern, pattern_params=None):
"""设置pattern"""
if self.current_timing:
if (
pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.WhiteVStrips
or pattern
== UCDEnum.VideoPatternInfo.VideoPatternParams.GradientRGBStripes
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.MotionPattern
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SquareWindow
and pattern_params
):
self.set_pattern_params(pattern, pattern_params)
return True
return False
def set_next_pattern(self):
"""设置下一个pattern"""
if self.current_pattern_index < len(self.current_pattern_params):
p = self.current_pattern_params[self.current_pattern_index]
self.set_pattern(self.current_pattern, p)
self.current_pattern_index += 1
else:
error_msg = (
f"No more patterns to set. (已设置 {self.current_pattern_index} 个图案)"
)
raise IndexError(error_msg)
def set_pattern_params(self, pattern, pattern_params):
"""设置pattern参数"""
if pattern:
if pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor:
self.current_pattern_param = UniTAP.SolidColorParams(
first=pattern_params[0],
second=pattern_params[1],
third=pattern_params[2],
)
return True
return False
def apply_pattern(self):
"""应用当前pattern"""
if self.current_pattern:
self.role.hdtx.pg.set_pattern(self.current_pattern)
if self.current_pattern_param:
self.role.hdtx.pg.set_pattern_params(self.current_pattern_param)
return True
return False
def search_timing(self, width, height, refresh_rate, resolution_type=None):
"""根据分辨率参数搜索合适的timing"""
if resolution_type:
resolution_type = resolution_type.lower()
standard = None
if resolution_type == "dmt":
standard = UniTAP.common.timing.Timing.Standard.SD_DMT
elif resolution_type == "cta":
standard = UniTAP.common.timing.Timing.Standard.SD_CTA
elif resolution_type == "cvt":
standard = UniTAP.common.timing.Timing.Standard.SD_CVT
timing = self.timing_manager.search(
h_active=width,
v_active=height,
f_rate=int(refresh_rate) * 1000,
standard=standard,
)
if timing:
return timing
else:
for res_type in ["dmt", "cta", "cvt", "ovt"]:
result = self.search_timing(width, height, refresh_rate, res_type)
if result:
return result
return None
def parse_formatted_timing(self, timing_str):
"""解析格式化的timing字符串"""
if not isinstance(timing_str, str):
raise ValueError("timing_str 必须是字符串")
s = " ".join(timing_str.strip().split())
s = s.replace(" x", "x").replace("x ", "x")
parts = s.split(" ", 1)
if len(parts) < 2:
raise ValueError(f"无法解析timing: {timing_str}")
type_str = parts[0].strip().upper()
rest = parts[1].strip()
if "@" not in rest:
raise ValueError(f"无法解析timing(缺少 @): {timing_str}")
left, right = [p.strip() for p in rest.split("@", 1)]
if "x" not in left:
raise ValueError(f"无法解析分辨率(缺少 x): {timing_str}")
wh = left.split("x")
if len(wh) != 2:
raise ValueError(f"无法解析分辨率: {timing_str}")
try:
width = int(wh[0])
height = int(wh[1])
except Exception:
raise ValueError(f"分辨率数字解析失败: {timing_str}")
hz_str = right.replace("Hz", "").replace("HZ", "").strip()
try:
refresh_rate = float(hz_str)
except Exception:
raise ValueError(f"刷新率解析失败: {timing_str}")
rtype_map = {
"DMT": "dmt",
"CTA": "cta",
"CVT": "cvt",
"OVT": "ovt",
}
if type_str not in rtype_map:
raise ValueError(f"未知的分辨率类型: {type_str}")
resolution_type = rtype_map[type_str]
def find_best_id_in_dict(res_map):
best_id, best_diff = None, float("inf")
for rid, info in res_map.items():
if info["width"] == width and info["height"] == height:
diff = abs(float(info["refresh_rate"]) - refresh_rate)
if diff < best_diff:
best_diff = diff
best_id = rid
return best_id if best_diff <= 1.0 else None
def find_best_id_in_list_map(res_map):
best_id, best_diff = None, float("inf")
for rid, infos in res_map.items():
for info in infos:
if info["width"] == width and info["height"] == height:
diff = abs(float(info["refresh_rate"]) - refresh_rate)
if diff < best_diff:
best_diff = diff
best_id = rid
return best_id if best_diff <= 1.0 else None
resolution_id = None
if resolution_type == "dmt":
resolution_id = find_best_id_in_dict(UCDEnum.TimingInfo.dmt_resolution_map)
elif resolution_type == "cta":
resolution_id = find_best_id_in_dict(UCDEnum.TimingInfo.cta_resolution_map)
elif resolution_type == "cvt":
resolution_id = find_best_id_in_list_map(
UCDEnum.TimingInfo.cvt_resolution_map
)
elif resolution_type == "ovt":
resolution_id = find_best_id_in_list_map(
UCDEnum.TimingInfo.ovt_resolution_map
)
result = {
"resolution_type": resolution_type,
"width": width,
"height": height,
"refresh_rate": refresh_rate,
"resolution_id": resolution_id,
}
return result
def set_timing_from_string(self, timing_str):
"""根据格式化timing字符串设置设备timing"""
spec = self.parse_formatted_timing(timing_str)
rtype = spec["resolution_type"]
width = spec["width"]
height = spec["height"]
fr = spec["refresh_rate"]
if rtype != "ovt":
timing = self.search_timing(width, height, fr, rtype)
if timing:
self.current_timing = timing
return True
return False
def set_timing_from_id(self, rtype, rid):
"""根据(type, id)设置设备timing"""
timing = None
if rtype.lower() == "dmt":
timing = self.timing_manager.get_dmt(rid)
elif rtype.lower() == "cta":
timing = self.timing_manager.get_cta(rid)
elif rtype.lower() == "cvt":
timing = self.timing_manager.get_cvt(rid)
else:
raise ValueError(f"不支持的分辨率类型: {rtype}")
if timing:
self.current_timing = timing
return True
else:
return False
def set_sdr_format(
self, color_space=None, gamma=None, data_range=None, bit_depth=None
):
"""设置SDR信号格式"""
def _get_colorimetry_from_color_space(color_space_name):
"""将色彩空间名称转换为UniTAP colorimetry枚举值"""
try:
colorimetry_map = {
"BT.709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
"BT.601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
"BT.2020": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB,
}
result = colorimetry_map.get(color_space_name)
return result if result else UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
except Exception as e:
return UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
def _set_gamma_transfer_characteristic(gamma_value_str):
"""设置Gamma传输特性"""
try:
gamma_value = float(gamma_value_str)
if abs(gamma_value - 2.2) < 0.1:
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
)
return True
elif abs(gamma_value - 2.4) < 0.1:
if hasattr(
UniTAP.ColorInfo.TransferCharacteristic, "TRANSFER_GAMMA24"
):
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_GAMMA24
)
return True
else:
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
)
return False
elif abs(gamma_value - 2.6) < 0.1:
if hasattr(
UniTAP.ColorInfo.TransferCharacteristic, "TRANSFER_GAMMA26"
):
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_GAMMA26
)
return True
else:
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
)
return False
else:
return False
except Exception as e:
return False
try:
if color_space:
colorimetry = _get_colorimetry_from_color_space(color_space)
if colorimetry:
self.color_mode.colorimetry = colorimetry
if gamma:
_set_gamma_transfer_characteristic(gamma)
if data_range:
if data_range == "Full":
self.color_mode.dynamic_range = (
UniTAP.ColorInfo.DynamicRange.DR_VESA
)
elif data_range == "Limited":
self.color_mode.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
if bit_depth:
bpc = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
self.color_mode.bpc = bpc
if self.current_timing:
self.set_video_mode()
return True
except Exception as e:
return False
def set_hdr_format(
self,
color_space=None,
data_range=None,
bit_depth=None,
max_cll=None,
max_fall=None,
):
"""设置HDR信号格式"""
try:
if color_space:
colorimetry = self._get_colorimetry_from_color_space(color_space)
if colorimetry:
self.color_mode.colorimetry = colorimetry
if data_range:
if data_range == "Full":
self.color_mode.dynamic_range = (
UniTAP.ColorInfo.DynamicRange.DR_VESA
)
elif data_range == "Limited":
self.color_mode.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
if bit_depth:
bpc = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
self.color_mode.bpc = bpc
if self.current_timing:
self.set_video_mode()
return True
except Exception as e:
return False
def _get_colorimetry_from_color_space(self, color_space):
"""将色彩空间字符串转换为UniTAP.ColorInfo.Colorimetry"""
colorimetry_map = {
"BT.709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
"BT.601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
"BT.2020": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB,
"DCI-P3": UniTAP.ColorInfo.Colorimetry.CM_DCI_P3,
}
return colorimetry_map.get(color_space)

117
utils/baseSerail.py Normal file
View File

@@ -0,0 +1,117 @@
# -*- coding: UTF-8 -*-
import serial
import time
import binascii
# CRC16矩阵表;
CRC_TABLE = [
0x0000, 0x1021, 0x2042, 0x3063,
0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B,
0xC18C, 0xD1AD, 0xE1CE, 0xF1EF ]
# 计算CRC16值;
def crc16(bys, bysLen):
uwCRC, ucTemp = 0xFFFF, 0x00
index = 0
while index < bysLen:
ucTemp = (uwCRC >> 0x0C) & 0xFF
uwCRC <<= 4
uwCRC ^= CRC_TABLE[ucTemp ^ ((bys[index]) >> 0x04)]
uwCRC &= 0xFFFF
ucTemp = (uwCRC >> 0x0C) & 0xFF
uwCRC <<= 4
uwCRC ^= CRC_TABLE[ucTemp ^ (bys[index] & 0x0F)]
uwCRC &= 0xFFFF
index += 1
return uwCRC
def convert_rgb16(rgb16_values):
data = bytearray()
for value in rgb16_values:
high_byte = (value & 0xFF00) >> 8
low_byte = value & 0x00FF
data.append(high_byte)
data.append(low_byte)
return data
class BaseSerial():
def __init__(self):
self.exception = None
self.ser = serial.Serial()
def open(self, port, baudrate=115200, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=3,
writeTimeout=3):
if type(port) == type(0):
self.ser.port = "COM" + str(port)
else:
self.ser.port = port
# 波特率;
self.ser.baudrate = baudrate
# 数据位;
self.ser.bytesize = bytesize
# 校验码;
self.ser.parity = parity # PARITY_NONE=0
# 停止位;
self.ser.stopbits = stopbits
# 串口读超时值
self.ser.timeout = timeout
# 串口写超时值
self.ser.write_timeout = writeTimeout
try:
self.ser.open()
return self.ser.is_open
except Exception as e:
self.exception = e
print('打开串口异常', str(e))
return False
def reOpen(self):
self.exception = None
self.ser.close()
try:
self.ser.open()
return self.ser.is_open
except Exception as e:
self.exception = e
return False
def close(self):
self.ser.close()
def write(self, cmd: bytearray):
try:
self.exception = None
writelen = self.ser.write(cmd)
self.ser.flush()
# print("writehex:" + str(binascii.b2a_hex(cmd)) + " writelen=" + str(writelen))
return True if writelen == len(cmd) else False
except Exception as e:
self.exception = e
print('写串口异常', str(e))
return False
def read(self, size=0):
try:
self.exception = None
bys = self.ser.read(1)
for i in range(0, 3):
while self.ser.in_waiting > 0:
d = self.ser.read(1)
bys = bys + d
time.sleep(0.01)
if bys.__len__() < size:
time.sleep(0.1)
if self.ser.in_waiting > 0:
d = self.ser.read(self.ser.in_waiting)
bys = bys + d
# print("readhex:" + str(binascii.hexlify(bys)) + " readlen=" + str(bys.__len__()))
return bys
except Exception as e:
self.exception = e
print('读串口异常', str(e))
return None

181
utils/caSerail.py Normal file
View File

@@ -0,0 +1,181 @@
# -*- coding: UTF-8 -*-
import re
import time
from utils.baseSerail import BaseSerial
# from baseSerail import BaseSerial
import colour
class CASerail(BaseSerial):
def __init__(self):
BaseSerial.__init__(self)
def __del__(self):
self.close()
def checkport(self):
if not self.ser.is_open:
self.reOpen()
return self.ser.is_open
def sendcmd(self, cmd:str):
self.write(cmd.encode('utf-8'))
return self.read()
'''开启通讯'''
def startCommunication(self):
self.sendcmd('COM,1\r')
'''结束通讯'''
def endCommunication(self):
self.sendcmd('COM,0\r')
'''设置用于测量的校准通道'''
def setChannel(self, channel:str):
self.sendcmd('MCH,%s\r'%(channel))
'''
设置显示模式
'''
def setDisplayMode(self, mode:int):
return self.sendcmd('MDS,%d\r'%(mode))
'''设置显示模式为XYZ'''
def set_XYZ_Display(self):
return self.sendcmd('MDS,%d\r'%(7))
'''设置显示模式为xyLv'''
def set_xyLv_Display(self):
return self.sendcmd('MDS,%d\r'%(0))
'''设置显示模式为xyLv'''
def set_all_Display(self):
return self.sendcmd('MDS,%d\r'%(0))
def set_Display(self, mode:int):
'''
0: x,y,LV
1: Tcp,duv,LV
5: u,v,LV
7: X,Y,Z
8: λd,Pe,LV
'''
return self.sendcmd('MDS,%d\r'%(mode))
'''
设置同步模式和同步频率
'''
def setSynchMode(self, mode: int, freq: int = None):
if freq == None:
return self.sendcmd('SCS,%d\r'%(mode))
else:
return self.sendcmd('SCS,%d,%d\r'%(mode,freq))
'''
设置测量速度
'''
def setMeasureSpeed(self, speed: int):
return self.sendcmd('FSC,%d\r'%(speed))
'''执行零点校准'''
def setZeroCalibration(self):
return self.sendcmd('ZRC\r')
'''读取当前显示模式下的三值'''
def readDisplay(self):
data = self.sendcmd('MES\r')
x,y,z = self.__get_display_data(data.decode('utf-8'))
return float(x), float(y), float(z)
'''获取当前显示模式值'''
def __get_display_data(self, data):
p = re.compile(r"(\D*)(\d+),P1 (.*);(.*);(.*)", re.DOTALL)
mo = p.search(data)
if mo is None:
print("无匹配正则")
pw = re.compile(r"(\D*)(\d+)", re.DOTALL)
mo = pw.search(data)
if mo is None:
# print("短匹配失败")
return None, None, None
else:
return None, None, None
else:
# print(mo.group(1), mo.group(2), mo.group(3), mo.group(4), mo.group(5))
return mo.group(3), mo.group(4), mo.group(5)
'''读取所有数据'''
def readAllDisplay(self):
data = self.sendcmd('MES,2\r')
x,y,lv,X,Y,Z = self.__get_all_display_data(data.decode('utf-8'))
return float(x), float(y), float(lv), float(X), float(Y), float(Z)
'''获取所有显示模式值'''
def __get_all_display_data(self, data):
p = re.compile(r"(\D*)(\d+),P1,(.*),(.*),(.*),(.*),(.*),(.*),(.*),(.*),(.*)", re.DOTALL)
mo = p.search(data)
if mo is None:
return None, None, None, None, None, None
else:
return mo.group(4), mo.group(5), mo.group(6), mo.group(9), mo.group(10), mo.group(11)
def xyY_to_XYZ( x, y, Y):
X = x * Y / y
Z = (1 - x - y) * Y / y
return X, Y, Z
def XYZ_to_lambda_pe(XYZ, illuminant='D65'):
xy = colour.XYZ_to_xy(XYZ)
white = colour.CCS_ILLUMINANTS['CIE 1931 2 Degree Standard Observer'][illuminant]
λd, comp, xy_intersection = colour.dominant_wavelength(xy, white)
Pe = colour.excitation_purity(xy, white)
return xy, λd, comp, Pe
def test_ca410():
print('\r\rtest ca410\r\r')
ca = CASerail()
ca.open("COM3", 19200, 7, 'E', 2)
ca.startCommunication()
ca.setSynchMode(3)
ca.setMeasureSpeed(2)
ca.setZeroCalibration()
ca.setChannel('01')
ca.set_Display(0)
xyYXYZ = ca.readAllDisplay()
print(f"xyYXYZ: {xyYXYZ}")
XYZ = xyYXYZ[3], xyYXYZ[4], xyYXYZ[5]
xy = colour.XYZ_to_xy(XYZ)
print(f"xy: {xy}")
ca.set_Display(1)
tcpduvlvXYZ = ca.readAllDisplay()
print(f"tcpduvlvXYZ: {tcpduvlvXYZ}")
XYZ = tcpduvlvXYZ[3], tcpduvlvXYZ[4], tcpduvlvXYZ[5]
xy = colour.XYZ_to_xy(XYZ)
TCP = colour.temperature.xy_to_CCT(xy, 'Hernandez 1999')
print(f"TCP: {TCP}K")
ca.set_Display(5)
uvlvXYZ = ca.readAllDisplay()
print(f"uvlvXYZ: {uvlvXYZ}")
XYZ = uvlvXYZ[3], uvlvXYZ[4], uvlvXYZ[5]
xy = colour.XYZ_to_xy(XYZ)
u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(XYZ)
print(f"u': {u_prime}, v': {v_prime}")
ca.set_Display(8)
lambdaPeXYZ = ca.readAllDisplay()
print(f"lambdaPeXYZ: {lambdaPeXYZ}")
XYZ = lambdaPeXYZ[3], lambdaPeXYZ[4], lambdaPeXYZ[5]
xy, λd, isComplementary, Pe = XYZ_to_lambda_pe(XYZ)
print(f"xy: {xy}, λd: {λd}, isComplementary: {isComplementary}, Pe: {Pe}")
ca.endCommunication()
# test_ca410()

View File

@@ -0,0 +1,288 @@
# -*- coding: UTF-8 -*-
"""
数据范围转换器
将 Full Range (0-255) 转换为 Limited Range (16-235)
使用方法:
from utils.data_range_converter import DataRangeConverter
converter = DataRangeConverter()
converted_params = converter.convert(pattern_params, "Limited")
"""
class DataRangeConverter:
"""数据范围转换器"""
def __init__(self, verbose=True):
"""
初始化转换器
Args:
verbose: 是否打印详细日志
"""
self.verbose = verbose
def convert_value(self, value):
"""
将单个 Full Range 值转换为 Limited Range
转换公式:
limited = 16 + (value / 255) × (235 - 16)
Args:
value: Full Range 值 (0-255)
Returns:
int: Limited Range 值 (16-235)
"""
# 边界值直接映射
if value == 0:
return 16
elif value == 255:
return 235
else:
# 线性映射
limited = 16 + round((value / 255.0) * (235 - 16))
# 限制范围
return max(16, min(235, limited))
def convert_rgb(self, r, g, b):
"""
转换单个 RGB 值
Args:
r, g, b: Full Range RGB 值 (0-255)
Returns:
tuple: Limited Range RGB 值 (16-235)
"""
return (self.convert_value(r), self.convert_value(g), self.convert_value(b))
def convert(self, pattern_params, data_range="Full"):
"""
转换图案参数列表
Args:
pattern_params: 图案参数列表 [[r,g,b], [r,g,b], ...]
data_range: "Full""Limited"
Returns:
list: 转换后的图案参数列表
"""
# Full Range 不需要转换
if data_range == "Full":
if self.verbose:
print("✓ 使用 Full Range (0-255),无需转换")
return pattern_params
# Limited Range 需要转换
if data_range == "Limited":
if self.verbose:
self._print_header()
converted = []
for i, rgb in enumerate(pattern_params):
# 获取原始值
r_orig, g_orig, b_orig = rgb[0], rgb[1], rgb[2]
# 转换
r_new, g_new, b_new = self.convert_rgb(r_orig, g_orig, b_orig)
# 保存
converted.append([r_new, g_new, b_new])
# 打印日志(关键值)
if self.verbose:
self._print_conversion(
i, r_orig, g_orig, b_orig, r_new, g_new, b_new
)
if self.verbose:
self._print_footer(len(pattern_params))
return converted
# 未知范围,返回原始值
else:
if self.verbose:
print(f"⚠ 未知的数据范围: {data_range},使用原始值")
return pattern_params
def _print_header(self):
"""打印转换头部信息"""
print("=" * 80)
print("【数据范围转换】Limited Range (16-235)")
print(" 转换公式: 16 + (value / 255) × (235 - 16)")
print("=" * 80)
def _print_conversion(self, index, r_orig, g_orig, b_orig, r_new, g_new, b_new):
"""
打印转换日志
策略:只打印关键值,避免刷屏
- 第一个和最后一个图案
- 0, 128, 255 等关键值
"""
# 判断是否需要打印
should_print = False
# 第一个和最后一个
if index == 0:
should_print = True
label = "黑色"
elif index == len([]) - 1: # 需要在外部判断
should_print = True
label = "白色"
# 关键 RGB 值
elif (
r_orig in [0, 128, 255]
or g_orig in [0, 128, 255]
or b_orig in [0, 128, 255]
):
should_print = True
if r_orig == 128:
label = "50%"
else:
label = ""
if should_print:
diff = abs(r_new - r_orig)
print(
f" 图案 {index+1:2d} {label:8s}: "
f"RGB({r_orig:3d},{g_orig:3d},{b_orig:3d}) → "
f"RGB({r_new:3d},{g_new:3d},{b_new:3d}) "
f"(差值: {diff:+3d})"
)
def _print_footer(self, total_count):
"""打印转换尾部信息"""
print(f"✓ 转换完成,共 {total_count} 个图案")
print("=" * 80)
def get_info(self):
"""获取转换器信息"""
return {
"name": "Data Range Converter",
"version": "1.0.0",
"full_range": "0-255",
"limited_range": "16-235",
"formula": "16 + (value / 255) × 219",
}
# ========== 便捷函数 ==========
def convert_pattern_params(pattern_params, data_range="Full", verbose=True):
"""
便捷函数:转换图案参数
Args:
pattern_params: 图案参数列表 [[r,g,b], [r,g,b], ...]
data_range: "Full""Limited"
verbose: 是否打印日志
Returns:
list: 转换后的图案参数列表
示例:
>>> from utils.data_range_converter import convert_pattern_params
>>> params = [[0,0,0], [255,255,255]]
>>> converted = convert_pattern_params(params, "Limited")
[[16,16,16], [235,235,235]]
"""
converter = DataRangeConverter(verbose=verbose)
return converter.convert(pattern_params, data_range)
def convert_single_rgb(r, g, b, data_range="Full"):
"""
便捷函数:转换单个 RGB 值
Args:
r, g, b: RGB 值 (0-255)
data_range: "Full""Limited"
Returns:
tuple: 转换后的 RGB 值
示例:
>>> from utils.data_range_converter import convert_single_rgb
>>> r, g, b = convert_single_rgb(0, 0, 0, "Limited")
(16, 16, 16)
"""
if data_range == "Full":
return (r, g, b)
converter = DataRangeConverter(verbose=False)
return converter.convert_rgb(r, g, b)
# ========== 测试代码 ==========
if __name__ == "__main__":
"""测试转换器"""
print("=" * 80)
print("数据范围转换器 - 测试")
print("=" * 80)
# 测试 1: 基本转换
print("\n[测试 1] 基本转换...")
converter = DataRangeConverter(verbose=False)
test_values = [0, 16, 64, 128, 192, 235, 255]
print(" Full Range → Limited Range:")
for v in test_values:
limited = converter.convert_value(v)
diff = limited - v
print(f" {v:3d}{limited:3d} (差值: {diff:+3d})")
# 测试 2: RGB 转换
print("\n[测试 2] RGB 转换...")
test_rgb = [
(0, 0, 0),
(128, 128, 128),
(255, 255, 255),
]
for r, g, b in test_rgb:
r_new, g_new, b_new = converter.convert_rgb(r, g, b)
print(f" RGB({r},{g},{b}) → RGB({r_new},{g_new},{b_new})")
# 测试 3: 完整转换流程
print("\n[测试 3] 完整转换流程...")
pattern_params = [
[255, 255, 255], # 100% 白
[230, 230, 230], # 90%
[204, 204, 204], # 80%
[128, 128, 128], # 50%
[0, 0, 0], # 0% 黑
]
converted = converter.convert(pattern_params, "Limited")
print("\n 对比:")
for i, (orig, conv) in enumerate(zip(pattern_params, converted)):
print(f" [{i+1}] {orig}{conv}")
# 测试 4: 便捷函数
print("\n[测试 4] 便捷函数...")
result = convert_pattern_params(
[[0, 0, 0], [255, 255, 255]], "Limited", verbose=False
)
print(f" 结果: {result}")
r, g, b = convert_single_rgb(128, 128, 128, "Limited")
print(f" RGB(128,128,128) → RGB({r},{g},{b})")
# 测试 5: 获取信息
print("\n[测试 5] 转换器信息...")
info = converter.get_info()
for key, value in info.items():
print(f" {key}: {value}")
print("\n" + "=" * 80)
print("测试完成")
print("=" * 80)

585
utils/local_dimming_test.py Normal file
View File

@@ -0,0 +1,585 @@
"""
Local Dimming 测试模块
功能:
- 生成不同百分比的白色窗口图片
- 通过 UCD 发送图片到显示器
- 自动采集 CA410 亮度数据
- 记录并导出测试结果
"""
import os
import sys
import time
import atexit
import shutil
import numpy as np
from PIL import Image, ImageDraw
import UniTAP
class LocalDimmingController:
"""Local Dimming 控制器 - 用于发送不同百分比窗口 Pattern"""
def __init__(self, ucd_controller):
"""
初始化 Local Dimming 控制器
Args:
ucd_controller: UCD323 控制器实例
"""
self.ucd = ucd_controller
# 兼容打包后的路径
if getattr(sys, "frozen", False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.getcwd()
self.temp_dir = os.path.join(base_dir, "temp_local_dimming")
# 创建临时目录
if not os.path.exists(self.temp_dir):
os.makedirs(self.temp_dir)
print(f"[LD] 创建临时目录: {self.temp_dir}")
self.cached_images = {} # 缓存已生成的图片 {(分辨率, 百分比): 文件路径}
# 注册退出时自动清理
atexit.register(self.cleanup)
print("[LD] Local Dimming 控制器已初始化")
def send_window_pattern_with_resolution(self, percentage, width, height):
"""
发送指定百分比和分辨率的白色窗口 Pattern
Args:
percentage: 窗口面积百分比 (1-100)
width: 图像宽度
height: 图像高度
Returns:
bool: 是否成功
"""
try:
# 检查设备连接状态
if not self.ucd.status:
print("[LD 错误] 设备未连接")
return False
print(f"\n[LD] 开始发送 {percentage}% 窗口 Pattern")
print(f"[LD] 使用分辨率: {width}x{height}")
# 获取 Pattern Generator 和 Audio Generator
# 兼容 UCDController仅 HDMI和 UCD323Controller多接口
if hasattr(self.ucd, 'current_interface'):
# UCD323Controller多接口支持
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
ag = self.ucd.role.dptx.ag
else:
print(f"[LD 错误] 不支持的接口类型: {interface}")
return False
else:
# UCDController仅 HDMI
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
# 先停止音频,避免蜂鸣声
try:
ag.stop_generate()
print("[LD] 已停止音频生成")
except Exception as e:
print(f"[LD 警告] 停止音频失败: {e}")
# 检查缓存
cache_key = (f"{width}x{height}", percentage)
if cache_key in self.cached_images:
image_path = self.cached_images[cache_key]
if os.path.exists(image_path):
print(f"[LD] 使用缓存图片: {image_path}")
else:
print(f"[LD] 缓存图片不存在,重新生成...")
image_path = self._generate_and_save_image(
width, height, percentage, cache_key
)
else:
print(f"[LD] 正在生成 {percentage}% 窗口图像...")
image_path = self._generate_and_save_image(
width, height, percentage, cache_key
)
# 发送图像到设备
print(f"[LD] 正在发送图像到设备...")
# 设置 ColorInfo
color_mode = UniTAP.ColorInfo()
color_mode.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color_mode.bpc = 8
color_mode.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
# 获取当前 timing
try:
current_vm = pg.get_vm()
timing = (
current_vm.timing
if current_vm and hasattr(current_vm, "timing")
else None
)
except:
timing = None
# 如果有 timing设置 VideoMode
if timing:
video_mode = UniTAP.VideoMode(timing=timing, color_info=color_mode)
pg.set_vm(vm=video_mode)
# 设置图片 Pattern
pg.set_pattern(pattern=image_path)
# 应用
pg.apply()
print(f"[LD] {percentage}% 窗口 Pattern 已发送到设备")
return True
except Exception as e:
print(f"[LD 异常] 发送 {percentage}% 窗口失败: {e}")
import traceback
traceback.print_exc()
return False
def send_window_pattern(self, percentage):
"""
发送指定百分比的白色窗口 Pattern从 GUI 获取分辨率)
Args:
percentage: 窗口面积百分比 (1-100)
Returns:
bool: 是否成功
"""
# 从设备当前 timing 获取分辨率
width, height = self.get_current_resolution()
return self.send_window_pattern_with_resolution(percentage, width, height)
def get_current_resolution(self):
"""
从设备当前 timing 获取显示器分辨率
Returns:
tuple: (width, height)
"""
try:
# 方式1从 Pattern Generator 的当前 VideoMode 获取
if hasattr(self.ucd, 'current_interface'):
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
else:
pg = None
else:
pg = self.ucd.role.hdtx.pg
if pg:
current_vm = pg.get_vm()
if current_vm and hasattr(current_vm, "timing") and current_vm.timing:
timing = current_vm.timing
if hasattr(timing, "h_active") and hasattr(timing, "v_active"):
width = timing.h_active
height = timing.v_active
print(f"[LD] 从当前 timing 获取分辨率: {width}x{height}")
return width, height
# 方式2从 current_timing 属性获取
if hasattr(self.ucd, "current_timing") and self.ucd.current_timing:
timing = self.ucd.current_timing
if hasattr(timing, "h_active") and hasattr(timing, "v_active"):
width = timing.h_active
height = timing.v_active
print(f"[LD] 从 current_timing 获取分辨率: {width}x{height}")
return width, height
except Exception as e:
print(f"[LD 警告] 获取分辨率失败: {e}")
print("[LD 警告] 使用默认分辨率 3840x2160")
return 3840, 2160
def _generate_and_save_image(self, width, height, percentage, cache_key):
"""
生成并保存窗口图像
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比
cache_key: 缓存键
Returns:
str: 图像文件路径
"""
# 生成图像
image_array = self._create_window_image(width, height, percentage)
# 保存到项目目录
filename = f"window_{width}x{height}_{percentage:03d}percent.png"
image_path = os.path.join(self.temp_dir, filename)
image = Image.fromarray(image_array, mode="RGB")
image.save(image_path, format="PNG")
# 缓存
self.cached_images[cache_key] = image_path
print(f"[LD] 图像已保存: {image_path}")
return image_path
def _create_window_image(self, width, height, percentage):
"""
创建窗口图像
黑色背景 + 居中白色矩形窗口(保持屏幕比例)
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比 (1-100)
Returns:
numpy.ndarray: RGB 图像数组 (height, width, 3)
"""
# 创建黑色背景
image = np.zeros((height, width, 3), dtype=np.uint8)
# 计算窗口尺寸(保持屏幕比例)
scale_factor = (percentage / 100.0) ** 0.5
window_width = int(width * scale_factor)
window_height = int(height * scale_factor)
# 100% 时强制全屏
if percentage == 100:
window_width = width
window_height = height
# 计算居中位置
x1 = (width - window_width) // 2
y1 = (height - window_height) // 2
x2 = x1 + window_width
y2 = y1 + window_height
# 绘制白色窗口
image[y1:y2, x1:x2] = [255, 255, 255]
print(
f"[LD] 图像生成完成: {width}x{height}, 窗口 {window_width}x{window_height}"
)
return image
def cleanup(self):
"""清理临时文件夹"""
if os.path.exists(self.temp_dir):
try:
shutil.rmtree(self.temp_dir)
print(f"[LD] 临时文件夹已删除: {self.temp_dir}")
except Exception as e:
print(f"[LD 警告] 删除临时文件夹失败: {e}")
def __del__(self):
"""析构函数:清理临时文件(备用机制)"""
try:
self.cleanup()
except:
pass
class LocalDimmingTest:
def __init__(self, ucd_controller, ca_serial, log_callback=None):
"""
初始化 Local Dimming 测试
Args:
ucd_controller: UCD323 控制器实例
ca_serial: CA410 串口实例
log_callback: 日志回调函数
"""
self.ucd = ucd_controller
self.ca = ca_serial
self.log = log_callback if log_callback else print
# 临时图片目录
self.temp_dir = self._init_temp_dir()
# 测试结果
self.test_results = []
# 测试配置
self.window_percentages = [1, 2, 5, 10, 18, 25, 50, 75, 100]
self.wait_time = 2.0 # 每次切换后等待时间(秒)
# 停止标志
self.stop_flag = False
self.log("✓ Local Dimming 测试模块已初始化")
def _init_temp_dir(self):
"""初始化临时目录"""
if getattr(sys, "frozen", False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.getcwd()
temp_dir = os.path.join(base_dir, "temp_local_dimming")
os.makedirs(temp_dir, exist_ok=True)
return temp_dir
def generate_window_image(self, width, height, percentage):
"""
生成窗口图片(黑色背景 + 居中白色矩形窗口)
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比 (1-100)
Returns:
str: 图片文件路径
"""
# 计算窗口尺寸(保持屏幕比例)
scale_factor = (percentage / 100.0) ** 0.5
window_width = int(width * scale_factor)
window_height = int(height * scale_factor)
# 100% 时强制全屏
if percentage == 100:
window_width = width
window_height = height
# 创建黑色背景
image = np.zeros((height, width, 3), dtype=np.uint8)
# 计算居中位置
x1 = (width - window_width) // 2
y1 = (height - window_height) // 2
x2 = x1 + window_width
y2 = y1 + window_height
# 绘制白色窗口
image[y1:y2, x1:x2] = [255, 255, 255]
# 保存图片
filename = f"window_{width}x{height}_{percentage:03d}percent.png"
image_path = os.path.join(self.temp_dir, filename)
pil_image = Image.fromarray(image, mode="RGB")
pil_image.save(image_path, format="PNG")
self.log(f" ✓ 图片已生成: {window_width}×{window_height} px")
return image_path
def send_image_to_ucd(self, image_path):
"""
通过 UCD 发送图片到显示器
Args:
image_path: 图片文件路径
Returns:
bool: 是否成功
"""
try:
# 获取 Pattern Generator 和 Audio Generator
# 兼容 UCDController仅 HDMI和 UCD323Controller多接口
if hasattr(self.ucd, 'current_interface'):
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
ag = self.ucd.role.dptx.ag
else:
self.log(f" ❌ 不支持的接口类型: {interface}")
return False
else:
# UCDController仅 HDMI
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
# 停止音频
try:
ag.stop_generate()
except:
pass
# 设置 ColorInfo
color_mode = UniTAP.ColorInfo()
color_mode.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color_mode.bpc = 8
color_mode.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
# 获取当前 timing
try:
current_vm = pg.get_vm()
timing = (
current_vm.timing
if current_vm and hasattr(current_vm, "timing")
else None
)
except:
timing = None
# 设置 VideoMode
if timing:
video_mode = UniTAP.VideoMode(timing=timing, color_info=color_mode)
pg.set_vm(vm=video_mode)
# 设置图片 Pattern
pg.set_pattern(pattern=image_path)
# 应用
pg.apply()
return True
except Exception as e:
self.log(f" ❌ 发送图片失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def measure_luminance(self):
"""
使用 CA410 采集亮度
Returns:
tuple: (x, y, lv, X, Y, Z) 或 None
"""
try:
if not self.ca:
self.log(" ❌ CA410 未连接")
return None
# 采集数据
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
if x is not None and y is not None and lv is not None:
self.log(f" ✓ 采集亮度: {lv:.2f} cd/m²")
return (x, y, lv, X, Y, Z)
else:
self.log(" ❌ 采集数据失败")
return None
except Exception as e:
self.log(f" ❌ 采集亮度异常: {str(e)}")
return None
def run_test(self, resolution="3840x2160"):
"""
执行完整的 Local Dimming 测试
Args:
resolution: 分辨率字符串,如 "3840x2160"
Returns:
list: 测试结果 [(百分比, x, y, lv, X, Y, Z), ...]
"""
self.log("=" * 60)
self.log("开始 Local Dimming 测试")
self.log("=" * 60)
# 重置停止标志
self.stop_flag = False
# 解析分辨率
try:
width, height = map(int, resolution.split("x"))
except:
width, height = 3840, 2160
self.log(f" ⚠️ 分辨率解析失败,使用默认值: {width}x{height}")
self.log(f" 分辨率: {width}x{height}")
self.log(f" 测试窗口: {self.window_percentages}")
self.log(f" 等待时间: {self.wait_time}")
self.log("")
self.test_results = []
for i, percentage in enumerate(self.window_percentages, start=1):
# 检查停止标志
if self.stop_flag:
self.log("⚠️ 测试已停止")
break
self.log(f"[{i}/{len(self.window_percentages)}] 测试 {percentage}% 窗口...")
# 1. 生成图片
image_path = self.generate_window_image(width, height, percentage)
# 2. 发送到 UCD
if not self.send_image_to_ucd(image_path):
self.log(f"{percentage}% 窗口发送失败,跳过")
continue
# 3. 等待稳定
self.log(f" ⏳ 等待 {self.wait_time} 秒...")
time.sleep(self.wait_time)
# 4. 采集亮度
result = self.measure_luminance()
if result:
x, y, lv, X, Y, Z = result
self.test_results.append((percentage, x, y, lv, X, Y, Z))
self.log(f"{percentage}% 窗口测试完成")
else:
self.log(f"{percentage}% 窗口采集失败")
self.log("")
self.log("=" * 60)
self.log("✅ Local Dimming 测试完成")
self.log(
f" 成功测试: {len(self.test_results)}/{len(self.window_percentages)} 个窗口"
)
self.log("=" * 60)
return self.test_results
def stop(self):
"""停止测试"""
self.stop_flag = True
self.log("⚠️ 正在停止测试...")
def get_results_summary(self):
"""获取测试结果摘要"""
if not self.test_results:
return None
luminances = [lv for _, _, _, lv, _, _, _ in self.test_results]
return {
"data_points": self.test_results,
"max_luminance": max(luminances),
"min_luminance": min(luminances),
"avg_luminance": sum(luminances) / len(luminances),
}
def cleanup(self):
"""清理临时文件"""
try:
import shutil
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
self.log(f"✓ 临时文件已清理: {self.temp_dir}")
except Exception as e:
self.log(f"⚠️ 清理临时文件失败: {e}")

601
utils/pq/pq_config.py Normal file
View File

@@ -0,0 +1,601 @@
# PQ自动化测试配置模块
import json
import copy
class PQConfig:
def __init__(self, current_test_type="screen_module", device_config={}, pattern={}):
self.default_test_types = {
"screen_module": {
"name": "屏模组性能测试",
"test_items": ["gamut", "gamma", "cct", "contrast"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
},
"sdr_movie": {
"name": "SDR Movie测试",
"test_items": ["gamut", "gamma", "cct", "contrast", "accuracy"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
},
"hdr_movie": {
"name": "HDR Movie测试",
"test_items": ["gamut", "eotf", "cct", "contrast", "accuracy"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
},
}
# 设备连接配置
self.device_config = {
"ca_com": "COM1",
"ucd_list": "0: UCD-323 [2128C209]",
"ca_channel": "0",
}
# ========== RGB Pattern 配置 ==========
self.default_pattern_rgb = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 2,
"pattern_params": [
[255, 0, 0], # 红色
[0, 255, 0], # 绿色
[0, 0, 255], # 蓝色
],
}
# ========== 灰阶 Pattern 配置 ==========
self.default_pattern_gray = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 10,
"pattern_params": [
[255, 255, 255], # 100% 白色
[230, 230, 230], # 90%
[205, 205, 205], # 80%
[179, 179, 179], # 70%
[154, 154, 154], # 60%
[128, 128, 128], # 50%
[102, 102, 102], # 40%
[78, 78, 78], # 30%
[52, 52, 52], # 20%
[26, 26, 26], # 10%
[0, 0, 0], # 0% 黑色
],
}
# ========== 色准 Pattern 配置29色 - SDR 和 HDR 通用)==========
self.default_pattern_accuracy = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 28, # 29个颜色最大索引是28
"pattern_params": [
# ========== 灰阶 (5个) ==========
[255, 255, 255], # 0: White
[230, 230, 230], # 1: Gray 80
[209, 209, 209], # 2: Gray 65
[186, 186, 186], # 3: Gray 50
[158, 158, 158], # 4: Gray 35
# ========== ColorChecker 24色 (18个) ==========
[115, 82, 66], # 5: Dark Skin
[194, 150, 130], # 6: Light Skin
[94, 122, 156], # 7: Blue Sky
[89, 107, 66], # 8: Foliage
[130, 128, 176], # 9: Blue Flower
[99, 189, 168], # 10: Bluish Green
[217, 120, 41], # 11: Orange
[74, 92, 163], # 12: Purplish Blue
[194, 84, 97], # 13: Moderate Red
[92, 61, 107], # 14: Purple
[158, 186, 64], # 15: Yellow Green
[230, 161, 46], # 16: Orange Yellow
[51, 61, 150], # 17: Blue (Legacy)
[71, 148, 71], # 18: Green (Legacy)
[176, 48, 59], # 19: Red (Legacy)
[237, 199, 33], # 20: Yellow (Legacy)
[186, 84, 145], # 21: Magenta (Legacy)
[0, 133, 163], # 22: Cyan (Legacy)
# ========== 100% 饱和色 (6个) ==========
[255, 0, 0], # 23: 100% Red
[0, 255, 0], # 24: 100% Green
[0, 0, 255], # 25: 100% Blue
[0, 255, 255], # 26: 100% Cyan
[255, 0, 255], # 27: 100% Magenta
[255, 255, 0], # 28: 100% Yellow
],
}
self.default_pattern_temp = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 146,
"pattern_params": [
[255, 255, 255],
[242, 242, 242],
[230, 230, 230],
[217, 217, 217],
[204, 204, 204],
[191, 191, 191],
[179, 179, 179],
[166, 166, 166],
[153, 153, 153],
[140, 140, 140],
[128, 128, 128],
[115, 115, 115],
[102, 102, 102],
[89, 89, 89],
[77, 77, 77],
[64, 64, 64],
[51, 51, 51],
[38, 38, 38],
[26, 26, 26],
[13, 13, 13],
[0, 0, 0],
[255, 0, 0],
[242, 0, 0],
[230, 0, 0],
[217, 0, 0],
[204, 0, 0],
[191, 0, 0],
[179, 0, 0],
[166, 0, 0],
[153, 0, 0],
[140, 0, 0],
[128, 0, 0],
[115, 0, 0],
[102, 0, 0],
[89, 0, 0],
[77, 0, 0],
[64, 0, 0],
[51, 0, 0],
[38, 0, 0],
[26, 0, 0],
[13, 0, 0],
[0, 0, 0],
[0, 255, 0],
[0, 242, 0],
[0, 230, 0],
[0, 217, 0],
[0, 204, 0],
[0, 191, 0],
[0, 179, 0],
[0, 166, 0],
[0, 153, 0],
[0, 140, 0],
[0, 128, 0],
[0, 115, 0],
[0, 102, 0],
[0, 89, 0],
[0, 77, 0],
[0, 64, 0],
[0, 51, 0],
[0, 38, 0],
[0, 26, 0],
[0, 13, 0],
[0, 0, 0],
[0, 0, 255],
[0, 0, 242],
[0, 0, 230],
[0, 0, 217],
[0, 0, 204],
[0, 0, 191],
[0, 0, 179],
[0, 0, 166],
[0, 0, 153],
[0, 0, 140],
[0, 0, 128],
[0, 0, 115],
[0, 0, 102],
[0, 0, 89],
[0, 0, 77],
[0, 0, 64],
[0, 0, 51],
[0, 0, 38],
[0, 0, 26],
[0, 0, 13],
[0, 0, 0],
[255, 255, 0],
[242, 242, 0],
[230, 230, 0],
[217, 217, 0],
[204, 204, 0],
[191, 191, 0],
[179, 179, 0],
[166, 166, 0],
[153, 153, 0],
[140, 140, 0],
[128, 128, 0],
[115, 115, 0],
[102, 102, 0],
[89, 89, 0],
[77, 77, 0],
[64, 64, 0],
[51, 51, 0],
[38, 38, 0],
[26, 26, 0],
[13, 13, 0],
[0, 0, 0],
[0, 255, 255],
[0, 242, 242],
[0, 230, 230],
[0, 217, 217],
[0, 204, 204],
[0, 191, 191],
[0, 179, 179],
[0, 166, 166],
[0, 153, 153],
[0, 140, 140],
[0, 128, 128],
[0, 115, 115],
[0, 102, 102],
[0, 89, 89],
[0, 77, 77],
[0, 64, 64],
[0, 51, 51],
[0, 38, 38],
[0, 26, 26],
[0, 13, 13],
[0, 0, 0],
[255, 0, 255],
[242, 0, 242],
[230, 0, 230],
[217, 0, 217],
[204, 0, 204],
[191, 0, 191],
[179, 0, 179],
[166, 0, 166],
[153, 0, 153],
[140, 0, 140],
[128, 0, 128],
[115, 0, 115],
[102, 0, 102],
[89, 0, 89],
[77, 0, 77],
[64, 0, 64],
[51, 0, 51],
[38, 0, 38],
[26, 0, 26],
[13, 0, 13],
[0, 0, 0],
]
}
# 自定义图案
self.custom_pattern = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 0,
"pattern_params": [],
}
self.current_test_types = self.default_test_types
self.current_test_type = current_test_type
self.current_pattern = self.default_pattern_rgb
# ========== 获取临时配置(用于 Full/Limited 转换)==========
def get_temp_config_with_converted_params(self, mode, converted_params):
"""
创建一个临时配置对象,包含转换后的 pattern 参数
Args:
mode: "rgb" | "gray" | "accuracy"
converted_params: 转换后的参数列表Full 或 Limited Range
Returns:
PQConfig: 临时配置对象(深拷贝,不影响原始配置)
"""
# 1. 深拷贝整个配置对象
temp_config = copy.deepcopy(self)
# 2. 设置正确的 pattern 模式
if mode == "rgb":
temp_config.current_pattern = copy.deepcopy(self.default_pattern_rgb)
elif mode == "gray":
temp_config.current_pattern = copy.deepcopy(self.default_pattern_gray)
elif mode == "accuracy":
temp_config.current_pattern = copy.deepcopy(self.default_pattern_accuracy)
# 3. 替换为转换后的参数
temp_config.current_pattern["pattern_params"] = converted_params
return temp_config
def to_dict(self):
"""将配置转换为字典格式"""
return {
"current_test_type": self.current_test_type,
"test_types": self.current_test_types,
"device_config": self.device_config,
"default_pattern_rgb": self.default_pattern_rgb,
"default_pattern_gray": self.default_pattern_gray,
"default_pattern_accuracy": self.default_pattern_accuracy,
"custom_pattern": self.custom_pattern,
}
def from_dict(self, config_dict):
"""从字典加载配置"""
self.current_test_type = config_dict.get("current_test_type", "screen_module")
self.current_test_types = config_dict.get("test_types", self.current_test_types)
self.device_config = config_dict.get("device_config", self.device_config)
self.default_pattern_rgb = config_dict.get(
"default_pattern_rgb", self.default_pattern_rgb
)
self.default_pattern_gray = config_dict.get(
"default_pattern_gray", self.default_pattern_gray
)
# ========== ✅ 强制使用新的 29色配置 ==========
loaded_accuracy = config_dict.get("default_pattern_accuracy", None)
# 检查加载的配置是否是旧的 10色
if loaded_accuracy and len(loaded_accuracy.get("pattern_params", [])) != 29:
print(
f"⚠️ 检测到旧的配置({len(loaded_accuracy.get('pattern_params', []))}色),强制使用新的 29色配置"
)
# 使用 __init__ 中定义的新配置
self.default_pattern_accuracy = self.default_pattern_accuracy
else:
self.default_pattern_accuracy = config_dict.get(
"default_pattern_accuracy", self.default_pattern_accuracy
)
# ==========================================
self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern)
def save_to_file(self, filename):
"""将配置保存到文件"""
with open(filename, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=4, ensure_ascii=False)
def set_current_test_type(self, test_type):
"""设置当前测试类型"""
if test_type in self.current_test_types:
self.current_test_type = test_type
return True
return False
def set_current_test_items(self, test_items):
"""设置当前测试类型的测试项"""
if self.current_test_type in self.current_test_types:
self.current_test_types[self.current_test_type]["test_items"] = test_items
return True
return False
def set_current_timing(self, timing):
if self.current_test_type in self.current_test_types:
self.current_test_types[self.current_test_type]["timing"] = timing
return True
return False
def set_device_config(self, ca_com, ucd_list, ca_channel):
"""设置设备连接配置"""
self.device_config["ca_com"] = ca_com
self.device_config["ucd_list"] = ucd_list
self.device_config["ca_channel"] = ca_channel
return True
def set_current_pattern(self, mode):
"""设置当前模式的测试图案"""
if mode == "rgb":
self.current_pattern = self.default_pattern_rgb
elif mode == "gray":
self.current_pattern = self.default_pattern_gray
elif mode == "accuracy": # ✅ 色准模式SDR 和 HDR 通用 29色
self.current_pattern = self.default_pattern_accuracy
elif mode == "custom":
# self.current_pattern = self.custom_pattern
self.current_pattern = self.default_pattern_temp
else:
return False
# 确保 measurement_max_value 是整数
if "measurement_max_value" in self.current_pattern:
value = self.current_pattern["measurement_max_value"]
if isinstance(value, str):
try:
self.current_pattern["measurement_max_value"] = int(value)
except ValueError:
self.current_pattern["measurement_max_value"] = (
len(self.current_pattern["pattern_params"]) - 1
)
return True
def set_custom_pattern(self, pattern_mode, pattern_params):
"""设置自定义模式的测试项"""
self.custom_pattern["pattern_mode"] = pattern_mode
self.custom_pattern["pattern_params"] = pattern_params
self.custom_pattern["measurement_max_value"] = len(pattern_params) - 1
return True
# ========== ✅ 获取 29色名称列表 ==========
def get_accuracy_color_names(self):
"""
获取色准测试的 29个颜色名称SDR 和 HDR 通用)
Returns:
list: 29个颜色名称
"""
return [
# 灰阶 (5个)
"White",
"Gray 80",
"Gray 65",
"Gray 50",
"Gray 35",
# ColorChecker 24色 (18个)
"Dark Skin",
"Light Skin",
"Blue Sky",
"Foliage",
"Blue Flower",
"Bluish Green",
"Orange",
"Purplish Blue",
"Moderate Red",
"Purple",
"Yellow Green",
"Orange Yellow",
"Blue (Legacy)",
"Green (Legacy)",
"Red (Legacy)",
"Yellow (Legacy)",
"Magenta (Legacy)",
"Cyan (Legacy)",
# 100% 饱和色 (6个)
"100% Red",
"100% Green",
"100% Blue",
"100% Cyan",
"100% Magenta",
"100% Yellow",
]
# ========== ✅ 获取 29色的 RGB 值 ==========
def get_accuracy_color_rgb(self):
"""
获取色准测试的 RGB 值(用于标准值计算)
Returns:
list: [(name, r, g, b), ...]
"""
names = self.get_accuracy_color_names()
rgb_values = self.default_pattern_accuracy["pattern_params"]
return [(name, r, g, b) for name, (r, g, b) in zip(names, rgb_values)]
def get_temp_pattern_names(self):
"""获取客户模板测试default_pattern_temp的固定 pattern 名称列表"""
percentages = list(range(100, -1, -5))
color_prefixes = ["W", "R", "G", "B", "Y", "C", "M"]
names = []
for prefix in color_prefixes:
for value in percentages:
names.append(f"{prefix} {value}%")
pattern_count = len(self.default_pattern_temp.get("pattern_params", []))
if pattern_count <= len(names):
return names[:pattern_count]
# 兜底:如果后续扩展了 pattern 数量,补充通用名称,避免索引越界。
for i in range(len(names), pattern_count):
names.append(f"P {i + 1}")
return names
def get_test_item_chinese_names(self, test_items):
"""获取测试项目的显示名称"""
item_names = []
for item in test_items:
if item == "gamut":
item_names.append("色域")
elif item == "gamma":
item_names.append("Gamma")
elif item == "eotf":
item_names.append("EOTF")
elif item == "cct":
item_names.append("色度一致性")
elif item == "contrast":
item_names.append("对比度")
elif item == "accuracy":
item_names.append("色准")
else:
item_names.append(item)
return item_names
def get_current_config(self):
"""返回当前测试类型相关的所有配置信息"""
if self.current_test_type not in self.current_test_types:
return {}
current_test = self.current_test_types[self.current_test_type]
config_info = {
"test_type": self.current_test_type,
"test_name": current_test.get("name", "未知测试"),
"test_items": current_test.get("test_items", []),
"test_items_chinese": self.get_test_item_chinese_names(
current_test.get("test_items", [])
),
"timing": current_test.get("timing", "DMT 1920x 1080 @ 60Hz"),
"color_format": current_test.get("color_format", "RGB"),
"bpc": current_test.get("bpc", 8),
"colorimetry": current_test.get("colorimetry", "sRGB"),
}
return config_info
# ========== ✅ 验证代码(测试完成后可删除)==========
if __name__ == "__main__":
print("=" * 60)
print("验证 pq_config.py 配置")
print("=" * 60)
config = PQConfig()
# 检查 default_pattern_accuracy
pattern_count = len(config.default_pattern_accuracy["pattern_params"])
max_value = config.default_pattern_accuracy["measurement_max_value"]
print(f"\ndefault_pattern_accuracy:")
print(f" 图案数量: {pattern_count}")
print(f" measurement_max_value: {max_value}")
if pattern_count == 29 and max_value == 28:
print("\n✅ 配置正确29色")
# 显示前 5 个图案
print("\n前5个图案:")
for i in range(5):
rgb = config.default_pattern_accuracy["pattern_params"][i]
names = config.get_accuracy_color_names()
print(f" [{i}] {names[i]:15s} RGB{rgb}")
# 显示后 5 个图案
print("\n后5个图案:")
for i in range(24, 29):
rgb = config.default_pattern_accuracy["pattern_params"][i]
names = config.get_accuracy_color_names()
print(f" [{i}] {names[i]:15s} RGB{rgb}")
# 测试 set_current_pattern
print("\n测试 set_current_pattern('accuracy'):")
config.set_current_pattern("accuracy")
current_count = len(config.current_pattern["pattern_params"])
print(f" current_pattern 图案数量: {current_count}")
if current_count == 29:
print(" ✅ set_current_pattern 工作正常")
else:
print(f" ❌ set_current_pattern 失败!只有 {current_count} 个图案")
else:
print(f"\n❌ 配置错误!")
print(f" 期望: 29 个图案, measurement_max_value=28")
print(f" 实际: {pattern_count} 个图案, measurement_max_value={max_value}")
print("\n❌ 请检查 default_pattern_accuracy 定义!")
print(" 应该包含:")
print(" - 5个灰阶")
print(" - 18个 ColorChecker 色块")
print(" - 6个 100% 饱和色")
print(" - 总计 29 个 RGB 数组")
print("=" * 60)

469
utils/pq/pq_result.py Normal file
View File

@@ -0,0 +1,469 @@
import json
import os
import datetime
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, asdict
@dataclass
class TestItemResult:
"""单个测试项的结果数据"""
item_name: str # 测试项名称 (如 "gamut", "gamma", "cct" 等)
item_display_name: str # 测试项显示名称 (如 "色域", "Gamma", "色温一致性" 等)
status: str # 测试状态: "completed", "failed", "skipped"
start_time: Optional[datetime.datetime] = None
end_time: Optional[datetime.datetime] = None
intermediate_data: Dict[str, Any] = None # 中间过程数据
final_result: Dict[str, Any] = None # 最终测试结果
error_message: Optional[str] = None # 错误信息
def __post_init__(self):
if self.intermediate_data is None:
self.intermediate_data = {}
if self.final_result is None:
self.final_result = {}
def to_dict(self):
"""转换为字典格式"""
data = asdict(self)
if self.start_time:
data["start_time"] = self.start_time.isoformat()
if self.end_time:
data["end_time"] = self.end_time.isoformat()
return data
@classmethod
def from_dict(cls, data):
"""从字典创建对象"""
if "start_time" in data and data["start_time"]:
data["start_time"] = datetime.datetime.fromisoformat(data["start_time"])
if "end_time" in data and data["end_time"]:
data["end_time"] = datetime.datetime.fromisoformat(data["end_time"])
return cls(**data)
class PQResult:
"""PQ测试结果管理类"""
def __init__(
self, test_type: str = "", test_name: str = "", output_dir: str = "results"
):
"""
初始化PQ测试结果管理器
Args:
test_type: 测试类型 ("screen_module", "sdr_movie", "hdr_movie")
test_name: 测试名称显示
output_dir: 结果输出目录
"""
self.test_id = self._generate_test_id()
self.test_type = test_type
self.test_name = test_name
self.output_dir = output_dir
# 测试基本信息
self.start_time = datetime.datetime.now()
self.end_time = None
self.status = "running" # "running", "completed", "failed", "stopped"
# 测试配置信息
self.test_config = {}
# 测试项结果
self.test_items: Dict[str, TestItemResult] = {}
# 全局测试数据
self.global_data = {
"device_info": {},
"environment_info": {},
"measurement_settings": {},
}
# 确保输出目录存在
self._ensure_output_dir()
# =============================================================================
# 存放当次测试的中间数据,方便调用
# =============================================================================
self.fix_pattern_rgb = None
self.fix_pattern_gray = None
def _generate_test_id(self) -> str:
"""生成唯一的测试ID"""
return datetime.datetime.now().strftime("PQ_%Y%m%d_%H%M%S_%f")
def _ensure_output_dir(self):
pass
def set_test_config(self, config: Dict[str, Any]):
"""设置测试配置信息"""
self.test_config = config.copy()
def set_global_data(
self,
device_info: Dict = None,
environment_info: Dict = None,
measurement_settings: Dict = None,
):
"""设置全局测试数据"""
if device_info:
self.global_data["device_info"].update(device_info)
if environment_info:
self.global_data["environment_info"].update(environment_info)
if measurement_settings:
self.global_data["measurement_settings"].update(measurement_settings)
def add_test_item(self, item_name: str, item_display_name: str) -> TestItemResult:
"""添加测试项"""
test_item = TestItemResult(
item_name=item_name, item_display_name=item_display_name, status="pending"
)
self.test_items[item_name] = test_item
return test_item
def start_test_item(self, item_name: str):
"""开始测试项"""
if item_name in self.test_items:
self.test_items[item_name].status = "running"
self.test_items[item_name].start_time = datetime.datetime.now()
def add_intermediate_data(self, item_name: str, data_key: str, data_value: Any):
"""添加测试项的中间过程数据"""
if item_name in self.test_items:
self.test_items[item_name].intermediate_data[data_key] = data_value
if data_key == "rgb":
self.fix_pattern_rgb = data_value
if data_key == "gray":
self.fix_pattern_gray = data_value
def get_intermediate_data(self, item_name: str, data_key: str) -> Any:
"""
获取测试项的中间过程数据
Args:
item_name: 测试项名称 (如 "gamut", "gamma", "cct", "shared")
data_key: 数据键名 (如 "rgb", "gray", "measurement_points")
Returns:
对应的数据,如果不存在则返回 None
Examples:
>>> pq_result.get_intermediate_data("gamut", "rgb")
[[0.64, 0.33, 100.5, ...], ...]
>>> pq_result.get_intermediate_data("shared", "gray")
[[0.31, 0.33, 50.2, ...], ...]
"""
# 方式1: 从 test_items 中获取
if item_name in self.test_items:
intermediate_data = self.test_items[item_name].intermediate_data
if data_key in intermediate_data:
return intermediate_data[data_key]
# 方式2: 从快捷属性中获取(用于 "shared" 数据)
if item_name == "shared":
if data_key == "rgb" and self.fix_pattern_rgb is not None:
return self.fix_pattern_rgb
if data_key == "gray" and self.fix_pattern_gray is not None:
return self.fix_pattern_gray
# 未找到数据
return None
def has_intermediate_data(self, item_name: str, data_key: str) -> bool:
"""
检查是否存在指定的中间数据
Args:
item_name: 测试项名称
data_key: 数据键名
Returns:
bool: 数据是否存在
"""
return self.get_intermediate_data(item_name, data_key) is not None
def get_all_intermediate_data(self, item_name: str) -> Dict[str, Any]:
"""
获取测试项的所有中间数据
Args:
item_name: 测试项名称
Returns:
包含所有中间数据的字典,如果测试项不存在则返回空字典
"""
if item_name in self.test_items:
return self.test_items[item_name].intermediate_data.copy()
if item_name == "shared":
return {
"rgb": self.fix_pattern_rgb,
"gray": self.fix_pattern_gray,
}
return {}
def clear_intermediate_data(self, item_name: str = None):
"""
清除中间数据
Args:
item_name: 测试项名称,如果为 None 则清除所有中间数据
"""
if item_name is None:
# 清除所有测试项的中间数据
for item in self.test_items.values():
item.intermediate_data.clear()
# 清除快捷属性
self.fix_pattern_rgb = None
self.fix_pattern_gray = None
elif item_name in self.test_items:
# 清除指定测试项的中间数据
self.test_items[item_name].intermediate_data.clear()
def set_test_item_result(
self,
item_name: str,
result_data: Dict[str, Any],
status: str = "completed",
error_message: str = None,
):
"""设置测试项的最终结果"""
if item_name in self.test_items:
self.test_items[item_name].final_result = result_data
self.test_items[item_name].status = status
self.test_items[item_name].end_time = datetime.datetime.now()
if error_message:
self.test_items[item_name].error_message = error_message
def complete_test(self, status: str = "completed"):
"""完成整个测试"""
self.end_time = datetime.datetime.now()
self.status = status
def get_test_summary(self) -> Dict[str, Any]:
"""获取测试摘要信息"""
completed_items = len(
[item for item in self.test_items.values() if item.status == "completed"]
)
failed_items = len(
[item for item in self.test_items.values() if item.status == "failed"]
)
total_items = len(self.test_items)
duration = None
if self.start_time and self.end_time:
duration = (self.end_time - self.start_time).total_seconds()
return {
"test_id": self.test_id,
"test_type": self.test_type,
"test_name": self.test_name,
"status": self.status,
"start_time": self.start_time.isoformat() if self.start_time else None,
"end_time": self.end_time.isoformat() if self.end_time else None,
"duration_seconds": duration,
"total_items": total_items,
"completed_items": completed_items,
"failed_items": failed_items,
}
def to_dict(self) -> Dict[str, Any]:
"""转换为完整的字典格式"""
return {
"test_summary": self.get_test_summary(),
"test_config": self.test_config,
"global_data": self.global_data,
"test_items": {
name: item.to_dict() for name, item in self.test_items.items()
},
"export_timestamp": datetime.datetime.now().isoformat(),
"format_version": "1.0",
}
def save_to_file(self, file_path: str) -> bool:
return False
def save_to_json(self, filename: str = None) -> str:
return ""
@classmethod
def load_from_json(cls, file_path: str) -> "PQResult":
"""从JSON文件加载测试结果"""
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 创建PQResult实例
test_summary = data.get("test_summary", {})
pq_result = cls(
test_type=test_summary.get("test_type", ""),
test_name=test_summary.get("test_name", ""),
)
# 恢复基本信息
pq_result.test_id = test_summary.get("test_id", pq_result.test_id)
pq_result.status = test_summary.get("status", "unknown")
if test_summary.get("start_time"):
pq_result.start_time = datetime.datetime.fromisoformat(
test_summary["start_time"]
)
if test_summary.get("end_time"):
pq_result.end_time = datetime.datetime.fromisoformat(
test_summary["end_time"]
)
# 恢复配置和全局数据
pq_result.test_config = data.get("test_config", {})
pq_result.global_data = data.get("global_data", {})
# 恢复测试项
test_items_data = data.get("test_items", {})
for item_name, item_data in test_items_data.items():
pq_result.test_items[item_name] = TestItemResult.from_dict(item_data)
return pq_result
def export_item_data(self, item_name: str, export_format: str = "json") -> str:
"""
导出单个测试项的数据
Args:
item_name: 测试项名称
export_format: 导出格式 ("json", "csv")
Returns:
导出文件路径
"""
if item_name not in self.test_items:
raise ValueError(f"测试项 {item_name} 不存在")
item = self.test_items[item_name]
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
if export_format == "json":
filename = f"{self.test_id}_{item_name}_{timestamp}.json"
if self.test_type:
file_path = os.path.join(self.output_dir, self.test_type, filename)
else:
file_path = os.path.join(self.output_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(item.to_dict(), f, ensure_ascii=False, indent=2)
elif export_format == "csv":
import csv
filename = f"{self.test_id}_{item_name}_{timestamp}.csv"
if self.test_type:
file_path = os.path.join(self.output_dir, self.test_type, filename)
else:
file_path = os.path.join(self.output_dir, filename)
with open(file_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
# 写入基本信息
writer.writerow(["测试项", item.item_display_name])
writer.writerow(["状态", item.status])
writer.writerow(
["开始时间", item.start_time.isoformat() if item.start_time else ""]
)
writer.writerow(
["结束时间", item.end_time.isoformat() if item.end_time else ""]
)
writer.writerow([])
# 写入最终结果数据
writer.writerow(["最终结果"])
for key, value in item.final_result.items():
writer.writerow([key, str(value)])
else:
raise ValueError(f"不支持的导出格式: {export_format}")
return file_path
def get_progress_info(self) -> Dict[str, Any]:
"""获取测试进度信息"""
total_items = len(self.test_items)
completed_items = len(
[
item
for item in self.test_items.values()
if item.status in ["completed", "failed"]
]
)
running_items = len(
[item for item in self.test_items.values() if item.status == "running"]
)
progress_percentage = (
(completed_items / total_items * 100) if total_items > 0 else 0
)
return {
"total_items": total_items,
"completed_items": completed_items,
"running_items": running_items,
"pending_items": total_items - completed_items - running_items,
"progress_percentage": progress_percentage,
"current_status": self.status,
}
# 使用示例和工具函数
def create_pq_result_from_config(config: Dict[str, Any]) -> PQResult:
"""根据配置创建PQResult实例"""
test_type = config.get("test_type", "")
test_name = config.get("test_name", "")
pq_result = PQResult(test_type=test_type, test_name=test_name)
pq_result.set_test_config(config)
# 添加测试项
test_items = config.get("test_items", [])
test_items_names = config.get("test_items_chinese", [])
for i, item in enumerate(test_items):
display_name = test_items_names[i] if i < len(test_items_names) else item
pq_result.add_test_item(item, display_name)
return pq_result
if __name__ == "__main__":
# 测试代码
print("PQResult类测试")
# 创建测试实例
pq_result = PQResult("screen_module", "屏模组性能测试")
# 设置配置
config = {
"test_type": "screen_module",
"test_name": "屏模组性能测试",
"test_items": ["gamut", "gamma", "cct"],
"test_items_chinese": ["色域", "Gamma", "色温一致性"],
}
pq_result.set_test_config(config)
# 添加测试项
pq_result.add_test_item("gamut", "色域")
pq_result.add_test_item("gamma", "Gamma")
pq_result.add_test_item("cct", "色温一致性")
# 模拟测试过程
pq_result.start_test_item("gamut")
pq_result.add_intermediate_data(
"gamut", "measurement_points", [[0.64, 0.33], [0.30, 0.60]]
)
pq_result.set_test_item_result("gamut", {"coverage": 95.2, "accuracy": 98.5})
# 完成测试
pq_result.complete_test()
print(f"测试结果已保存到: {pq_result.save_to_json()}")
print("测试摘要:", pq_result.get_test_summary())

423
utils/tvSerail.py Normal file
View File

@@ -0,0 +1,423 @@
# -*- coding: UTF-8 -*-
import zlib
from xmlrpc.client import Boolean
from utils.baseSerail import BaseSerial
import binascii
import utils.baseSerail as baseSerail
# 包头码(包引导码)
PHeader = {
"TV_Debug": 0xAA,
"TV_Return": 0xAB,
"TV_Panel_Debug": 0xAC,
"TV_Panel_Return": 0xAD,
"TV_Debug_Other": 0xAE,
"TV_Other_Return": 0xAF,
}
# 命令结果码;
RCode = {
"RC_OK": 0x0A, # 命令执行通过;
"RC_ERR": 0x0E, # 命令错误或无法执行;
"RC_LOSE": 0x0F, # 命令丢失(链路层出错);
}
# 命令的封装与解析;
class TvParse():
def __init__(self):
# 头引导码(默认1字节,0x86命令二字节);
self.Header = [PHeader['TV_Debug']]
# 包长(默认1字节,0x86长度二字节);
self.Length = [0x00]
# 命令码,1字节(int类型);
self.Command = [0x00]
# 子命令参数;
self.SubCommand = []
# 数据,bytearray格式;
self.Data = []
# crch,1字节;
self.CRCH = 0xFF
# crcl,1字节;
self.CRCL = 0xFF
# 是否是特殊命令;
self.FEFlag = False
# 是否是多参数命令;(同一个Command有多个含义的参数)
self.isMultipleParams = False
# 正确执行后的结果值bytearray格式的二维数组;
self.successData = []
def parseCommand(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), isMultipleParams=False, FEFlag=False):
self.Header[0] = head
self.Command = command
self.SubCommand = subCommand
self.Data = data
self.FEFlag = FEFlag
self.isMultipleParams = isMultipleParams
if FEFlag:
self.Header.append(0xFE)
# 注意4 = crch + crcl + lenh + lenl
length = 4 + self.Header.__len__() + self.Data.__len__() + self.Command.__len__() + self.SubCommand.__len__()
# 高字节;
self.Length[0] = length >> 8
# 低字节;
self.Length.append(length & 0xFF)
else:
# 注意3 = crch + crcl + len
self.Length[0] = 3 + self.Header.__len__() + self.Data.__len__() + self.Command.__len__() + self.SubCommand.__len__()
# 生成package;
package = bytearray(self.Header + self.Length + self.Command + self.SubCommand) + self.Data
self.calculateCRC(package)
# 形成最终的命令;
package = bytearray(self.Header + self.Length + self.Command + self.SubCommand) + self.Data + bytearray([self.CRCH, self.CRCL])
# 打印最终命令的十六进制形式,每两个字符之间加一个空格
# hex_string = ' '.join(f'{byte:02X}' for byte in package)
# print("最终命令的十六进制形式:", hex_string)
return package
def calculateCRC(self, data: bytearray):
crc = baseSerail.crc16(data, data.__len__())
# 高字节;
self.CRCH = crc >> 8
# 低字节;
self.CRCL = crc & 0xFF
def parseCommandFc(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), isMultipleParams=False):
self.Header[0] = head
self.Command = command
self.SubCommand = subCommand
self.Data = data
self.isMultipleParams = isMultipleParams
self.Length[0] = 3 + self.Header.__len__() + self.Data.__len__() + self.Command.__len__() + self.SubCommand.__len__()
package = bytearray(self.Header + self.Length + self.Command + self.SubCommand) + self.Data
self.calculateCRC(package)
package = bytearray(self.Header + self.Length + self.Command + self.SubCommand) + self.Data + bytearray([self.CRCH]) + bytearray([self.CRCL])
return package
# 成功返回AB 08 FC 08 EC 80 CR1 CR2
# 失败返回AB 08 FC 08 EC 00 CR1 CR2
def parseResultFc(self,data):
data = bytearray(data)
return False
def parseResult(self, data):
data = bytearray(data)
if data.__len__() < 5:
return False
retCode = 0
if self.Header[0] == PHeader['TV_Debug']:
retCode = PHeader['TV_Return']
elif self.Header[0] == PHeader['TV_Panel_Debug']:
retCode = PHeader['TV_Panel_Return']
elif self.Header[0] == PHeader['TV_Debug_Other']:
retCode = PHeader['TV_Other_Return']
if retCode != data[0]:
return False
package = []
tooken_len = 0
return True
def parseString(self, data):
data = bytearray(data)
if data.__len__() < 5:
return False
retCode = 0
if self.Header[0] == PHeader['TV_Debug']:
retCode = PHeader['TV_Return']
elif self.Header[0] == PHeader['TV_Panel_Debug']:
retCode = PHeader['TV_Panel_Return']
elif self.Header[0] == PHeader['TV_Debug_Other']:
retCode = PHeader['TV_Other_Return']
if retCode != data[0]:
return False
package = []
tooken_len = 0
while True:
if tooken_len >= data.__len__():
break
if self.FEFlag:
package_len = data[tooken_len + 1] << 8 + data[tooken_len + 2]
else:
package_len = data[tooken_len + 1]
package = data[tooken_len:tooken_len + package_len]
if package[0] != retCode:
print('Incorrect package head!\n')
return False
crc = crc16(package, package_len - 2)
CRCH = crc >> 8
CRCL = crc & 0xFF
if CRCH != package[-2] and CRCL != package[-1]:
return False
if tooken_len == 0:
if package[2] != RCode['RC_OK']:
return False
else:
if self.Command[0] == 0xFC:
pass
else:
if package[2] - 1 != self.Command[0]:
return False
if package[2] == 0xFE:
self.successData.append(package[5:-2])
else:
self.successData.append(package[3:-2])
tooken_len += package_len
# print('successData:', binascii.b2a_hex(bytearray(self.Command)), self.successData)
return self.successData
class tvSerial(BaseSerial):
def __init__(self):
BaseSerial.__init__(self)
def __del__(self):
self.close()
def checkport(self):
if not self.ser.is_open:
self.reOpen()
return self.ser.is_open
def sendcmd(self, cmd: list[int]):
self.write(bytearray(cmd))
return self.read()
'''协议模式发送命令'''
def sendcmdEx(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), FEFlag: Boolean = False, returnParam: Boolean = False):
cmd = TvParse()
package = cmd.parseCommand(head, command, subCommand, data, returnParam, FEFlag)
if self.write(package):
package = self.read()
return cmd.parseResult(package)
return False
def sendcmdEx_string_return(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), FEFlag: Boolean = False, returnParam: Boolean = False):
cmd = TvParse()
package = cmd.parseCommand(head, command, subCommand, data, returnParam, FEFlag)
if self.write(package):
package = self.read()
return cmd.parseString(package)
return ""
def sendcmdFc(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), FEFlag: Boolean = False, returnParam: Boolean = False):
cmd = TvParse()
package = cmd.parseCommand(head, command, subCommand, data, returnParam, FEFlag)
if self.write(package):
package = self.read()
return cmd.parseResult(package)
return False
def gen_zzip(selfe,gm_file,save_file):
with open(gm_file, 'rb') as f:
data = f.read()
compressed_data = zlib.compress(data)
with open(save_file, 'wb') as f:
f.write(compressed_data)
'''发送pattern'''
def send_parttern(self, rgb: list[int]):
cmd = TvParse()
package = cmd.parseCommand(0xAA, [0x28], [], bytearray(rgb))
if self.write(package):
package = self.read()
return cmd.parseResult(package)
return False
# '''发送10bit-pattern'''
# def send_10_bit_parttern(self, rgb: list[int]):
# cmd = MokaParse()
# rgb16 = convert_rgb16(rgb)
# package = cmd.parseCommandFc(0xAA, [0xFC,0x07,0x06], [0xEC,0x01,0x86,0x00], rgb16)
# if self.write(package):
# package = self.read()
# return cmd.parseResult(package)
# return False
'''发送12bit-pattern'''
def send_12_bit_parttern(self, rgb: list[int]):
cmd = TvParse()
rgb16 = baseSerail.convert_rgb16(rgb)
package = cmd.parseCommand(0xAA, [0x28], [], rgb16)
if self.write(package):
package = self.read()
return cmd.parseResult(package)
return False
'''发送gamma文件'''
def send_gamma(self, file_path: str):
fp = open(file_path, 'rb')
cmd = TvParse()
data = fp.read()
# 生成命令包
package = cmd.parseCommand(0xAA, [0xE9], [0x02], data, False, True)
# 将命令包写入临时文件
if self.write(package):
package = self.read()
print(cmd.parseResult(package))
return cmd.parseResult(package)
'''激活gamma文件'''
def send_gamma_active(self, ini_file: str):
ini_fp = open(ini_file, 'rb')
ind_data = ini_fp.read()
ini_fp.close()
# 计算crc
crc = crc16(ind_data, ind_data.__len__())
CRC_GM = [crc >> 8, crc & 0xFF]
cmd = TvParse()
package = cmd.parseCommand(0xAA, [0x99], [0x06], bytearray(CRC_GM))
if self.write(package):
package = self.read()
print("---------------------")
print(cmd.parseResult(package))
return cmd.parseResult(package)
'''激活gamma文件-12bit版本'''
def send_gamma_active_12bit(self, ini_file: str):
ini_fp = open(ini_file, 'rb')
ind_data = ini_fp.read()
ini_fp.close()
# 计算crc
crc = baseSerail.crc16(ind_data, ind_data.__len__())
CRC_GM = [crc >> 8, crc & 0xFF]
cmd = TvParse()
package = cmd.parseCommandFc(0xAA, [0xFC,0x07], [0x0E], bytearray(CRC_GM))
if self.write(package):
package = self.read()
print("---------------------")
print(cmd.parseResult(package))
return cmd.parseResult(package)
'''进工厂模式'''
def enterFactory(self):
return self.sendcmdEx(0xAA, [0x10], [0x01])
'''白平衡初始化'''
def initWhiteBalance(self):
return self.sendcmdEx(0xAA, [0x16], [0x01])
'''关闭localdimming'''
def closeLocaldimming(self):
return self.sendcmdEx(0xAA, [0x9F,0x07], [0x00])
'''打开内置pattern'''
def openBuiltInPattern(self):
return self.sendcmdEx(0xAA, [0x27], [0x01])
'''关闭内置pattern'''
def closeBuiltInPattern(self):
return self.sendcmdEx(0xAA, [0x27], [0x00])
'''切换标准色温'''
def switchStdColorTemperature(self):
return self.sendcmdEx(0xAA, [0x31], [0x01])
'''切换冷色温'''
def switchColdColorTemperature(self):
return self.sendcmdEx(0xAA, [0x31], [0x02])
'''切换暖色温'''
def switchWarmColorTemperature(self):
return self.sendcmdEx(0xAA, [0x31], [0x03])
'''切换暖2色温'''
def switchWarm2ColorTemperature(self):
return self.sendcmdEx(0xAA, [0x31], [0x04])
'''初始化gamma'''
def initGamma(self):
return self.sendcmdEx(0xAA, [0x9F,0x09], [0x01])
'''进老化模式'''
def enterAgingMode(self):
return self.sendcmdEx(0xAA, [0x13], [0x01])
'''退出老化模式'''
def exitAgingMode(self):
return self.sendcmdEx(0xAA, [0x13], [0x00])
'''软件版本查询'''
def sendSoftwareVersionQuery(self):
return self.sendcmdEx_string_return(0xAA, [0x57], [0x00])
'''PID查询'''
def sendPIDQuery(self):
return self.sendcmdEx_string_return(0xAA, [0x84], [0x00])
def switchHDMI1(self):
"""切换到HDMI1信源"""
return self.sendcmdEx(0xAA, [0x25], [0x01])
def switchHDMI2(self):
"""切换到HDMI2信源"""
return self.sendcmdEx(0xAA, [0x25], [0x02])
def switchHDMI3(self):
"""切换到HDMI3信源"""
return self.sendcmdEx(0xAA, [0x25], [0x03])
def switchVGA(self):
"""切换到VGA信源"""
return self.sendcmdEx(0xAA, [0x24], [0x01])
def switchAV1(self):
"""切换到AV1信源"""
return self.sendcmdEx(0xAA, [0x22], [0x01])
def switchAV2(self):
"""切换到AV2信源"""
return self.sendcmdEx(0xAA, [0x22], [0x02])
def switchAV3(self):
"""切换到AV3信源"""
return self.sendcmdEx(0xAA, [0x22], [0x03])
def switchDisplayMode(self, mode="natural"):
"""切换图像预设模式 """
if mode == "natural":
return self.sendcmdEx(0xAA, [0x30], [0x01])
elif mode == "soft":
return self.sendcmdEx(0xAA, [0x30], [0x02])
elif mode == "bright":
return self.sendcmdEx(0xAA, [0x30], [0x03])
elif mode == "personal":
return self.sendcmdEx(0xAA, [0x30], [0x04])
elif mode == "cinematic":
return self.sendcmdEx(0xAA, [0x30], [0x05])
else:
raise ValueError("Invalid display mode. Use 'natural', 'soft', 'bright', 'personal', or 'cinematic'.")
if __name__ == "__main__":
pass