mirror of
https://github.com/phillipberndt/autorandr
synced 2025-05-09 11:37:54 +00:00
Extended --match-edid to respect a device's serial
Autorandr parses a device's EDID to extract its serial number. If --match-edid is used and the serial is available, a profile is first matched based on the serial number. If not available, the old EDID-based behavior is used. This way a profile can be matched precisly, even if the EDID data changes. This might be the case if a monitor's connection is switched from HDMI to VGA, for example.
This commit is contained in:
parent
6a0c9873cd
commit
d9b195311d
@ -37,6 +37,7 @@ Contributors to this version of autorandr are:
|
|||||||
|
|
||||||
* Adrián López
|
* Adrián López
|
||||||
* andersonjacob
|
* andersonjacob
|
||||||
|
* Alexander Lochmann
|
||||||
* Alexander Wirt
|
* Alexander Wirt
|
||||||
* Brice Waegeneire
|
* Brice Waegeneire
|
||||||
* Chris Dunder
|
* Chris Dunder
|
||||||
|
68
autorandr.py
68
autorandr.py
@ -238,7 +238,7 @@ class XrandrOutput(object):
|
|||||||
EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
|
EDID_UNAVAILABLE = "--CONNECTED-BUT-EDID-UNAVAILABLE-"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s%s %s>" % (self.output, self.short_edid, " ".join(self.option_vector))
|
return "<%s%s %s>" % (self.output, self.fingerprint, " ".join(self.option_vector))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def short_edid(self):
|
def short_edid(self):
|
||||||
@ -317,8 +317,32 @@ class XrandrOutput(object):
|
|||||||
self.edid = edid
|
self.edid = edid
|
||||||
self.options = options
|
self.options = options
|
||||||
self.ignored_options = []
|
self.ignored_options = []
|
||||||
|
self.parse_serial_from_edid()
|
||||||
self.remove_default_option_values()
|
self.remove_default_option_values()
|
||||||
|
|
||||||
|
def parse_serial_from_edid(self):
|
||||||
|
self.serial = None
|
||||||
|
if self.edid:
|
||||||
|
# Thx to pyedid project, the following code was
|
||||||
|
# copied (and modified) from pyedid/__init__py:21 [parse_edid()]
|
||||||
|
raw = bytes.fromhex(self.edid)
|
||||||
|
# Check EDID header, and checksum
|
||||||
|
if raw[:8] != b'\x00\xff\xff\xff\xff\xff\xff\x00' or sum(raw) % 256 != 0:
|
||||||
|
return
|
||||||
|
serial_no = int.from_bytes(raw[15:11:-1], byteorder='little')
|
||||||
|
|
||||||
|
serial_text = None
|
||||||
|
# Offsets of standard timing information descriptors 1-4
|
||||||
|
# (see https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#EDID_1.4_data_format)
|
||||||
|
for timing_bytes in (raw[54:72], raw[72:90], raw[90:108], raw[108:126]):
|
||||||
|
if timing_bytes[0:2] == b'\x00\x00':
|
||||||
|
timing_type = timing_bytes[3]
|
||||||
|
if timing_type == 0xFF:
|
||||||
|
buffer = timing_bytes[5:]
|
||||||
|
buffer = buffer.partition(b'\x0a')[0]
|
||||||
|
serial_text = buffer.decode('cp437')
|
||||||
|
self.serial = serial_text if serial_text else "0x{:x}".format(serial_no) if serial_no != 0 else None
|
||||||
|
|
||||||
def set_ignored_options(self, options):
|
def set_ignored_options(self, options):
|
||||||
"Set a list of xrandr options that are never used (neither when comparing configurations nor when applying them)"
|
"Set a list of xrandr options that are never used (neither when comparing configurations nor when applying them)"
|
||||||
self.ignored_options = list(options)
|
self.ignored_options = list(options)
|
||||||
@ -466,6 +490,16 @@ class XrandrOutput(object):
|
|||||||
|
|
||||||
return XrandrOutput(output, edid, options)
|
return XrandrOutput(output, edid, options)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fingerprint(self):
|
||||||
|
return str(self.serial) if self.serial else self.short_edid
|
||||||
|
|
||||||
|
def fingerprint_equals(self, other):
|
||||||
|
if self.serial and other.serial:
|
||||||
|
return self.serial == other.serial
|
||||||
|
else:
|
||||||
|
return self.edid_equals(other)
|
||||||
|
|
||||||
def edid_equals(self, other):
|
def edid_equals(self, other):
|
||||||
"Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
|
"Compare to another XrandrOutput's edid and on/off-state, taking legacy autorandr behaviour (md5sum'ing) into account"
|
||||||
if self.edid and other.edid:
|
if self.edid and other.edid:
|
||||||
@ -483,13 +517,13 @@ class XrandrOutput(object):
|
|||||||
return not (self == other)
|
return not (self == other)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.edid_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
|
return self.fingerprint_equals(other) and self.output == other.output and self.filtered_options == other.filtered_options
|
||||||
|
|
||||||
def verbose_diff(self, other):
|
def verbose_diff(self, other):
|
||||||
"Compare to another XrandrOutput and return a list of human readable differences"
|
"Compare to another XrandrOutput and return a list of human readable differences"
|
||||||
diffs = []
|
diffs = []
|
||||||
if not self.edid_equals(other):
|
if not self.fingerprint_equals(other):
|
||||||
diffs.append("EDID `%s' differs from `%s'" % (self.short_edid, other.short_edid))
|
diffs.append("EDID `%s' differs from `%s'" % (self.fingerprint, other.fingerprint))
|
||||||
if self.output != other.output:
|
if self.output != other.output:
|
||||||
diffs.append("Output name `%s' differs from `%s'" % (self.output, other.output))
|
diffs.append("Output name `%s' differs from `%s'" % (self.output, other.output))
|
||||||
if "off" in self.options and "off" not in other.options:
|
if "off" in self.options and "off" not in other.options:
|
||||||
@ -648,33 +682,33 @@ def match_asterisk(pattern, data):
|
|||||||
|
|
||||||
|
|
||||||
def update_profiles_edid(profiles, config):
|
def update_profiles_edid(profiles, config):
|
||||||
edid_map = {}
|
fp_map = {}
|
||||||
for c in config:
|
for c in config:
|
||||||
if config[c].edid is not None:
|
if config[c].fingerprint is not None:
|
||||||
edid_map[config[c].edid] = c
|
fp_map[config[c].fingerprint] = c
|
||||||
|
|
||||||
for p in profiles:
|
for p in profiles:
|
||||||
profile_config = profiles[p]["config"]
|
profile_config = profiles[p]["config"]
|
||||||
|
|
||||||
for edid in edid_map:
|
for fingerprint in fp_map:
|
||||||
for c in list(profile_config.keys()):
|
for c in list(profile_config.keys()):
|
||||||
if profile_config[c].edid != edid or c == edid_map[edid]:
|
if profile_config[c].fingerprint != fingerprint or c == fp_map[fingerprint]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print("%s: renaming display %s to %s" % (p, c, edid_map[edid]))
|
print("%s: renaming display %s to %s" % (p, c, fp_map[fingerprint]))
|
||||||
|
|
||||||
tmp_disp = profile_config[c]
|
tmp_disp = profile_config[c]
|
||||||
|
|
||||||
if edid_map[edid] in profile_config:
|
if fp_map[fingerprint] in profile_config:
|
||||||
# Swap the two entries
|
# Swap the two entries
|
||||||
profile_config[c] = profile_config[edid_map[edid]]
|
profile_config[c] = profile_config[fp_map[fingerprint]]
|
||||||
profile_config[c].output = c
|
profile_config[c].output = c
|
||||||
else:
|
else:
|
||||||
# Object is reassigned to another key, drop this one
|
# Object is reassigned to another key, drop this one
|
||||||
del profile_config[c]
|
del profile_config[c]
|
||||||
|
|
||||||
profile_config[edid_map[edid]] = tmp_disp
|
profile_config[fp_map[fingerprint]] = tmp_disp
|
||||||
profile_config[edid_map[edid]].output = edid_map[edid]
|
profile_config[fp_map[fingerprint]].output = fp_map[fingerprint]
|
||||||
|
|
||||||
|
|
||||||
def find_profiles(current_config, profiles):
|
def find_profiles(current_config, profiles):
|
||||||
@ -684,12 +718,12 @@ def find_profiles(current_config, profiles):
|
|||||||
config = profile["config"]
|
config = profile["config"]
|
||||||
matches = True
|
matches = True
|
||||||
for name, output in config.items():
|
for name, output in config.items():
|
||||||
if not output.edid:
|
if not output.fingerprint:
|
||||||
continue
|
continue
|
||||||
if name not in current_config or not output.edid_equals(current_config[name]):
|
if name not in current_config or not output.fingerprint_equals(current_config[name]):
|
||||||
matches = False
|
matches = False
|
||||||
break
|
break
|
||||||
if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].edid)):
|
if not matches or any((name not in config.keys() for name in current_config.keys() if current_config[name].fingerprint)):
|
||||||
continue
|
continue
|
||||||
if matches:
|
if matches:
|
||||||
closeness = max(match_asterisk(output.edid, current_config[name].edid), match_asterisk(
|
closeness = max(match_asterisk(output.edid, current_config[name].edid), match_asterisk(
|
||||||
|
Loading…
Reference in New Issue
Block a user