chore: 文件夹更名 HDMI-Tool -> DLPilot(路径与文档此前已对齐)
This commit is contained in:
@@ -0,0 +1,548 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
EDID 1.4 / CEA-861 解析(纯标准库)
|
||||
================================================================
|
||||
parse_edid() 原始 EDID 字节 -> 结构化 dict(DTD/已建立/标准/CEA-VIC/数据块)
|
||||
edid_info_lines() EDID 信息卡片用的 (项目, 值) 行
|
||||
edid_field_rows()/edid_field_text() 逐字节字段解析
|
||||
hex_dump() 经典 hex + ASCII 排版
|
||||
"""
|
||||
|
||||
import math
|
||||
import struct
|
||||
|
||||
# ============================================================
|
||||
# EDID 1.4 / CEA-861 解析
|
||||
# ============================================================
|
||||
# CTA-861 VIC 1..64 常用映射(65+ 及扩展 VIC 不展开)
|
||||
VIC_TABLE = [
|
||||
'640x480@60', '720x480@60 4:3', '720x480@60 16:9', '1280x720@60', '1920x1080i@60',
|
||||
'720x480i@60 4:3', '720x480i@60 16:9', '720x240p@60 4:3', '720x240p@60 16:9',
|
||||
'1440x480i@60 4:3', '1440x480i@60 16:9', '1440x240p@60 4:3', '1440x240p@60 16:9',
|
||||
'1440x480p@60 4:3', '1440x480p@60 16:9', '1920x1080p@60', '720x576p@50 4:3', '720x576p@50 16:9',
|
||||
'1280x720p@50', '1920x1080i@50', '720x576i@50 4:3', '720x576i@50 16:9',
|
||||
'720x288p@50 4:3', '720x288p@50 16:9',
|
||||
'1440x576i@50 4:3', '1440x576i@50 16:9', '1440x288p@50 4:3', '1440x288p@50 16:9',
|
||||
'1440x576p@50 4:3', '1440x576p@50 16:9', '1920x1080p@50', '1920x1080p@24',
|
||||
'1920x1080p@25', '1920x1080p@30',
|
||||
'2880x480p@60 4:3', '2880x480p@60 16:9', '2880x576p@50 4:3', '2880x576p@50 16:9',
|
||||
'1920x1080i@50(1250)', '1920x1080i@100', '1280x720p@100',
|
||||
'720x576p@100 4:3', '720x576p@100 16:9', '720x576i@100 4:3', '720x576i@100 16:9',
|
||||
'1920x1080i@120', '1280x720p@120',
|
||||
'720x480p@120 4:3', '720x480p@120 16:9', '720x480i@120 4:3', '720x480i@120 16:9',
|
||||
'720x576p@200 4:3', '720x576p@200 16:9', '720x576i@200 4:3', '720x576i@200 16:9',
|
||||
'720x480p@240 4:3', '720x480p@240 16:9', '720x480i@240 4:3', '720x480i@240 16:9',
|
||||
'1280x720p@24', '1280x720p@25', '1280x720p@30', '1920x1080p@120', '1920x1080p@240',
|
||||
# VIC 65-88 为 3D/特殊格式, 少见不展开
|
||||
*[None] * 24,
|
||||
'2560x1080p@50', '2560x1080p@60', '2560x1080p@100', '2560x1080p@120',
|
||||
'3840x2160p@24', '3840x2160p@25', '3840x2160p@30',
|
||||
'3840x2160p@50', '3840x2160p@60', '4096x2160p@24',
|
||||
]
|
||||
|
||||
# 已建立时序位域表(bytes 35-37, 共 24 位, '-' 为保留位)
|
||||
EST_TABLE = [
|
||||
'720x400@70', '720x400@88', '640x480@60', '640x480@67', '640x480@72', '640x480@75',
|
||||
'800x600@56', '800x600@60',
|
||||
'800x600@72', '800x600@75', '832x624@75', '1024x768@87i', '1024x768@60', '1024x768@70',
|
||||
'1024x768@75', '1280x1024@75',
|
||||
'1152x870@75', '-', '-', '-', '-', '-', '-', '-',
|
||||
]
|
||||
|
||||
STD_ASPECT = {0: (10 / 16, '16:10'), 1: (3 / 4, '4:3'), 2: (4 / 5, '5:4'), 3: (9 / 16, '16:9')}
|
||||
DEPTHS = [0, 6, 8, 10, 12, 14, 16, 0]
|
||||
INTERFACES = ['未定义', 'DVI', 'HDMI-A', 'HDMI-B', 'MDDI', 'DisplayPort', '保留', '保留']
|
||||
SYNC_TYPES = ['模拟合成', '双极模拟合成', '数字合成', '数字分离']
|
||||
|
||||
# CEA-861 数据块 tag(CTA-861 标准): 1=音频 2=视频 3=厂商专用(VSDB)
|
||||
# 4=扬声器分配 5=VESA DTC 6=视频格式 7=扩展 tag 集合
|
||||
EXT_TAG_NAMES = {
|
||||
0x00: '视频能力', 0x01: '厂商扩展', 0x02: 'VESA 显示设备', 0x04: 'HDMI 4K/8K 视频格式',
|
||||
0x05: '色度学', 0x06: 'HDR 静态元数据', 0x07: 'HDR 动态元数据', 0x0C: 'YCbCr4:2:0 能力',
|
||||
0x0E: 'YCbCr4:2:0 视频格式', 0x78: 'HDMI 论坛 SCDB',
|
||||
}
|
||||
|
||||
|
||||
def parse_dtd(raw, o):
|
||||
"""解析 18 字节详细时序描述符, 返回 dict"""
|
||||
t = {}
|
||||
t['clk_khz'] = struct.unpack_from('<H', raw, o)[0] * 10 # 10kHz 单位
|
||||
t['hact'] = raw[o + 2] | ((raw[o + 4] & 0xF0) << 4)
|
||||
t['hblank'] = raw[o + 3] | ((raw[o + 4] & 0x0F) << 8)
|
||||
t['vact'] = raw[o + 5] | ((raw[o + 7] & 0xF0) << 4)
|
||||
t['vblank'] = raw[o + 6] | ((raw[o + 7] & 0x0F) << 8)
|
||||
t['hfront'] = raw[o + 8] | ((raw[o + 11] & 0xC0) << 2)
|
||||
t['hsync'] = raw[o + 9] | ((raw[o + 11] & 0x30) << 4)
|
||||
t['vfront'] = ((raw[o + 10] & 0xF0) >> 4) | ((raw[o + 11] & 0x0C) << 2)
|
||||
t['vsync'] = (raw[o + 10] & 0x0F) | ((raw[o + 11] & 0x03) << 4)
|
||||
t['him_mm'] = raw[o + 12] | ((raw[o + 14] & 0xF0) << 4)
|
||||
t['vim_mm'] = raw[o + 13] | ((raw[o + 14] & 0x0F) << 8)
|
||||
fl = raw[o + 17]
|
||||
t['interlaced'] = bool(fl & 0x80)
|
||||
t['sync_type'] = SYNC_TYPES[(fl >> 2) & 3]
|
||||
t['polarity'] = ('V+' if fl & 0x02 else 'V-') + ' ' + ('H+' if fl & 0x01 else 'H-')
|
||||
t['htotal'] = t['hact'] + t['hblank']
|
||||
t['vtotal'] = t['vact'] + t['vblank']
|
||||
t['refresh'] = round(t['clk_khz'] * 1000.0 / (t['htotal'] * t['vtotal']), 1)
|
||||
return t
|
||||
|
||||
|
||||
def _ascii(raw, o, max_len):
|
||||
out = []
|
||||
for i in range(max_len):
|
||||
c = raw[o + i]
|
||||
if c in (0x0A, 0x00):
|
||||
break
|
||||
if 32 <= c < 127:
|
||||
out.append(chr(c))
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def parse_edid(raw):
|
||||
"""解析 EDID(128 或 256 字节), 返回汇总 dict"""
|
||||
e = {'raw': raw, 'dtds': [], 'est': [], 'std': [], 'cea_video': [], 'cea_blocks': []}
|
||||
n = len(raw)
|
||||
|
||||
e['cksum_base'] = (sum(raw[0:128]) & 0xFF) == 0
|
||||
|
||||
# 厂商 ID: 3 个 5bit 压缩大写字母
|
||||
b0, b1 = raw[8], raw[9]
|
||||
e['manufacturer'] = ''.join([
|
||||
chr(((b0 & 0x7C) >> 2) + 64),
|
||||
chr((((b0 & 0x03) << 3) | ((b1 & 0xE0) >> 5)) + 64),
|
||||
chr((b1 & 0x1F) + 64)])
|
||||
e['product'] = struct.unpack_from('<H', raw, 10)[0]
|
||||
e['serial_num'] = struct.unpack_from('<I', raw, 12)[0]
|
||||
e['week'] = raw[16]
|
||||
e['year'] = 1990 + raw[17]
|
||||
e['ver'] = f'{raw[18]}.{raw[19]}'
|
||||
|
||||
if raw[20] & 0x80:
|
||||
bpc = DEPTHS[(raw[20] >> 4) & 0x7]
|
||||
e['input'] = '数字 / ' + INTERFACES[raw[20] & 0x7] + \
|
||||
(f' / {bpc}bpc' if bpc else ' / 色深未定义')
|
||||
else:
|
||||
e['input'] = '模拟 (VGA 输入)'
|
||||
|
||||
e['wcm'], e['hcm'] = raw[21], raw[22]
|
||||
e['gamma'] = 0 if raw[23] == 0xFF else (raw[23] + 100) / 100.0
|
||||
|
||||
feats = []
|
||||
if raw[24] & 0x80: feats.append('DPMS 待机')
|
||||
if raw[24] & 0x40: feats.append('DPMS 挂起')
|
||||
if raw[24] & 0x20: feats.append('DPMS 关闭')
|
||||
if raw[24] & 0x04: feats.append('sRGB 特性')
|
||||
if raw[24] & 0x02: feats.append('首选时序=原生')
|
||||
e['features'] = ', '.join(feats)
|
||||
|
||||
# 已建立时序位域
|
||||
for i in range(24):
|
||||
bit = 7 - (i % 8)
|
||||
if ((raw[35 + i // 8] >> bit) & 1) and EST_TABLE[i] != '-':
|
||||
e['est'].append(EST_TABLE[i])
|
||||
|
||||
# 标准时序(8 组 2 字节, 0x0101 = 未用)
|
||||
for i in range(38, 54, 2):
|
||||
a, b = raw[i], raw[i + 1]
|
||||
if (a == 1 and b == 1) or (a == 0 and b == 0):
|
||||
continue
|
||||
h = (a + 31) * 8
|
||||
v = round(h * STD_ASPECT[(a >> 6) & 3][0])
|
||||
e['std'].append(f'{h}x{v}@{b + 60}')
|
||||
|
||||
# 基础块 4 个 18 字节描述符区: 前两字节非 0 即 DTD, 否则是监视器描述符
|
||||
e['name'] = e['serial_text'] = e['range_text'] = ''
|
||||
for d in range(54, 126, 18):
|
||||
if raw[d] == 0 and raw[d + 1] == 0:
|
||||
tag = raw[d + 2]
|
||||
if tag == 0xFC:
|
||||
e['name'] = _ascii(raw, d + 5, 13)
|
||||
elif tag == 0xFF:
|
||||
e['serial_text'] = _ascii(raw, d + 5, 13)
|
||||
elif tag == 0xFD:
|
||||
e['range_text'] = (f"V {raw[d+5]}~{raw[d+6]}Hz, H {raw[d+7]}~{raw[d+8]}kHz, "
|
||||
f"像素时钟上限 {raw[d+9] * 10}MHz")
|
||||
else:
|
||||
e['dtds'].append(parse_dtd(raw, d))
|
||||
|
||||
# CEA-861 扩展块
|
||||
e['has_ext'] = raw[126] > 0 and n >= 256
|
||||
e['has_cea'] = False
|
||||
e['cea_rev'] = 0
|
||||
e['cea_caps'] = ''
|
||||
e['cea_phys'] = ''
|
||||
e['cksum_ext'] = False
|
||||
if e['has_ext']:
|
||||
e['cksum_ext'] = (sum(raw[128:256]) & 0xFF) == 0
|
||||
if raw[128] == 0x02:
|
||||
e['has_cea'] = True
|
||||
e['cea_rev'] = raw[129]
|
||||
caps = []
|
||||
if raw[131] & 0x80: caps.append('欠扫描')
|
||||
if raw[131] & 0x40: caps.append('基本音频')
|
||||
if raw[131] & 0x20: caps.append('YCbCr4:4:4')
|
||||
if raw[131] & 0x10: caps.append('YCbCr4:2:2')
|
||||
e['cea_caps'] = ', '.join(caps)
|
||||
|
||||
dtd_off = raw[130]
|
||||
p = 132
|
||||
while dtd_off > 4 and (p - 128) < dtd_off and p + 1 < 256:
|
||||
tag = (raw[p] >> 5) & 7
|
||||
length = raw[p] & 0x1F
|
||||
if length == 0:
|
||||
break
|
||||
data = raw[p + 1:p + 1 + length]
|
||||
if tag == 2: # 视频数据块: VIC 列表
|
||||
for k in range(length):
|
||||
vic = data[k] & 0x7F
|
||||
native = ' (原生)' if data[k] & 0x80 else ''
|
||||
m = (VIC_TABLE[vic - 1] if 1 <= vic <= len(VIC_TABLE) else None) \
|
||||
or f'VIC#{vic}'
|
||||
e['cea_video'].append(m + native)
|
||||
elif tag == 1: # 音频数据块: N 个 3 字节 SAD
|
||||
e['cea_blocks'].append(f'音频数据块 x{length // 3}')
|
||||
elif tag == 4:
|
||||
e['cea_blocks'].append('扬声器分配块')
|
||||
elif tag == 5:
|
||||
e['cea_blocks'].append('VESA DTC 块')
|
||||
elif tag == 6:
|
||||
e['cea_blocks'].append('视频格式块(VFDB)')
|
||||
elif tag == 3:
|
||||
# 厂商专用块: 前 3 字节 IEEE OUI(小端)
|
||||
oui = bytes(data[0:3])
|
||||
if oui == b'\x03\x0c\x00': # HDMI 1.x VSDB: OUI 后 4 字节物理地址
|
||||
if length >= 7:
|
||||
e['cea_phys'] = '.'.join(str(b) for b in data[3:7])
|
||||
e['cea_blocks'].append('HDMI VSDB')
|
||||
elif oui == b'\xd8\x5d\xc4': # HDMI 2.0 HF-VSDB
|
||||
e['cea_blocks'].append('HDMI 2.0 HF-VSDB')
|
||||
else:
|
||||
e['cea_blocks'].append(f'厂商块 OUI={oui.hex(" ")}')
|
||||
elif tag == 7:
|
||||
ext = data[0] if length >= 1 else -1
|
||||
name = EXT_TAG_NAMES.get(ext, f'0x{ext:02X}')
|
||||
e['cea_blocks'].append(f'扩展块({name})')
|
||||
else:
|
||||
e['cea_blocks'].append(f'保留块 tag{tag}')
|
||||
p += 1 + length
|
||||
|
||||
if dtd_off >= 4: # 扩展块 DTD 区
|
||||
q = 128 + dtd_off
|
||||
while q + 18 <= n:
|
||||
if raw[q] == 0 and raw[q + 1] == 0:
|
||||
break
|
||||
e['dtds'].append(parse_dtd(raw, q))
|
||||
q += 18
|
||||
return e
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 文本化
|
||||
# ============================================================
|
||||
def edid_info_lines(e):
|
||||
"""[(项目, 值), ...] 摘要行"""
|
||||
lines = []
|
||||
inch = 0
|
||||
if e['wcm'] > 0 and e['hcm'] > 0:
|
||||
inch = round(math.hypot(e['wcm'], e['hcm']) / 2.54, 1)
|
||||
lines.append(('厂商 / 产品码', f"{e['manufacturer']} / 0x{e['product']:04X}"))
|
||||
lines.append(('显示器名称', e['name'] or '(未声明)'))
|
||||
if e['serial_text']:
|
||||
lines.append(('序列号(文本)', e['serial_text']))
|
||||
lines.append(('生产周 / 年', f"第 {e['week']} 周 / {e['year']} 年"))
|
||||
lines.append(('EDID 版本', e['ver']))
|
||||
lines.append(('输入类型', e['input']))
|
||||
if inch > 0:
|
||||
lines.append(('屏幕尺寸', f"{e['wcm']}cm x {e['hcm']}cm (约 {inch} 英寸)"))
|
||||
if e['gamma'] > 0:
|
||||
lines.append(('Gamma', e['gamma']))
|
||||
if e['features']:
|
||||
lines.append(('特性', e['features']))
|
||||
if e['range_text']:
|
||||
lines.append(('范围限制', e['range_text']))
|
||||
if e['has_ext']:
|
||||
ck = 'OK' if e['cksum_ext'] else '错误!'
|
||||
lines.append(('扩展块', f"CEA-861 rev.{e['cea_rev']} 校验和 {ck}"))
|
||||
if e['cea_caps']:
|
||||
lines.append(('CEA 能力', e['cea_caps']))
|
||||
if e['cea_phys']:
|
||||
lines.append(('HDMI 物理地址', e['cea_phys']))
|
||||
else:
|
||||
lines.append(('扩展块', '无'))
|
||||
lines.append(('基础块校验和', 'OK (0x00)' if e['cksum_base'] else '错误!'))
|
||||
|
||||
# 每个 DTD 的完整时序参数(硬件调试重点)
|
||||
for k, t in enumerate(e['dtds']):
|
||||
tag = 'DTD1(首选)' if k == 0 else f'DTD{k + 1}'
|
||||
lines.append((f'{tag} 模式', f"{t['hact']}x{t['vact']}@{t['refresh']} {t['clk_khz'] / 1000:.3f} MHz"))
|
||||
lines.append((f'{tag} 总时序', f"{t['htotal']}x{t['vtotal']}"))
|
||||
lines.append((f'{tag} 水平', f"有效 {t['hact']} | 前沿 {t['hfront']} | 同步 {t['hsync']} | "
|
||||
f"后沿 {t['htotal'] - t['hact'] - t['hfront'] - t['hsync']}"))
|
||||
lines.append((f'{tag} 垂直', f"有效 {t['vact']} | 前沿 {t['vfront']} | 同步 {t['vsync']} | "
|
||||
f"后沿 {t['vtotal'] - t['vact'] - t['vfront'] - t['vsync']}"))
|
||||
lines.append((f'{tag} 同步', f"{t['sync_type']} {t['polarity']} "
|
||||
f"{'(隔行)' if t['interlaced'] else '(逐行)'}"))
|
||||
if t['him_mm'] > 0:
|
||||
lines.append((f'{tag} 图像尺寸', f"{t['him_mm']}x{t['vim_mm']} mm"))
|
||||
return lines
|
||||
|
||||
|
||||
def hex_dump(b):
|
||||
out = []
|
||||
for row in range((len(b) + 15) // 16):
|
||||
off = row * 16
|
||||
chunk = b[off:off + 16]
|
||||
# 8+8 分组, 组间双空格: 同步头/厂商段一眼分开
|
||||
hx = ' '.join(f'{v:02X}' for v in chunk[:8]) + ' ' + \
|
||||
' '.join(f'{v:02X}' for v in chunk[8:])
|
||||
asc = ''.join(chr(v) if 32 <= v < 127 else '.' for v in chunk)
|
||||
out.append(f'{off:04X}: {hx:<50} {asc}')
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# EDID 逐字节字段解析(调试视图): 基础块 + CTA-861 扩展块
|
||||
# 每个字段一行: [偏移] 原始字节 | 含义 | 解码值 | 备注
|
||||
# ============================================================
|
||||
B_HEAD = '同步头 00 FF FF FF FF FF FF 00'
|
||||
|
||||
|
||||
def _range_limits(d):
|
||||
"""FD 频率范围描述符(18 字节常规布局) -> V/H 范围与像素时钟上限"""
|
||||
try:
|
||||
return (f"V {d[5]}~{d[6]}Hz, H {d[7]}~{d[8]}kHz, "
|
||||
f"像素时钟上限 {d[9] * 10}MHz")
|
||||
except (IndexError, ValueError):
|
||||
return ''
|
||||
|
||||
|
||||
def _bits(b, on, off=None):
|
||||
return f'{b:02X}: ' + ('; '.join(on) if on else (off or '无置位'))
|
||||
|
||||
|
||||
def edid_field_rows(raw):
|
||||
"""把 EDID(128/256 字节)逐字段解成 [(偏移, 原始字节, 含义, 解码值, 备注), ...]"""
|
||||
rows = []
|
||||
n = len(raw)
|
||||
|
||||
def add(off, ln, meaning, val, note=''):
|
||||
hx = raw[off:off + ln].hex(' ').upper()
|
||||
if ln > 18: # 超长区域(如零填充)截断, 完整数据看 hex dump
|
||||
hx = raw[off:off + 18].hex(' ').upper() + f' …(共 {ln} 字节)'
|
||||
rows.append((f'0x{off:02X}-{off + ln - 1:02X}' if ln > 1 else f'0x{off:02X}',
|
||||
hx, meaning, str(val), note))
|
||||
|
||||
add(0, 8, B_HEAD, 'OK' if raw[0:8] == bytes.fromhex('00FFFFFFFFFFFF00') else '不匹配!',
|
||||
'不许改, 用于同步识别')
|
||||
# 8-9 厂商 ID: 16 位大端, 3 组 5bit 压缩大写字母(块内唯一大端字段)
|
||||
mfr = ''.join([chr(((raw[8] & 0x7C) >> 2) + 64),
|
||||
chr((((raw[8] & 0x03) << 3) | ((raw[9] & 0xE0) >> 5)) + 64),
|
||||
chr((raw[9] & 0x1F) + 64)])
|
||||
add(8, 2, '厂商 ID(大端)', mfr,
|
||||
f"3×5bit 压缩字母, 原始 0x{(raw[8] << 8) | raw[9]:04X}; 正式产品应使用注册的 PNP ID")
|
||||
add(10, 2, '产品码(小端)', f"0x{struct.unpack_from('<H', raw, 10)[0]:04X} ({struct.unpack_from('<H', raw, 10)[0]})",
|
||||
'厂商自定')
|
||||
add(12, 4, '序列号(小端)', f"0x{struct.unpack_from('<I', raw, 12)[0]:08X}",
|
||||
'厂商自定')
|
||||
add(16, 1, '生产周', f"第 {raw[16]} 周", '0 = 不指定')
|
||||
add(17, 1, '生产年', f"{1990 + raw[17]} 年", '字节值 + 1990')
|
||||
add(18, 2, 'EDID 版本号', f"{raw[18]}.{raw[19]}", '常用 1.4 / 1.3(即字节 01 04 / 01 03)')
|
||||
|
||||
# 字节 0x14 输入类型
|
||||
if raw[20] & 0x80:
|
||||
itf, bpc = INTERFACES[raw[20] & 0x7], DEPTHS[(raw[20] >> 4) & 0x7]
|
||||
val = f'数字 / {itf}' + (f' / {bpc}bpc' if bpc else ' / 色深未定义')
|
||||
else:
|
||||
val = '模拟输入'
|
||||
add(20, 1, '输入类型', val,
|
||||
'bit7=1 数字; 低 4 位: 1=HDMI-A 2=HDMI-B 4=DP 5=eDP')
|
||||
|
||||
add(21, 1, '最大画面宽(cm)', raw[21], '0 = 未知(横竖比由 DTD 图像尺寸给出)')
|
||||
add(22, 1, '最大画面高(cm)', raw[22], '0 = 未知')
|
||||
add(23, 1, 'Gamma', f"{(raw[23] + 100) / 100:.2f}" if raw[23] else '(未提供)',
|
||||
'存储值 = gamma×100 − 100(如 0x78 → 2.20); 0xFF = 不提供')
|
||||
add(24, 1, '特性位', _bits(raw[24], [
|
||||
*(['DPMS 待机'] if raw[24] & 0x80 else []),
|
||||
*(['DPMS 挂起'] if raw[24] & 0x40 else []),
|
||||
*(['DPMS 关闭'] if raw[24] & 0x20 else []),
|
||||
*([f"显示色彩 {['RGB 4:4:4', 'RGB+YCrCb 4:4:4', 'RGB+YCrCb 4:2:2', '未定义'][(raw[24] >> 3) & 3]}"]
|
||||
if raw[24] & 0x80 else []),
|
||||
*(['sRGB 默认'] if raw[24] & 0x04 else []),
|
||||
*(['首选时序=原生分辨率'] if raw[24] & 0x02 else []),
|
||||
*(['连续频率/GTF'] if raw[24] & 0x01 else []),
|
||||
]), 'bit2 sRGB / bit1 首选时序有效 / bit0 连续频率')
|
||||
add(25, 10, '色度坐标(CIE xy)', '默认 sRGB 模板' if raw[25:35] == bytes.fromhex(
|
||||
'78EE95A3554D9EA6544C') else '(打包位段)', '红绿蓝白四点 xy 各 10bit 打包, 通常照抄模板')
|
||||
est = []
|
||||
est_bits = 0
|
||||
for i in range(24):
|
||||
bit = 7 - (i % 8)
|
||||
if (raw[35 + i // 8] >> bit) & 1:
|
||||
est_bits |= 1 << (23 - i)
|
||||
if EST_TABLE[i] != '-':
|
||||
est.append(EST_TABLE[i])
|
||||
if est:
|
||||
add(35, 3, 'Established Timing 位图', ', '.join(est),
|
||||
'VESA 老 VGA 时序; 数字屏常写 FF FF 00=全支持')
|
||||
else:
|
||||
add(35, 3, 'Established Timing 位图',
|
||||
'无有效声明' + ('(仅保留位置位)' if est_bits else ''),
|
||||
'VESA 老 VGA 时序; 数字屏常写 FF FF 00=全支持')
|
||||
stds = []
|
||||
for i in range(38, 54, 2):
|
||||
a, b = raw[i], raw[i + 1]
|
||||
if (a == 1 and b == 1) or (a == 0 and b == 0):
|
||||
continue
|
||||
h = (a + 31) * 8
|
||||
ar = STD_ASPECT[(a >> 6) & 3][1]
|
||||
stds.append(f"{h}x{round(h * STD_ASPECT[(a >> 6) & 3][0])}@{b + 60} {ar}")
|
||||
add(38, 16, '标准时序 ×8', '; '.join(stds) if stds else '8 槽全部未用(01 01)',
|
||||
'2 字节/槽: 6bit 宽 + 2bit 比例 + 刷新率−60; 未用槽填 01 01')
|
||||
|
||||
# 0x36-0x7D: 4 × 18 字节描述符
|
||||
for k, d in enumerate(range(54, 126, 18)):
|
||||
grp = raw[d:d + 18]
|
||||
if grp[0] or grp[1]:
|
||||
t = parse_dtd(raw, d)
|
||||
note = (f"{'(隔行)' if t['interlaced'] else '(逐行)'} "
|
||||
f"{t['sync_type']} {'/'.join(t['polarity'].split())}; "
|
||||
f"消隐 H{t['hblank']}/V{t['vblank']}, "
|
||||
f"前/同/后 H{t['hfront']}/{t['hsync']}/{t['htotal'] - t['hact'] - t['hfront'] - t['hsync']} "
|
||||
f"V{t['vfront']}/{t['vsync']}/{t['vtotal'] - t['vact'] - t['vfront'] - t['vsync']}")
|
||||
add(d, 18, f'描述符{k + 1}: DTD(详细时序)',
|
||||
f"{t['hact']}×{t['vact']}@{t['refresh']}Hz "
|
||||
f"总 {t['htotal']}×{t['vtotal']} {t['clk_khz'] / 1000:.3f}MHz"
|
||||
+ (' [首选时序]' if k == 0 else ''),
|
||||
note + ('; 图像尺寸 %dx%d mm' % (t['him_mm'], t['vim_mm']) if t['him_mm'] else ''))
|
||||
else:
|
||||
tag = grp[3]
|
||||
txt = _ascii(raw, d + 5, 13)
|
||||
if tag == 0xFF:
|
||||
add(d, 18, f'描述符{k + 1}: 序列号串(0xFF)', repr(txt), 'ASCII, 上限 13 字节')
|
||||
elif tag == 0xFC:
|
||||
add(d, 18, f'描述符{k + 1}: 显示器名(0xFC)', repr(txt), 'ASCII, 上限 13 字节')
|
||||
elif tag == 0xFD:
|
||||
add(d, 18, f'描述符{k + 1}: 范围限制(0xFD)', _range_limits(grp), '频率范围描述符')
|
||||
elif tag == 0xFE:
|
||||
add(d, 18, f'描述符{k + 1}: 无界文本(0xFE)', repr(txt), '')
|
||||
elif tag == 0x10:
|
||||
add(d, 18, f'描述符{k + 1}: 数据串(0x10)', grp[5:18].hex(' ').upper(), '二进制用途')
|
||||
elif tag == 0 and not any(grp):
|
||||
add(d, 18, f'描述符{k + 1}: 空', '(全 00)', '')
|
||||
else:
|
||||
add(d, 18, f'描述符{k + 1}: 未知类型 0x{tag:02X}', grp[5:18].hex(' ').upper(), '')
|
||||
|
||||
add(126, 1, '扩展块数量', raw[126], '必须与实际扩展块数一致')
|
||||
cksum = (256 - sum(raw[0:127])) & 0xFF
|
||||
add(127, 1, '基础块校验和', f"0x{raw[127]:02X} → " + ('正确' if (sum(raw[0:128]) & 0xFF) == 0 else '错误!'),
|
||||
f"整块 128 字节和 ≡ 0 (mod 256); 期望 0x{cksum:02X} = 256 − 前 127 字节和")
|
||||
|
||||
# ---- CTA-861 扩展块 ----
|
||||
n_ext = raw[126] if n > 126 else 0
|
||||
for bi in range(n_ext):
|
||||
base = 128 + bi * 128
|
||||
if base + 128 > n:
|
||||
add(base, 0, f'扩展块{bi + 1}', '(数据不完整)', '')
|
||||
break
|
||||
blk = raw[base:base + 128]
|
||||
add(base, 1, f'扩展块{bi + 1} @0x{base:02X} Tag',
|
||||
'CTA-861' if blk[0] == 0x02 else f'0x{blk[0]:02X}(非 CTA)',
|
||||
'固定 0x02 = CTA 扩展; HDMI 必须有')
|
||||
if blk[0] != 0x02:
|
||||
add(base + 1, 127, '扩展块数据', blk[1:128].hex(' ').upper(), '未知扩展类型, 跳过')
|
||||
continue
|
||||
add(base + 1, 1, '版本', f"rev {blk[1]}", '常用 03')
|
||||
add(base + 2, 1, 'DTD 起始偏移', f"0x{blk[2]:02X} (= 块内 {blk[2]} 字节处)", '数据块链结束、DTD 区开始的偏移')
|
||||
add(base + 3, 1, '标志位', _bits(blk[3], [
|
||||
*(['bit7 支持 YCbCr 4:2:0'] if blk[3] & 0x80 else []),
|
||||
*(['bit6 支持基础音频'] if blk[3] & 0x40 else []),
|
||||
*(['bit5 支持 YCbCr 4:2:2'] if blk[3] & 0x20 else []),
|
||||
*(['bit4 支持 YCbCr 4:4:4'] if blk[3] & 0x10 else []),
|
||||
*(['bit0 支持欠扫描'] if blk[3] & 0x01 else []),
|
||||
]), 'bit6=基础音频, bit5=4:2:2, bit7=4:2:0, bit0=欠扫描')
|
||||
|
||||
# 数据块链: 每块 1 字节头(高 3 位 tag + 低 5 位长度) + payload
|
||||
TAG_NAMES = {1: '音频', 2: '视频 VIC', 3: '厂商专用 VSDB', 4: '扬声器分配',
|
||||
5: 'VESA DTC', 6: '视频格式 VFDB', 7: '扩展 tag'}
|
||||
p, first = 4, True
|
||||
while base + p < base + 128 and p < blk[2] and p + 1 < 128:
|
||||
tag, length = (blk[p] >> 5) & 7, blk[p] & 0x1F
|
||||
payload = blk[p + 1:p + 1 + length]
|
||||
if first and blk[p] == 0:
|
||||
add(base + p, 1, '数据块链', '(无数据块, 直接进 DTD 区)', '4 至 DTD 区前为数据块链')
|
||||
break
|
||||
first = False
|
||||
if tag == 2:
|
||||
vics = []
|
||||
for v in payload:
|
||||
vi = v & 0x7F
|
||||
name = (VIC_TABLE[vi - 1] if 1 <= vi <= len(VIC_TABLE) else None) or f'VIC#{vi}'
|
||||
vics.append(name + ('(原生)' if v & 0x80 else ''))
|
||||
add(base + p, 1 + length, '数据块: 视频', ' '.join(vics) or '(空)', f"tag 2, {length} 字节")
|
||||
elif tag == 1:
|
||||
fmts = []
|
||||
codes = ['保留', 'LPCM', 'AAC', 'AC-3', 'MPEG1', 'MP3', 'MPEG2', 'AAC LC',
|
||||
'DTS', 'ATRAC', 'DSD', 'E-AC-3', 'DTS-HD', 'MLP', 'DST', 'WMAPro']
|
||||
for si in range(length // 3):
|
||||
sad = payload[si * 3:(si + 1) * 3]
|
||||
ch = (sad[0] & 7) + 1
|
||||
rates = '/'.join(str(r) for r, b in ((32, 1), (44, 2), (48, 4)) if sad[2] & b) or '?'
|
||||
mults = ''.join(f'×{m}' for m, b in ((2, 8), (3, 16), (4, 32)) if sad[2] & b)
|
||||
if (sad[0] >> 3) & 0xF == 1: # LPCM: 位宽在字节1低3位(位图)
|
||||
bits = '/'.join(str(v) for v, b in ((16, 1), (20, 2), (24, 4)) if sad[1] & b) or '?'
|
||||
fmts.append(f'LPCM {ch}ch {bits}bit {rates}kHz{mults}')
|
||||
else: # 压缩格式: 字节1高4位=最高采样率档
|
||||
fmts.append(f"{codes[(sad[0] >> 3) & 0xF]} {ch}ch {rates}kHz{mults}")
|
||||
add(base + p, 1 + length, '数据块: 音频', '; '.join(fmts), f"tag 1, {length // 3} × 3 字节 SAD")
|
||||
elif tag == 3:
|
||||
oui = bytes(payload[0:3])
|
||||
names = {b'\x03\x0c\x00': 'HDMI 1.x VSDB', b'\xd8\x5d\xc4': 'HDMI 2.0 HF-VSDB'}
|
||||
val = names.get(oui, f'OUI={oui.hex(" ").upper()}')
|
||||
if oui == b'\x03\x0c\x00' and length >= 7:
|
||||
val += f", 物理地址 {'.'.join(str(b) for b in payload[3:7])}"
|
||||
add(base + p, 1 + length, '数据块: 厂商专用', val,
|
||||
f"tag 3, 头 3 字节 IEEE OUI(小端){'; HDMI 2.0 需 HF-VSDB' if oui == b'\x03\x0c\x00' else ''}")
|
||||
else:
|
||||
name = TAG_NAMES.get(tag, f'tag{tag}')
|
||||
add(base + p, 1 + length, f'数据块: {name}',
|
||||
payload.hex(' ').upper() if length else '(空)', f"{length} 字节 payload")
|
||||
if length == 0:
|
||||
break
|
||||
p += 1 + length
|
||||
|
||||
# DTD 区
|
||||
if blk[2] >= 4:
|
||||
q, cnt = blk[2], 0
|
||||
while q + 18 <= 128 and any(blk[q:q + 2]):
|
||||
t = parse_dtd(blk, q)
|
||||
add(base + q, 18, f'扩展块 DTD{cnt + 1}',
|
||||
f"{t['hact']}×{t['vact']}@{t['refresh']}Hz 总 {t['htotal']}×{t['vtotal']} "
|
||||
f"{t['clk_khz'] / 1000:.3f}MHz",
|
||||
"前/同/后 H%d/%d/%d V%d/%d/%d %s" % (
|
||||
t['hfront'], t['hsync'], t['htotal'] - t['hact'] - t['hfront'] - t['hsync'],
|
||||
t['vfront'], t['vsync'], t['vtotal'] - t['vact'] - t['vfront'] - t['vsync'],
|
||||
'(隔行)' if t['interlaced'] else '(逐行)'))
|
||||
cnt += 1
|
||||
q += 18
|
||||
if not cnt:
|
||||
add(base + blk[2], 0, '扩展块 DTD 区', '(空)', '未用空间填 0')
|
||||
rem = 128 - max(blk[2], 4) - 18 * cnt if blk[2] >= 4 else 0
|
||||
if blk[2] >= 4 and rem > 0 and not any(blk[128 - rem:128 - 1]):
|
||||
add(base + 128 - rem, rem - 1, '未用空间', '0 填充', '')
|
||||
add(base + 127, 1, f'扩展块{bi + 1} 校验和',
|
||||
f"0x{blk[127]:02X} → " + ('正确' if (sum(blk) & 0xFF) == 0 else '错误!'),
|
||||
f"期望 0x{(256 - sum(blk[0:127])) & 0xFF:02X} (同基础块规则)")
|
||||
return rows
|
||||
|
||||
|
||||
def edid_field_text(raw):
|
||||
"""逐字段解析 -> 多行文本(命令行 / 文本框共用)"""
|
||||
w = (7, 26, 20)
|
||||
out = ['%-*s %-*s %-*s %s' % (w[0], '偏移', w[1], '含义', w[2], '解码值', '备注 / 原始字节')]
|
||||
for off, hx, meaning, val, note in edid_field_rows(raw):
|
||||
line = '%-*s %-*s %-*s %s' % (w[0], off, w[1], meaning, w[2], val, note)
|
||||
out.append(line.rstrip())
|
||||
if hx:
|
||||
out.append('%-*s %-*s %s %s' % (w[0], '', w[1], '', '原始', hx))
|
||||
return '\n'.join(out)
|
||||
Reference in New Issue
Block a user