$ ddcutil --display 1 getvcp 10
VCP code 0x10 (Brightness): current value = 83, max value = 100
$ ddcutil --display 1 getvcp 10
VCP code 0x10 (Brightness): current value = 83, max value = 100
$ ddcutil --display 1 getvcp 10
VCP code 0x10 (Brightness): current value = 83, max value = 100
Model: 27M2N5500Q
MCCS version: 2.2
VCP Features: Feature: 10 (Brightness) Feature: 12 (Contrast) Feature: 14 (Select color preset) Values: 02 04 05 06 08 0B Feature: 60 (Input Source) Values: 11 12 0F ...
Model: 27M2N5500Q
MCCS version: 2.2
VCP Features: Feature: 10 (Brightness) Feature: 12 (Contrast) Feature: 14 (Select color preset) Values: 02 04 05 06 08 0B Feature: 60 (Input Source) Values: 11 12 0F ...
Model: 27M2N5500Q
MCCS version: 2.2
VCP Features: Feature: 10 (Brightness) Feature: 12 (Contrast) Feature: 14 (Select color preset) Values: 02 04 05 06 08 0B Feature: 60 (Input Source) Values: 11 12 0F ...
Line 18: Feature: 10 (Brightness)
Line 19: Feature: 12 (Contrast)
...
Line 178: Feature: E2 (Manufacturer specific feature)
Line 179: Feature: A0 (6 axis hue control: Magenta)
Line 180: Feature: 10 (Brightness)
Line 181: Values: 00 01 02 03 04 (interpretation unavailable)
Line 182: Feature: E2 (Manufacturer specific feature)
Line 18: Feature: 10 (Brightness)
Line 19: Feature: 12 (Contrast)
...
Line 178: Feature: E2 (Manufacturer specific feature)
Line 179: Feature: A0 (6 axis hue control: Magenta)
Line 180: Feature: 10 (Brightness)
Line 181: Values: 00 01 02 03 04 (interpretation unavailable)
Line 182: Feature: E2 (Manufacturer specific feature)
Line 18: Feature: 10 (Brightness)
Line 19: Feature: 12 (Contrast)
...
Line 178: Feature: E2 (Manufacturer specific feature)
Line 179: Feature: A0 (6 axis hue control: Magenta)
Line 180: Feature: 10 (Brightness)
Line 181: Values: 00 01 02 03 04 (interpretation unavailable)
Line 182: Feature: E2 (Manufacturer specific feature)
feature_map = {}
for feature_text in capabilities_text.split(' Feature: '): if feature_match := _FEATURE_PATTERN.match(feature_text): vcp_code = feature_match.group(1) # ... figure out vcp_type and values ... feature_map[vcp_code] = VcpCapability(vcp_code, ...)
return feature_map
feature_map = {}
for feature_text in capabilities_text.split(' Feature: '): if feature_match := _FEATURE_PATTERN.match(feature_text): vcp_code = feature_match.group(1) # ... figure out vcp_type and values ... feature_map[vcp_code] = VcpCapability(vcp_code, ...)
return feature_map
feature_map = {}
for feature_text in capabilities_text.split(' Feature: '): if feature_match := _FEATURE_PATTERN.match(feature_text): vcp_code = feature_match.group(1) # ... figure out vcp_type and values ... feature_map[vcp_code] = VcpCapability(vcp_code, ...)
return feature_map
Feature: 10 (Brightness) Values: 20..90
Feature: 10 (Brightness) Values: 20..90
Feature: 10 (Brightness) Values: 20..90
_RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)..([0-9]+)')
_RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)..([0-9]+)')
_RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)..([0-9]+)')
- _RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)..([0-9]+)')
+ _RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)[.][.]([0-9]+)')
- _RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)..([0-9]+)')
+ _RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)[.][.]([0-9]+)')
- _RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)..([0-9]+)')
+ _RANGE_PATTERN = re.compile(r'Values:\s+([0-9]+)[.][.]([0-9]+)') - Laptop: Kubuntu 24.04, KDE Plasma 5.27 on X11, Intel iGPU
- The new monitor: Philips Evnia 27M2N5500Q, 27" 2560x1440, connected over HDMI
- The tool: vdu_controls — a small Qt tray app that lets you adjust brightness/contrast/etc. on external monitors. It's the closest thing Linux has to Windows' Twinkle Tray. - The backlight intensity
- Contrast, color balance, gamma curves
- Which physical input is active (HDMI-1 / HDMI-2 / DisplayPort)
- The on-screen menu (OSD) you see when you press the button on the back
- Sometimes audio, USB hub switching, HDR mode, KVM - Continuous (C): a number on a range. Brightness 0x10 is C — pick any value between 0 and a maximum the monitor reports (usually 100). Like a slider.
- Non-Continuous (NC): pick from a fixed list. Input source 0x60 is NC — only specific values like 0x11 = HDMI-1, 0x12 = HDMI-2, 0x0F = DisplayPort-1 mean anything. Like a dropdown. - ddcutil — the command-line client. Opens /dev/i2c-N (the kernel's interface to the tiny serial bus inside your video cable) and speaks DDC/CI directly. Lets you do ddcutil --display 1 setvcp 10 50.
- vdu_controls — a Qt tray GUI built on top of ddcutil. It calls ddcutil capabilities once per monitor at startup, parses the capability string, and renders sliders or dropdowns based on what each feature's type turns out to be. When you drag a slider, it shells out to ddcutil setvcp to push the new value. - feature_map is a dict keyed by VCP code. If the same code is parsed twice, the second assignment silently overwrites the first.
- The type-classification logic (Continuous vs Non-Continuous) is based on whether a Values: block was found for that occurrence. - First pass through Feature: 10: no Values: block → classified as Continuous → stored as "brightness, 0..(max from getvcp)" → good.
- Second pass through Feature: 10: has a Values: block → classified as Non-Continuous → stored as "brightness, discrete options 00/01/02/03/04" → overwrites the first entry. - 00 → first capture group
- 0 (space + the next 0, both matched by the unescaped ..)
- 1 → second capture group - The Philips firmware double-lists Feature: 10 and dumps garbage values on the second copy.
- A regex bug interprets that garbage as a restricted range of 0..1.
- The dict-overwrite means the corrupted range definition wins over the correct one.
- The widget renders a 0..1 slider. - PR: https://github.com/digitaltrails/vdu_controls/pull/128
- Bug report: https://github.com/digitaltrails/vdu_controls/issues/127
- Maintainer's follow-up regex fix: 6d72a377
- vdu_controls: https://github.com/digitaltrails/vdu_controls
- ddcutil: https://www.ddcutil.com/
- VESA MCCS 2.2 spec (paywalled, but described in the ddcutil docs)