diff --git a/monitorDaemon.py b/monitorDaemon.py new file mode 100755 --- /dev/null +++ b/monitorDaemon.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python +import subprocess +import os +import hashlib +import binascii +import re +import sys +import configparser +from Xlib import X, display, Xutil +from Xlib.ext import randr + +class xrandrInterface: + def __init__(self): + self.display = display.Display() + self.screen = self.display.screen() + self.window = self.screen.root.create_window(50, 50, 300, 200, 2, + self.screen.root_depth, + event_mask = (X.ExposureMask | + X.StructureNotifyMask | + X.ButtonPressMask | + X.ButtonReleaseMask | + X.Button1MotionMask) + ) + #self.window.xrandr_select_input( + # randr.RRScreenChangeNotifyMask + # | randr.RRCrtcChangeNotifyMask + # | randr.RROutputChangeNotifyMask + # | randr.RROutputPropertyNotifyMask + # ) + self.outputStatus = {} + self.crtcStatus = {} + self.getScreenInformation() + + def getScreenInformation(self): + resources = self.window.xrandr_get_screen_resources()._data + outputs = resources['outputs'] + self.outputStatus = {} + self.crtcStatus = {} + for crtc in resources['crtcs']: + crtcinfo = self.display.xrandr_get_crtc_info(crtc, resources['config_timestamp'])._data + self.crtcStatus[crtc] = {} + #self.crtcStatus[crtc]['width'] = crtcinfo['width'] + #self.crtcStatus[crtc]['height'] = crtcinfo['height'] + self.crtcStatus[crtc]['x'] = crtcinfo['x'] + self.crtcStatus[crtc]['y'] = crtcinfo['y'] + self.crtcStatus[crtc]['mode'] = crtcinfo['mode'] + self.crtcStatus[crtc]['outputs'] = crtcinfo['outputs'] + self.crtcStatus[crtc]['possible_outputs'] = crtcinfo['possible_outputs'] + + modedata = self.parseModes(resources['mode_names'], resources['modes']) + #print(self.crtcStatus) + for output in outputs: + info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data + #print(info) + if info['connection'] != 0: + continue + edid = self.get_edid(output) + + name = info['name'] + crtc = info['crtc'] + if crtc == 0: + continue + else: + mode = self.crtcStatus[crtc]['mode'] + modename = modedata[mode]['name'] + posx = self.crtcStatus[crtc]['x'] + posy = self.crtcStatus[crtc]['y'] + + self.outputStatus[name] = {} + self.outputStatus[name]['edid'] = edid + self.outputStatus[name]['crtc'] = crtc + self.outputStatus[name]['modename'] = modename + self.outputStatus[name]['posx'] = posx + self.outputStatus[name]['posy'] = posy + #print(self.display.xrandr_list_output_properties(output)._data) + """ + print(self.outputStatus) + op = self.getOutputForOutputname('eDP1') + print(op) + md = self.findModeByName(op, '1920x1080') + print(md) + cr = self.getUsableCrtcForOutput(op) + if not cr: + #print('Disabling CRTC') + cr = self.getCurrentCrtcForOutput(op) + self.disableCrtc(cr) + else: + #print('Enabling CRTC') + print(cr) + self.configureCrtcForOutputAndMode(cr, op, md, 0, 0) + """ + + def applyConfiguration(self, config): + # Get list of all outputs + # Disable all outputs not in the config + # Check if an output already has a CRTC + # If yes, reconfigure the current CRTC + # If not, find a new CRTC and configure it accordingly + print('FIXME') + pass + + def getScreenConfiguration(self): + return self.outputStatus + + def disableCrtc(self, crtc): + resources = self.window.xrandr_get_screen_resources()._data + self.display.xrandr_set_crtc_config(65, resources['config_timestamp'], 0, 0, 0, randr.Rotate_0, []) + + def getCurrentCrtcForOutput(self, output): + resources = self.window.xrandr_get_screen_resources()._data + info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data + return info['crtc'] + + def configureCrtcForOutputAndMode(self, crtc, output, mode, posx, posy): + resources = self.window.xrandr_get_screen_resources()._data + self.display.xrandr_set_crtc_config(crtc, resources['config_timestamp'], posx, posy, mode, randr.Rotate_0, [output]) + + def getOutputForOutputname(self, name): + resources = self.window.xrandr_get_screen_resources()._data + for output in resources['outputs']: + info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data + if info['name'] == name: + return output + return None + + def getUsableCrtcForOutput(self, output): + resources = self.window.xrandr_get_screen_resources()._data + for crtc in resources['crtcs']: + crtcinfo = self.display.xrandr_get_crtc_info(crtc, resources['config_timestamp'])._data + if len(crtcinfo['outputs']) == 0: + if output in crtcinfo['possible_outputs']: + return crtc + return None + + def findModeByName(self, output, modename): + resources = self.window.xrandr_get_screen_resources()._data + modedata = self.parseModes(resources['mode_names'], resources['modes']) + info = self.display.xrandr_get_output_info(output, resources['config_timestamp'])._data + modes = info['modes'] + for mode in modes: + if modedata[mode]['name'] == modename: + return mode + return None + + def parseModes(self, mode_names, modes): + lastIdx = 0 + modedatas = dict() + for mode in modes: + modedata = dict(mode._data) + modedata['name'] = mode_names[lastIdx:lastIdx + modedata['name_length']] + modedatas[modedata['id']] = modedata + lastIdx += modedata['name_length'] + return modedatas + + # query the edid module for output_nr + def get_edid(self, output_nr): + PROPERTY_EDID = self.display.intern_atom("EDID", 0) + INT_TYPE = 19 + + props = self.display.xrandr_list_output_properties(output_nr) + if PROPERTY_EDID in props.atoms: + try: + rawedid = self.display.xrandr_get_output_property(output_nr, PROPERTY_EDID, INT_TYPE, 0, 400) + except: + print("Error loading EDID data of output ", output_nr) + return None + edidstream = rawedid._data['value'] + hx = bytearray() + hx.extend(edidstream) + hs = hashlib.sha1(hx).hexdigest() + + return hs + else: + return None + +class ConfigManager: + def __init__(self, cfgfile): + self.cfgfile = cfgfile + if not os.path.exists(cfgfile): + self.recreateCfg() + self.dic = {} + self.cfg2dic() + + def getDic(self): + return self.dic + + def recreateCfg(self): + print('Creating new config file: ' + self.cfgfile) + config = configparser.ConfigParser() + config.add_section('General') + config.set('General', 'numEntries', '0') + with open(self.cfgfile, 'w') as configfile: + config.write(configfile) + + def cfg2dic(self): + dic = {} + config = configparser.ConfigParser() + config.read(self.cfgfile) + + for section in config.sections(): + dic[section] = {} + for key in config[section]: + dic[section][key] = config.get(section, key) + + self.dic = dic + return dic + + def dic2cfg(self): + config = configparser.ConfigParser() + for section in self.dic: + config.add_section(section) + for key in self.dic[section]: + config.set(section, key, self.dic[section][key]) + with open(self.cfgfile, 'w') as configfile: + config.write(configfile) + + def getConfig(self, screenConfiguration): + ne = int(self.dic['General']['numentries']) + for ii in range(0, ne): + name = 'Config' + str(ii+1) + if int(self.dic[name]['edidcount']) != len(screenConfiguration): + continue + found = True + for outputName in screenConfiguration: + lFound = False + for jj in range(0, len(screenConfiguration)): + if self.dic[name]['edid' + str(jj+1)] == screenConfiguration[outputName]['edid']: + lFound = True + if lFound and found: + found = True + else: + found = False + if found: + for jj in range(0, len(screenConfiguration)): + return self.dic[name] + return None + + def saveConfiguration(self, screenConfiguration): + ne = int(self.dic['General']['numentries']) + ne += 1 + self.dic['General']['numentries'] = str(ne) + name = 'Config' + str(ne) + self.dic[name] = {} + self.dic[name]['edidcount'] = str(len(screenConfiguration)) + cnt = 1 + for outputName in screenConfiguration.keys(): + self.dic[name]['edid'+str(cnt)] = screenConfiguration[outputName]['edid'] + self.dic[name]['output'+str(cnt)] = outputName + self.dic[name]['mode'+str(cnt)] = screenConfiguration[outputName]['modename'] + self.dic[name]['posx'+str(cnt)] = str(screenConfiguration[outputName]['posx']) + self.dic[name]['posy'+str(cnt)] = str(screenConfiguration[outputName]['posy']) + cnt += 1 + self.dic2cfg(cfgfile) + return True + + def loadAndApplyConfig(self, dic, edids): + cfg = getConfig(dic, edids) + print(cfg) + for edid in cfg: + #FIXME: This does not work with negative numbers! + if edid[2] == '0': + subprocess.call(['xrandr', '--output', edid[1], '--off']) + continue + modepos = re.findall(r'\d+', edid[2]) + mode = modepos[0] + 'x' + modepos[1] + pos = modepos[2] + 'x' + modepos[3] + print(pos) + subprocess.call(['xrandr', '--output', edid[1], '--mode', mode, '--pos', pos]) + return True + + + + +settingsdir = os.path.join(os.environ['HOME'], '.config') +cfgfile = os.path.join(settingsdir, 'monitorDaemon.ini') +cm = ConfigManager(cfgfile) + +cfg = cm.getDic() +#print(cfg) + +xri = xrandrInterface() +screenConfig = xri.getScreenConfiguration() +#print(screenConfig) + +storedConfig = cm.getConfig(screenConfig) + +if storedConfig: + print('LoadAndApply') + xri.applyConfiguration(screenConfig) +else: + print('SaveConfig') + cm.saveConfiguration(screenConfig) + +sys.exit(0) +