brightness: avoid DDC on internal panels, add timeouts, auto-blacklist bad DDC buses

Signed-off-by: Oleksiy Nedobiychuk <oleksiy12345@live.it>
This commit is contained in:
Oleksiy Nedobiychuk 2025-08-29 00:43:52 +02:00
parent 3cc8c8fb03
commit 5dedf5c1b5

View file

@ -11,44 +11,46 @@ Singleton {
property list<var> ddcMonitors: [] property list<var> ddcMonitors: []
readonly property list<Monitor> monitors: variants.instances readonly property list<Monitor> monitors: variants.instances
property bool appleDisplayPresent: false property bool appleDisplayPresent: false
// Blacklist DDC buses that error or hang
property var ddcBlacklist: []
function getMonitorForScreen(screen: ShellScreen): var { function getMonitorForScreen(screen: ShellScreen): var {
return monitors.find(m => m.modelData === screen) return monitors.find(m => m.modelData === screen);
} }
function getAvailableMethods(): list<string> { function getAvailableMethods(): list<string> {
var methods = [] var methods = [];
if (monitors.some(m => m.isDdc)) if (monitors.some(m => m.isDdc))
methods.push("ddcutil") methods.push("ddcutil");
if (monitors.some(m => !m.isDdc)) if (monitors.some(m => !m.isDdc))
methods.push("internal") methods.push("internal");
if (appleDisplayPresent) if (appleDisplayPresent)
methods.push("apple") methods.push("apple");
return methods return methods;
} }
// Global helpers for IPC and shortcuts // Global helpers for IPC and shortcuts
function increaseBrightness(): void { function increaseBrightness(): void {
monitors.forEach(m => m.increaseBrightness()) monitors.forEach(m => m.increaseBrightness());
} }
function decreaseBrightness(): void { function decreaseBrightness(): void {
monitors.forEach(m => m.decreaseBrightness()) monitors.forEach(m => m.decreaseBrightness());
} }
function getDetectedDisplays(): list<var> { function getDetectedDisplays(): list<var> {
return detectedDisplays return detectedDisplays;
} }
reloadableId: "brightness" reloadableId: "brightness"
Component.onCompleted: { Component.onCompleted: {
Logger.log("Brightness", "Service started") Logger.log("Brightness", "Service started");
} }
onMonitorsChanged: { onMonitorsChanged: {
ddcMonitors = [] ddcMonitors = [];
ddcProc.running = true ddcProc.running = true;
} }
Variants { Variants {
@ -69,19 +71,20 @@ Singleton {
// Detect DDC monitors // Detect DDC monitors
Process { Process {
id: ddcProc id: ddcProc
command: ["ddcutil", "detect", "--brief"] // Add a timeout so detect can't hang the UI
command: ["sh", "-c", "timeout 3s ddcutil detect --brief || true"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
// Do not filter out invalid displays. For some reason --brief returns some invalid which works fine // Do not filter out invalid displays. For some reason --brief returns some invalid which works fine
var displays = text.trim().split("\n\n") var displays = text.trim().split("\n\n");
root.ddcMonitors = displays.map(d => { root.ddcMonitors = displays.map(d => {
var modelMatch = d.match(/Monitor:.*:(.*):.*/) var modelMatch = d.match(/Monitor:.*:(.*):.*/);
var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/);
return { return {
"model": modelMatch ? modelMatch[1] : "", "model": modelMatch ? modelMatch[1] : "",
"busNum": busMatch ? busMatch[1] : "" "busNum": busMatch ? busMatch[1] : ""
} };
}) });
} }
} }
} }
@ -90,8 +93,11 @@ Singleton {
id: monitor id: monitor
required property ShellScreen modelData required property ShellScreen modelData
readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model)
readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? "" readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? ""
// Treat embedded panels as internal only
readonly property bool isInternalPanel: modelData.name.startsWith("eDP") || modelData.name.startsWith("LVDS") || modelData.name.startsWith("DSI")
// Only use DDC if not internal and not blacklisted
readonly property bool isDdc: busNum !== "" && !isInternalPanel && root.ddcBlacklist.indexOf(busNum) === -1
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal")
@ -106,43 +112,55 @@ Singleton {
readonly property Process initProc: Process { readonly property Process initProc: Process {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var dataText = text.trim() var dataText = text.trim();
if (dataText === "") { if (dataText === "") {
return return;
} }
Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
// If DDC responded with an error, blacklist this bus and fall back to internal
if (monitor.isDdc && dataText.indexOf("ERR") !== -1) {
if (root.ddcBlacklist.indexOf(monitor.busNum) === -1) {
Logger.warn("Brightness", "Blacklisting DDC bus", monitor.busNum);
root.ddcBlacklist = root.ddcBlacklist.concat([monitor.busNum]);
}
// Re-init using the new method (will now be 'internal')
monitor.initBrightness();
return;
}
Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText);
if (monitor.isAppleDisplay) { if (monitor.isAppleDisplay) {
var val = parseInt(dataText) var val = parseInt(dataText);
if (!isNaN(val)) { if (!isNaN(val)) {
monitor.brightness = val / 101 monitor.brightness = val / 101;
Logger.log("Brightness", "Apple display brightness:", monitor.brightness) Logger.log("Brightness", "Apple display brightness:", monitor.brightness);
} }
} else if (monitor.isDdc) { } else if (monitor.isDdc) {
var parts = dataText.split(" ") var parts = dataText.split(" ");
if (parts.length >= 4) { if (parts.length >= 4) {
var current = parseInt(parts[3]) var current = parseInt(parts[3]);
var max = parseInt(parts[4]) var max = parseInt(parts[4]);
if (!isNaN(current) && !isNaN(max) && max > 0) { if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.brightness = current / max monitor.brightness = current / max;
Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness) Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness);
} }
} }
} else { } else {
// Internal backlight // Internal backlight
var parts = dataText.split(" ") var parts = dataText.split(" ");
if (parts.length >= 2) { if (parts.length >= 2) {
var current = parseInt(parts[0]) var current = parseInt(parts[0]);
var max = parseInt(parts[1]) var max = parseInt(parts[1]);
if (!isNaN(current) && !isNaN(max) && max > 0) { if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.brightness = current / max monitor.brightness = current / max;
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness) Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness);
} }
} }
} }
// Always update // Always update
monitor.brightnessUpdated(monitor.brightness) monitor.brightnessUpdated(monitor.brightness);
} }
} }
} }
@ -152,65 +170,66 @@ Singleton {
interval: 200 interval: 200
onTriggered: { onTriggered: {
if (!isNaN(monitor.queuedBrightness)) { if (!isNaN(monitor.queuedBrightness)) {
monitor.setBrightness(monitor.queuedBrightness) monitor.setBrightness(monitor.queuedBrightness);
monitor.queuedBrightness = NaN monitor.queuedBrightness = NaN;
} }
} }
} }
function increaseBrightness(): void { function increaseBrightness(): void {
var stepSize = Settings.data.brightness.brightnessStep / 100.0 var stepSize = Settings.data.brightness.brightnessStep / 100.0;
setBrightnessDebounced(brightness + stepSize) setBrightnessDebounced(brightness + stepSize);
} }
function decreaseBrightness(): void { function decreaseBrightness(): void {
var stepSize = Settings.data.brightness.brightnessStep / 100.0 var stepSize = Settings.data.brightness.brightnessStep / 100.0;
setBrightnessDebounced(monitor.brightness - stepSize) setBrightnessDebounced(monitor.brightness - stepSize);
} }
function setBrightness(value: real): void { function setBrightness(value: real): void {
value = Math.max(0, Math.min(1, value)) value = Math.max(0, Math.min(1, value));
var rounded = Math.round(value * 100) var rounded = Math.round(value * 100);
if (Math.round(brightness * 100) === rounded) if (Math.round(brightness * 100) === rounded)
return return;
if (isDdc && timer.running) { if (isDdc && timer.running) {
queuedBrightness = value queuedBrightness = value;
return return;
} }
brightness = value brightness = value;
brightnessUpdated(brightness) brightnessUpdated(brightness);
if (isAppleDisplay) { if (isAppleDisplay) {
Quickshell.execDetached(["asdbctl", "set", rounded]) Quickshell.execDetached(["asdbctl", "set", rounded]);
} else if (isDdc) { } else if (isDdc) {
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]) // Add timeout so ddcutil can't hang
Quickshell.execDetached(["sh", "-c", "timeout 1s ddcutil -b " + busNum + " setvcp 10 " + rounded + " >/dev/null 2>&1 || true"]);
} else { } else {
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"]) Quickshell.execDetached(["brightnessctl", "s", rounded + "%"]);
} }
if (isDdc) { if (isDdc) {
timer.restart() timer.restart();
} }
} }
function setBrightnessDebounced(value: real): void { function setBrightnessDebounced(value: real): void {
queuedBrightness = value queuedBrightness = value;
timer.restart() timer.restart();
} }
function initBrightness(): void { function initBrightness(): void {
if (isAppleDisplay) { if (isAppleDisplay) {
initProc.command = ["asdbctl", "get"] initProc.command = ["asdbctl", "get"];
} else if (isDdc) { } else if (isDdc) {
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] // Add timeout and a fallback ERR marker to trigger blacklist
initProc.command = ["sh", "-c", "timeout 1s ddcutil -b " + busNum + " getvcp 10 --brief || echo 'VCP 10 ERR'"];
} else { } else {
// Internal backlight - try to find the first available backlight device // Internal backlight - try to find the first available backlight device
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"] initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"];
} }
initProc.running = true initProc.running = true;
} }
onBusNumChanged: initBrightness() onBusNumChanged: initBrightness()