From f7f63418e36bd039162e6ba416e170027a852995 Mon Sep 17 00:00:00 2001 From: Benjamin Berg Date: Jul 20 2017 14:39:12 +0000 Subject: Add a simple DRM based monitor output test This test is entirely manual for now. An important improvement would be to show if a monitor has been correctly detected and configured by GNOME. --- diff --git a/fed_laptoptest/edid.py b/fed_laptoptest/edid.py index 8b7315d..c70e93d 100644 --- a/fed_laptoptest/edid.py +++ b/fed_laptoptest/edid.py @@ -51,6 +51,28 @@ class Edid(object): raise ValueError('Not a valid EDID.') self.data = data + def iter_descriptors(self): + descr_len = 18 + for offset in [54, 72, 90, 108]: + if offset + descr_len > len(self.data): + continue + yield self.data[offset:offset+descr_len] + + def extract_string(self, bytestr): + idx = bytestr.find('\x0A') + if idx != -1: + return str(bytestr[:idx]) + else: + return str(bytestr) + + @property + def model(self): + for descr in self.iter_descriptors(): + if descr[:5] != b'\x00\x00\x00\xFC\x00': + continue + return self.extract_string(descr[5:]) + + return None def get_size(self): # https://en.wikipedia.org/wiki/Extended_Display_Identification_Data diff --git a/fed_laptoptest/utils/drm.py b/fed_laptoptest/utils/drm.py new file mode 100644 index 0000000..8e06faf --- /dev/null +++ b/fed_laptoptest/utils/drm.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python2 + +from fed_laptoptest import edid + +import os +import sys + +import gi +gi.require_version('GUdev', '1.0') +from gi.repository import GUdev, GObject, GLib + +class Card(object): + def __init__(self, drm_monitor, udev): + self._drm_monitor = drm_monitor + self.udev = udev + self._outputs = dict() + + @property + def descr(self): + parent = self.udev.get_parent() + model_name = parent.get_property('ID_MODEL_FROM_DATABASE') + vendor_name = parent.get_property('ID_VENDOR_FROM_DATABASE') + + if model_name and vendor_name: + return '{:s} - {:s}'.format(vendor_name, model_name) + elif vendor_name: + return '{:s} ({:s})'.format(vendor_name, self.udev.get_name()) + else: + return self.udev.get_name() + + @property + def name(self): + return self.udev.get_name() + + @property + def driver(self): + return self.udev.get_parent().get_sysfs_attr('driver') + + def _sync(self): + for output in self._outputs.itervalues(): + output._sync() + +class Output(object): + def __init__(self, drm_monitor, card, udev): + self._drm_monitor = drm_monitor + self.card = card + self.udev = udev + self.card._outputs[self.name] = self + + self._status = None + self._edid = None + + self._sync() + + @property + def connected(self): + return self._status == 'connected' + + @property + def name(self): + return self.udev.get_name().split('-', 1)[1] + + @property + def monitor(self): + if self._edid is None: + return None + + try: + edid_obj = edid.Edid(self._edid) + return edid_obj.model + except ValueError: + print('Invalid EDID information on %s'.format(self.name)) + return None + + def _resync(self): + self._sync() + return GLib.SOURCE_REMOVE + + def _sync(self): + # GUDev caches the results and there is no uevent for the subdevice + self.udev = self._drm_monitor._client.query_by_sysfs_path(self.udev.get_sysfs_path()) + + old_status = self._status + old_edid = self._edid + + self._status = self.udev.get_sysfs_attr('status') + try: + self._edid = open(os.path.join(self.udev.get_sysfs_path(), 'edid')).read() + except OSError: + self._edid = None + sys.stderr.write('Error reading EDID information on %s, will retry again later.'.format(self.name)) + GLib.timeout_add(500, self._resync) + + if old_status != self._status or old_edid != self._edid: + self._drm_monitor.output_changed.emit(self) + +class DRMMonitor(GObject.Object): + def __init__(self): + GObject.Object.__init__(self) + + self._cards = dict() + + self._client = GUdev.Client.new(['drm']) + self._client.connect('uevent', self._uevent) + + # Initialize the list from an idle handler so that any connect'ed + # signals will be called! + GLib.idle_add(self._idle_init, priority=GLib.PRIORITY_HIGH) + + def _idle_init(self): + for udev in self._client.query_by_subsystem('drm'): + if udev.get_devtype() == 'drm_minor': + card = Card(self, udev) + self._cards[card.name] = card + elif self._get_card_by_name(udev.get_parent().get_name()) is not None: + card = self._get_card_by_name(udev.get_parent().get_name()) + # Assume it is an output, will add itself to the card + output = Output(self, card, udev) + + return GLib.SOURCE_REMOVE + + + def _sync(self): + for c in self._cards.itervalues(): + c._sync() + + + def _get_card_by_name(self, name): + try: + return self._cards[name] + except KeyError: + return None + + def _uevent(self, client, action, device): + c = self._get_card_by_name(device.get_name()) + if c is not None: + c._sync() + + @GObject.Signal(arg_types=[object]) + def output_changed(self, output): + pass + + +if __name__ == '__main__': + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk + + monitor = DRMMonitor() + def output_changed_cb(m, output): + print(output.card.descr, output.card.driver, output.name, output.monitor, output.connected) + monitor.connect('output-changed', output_changed_cb) + Gtk.main() + diff --git a/tests/simple_drm_monitor_plug.py b/tests/simple_drm_monitor_plug.py new file mode 100644 index 0000000..9ea2dcd --- /dev/null +++ b/tests/simple_drm_monitor_plug.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python2 +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# See LICENSE for more details. +# +# Copyright: Red Hat Inc. 2017 +# Author: Benjamin Berg + +import os +import os.path +import re +import evdev +import select +import time + +from avocado import main +from avocado.core import exceptions + +from fed_laptoptest.test import SessionTest, interactive +from fed_laptoptest.ui import Event, TestUI +from fed_laptoptest.utils import drm, ui as uiutils + +from gi.repository import Gtk, GLib + +class SimpleDRMPlugUI(TestUI): + + def update_info(self, output): + row = output.row + hbox = row.get_child() + if not hbox: + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + hbox.img = Gtk.Image() + hbox.pack_start(hbox.img, fill=True, expand=False, padding=5) + + hbox.label = Gtk.Label(xalign=0) + hbox.pack_start(hbox.label, fill=True, expand=True, padding=5) + row.add(hbox) + + if not hasattr(output, 'was_connected'): + output.was_connected = output.connected + + if output.connected: + output.was_connected = True + + hbox.img.set_from_gicon(uiutils.status_to_icon('PASS' if output.was_connected else 'FAIL'), Gtk.IconSize.LARGE_TOOLBAR) + + if not output.connected: + monitor = 'Disconnected' + else: + if output.monitor: + monitor = output.monitor + elif output.name.startswith('eDP'): + monitor = 'Internal Monitor' + else: + monitor = 'Unknown Monitor' + + hbox.label.set_markup( + '{output:s} on card {card:s} ({driver:s})\n' + 'Monitor: {monitor:s}'.format( + card=output.card.descr, + output=output.name, + monitor=monitor, + driver=output.card.driver, + )) + + row.show_all() + + def _output_changed(self, monitor, output): + if not hasattr(output, 'row'): + output.row = Gtk.ListBoxRow() + self.listbox.add(output.row) + self.update_info(output) + + def get_widget(self): + scroll = Gtk.ScrolledWindow() + self.listbox = Gtk.ListBox() + + scroll.add(self.listbox) + scroll.show_all() + + self._monitor = drm.DRMMonitor() + self._monitor.connect('output-changed', self._output_changed) + + return scroll + + +class SimpleDRMPlugTest(SessionTest): + """ + .. title:: Basic Monitor Output Testing + + This test will show the monitors currently detected by the system. Right now + it does *not* yet check whether GNOME has also configured the monitor + correctly and uses it for output. + + Please note that multiple outputs may be listed for a single port (e.g. + a DisplayPort output may have multiple DP or HDMI connections). Some outputs + may only be accessible through a docking station or special adapters. + + This is a simple user driven test. Please only mark it as a failure if you + had an issue with a display not being detected. + + 1. .. class:: action-wait-user + + Any monitor that you plug in should show up on the right side. Please + mark the test as failure if a monitor is not detected for some reason. + Feel free to *Cancel* it if you cannot sufficiently test it at this time. + + :avocado: enable + :avocado: tags=manual + :categories: graphics + """ + + ui_class = SimpleDRMPlugUI + + def test(self): + context = GLib.main_context_default() + + # State is purely decided by the user + self.app_ctrl.status.mark_action('wait-user') + self.app_ctrl.status.wait_finished() + + +if __name__ == "__main__": + main() +