Linux HDMI 显示器信息提取与 monitors.xml 匹配完全指南

1. 核心概念:EDID(扩展显示识别数据)

显示器插入后,显卡通过 I²C 总线读取显示器固件中存储的 EDID 数据块(128 字节或 256 字节)。
Linux 内核(DRM 子系统)会将此二进制数据暴露给用户空间。所有厂商、序列号、支持的分辨率都来源于此。


2. 信息获取的数据源(三层架构)

层级路径/命令作用可靠性
内核原始数据/sys/class/drm/card*-HDMI-*/edid直接读取显示器的 EDID 原始二进制字节最高(硬件级)
内核解析属性/sys/class/drm/card*-HDMI-*/edid_parsed (部分内核有)内核预解析的文本信息中等
用户态工具xrandr --verboseX/Wayland 服务层解析后的显示信息高(含当前模式)
GNOME 配置~/.config/monitors.xmlGNOME 根据上述信息生成的持久化配置用于匹配

3. 关键字段提取方法论(EDID 二进制解析)

根据你的脚本,以下是针对 EDID 字节流的精确提取逻辑(遵循 VESA E-EDID 标准 1.3/1.4):

3.1 序列号 (Serial Number) —— 重点

EDID 中存在两种序列号存储方式,脚本采用了优先策略

  1. 优先(现代显示器)描述符块 (Descriptor Block) 中的 Tag 0xFF(字符串序列号)。

    • 位置:EDID 偏移量 54, 72, 90, 108 处(每块 18 字节)。
    • 判定:检查 byte[offset+2] == 0xFF
    • 提取:取 offset+5offset+18,截断换行符 0x0A 和空字符 0x00,转为 ASCII。
    • 优点:通常是纯数字/字母组合,如 “SN123456”,GNOME 直接使用此字符串。
  2. 回退(老款/廉价显示器)基础 EDID 结构体中的 4 字节整数

    • 位置:Byte 12 - 15(小端序 Little-Endian)。
    • 提取:struct.unpack_from("<I", edid_bytes, 12)[0]
    • 格式化:转为 16 进制字符串(如 0x0000A1B2)。

关键经验:如果显示器序列号在 monitors.xml 中显示为 0x...,说明它没有字符串序列号,走的是回退逻辑。


3.2 厂商 ID (Vendor / Manufacturer)

  • 位置:EDID Byte 8 - 9
  • 编码:非 ASCII 直接存储,而是压缩的 5-bit 字符(基于 ASCII 字母序号)。
  • 脚本解码公式
    w = (byte8 << 8) | byte9  # 大端序读取
    char1 = chr(((w >> 10) & 0x1F) + 0x40)  # 0x40 = '@',因为 PNP ID 从 'A' 开始
    char2 = chr(((w >> 5) & 0x1F) + 0x40)
    char3 = chr((w & 0x1F) + 0x40)
    
  • 示例:AOC 的 PNP ID 是 ACI,三星是 SAM,LG 是 GSM

3.3 产品型号 / 名称 (Product / Model Name)

脚本采用两阶段搜索,极其健壮:

  1. 标准方法:在描述符块中查找 Tag 0xFC(Monitor Name)。

    • 头部标识:00 00 00 FC
    • 位置:找到头部后,从 index + 5 开始读取最多 13 字节。
    • 清洗:截断 0x0A0x00,解码 ASCII。
    • 示例:提取出 “Q27N2”。
  2. 模糊启发式(兜底):针对损坏或非标准 EDID,遍历偏移 54~126 之间的字节,寻找连续 5 个以上可打印 ASCII 字符,并提取为产品名。


3.4 分辨率与刷新率 (Resolution & Refresh Rate)

脚本不依赖 EDID 解析(因为 EDID 包含所有支持的模式,但无法确定当前正在使用的模式),而是调用 xrandr

  • 获取当前活动模式:执行 xrandr --query
  • 正则匹配:查找 HDMI-2 connected 下带 *(星号)的行,匹配 (\d+)x(\d+).*?([\d.]+)Hz
  • 原因:当前分辨率由桌面环境/用户决定,不一定等于 EDID 首选模式。

4. 完整的自动识别工作流(脚本流程拆解)

若需自己实现类似功能,推荐按以下顺序执行:
在这里插入图片描述


5. 与 GNOME monitors.xml 的映射关系

GNOME 的 monitors.xml<monitorspec>唯一识别一台物理显示器。脚本生成的新配置完美对应如下结构:

<monitorspec>
  <connector>HDMI-2</connector>   <!-- 来自扫描的硬件接口 -->
  <vendor>AOC</vendor>            <!-- 来自 EDID 解码的 PNP ID -->
  <product>Q27N2</product>        <!-- 来自 EDID 0xFC 描述符 -->
  <serial>0x000002e4</serial>     <!-- 来自 EDID 序列号提取 -->
</monitorspec>

匹配铁律:GNOME 优先使用 connector + serial 组合匹配。如果 serial 提取错误,会导致配置无法应用(即使 vendor/product 对)。


6. 实战故障排查及手把手命令

如果不想跑脚本,想手动查看这些信息,可以使用以下命令:

6.1 查看 EDID 原始十六进制(肉眼识别序列号)

# 找到你的 HDMI 接口
ls /sys/class/drm/card*-HDMI-*/edid

# 用 hexdump 查看前 128 字节
sudo hexdump -C /sys/class/drm/card0-HDMI-1/edid | head -n 10
  • 偏移 0x0C (12) 处为整型序列号。
  • 描述符区域(0x36, 0x48, 0x5A, 0x6C)找 FFFC 标签。

6.2 用 edid-decode 一键解析(推荐安装)

sudo apt install edid-decode
sudo edid-decode /sys/class/drm/card0-HDMI-1/edid

输出会清晰显示:

Manufacturer: AOC Model 0x1234 Serial Number 123456
Monitor Name: Q27N2

6.3 查看 xrandr 中的 EDID 解析信息

xrandr --verbose | grep -A 10 "HDMI-2 connected" | grep -E "Manufacturer|Model|EDID"

7. 常见坑点与解决(知识库避坑指南)

问题现象根本原因解决方案
monitors.xml 替换失败,提示找不到序列号显示器 EDID 无字符串序列号,脚本回退生成 0x0000...,但配置文件中存的是整数格式(如 12345使用 edid-decode 确认实际值,强制修改 XML 中的 <serial> 标签格式为 0x... 或整数字符串
产品名显示乱码或空格EDID 描述符包含了填充空格(0x20)或非 ASCII 码脚本中已做 .strip() 处理;若仍有问题,手动截断尾部无效字符
刷新率小数位不匹配GNOME 要求极高的浮点数精度匹配生成 XML 时保留 15 位小数(f"{rate:.15f}"),xrandr 输出多少就保留多少
重启后配置未生效monitors.xml 被系统或 GDM 锁定缓存执行 sudo systemctl restart gdm 或注销重新登录,而非仅重启 X

8. 总结:核心实现伪代码

如果你只想复制最核心的 Python 逻辑用于自己的项目,记住这三板斧:

# 1. 读硬件
edid = open(f"/sys/class/drm/card0-HDMI-{id}/edid", "rb").read()

# 2. 解码三大件(自写或调用 edid-decode)
serial = extract_serial_from_descriptor(edid) or unpack_int(edid[12:16])
vendor = decode_pnp_id(edid[8:10])
model = extract_monitor_name(edid)

# 3. 查当前状态
output = subprocess.check_output(["xrandr", "--query"]).decode()
res = parse_current_mode(output, "HDMI-2")
#!/usr/bin/env python3
"""
auto-fix-monitors-xml.py

功能:
1. 读取已连接的 HDMI-2 显示器的 EDID,获取序列号、厂商、产品名、分辨率、刷新率
2. 在 monitors.xml 中找到该序列号对应的 <configuration> 块
3. 将该配置块替换为:HDMI-2 开启为主屏,HDMI-1(NCS) 禁用,DSI-1 禁用
4. 保存修改后的 monitors.xml

用法:
  python3 auto-fix-monitors-xml.py                    # 默认路径 /mnt/f/monitors.xml
  python3 auto-fix-monitors-xml.py --path /path/to/monitors.xml
"""

import os
import re
import sys
import glob
import struct
import subprocess
import argparse
import shutil
from xml.etree import ElementTree as ET
from datetime import datetime


'''
# 调试日志 后期屏蔽
class Tee:
    """同时输出到多个文件对象(如控制台 + 日志文件)"""
    def __init__(self, *files):
        self.files = files

    def write(self, obj):
        for f in self.files:
            f.write(obj)
            f.flush()

    def flush(self):
        for f in self.files:
            f.flush()
# 调试日志 后期屏蔽
'''

# ============================================================
# 1. EDID 读取
# ============================================================

def find_hdmi2_edid():
    """查找 HDMI-2 对应的 EDID 文件路径"""
    patterns = [
        "/sys/class/drm/card*-HDMI-2/edid",
        "/sys/class/drm/card*-HDMI-A-2/edid",
    ]
    for pattern in patterns:
        paths = sorted(glob.glob(pattern))
        if paths:
            return paths[0]
    return None


def read_edid_bytes(edid_path):
    """读取 EDID 二进制文件"""
    with open(edid_path, "rb") as f:
        return f.read()

'''
def extract_serial(edid_bytes):
    """从 EDID byte 12-15 提取序列号,返回 '0x000002e4' 格式"""
    serial_int = struct.unpack_from("<I", edid_bytes, 12)[0]
    return f"0x{serial_int:08x}"
'''

def extract_serial(edid_bytes):
    """
    智能提取 EDID 序列号
    1. 优先搜索 Tag 0xFF 的描述符块 (标准字符串格式)
    2. 若未找到,回退读取 Byte 12-15 (传统整数格式)
    """
    if not edid_bytes or len(edid_bytes) < 128:
        return "N/A"

    # --- 策略 1: 搜索描述符块中的序列号字符串 (Tag 0xFF) ---
    # 结构: 00 00 00 FF 00 [Serial String...]
    target_header = b'\x00\x00\x00\xFF'
    index = edid_bytes.find(target_header)

    if index != -1:
        # 字符串内容从 index + 5 开始 (跳过 4字节头 + 1字节填充)
        start_pos = index + 5
        raw_data = edid_bytes[start_pos : start_pos + 13]

        # 清洗数据:截取到换行符(0x0A)或空字符(0x00)之前
        serial_str = raw_data.split(b'\x0A')[0].split(b'\x00')[0]

        try:
            result = serial_str.decode('ascii').strip()
            if result:  # 确保解码后不为空
                return result
        except UnicodeDecodeError:
            pass  # 如果解码失败,说明不是有效 ASCII,继续执行策略 2

    # --- 策略 2: 回退到固定位置 Byte 12-15 (小端序整数) ---
    try:
        serial_int = struct.unpack_from("<I", edid_bytes, 12)[0]
        # 只有当数值不为 0 时才返回,避免返回无意义的 0x00000000
        if serial_int != 0:
            return f"0x{serial_int:08x}"
    except Exception:
        pass

    return "N/A"


def extract_vendor_from_edid(edid_bytes):
    """从 EDID byte 8-9 提取厂商 ID(3个字符的 PNP ID)"""
    # EDID 厂商 ID 编码在 byte 8-9,使用 5-bit 压缩
    # 参考 VESA EDID 规范
    w = struct.unpack_from(">H", edid_bytes, 8)[0]
    char1 = chr(((w >> 10) & 0x1F) + 0x40)
    char2 = chr(((w >> 5) & 0x1F) + 0x40)
    char3 = chr((w & 0x1F) + 0x40)
    return char1 + char2 + char3


def extract_model_name_robust(edid_bytes):
    """
    增强版 EDID 型号提取器
    能够处理非标准偏移或错位的描述符
    """
    if not edid_bytes or len(edid_bytes) < 128:
        return "N/A"

    # 策略 1: 搜索标准的 Monitor Name 描述符头 (00 00 00 FC)
    # 标准结构: [2字节任意] [00] [FC] [00] [Name...]
    target_header = b'\x00\x00\x00\xFC'
    index = edid_bytes.find(target_header)

    if index != -1:
        # 找到头后,字符串通常从 index + 5 开始
        start_pos = index + 5
        raw_str = edid_bytes[start_pos : start_pos + 13]
        
        # 清洗数据:截取到换行符(0x0A)或空字符(0x00)之前
        model_name = raw_str.split(b'\x0A')[0].split(b'\x00')[0]
        
        try:
            return model_name.decode('ascii').strip()
        except UnicodeDecodeError:
            pass

    # 策略 2: 针对你的特殊情况 (硬编码搜索或模糊搜索)
    # 如果标准方法失败,尝试在常见的描述符区域 (54字节之后) 寻找可打印的 ASCII 序列
    # 你的数据中 Q27N2 位于 offset 120 (0x78)
    for i in range(54, 126):
        # 简单的启发式:如果连续 5 个字节都是可打印 ASCII,且看起来像型号
        chunk = edid_bytes[i:i+5]
        if all(32 <= b < 127 for b in chunk):
            # 进一步验证:检查前后是否是非打印字符(作为边界)
            # 或者是已知的厂商前缀
            try:
                text = chunk.decode('ascii')
                # 排除纯数字或纯空格
                if text.strip() and not text.isdigit():
                    # 这里为了匹配你的 Q27N2,我们取更长一点的串
                    long_chunk = edid_bytes[i:i+10]
                    clean_text = long_chunk.split(b'\x0A')[0].split(b'\x00')[0].decode('ascii').strip()
                    if len(clean_text) >= 3: 
                        return clean_text
            except:
                continue
            
    return "N/A"

# 使用示例
# model = extract_model_name_robust(edid_bytes)


def extract_product(edid_bytes):
    """从 EDID byte 10-11 提取产品代码"""
    product_code = struct.unpack_from("<H", edid_bytes, 10)[0]
    return f"{product_code}"


def get_xrandr_info():
    """通过 xrandr --verbose 获取 HDMI-2 的厂商名、产品名、分辨率、刷新率"""
    info = {
        "vendor": None,
        "product": None,
        "width": None,
        "height": None,
        "rate": None,
    }
    try:
        output = subprocess.check_output(
            ["xrandr", "--query", "--verbose"],
            text=True, stderr=subprocess.DEVNULL
        )
    except (subprocess.CalledProcessError, FileNotFoundError):
        print("[警告] 无法执行 xrandr,将只使用 EDID 信息")
        return info

    # 找到 HDMI-2 的段落并解析
    lines = output.split("\n")
    in_hdmi2 = False
    for i, line in enumerate(lines):
        if line.startswith("HDMI-2 connected"):
            in_hdmi2 = True
            # 解析分辨率(第一行包含分辨率信息)
            # 例如: HDMI-2 connected 2560x1440+0+0 (normal left inverted right x axis y axis) ...
            m = re.search(r'connected\s+(\d+)x(\d+)', line)
            if m:
                info["width"] = int(m.group(1))
                info["height"] = int(m.group(2))
            continue

        if in_hdmi2:
            # 遇到下一个输出端口行则结束
            if re.match(r'^(HDMI|DSI|DP|eDP|VGA)-\d', line):
                break

            # 提取厂商信息(从 EDID 数据或属性中)
            # xrandr --verbose 会显示 "Manufacturer: XXX"
            m = re.search(r'Manufacturer:\s*(\S+)', line, re.IGNORECASE)
            if m:
                info["vendor"] = m.group(1).strip()

            # 提取产品名
            m = re.search(r'Model:\s*(.+)', line, re.IGNORECASE)
            if m:
                info["product"] = m.group(1).strip()

            # 提取 EDID 原始数据行(如果 vendor/product 没找到,从中解析)
            if info["vendor"] is None or info["product"] is None:
                m = re.search(r'EDID(_DATA)?:\s*"?(.{256})', line)
                # 复杂的 EDID 解析跳过,后面用 EDID 文件

            # 提取当前活动的刷新率
            # 例如: 2560x1440 (0x05e) 59.950Hz *Current
            m = re.search(r'(\d+)x(\d+).*?([\d.]+)Hz\s*\*Current', line)
            if m:
                info["width"] = int(m.group(1))
                info["height"] = int(m.group(2))
                info["rate"] = float(m.group(3))

    return info


def get_current_resolution_from_xrandr():
    """从 xrandr 查询当前活动的分辨率和刷新率"""
    try:
        # xrandr 标准输出(非 verbose)更容易解析当前分辨率
        output = subprocess.check_output(
            ["xrandr", "--query"],
            text=True, stderr=subprocess.DEVNULL
        )
        for line in output.split("\n"):
            if line.startswith("HDMI-2 connected"):
                # 找到当前活动分辨率(带 * 号的行)
                # HDMI-2 connected 2560x1440+0+0 (normal left inverted right x axis y axis) 477mm x 268mm
                # 下一行是分辨率列表,带 * 的是当前使用的
                continue
            if "HDMI-2" in line and "connected" in line:
                m = re.search(r'connected\s+(\d+)x(\d+)', line)
                if m:
                    return int(m.group(1)), int(m.group(2))

        # 查找 * 号标记的当前分辨率
        lines = output.split("\n")
        for i, line in enumerate(lines):
            if line.startswith("HDMI-2 connected"):
                # 在后续行中找到带 * 的
                for j in range(i+1, min(i+10, len(lines))):
                    m = re.search(r'^\s+(\d+)x(\d+).*?([\d.]+)Hz\s*\*', lines[j])
                    if m:
                        return int(m.group(1)), int(m.group(2)), float(m.group(3))
                break
    except:
        pass
    return None, None, None


# ============================================================
# 2. monitors.xml 操作
# ============================================================

def find_config_index_by_serial(xml_root, serial, connector="HDMI-2"):
    """
    在 monitors.xml 中找到第一个匹配给定序列号和连接器的 <configuration> 块
    返回 (索引, 配置元素)
    """
    configurations = xml_root.findall("configuration")
    for idx, config in enumerate(configurations):
        for logicalmonitor in config.findall("logicalmonitor"):
            monitor = logicalmonitor.find("monitor")
            if monitor is not None:
                spec = monitor.find("monitorspec")
                if spec is not None:
                    conn = spec.find("connector")
                    ser = spec.find("serial")
                    if (conn is not None and conn.text == connector
                            and ser is not None and ser.text == serial):
                        return idx, config
    return None, None


def remove_config_at_index(xml_root, idx):
    """移除指定索引的 <configuration> 块"""
    configurations = xml_root.findall("configuration")
    if 0 <= idx < len(configurations):
        xml_root.remove(configurations[idx])
        return True
    return False


def build_new_config_xml(serial, vendor, product, width, height, rate):
    """根据检测到的信息构建新的配置 XML 字符串"""
    
    # 如果某些字段缺失,使用默认值
    if vendor is None:
        vendor = "AOC"
    if product is None:
        product = "Q27V3"
    if width is None:
        width = 1920
    if height is None:
        height = 1200
    if rate is None:
        rate = 59.884601593017578

    # 格式化 rate 确保有足够的小数位
    rate_str = f"{rate:.15f}"

    xml_str = f"""  <configuration>
    <logicalmonitor>
      <x>0</x>
      <y>0</y>
      <scale>1</scale>
      <primary>yes</primary>
      <monitor>
        <monitorspec>
          <connector>HDMI-2</connector>
          <vendor>{vendor}</vendor>
          <product>{product}</product>
          <serial>{serial}</serial>
        </monitorspec>
        <mode>
          <width>{width}</width>
          <height>{height}</height>
          <rate>{rate_str}</rate>
        </mode>
      </monitor>
    </logicalmonitor>
    <disabled>
      <monitorspec>
        <connector>HDMI-1</connector>
        <vendor>NCS</vendor>
        <product>NCS HDMI     </product>
        <serial>0x00000000</serial>
      </monitorspec>
      <monitorspec>
        <connector>DSI-1</connector>
        <vendor>unknown</vendor>
        <product>unknown</product>
        <serial>unknown</serial>
      </monitorspec>
    </disabled>
  </configuration>"""
    return xml_str


def replace_config_in_file(xml_path, old_idx, new_config_xml):
    """
    在文本级别替换 monitors.xml 中的配置块
    这样可以保留原始的 XML 格式和缩进风格
    """
    with open(xml_path, "r", encoding="utf-8") as f:
        content = f.read()

    # 找到所有 <configuration>...</configuration> 块
    pattern = re.compile(r'  <configuration>.*?</configuration>', re.DOTALL)
    matches = list(pattern.finditer(content))

    if old_idx >= len(matches):
        print(f"[错误] 索引 {old_idx} 超出配置块数量 {len(matches)}")
        return False

    start, end = matches[old_idx].start(), matches[old_idx].end()
    
    new_content = content[:start] + new_config_xml + content[end:]

    # 备份原文件
    backup_path = xml_path + ".bak." + datetime.now().strftime("%Y%m%d_%H%M%S")
    shutil.copy2(xml_path, backup_path)
    print(f"[信息] 原文件已备份到: {backup_path}")

    with open(xml_path, "w", encoding="utf-8") as f:
        f.write(new_content)

    return True


# ============================================================
# 3. 主流程
# ============================================================

def main():
    parser = argparse.ArgumentParser(description="自动修正 monitors.xml 中 HDMI-2 的配置")
    parser.add_argument(
        "--path", "-p",
        default=None,
        help="monitors.xml 的路径(默认: ~/.config/monitors.xml)"
    )
    parser.add_argument(
        "--dry-run", "-n",
        action="store_true",
        help="仅预览,不实际修改文件"
    )
    args = parser.parse_args()

    '''
    # 调试信息 后期屏蔽
    # 将输出同时写入控制台和日志文件
    try:
        log_file = open("/var/log/hdmi_debug.log", "a", encoding="utf-8")
        sys.stdout = Tee(sys.stdout, log_file)
        sys.stderr = Tee(sys.stderr, log_file)
    except PermissionError:
        print("[警告] 无法写入 /var/log/hdmi_debug.log(无权限),仅输出到控制台")
    # 调试信息 后期屏蔽
    '''

    #xml_path = os.path.abspath(args.path)
    if args.path is None:
        import pwd

        # 1. 如果通过 sudo 运行且有 SUDO_USER(非 root),使用该用户的家目录
        sudo_user = os.environ.get("SUDO_USER")
        if sudo_user and sudo_user != "root":
            home = pwd.getpwnam(sudo_user).pw_dir
            xml_path = os.path.join(home, ".config/monitors.xml")
        else:
            # 2. 以 root 直接运行时,扫描 /home/ 下第一个存在的 monitors.xml
            import glob
            candidates = sorted(glob.glob("/home/*/.config/monitors.xml"))
            if candidates:
                xml_path = candidates[0]
                print(f"[信息] 自动检测到 monitors.xml: {xml_path}")
            else:
                # 3. 最后 fallback
                home = os.path.expanduser("~")
                xml_path = os.path.join(home, ".config/monitors.xml")
    else:
        xml_path = os.path.abspath(args.path)
    dry_run = args.dry_run

    print("=" * 60)
    print("  monitors.xml 自动配置修正工具")
    print("=" * 60)

    # ---- 第一步:读取 EDID ----
    print("\n[步骤1] 读取 HDMI-2 EDID ...")
    edid_path = find_hdmi2_edid()
    if not edid_path:
        print("[错误] 未找到 HDMI-2 的 EDID 文件!")
        print("       请确认 HDMI-2 已连接显示器")
        print("       当前 DRM 设备列表:")
        for p in sorted(glob.glob("/sys/class/drm/card*")):
            print(f"         - {os.path.basename(p)}")
        sys.exit(1)

    print(f"  EDID 文件: {edid_path}")
    edid_bytes = read_edid_bytes(edid_path)
    
    serial = extract_serial(edid_bytes)
    edid_vendor = extract_vendor_from_edid(edid_bytes)
    #edid_product = extract_product_from_edid(edid_bytes)
    edid_modelname =  extract_model_name_robust(edid_bytes)
    # EDID 厂商 ID 是 PNP ID(如 AOC="ACI"),xrandr 显示的是品牌名
    # 优先使用 xrandr 的信息
    print(f"  序列号: {serial}")
    print(f"  厂商(EDID PNP): {edid_vendor}")
    #print(f"  制造商(EDID PRODUCT): {edid_product}")
    print(f"  型号名称: {edid_modelname}")

    # ---- 第二步:从 xrandr 获取详细显示信息 ----
    print("\n[步骤2] 从 xrandr 获取显示信息 ...")
    xrandr_info = get_xrandr_info()
    width, height, rate = get_current_resolution_from_xrandr()
    
    #vendor = xrandr_info.get("vendor")
    vendor = extract_vendor_from_edid(edid_bytes)
    #product = xrandr_info.get("product")
    product = extract_model_name_robust(edid_bytes)

    print(f"  厂商: {vendor or '未检测到(将使用默认)'}")
    print(f"  产品: {product or '未检测到(将使用默认)'}")
    print(f"  分辨率: {width}x{height}" if width else "  分辨率: 未检测到(将使用默认)")
    print(f"  刷新率: {rate} Hz" if rate else "  刷新率: 未检测到(将使用默认)")

    # ---- 第三步:读取 monitors.xml ----
    print(f"\n[步骤3] 读取 monitors.xml: {xml_path} ...")
    if not os.path.exists(xml_path):
        print(f"[错误] 文件不存在: {xml_path}")
        sys.exit(1)

    # 用 ElementTree 解析来查找匹配的配置
    tree = ET.parse(xml_path)
    root = tree.getroot()

    idx, config_elem = find_config_index_by_serial(root, serial, "HDMI-2")

    # 先生成目标配置,后续用于对比和操作
    new_config_xml = build_new_config_xml(serial, vendor, product, width, height, rate)

    if idx is None:
        print(f"[警告] monitors.xml 中未找到序列号 {serial} 对应的配置")
        print("       这可能是因为该显示器从未在此系统上配置过")
        print("       脚本将尝试添加到文件末尾")
        action = "append"
    else:
        print(f"  找到匹配配置: 第 {idx+1} 个 <configuration>")
        # 读取现有配置文本,判断是否与目标配置一致
        with open(xml_path, "r", encoding="utf-8") as f:
            content = f.read()
        config_pattern = re.compile(r'  <configuration>.*?</configuration>', re.DOTALL)
        config_matches = list(config_pattern.finditer(content))
        existing_config_text = content[config_matches[idx].start():config_matches[idx].end()]

        if existing_config_text.strip() == new_config_xml.strip():
            print(f"  现有配置与目标配置一致,无需修改")
            action = "skip"
        else:
            action = "replace"

    # ---- 第四步:生成并替换配置 ----
    action_label = {"replace": "替换", "append": "追加", "skip": "跳过"}.get(action, "未知")
    print(f"\n[步骤4] {action_label}配置 ...")

    print("  新配置内容:")
    for line in new_config_xml.split("\n"):
        print(f"    {line}")

    if dry_run:
        print(f"\n[dry-run 模式] 未做任何修改")
        print("  如需实际修改,去掉 --dry-run / -n 参数再运行")
        return

    if action == "replace":
        success = replace_config_in_file(xml_path, idx, new_config_xml)
    elif action == "skip":
        print("  配置一致,无需变更")
        success = True
    else:
        # 追加到文件末尾(在 </monitors> 之前)
        with open(xml_path, "r", encoding="utf-8") as f:
            content = f.read()
        
        # 备份
        backup_path = xml_path + ".bak." + datetime.now().strftime("%Y%m%d_%H%M%S")
        shutil.copy2(xml_path, backup_path)
        print(f"[信息] 原文件已备份到: {backup_path}")

        if "</monitors>" in content:
            content = content.replace("</monitors>", new_config_xml + "\n</monitors>")
        else:
            content += "\n" + new_config_xml + "\n</monitors>"

        with open(xml_path, "w", encoding="utf-8") as f:
            f.write(content)
        success = True

    if success:
        print("\n[✓] 修改成功!")
        print(f"    已更新: {xml_path}")
        print("")
        print("    提示:需要重新登录 GNOME 或重启显示器配置才能生效")
        print("    也可运行以下命令立即生效:")
        print("      sudo systemctl restart gdm  (或 lightdm/sddm,视你的 DM 而定)")
        print("    或者直接拔出再插入 HDMI-2 线缆")
    else:
        print("\n[✗] 修改失败")
        sys.exit(1)


if __name__ == "__main__":
    main()


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值