blob: 6ef2597867b37bd8ac3cbb3fcf5996fba988a70d [file] [log] [blame]
# Copyright 2016 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wrapper for EDID data parsing and loading.
See for more info:
https://en.wikipedia.org/wiki/Extended_display_identification_data
"""
import binascii
import fcntl
import glob
import logging
import os
import re
import time
from cros.factory.gooftool.common import Shell
from cros.factory.probe import function
from cros.factory.probe.lib import probe_function
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import file_utils
from cros.factory.utils import sys_utils
# Constants lifted from EDID documentation.
VERSION = 0x01
MAGIC = b'\x00\xff\xff\xff\xff\xff\xff\x00'
MAGIC_OFFSET = 0
MANUFACTURER_ID_OFFSET = 8
PRODUCT_ID_OFFSET = 10
VERSION_OFFSET = 18
REVISION_OFFSET = 19
PIXEL_CLOCK_OFFSET = 54
HORIZONTAL_OFFSET = 56
HORIZONTAL_HIGH_OFFSET = 58
VERTICAL_OFFSET = 59
VERTICAL_HIGH_OFFSET = 61
CHECKSUM_OFFSET = 127
I2C_LVDS_ADDRESS = 0x50
MINIMAL_SIZE = 128
MANUFACTURER_ID_BITS = 5
PREFIX_TEGRA = b'edid[000]'
def Parse(content):
"""Public interface of EDID parser.
Args:
content: EDID data retrieved from device.
"""
# Check if it's from Tegra.
if content.startswith(PREFIX_TEGRA):
return _ParseTegra(content)
return _ParseBinaryBlob(content)
def _ParseTegra(content):
"""Parser for EDID data exported by tegra_edid driver.
When tegra_edid driver is used, the exported EDID is in text format, ex:
edid[000] = 00 ff ff ff ff ff ff 00 06 af 2c 13 00 00 00 00
edid[010] = 00 18 01 03 80 1d 10 78 0a bb f5 94 55 54 90 27
edid[020] = 23 50 54 00 00 00 01 01 01 01 01 01 01 01 01 01
edid[030] = 01 01 01 01 01 01 26 1b 56 64 50 00 16 30 30 20
edid[040] = 36 00 25 a4 10 00 00 18 00 00 00 0f 00 00 00 00
edid[050] = 00 00 00 00 00 00 00 00 00 20 00 00 00 fe 00 41
edid[060] = 55 4f 0a 20 20 20 20 20 20 20 20 20 00 00 00 fe
edid[070] = 00 42 31 33 33 58 54 4e 30 31 2e 33 20 0a 00 4b
Thus, we have to strip the first 12 characters, all white spaces, and all
newline characters, then transform the rest from hex code into a binary blob.
Args:
content: EDID file content from a Tegra device.
"""
return _ParseBinaryBlob(binascii.unhexlify(
re.sub(r'\s|(edid\[\d{3}\] = )', '', content)))
def _ParseBinaryBlob(blob):
"""Binary EDID Parser (light-weight parse-edid replacement).
Simple parsing of EDID. The full-feature parser (parse-edid) has
many more dependencies, and so is too heavy-weight for use here.
TODO(hungte) Use parse-edid if it becomes practical/available.
Specifically once we know that it will be available on all of our
systems.
Args:
blob: a binary blob with encoded EDID.
Returns:
A dict of extracted keys to extracted EDID fields. Return None if
the blob is not a valid EDID record, and also log warning messages
indicating the reason for parsing failure.
"""
def ReadShortBE(offset):
return (blob[offset] << 8) | blob[offset + 1]
def ReadShortLE(offset):
return (blob[offset + 1] << 8) | blob[offset]
# Check size, magic, and version.
if len(blob) < MINIMAL_SIZE:
logging.warning('EDID parsing error: length too small.')
return None
if blob[MAGIC_OFFSET:(MAGIC_OFFSET + len(MAGIC))] != MAGIC:
logging.warning('EDID parse error: incorrect header.')
return None
if blob[VERSION_OFFSET] != VERSION:
logging.warning('EDID parse error: unsupported EDID version.')
return None
# Verify checksum.
if sum(list(blob[:CHECKSUM_OFFSET + 1])) % 0x100 != 0:
logging.warning('EDID parse error: checksum error.')
return None
# Currently we don't support EDID not using pixel clock.
pixel_clock = ReadShortLE(PIXEL_CLOCK_OFFSET)
if not pixel_clock:
logging.warning('EDID parse error: '
'non-pixel clock format is not supported yet.')
return None
# Extract manufacturer.
vendor_name = ''
vendor_code = ReadShortBE(MANUFACTURER_ID_OFFSET)
# vendor_code: [0 | char1 | char2 | char3]
for i in range(2, -1, -1):
vendor_char = (vendor_code >> (i * MANUFACTURER_ID_BITS)) & 0x1F
vendor_char = chr(vendor_char + ord('@'))
vendor_name += vendor_char
product_id = ReadShortLE(PRODUCT_ID_OFFSET)
width = (blob[HORIZONTAL_OFFSET] | ((blob[HORIZONTAL_HIGH_OFFSET] >> 4) << 8))
height = (blob[VERTICAL_OFFSET] | ((blob[VERTICAL_HIGH_OFFSET] >> 4) << 8))
return {'vendor': vendor_name,
'product_id': '%04x' % product_id,
'width': str(width),
'height': str(height)}
def _I2CDump(bus, address, size):
"""Reads binary dump from i2c bus."""
fd = -1
I2C_PERIPHERAL = 0x0703
blob = None
try:
fd = os.open(bus, os.O_RDWR)
if fcntl.ioctl(fd, I2C_PERIPHERAL, address) != 0:
return None
time.sleep(0.05) # Wait i2c to get ready
if os.write(fd, b'\x00') == 1:
blob = os.read(fd, size)
except Exception:
logging.exception('Fail to dump from i2c bus.')
finally:
if fd >= 0:
os.close(fd)
return blob
def LoadFromI2C(path):
"""Runs Parse() against the output of _I2CDump on the specified path.
Args:
path: i2c path, can be either int type (ex: 0) or string type (ex:
'/dev/i2c-0')
Returns:
Parsed I2c output, None if it fails to dump something for the specific I2C.
"""
if isinstance(path, int):
path = '/dev/i2c-%d' % path
command = 'i2cdetect -y -r %s %d %d' % (
path.split('-')[1], I2C_LVDS_ADDRESS, I2C_LVDS_ADDRESS)
# Make sure there is a device in I2C_LVDS_ADDRESS
blob = None
if not '--' in Shell(command).stdout:
blob = _I2CDump(path, I2C_LVDS_ADDRESS, MINIMAL_SIZE)
return None if blob is None else Parse(blob)
def LoadFromFile(path):
return Parse(file_utils.ReadFile(path, encoding=None))
class EDIDFunction(probe_function.ProbeFunction):
# pylint: disable=line-too-long
"""Probe EDID information from file or I2C bus.
Description
-----------
This function tries to search for exported edid files in sysfs matching the
pattern ``/sys/class/drm/*/edid``. If no edid files are found in sysfs, it
probes all i2c buses for EDID blobs.
Once this function finds an EDID data, this function parses the data to
obtain ``vendor``, ``product_id``, ``width`` and ``height`` fields from
the EDID data. The format of an EDID data structure can be found in the
`wiki page
<https://en.wikipedia.org/wiki/Extended_display_identification_data>`_.
The ``vendor`` field this function obtains is actually the manufacturer ID
(byte 8~9); ``product_id`` is the manufacturer product ID
listed in the wiki page.
Examples
--------
If you want this function just outputs all EDID data, the probe statement
is simply::
{
"eval": "edid"
}
Let's assume the output is ::
[
{
"vendor": "IBM",
"product_id": "abcd",
"width": "19200",
"height": "10800",
"sysfs_path": "/sys/class/drm/aa/edid"
},
{
"vendor": "IBX",
"product_id": "1234",
"width": "192",
"height": "108",
"sysfs_path": "/sys/class/drm/bb/edid"
}
]
If you are only interested in the monitor made by IBM, you can modify
the probe statement to ::
{
"eval": "edid",
"expect": {
"vendor": "IBM"
}
}
so that the probed results will only contain the one with ``"vendor": "IBM"``.
Another use case is to ask this function to probe the EDID file in/on a
specific path or i2c bus. For example::
{
"eval": "edid:/dev/i2c-3"
}
"""
path_to_identity = {}
identity_to_edid = {}
ARGS = [
Arg('path', str,
'EDID file path or the number of I2C bus or the path of the i2c '
'bus (i.e. "/dev/i2c-<bus_number>").',
default=None),
]
ROOT_PATH = '/'
I2C_DEVICE_PREFIX = 'dev/i2c-'
SYSFS_PATH_PATTERN = 'sys/class/drm/*/edid'
DEV_PATH_PATTERN = I2C_DEVICE_PREFIX + '[0-9]*'
def Probe(self):
self.MayInitCachedData()
if not self.args.path:
return list(self.identity_to_edid.values())
if self.args.path in self.path_to_identity:
return self.identity_to_edid[self.path_to_identity[self.args.path]]
if self.args.path.isdigit():
path = os.path.join(self.ROOT_PATH,
self.I2C_DEVICE_PREFIX + self.args.path)
if path in self.path_to_identity:
return self.identity_to_edid[self.path_to_identity[path]]
logging.error('The given path is invalid, available edid path are: %r',
list(self.path_to_identity))
return function.NOTHING
@classmethod
def MayInitCachedData(cls):
if cls.path_to_identity:
return
for pattern_type, glob_pattern in [
('sysfs_path', os.path.join(cls.ROOT_PATH, cls.SYSFS_PATH_PATTERN)),
('dev_path', os.path.join(cls.ROOT_PATH, cls.DEV_PATH_PATTERN))]:
for path in glob.glob(glob_pattern):
result = cls.ProbeEDID(path)
if not result:
continue
identity = (result['vendor'], result['product_id'])
cls.path_to_identity[path] = identity
cls.identity_to_edid.setdefault(identity, result)
cls.identity_to_edid[identity][pattern_type] = path
# If we already get results from sysfs, we don't need to probe i2c.
if cls.path_to_identity:
break
@classmethod
def ProbeEDID(cls, path):
if path.startswith(os.path.join(cls.ROOT_PATH, cls.I2C_DEVICE_PREFIX)):
sys_utils.LoadKernelModule('i2c_dev')
parsed_edid = LoadFromI2C(path)
else:
parsed_edid = LoadFromFile(path)
return parsed_edid