Skip to content

Commit 05ad0c0

Browse files
committed
Add UIDs to all Linux brightness methods
The UID here is based on the I2C path of the display, which is easily exposed for I2C and DDCutil. For SysFiles and XRandr, the process is a bit more complicated. It relies on the idea that we can link the entries in `/sys/class/backlight` to `/sys/class/drm` which should contain a link to the I2C system. For SysFiles, we expand the `/sys/class/backlight/*/device` symlink to expose the path to the underlying display connection (in this case, usually eDP). We can follow this link to the equivalent path in `/sys/class/drm` which will usually have an `i2c-X` or `ddc/i2c-dev` folder inside that maps to a relevant I2C connection. On XRandr, it's a similar process, except we start by looking in `/sys/class/drm` and matching the connection names to our interface name. For example: HDMI-1 -> card0-HDMI-A-1 or DP-1 -> card0-DP-1. We can then find the I2C connection by browsing the folder tree. I have tested this on multiple devices and in all cases, both laptop and desktop displays were able to be uniquely identified in this manner across brightness methods. This means that displays can avoid being duplicated across all methods even if there is no EDID information available. There are a few cases in which this doesn't work. On my Pinebook Pro, the edp-backlight subsystem seems to be a completely different subsystem to the actual display, with no tangible connection between the two. In this case, a UID cannot be found. On a laptop running a Fedora Wayland live CD, there was no `intel_backlight` folder, which meant that no UID was found. On the same laptop running X11 mint, `intel_backlight` was present and the UID was found so I believe this is a driver/kernel module issue.
1 parent 011a847 commit 05ad0c0

File tree

1 file changed

+83
-4
lines changed

1 file changed

+83
-4
lines changed

screen_brightness_control/linux.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def get_display_info(cls, display: Optional[DisplayIdentifier] = None) -> List[d
4242
displays_by_edid = {}
4343
index = 0
4444

45+
# map drm devices to their pci device paths
46+
drm_paths = {}
47+
for folder in glob.glob('/sys/class/drm/card*-*'):
48+
drm_paths[os.path.realpath(folder)] = folder
49+
4550
for subsystem in subsystems:
4651

4752
device: dict = {
@@ -54,7 +59,8 @@ def get_display_info(cls, display: Optional[DisplayIdentifier] = None) -> List[d
5459
'manufacturer': None,
5560
'manufacturer_id': None,
5661
'edid': None,
57-
'scale': None
62+
'scale': None,
63+
'uid': None
5864
}
5965

6066
for folder in subsystem:
@@ -77,6 +83,12 @@ def get_display_info(cls, display: Optional[DisplayIdentifier] = None) -> List[d
7783
)
7884
continue
7985

86+
# check if backlight subsystem device matches any of the PCI devices discovered earlier
87+
# if so, extract the i2c bus from the drm device folder
88+
pci_path = os.path.realpath(f'/sys/class/backlight/{folder}/device')
89+
if pci_path in drm_paths:
90+
device['uid'] = device['uid'] or i2c_bus_from_drm_device(drm_paths[pci_path])
91+
8092
if os.path.isfile('%s/device/edid' % device['path']):
8193
device['edid'] = EDID.hexdump('%s/device/edid' % device['path'])
8294

@@ -371,7 +383,8 @@ def get_display_info(cls, display: Optional[DisplayIdentifier] = None) -> List[d
371383
'index': index,
372384
# convert edid to hex string
373385
'edid': ''.join(f'{i:02x}' for i in edid),
374-
'i2c_bus': i2c_path
386+
'i2c_bus': i2c_path,
387+
'uid': i2c_path.split('-')[-1]
375388
}
376389
)
377390
index += 1
@@ -439,6 +452,37 @@ class XRandr(BrightnessMethodAdv):
439452
executable: str = 'xrandr'
440453
'''the xrandr executable to be called'''
441454

455+
@staticmethod
456+
def _get_uid(interface: str) -> Optional[str]:
457+
'''
458+
Attempts to find a UID (I2C bus path) for a given display interface.
459+
460+
This works by parsing the interface name and matching it up to the entries in `/sys/class/drm`.
461+
`i2c_bus_from_drm_device` is then used to extract the bus number
462+
463+
Args:
464+
interface: the interface in question. EG: `eDP-1`, `eDP1`, `HDMI-1`...
465+
466+
Returns:
467+
The bus number as a string if found. Otherwise, none.
468+
'''
469+
if not os.path.isdir('/sys/class/drm'):
470+
return None
471+
472+
# use regex because sometimes it can be `eDP-1` and sometimes it's `eDP1`
473+
if interface_match := re.match(r'([a-z]+)-?(\d+)', interface, re.I):
474+
interface, count = interface_match.groups()
475+
else:
476+
return None
477+
478+
for dir in os.listdir('/sys/class/drm/'):
479+
# use regex here for case insensitivity on the interface
480+
if not re.match(r'card\d+-%s(?:-[A-Z])?-%s' % (interface, count), dir, re.I):
481+
continue
482+
dir = f'/sys/class/drm/{dir}'
483+
if bus := i2c_bus_from_drm_device(dir):
484+
return bus
485+
442486
@classmethod
443487
def _gdi(cls):
444488
'''
@@ -472,7 +516,8 @@ def _gdi(cls):
472516
'manufacturer': None,
473517
'manufacturer_id': None,
474518
'edid': None,
475-
'unsupported': line.startswith('XWAYLAND') or 'WAYLAND_DISPLAY' in os.environ
519+
'unsupported': line.startswith('XWAYLAND') or 'WAYLAND_DISPLAY' in os.environ,
520+
'uid': cls._get_uid(line.split(' ')[0])
476521
}
477522
display_count += 1
478523

@@ -606,14 +651,16 @@ def _gdi(cls):
606651
'manufacturer': None,
607652
'manufacturer_id': None,
608653
'edid': None,
609-
'unsupported': 'invalid display' in line.lower()
654+
'unsupported': 'invalid display' in line.lower(),
655+
'uid': None
610656
}
611657
display_count += 1
612658

613659
elif 'I2C bus' in line:
614660
tmp_display['i2c_bus'] = line[line.index('/'):]
615661
tmp_display['bus_number'] = int(
616662
tmp_display['i2c_bus'].replace('/dev/i2c-', ''))
663+
tmp_display['uid'] = tmp_display['i2c_bus'].split('-')[-1]
617664

618665
elif 'Mfg id' in line:
619666
# Recently ddcutil has started reporting manufacturer IDs like
@@ -739,6 +786,38 @@ def set_brightness(cls, value: IntPercentage, display: Optional[int] = None):
739786
)
740787

741788

789+
def i2c_bus_from_drm_device(dir: str) -> Optional[str]:
790+
'''
791+
Extract the relevant I2C bus number from a device in `/sys/class/drm`.
792+
793+
This function works by searching the directory for `i2c-*` and `ddc/i2c-dev/i2c-*` folders.
794+
795+
Args:
796+
dir: the DRM directory, in the format `/sys/class/drm/<device>`
797+
798+
Returns:
799+
Returns the I2C bus number as a string if found. Otherwise, returns None
800+
'''
801+
# check for enabled file and skip device if monitor inactive
802+
if os.path.isfile(f'{dir}/enabled'):
803+
with open(f'{dir}/enabled') as f:
804+
if f.read().strip().lower() != 'enabled':
805+
return
806+
807+
# sometimes the i2c path is in /sys/class/drm/*/i2c-*
808+
# do this first because, in my testing, sometimes a device can have both `.../i2c-X` and `.../ddc/i2c-dev/...`
809+
# and the latter is usually the wrong i2c path
810+
paths = glob.glob(f'{dir}/i2c-*')
811+
if paths:
812+
return paths[0].split('-')[-1]
813+
814+
# sometimes the i2c path is in /sys/class/drm/*/ddc/i2c-dev
815+
if os.path.isdir(f'{dir}/ddc/i2c-dev'):
816+
paths = os.listdir(f'{dir}/ddc/i2c-dev')
817+
if paths:
818+
return paths[0].replace('i2c-', '')
819+
820+
742821
def list_monitors_info(
743822
method: Optional[str] = None, allow_duplicates: bool = False, unsupported: bool = False
744823
) -> List[dict]:

0 commit comments

Comments
 (0)