From 0d8ae1e09bb5196bdfed3fb8d702ec917a19997b Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 19 Aug 2025 11:06:11 -0400 Subject: [PATCH] incorporate ddcutil support --- Common/SessionData.qml | 10 +- Modules/BrightnessPopup.qml | 5 +- Modules/ControlCenter/DisplayTab.qml | 51 +++- Services/BrightnessService.qml | 343 ++++++++++++++++++++++++--- 4 files changed, 372 insertions(+), 37 deletions(-) diff --git a/Common/SessionData.qml b/Common/SessionData.qml index 2d93e8c8..e1857ec5 100644 --- a/Common/SessionData.qml +++ b/Common/SessionData.qml @@ -28,6 +28,7 @@ Singleton { property string wallpaperCyclingMode: "interval" // "interval" or "time" property int wallpaperCyclingInterval: 300 // seconds (5 minutes) property string wallpaperCyclingTime: "06:00" // HH:mm format + property string lastBrightnessDevice: "" Component.onCompleted: { loadSettings() @@ -58,6 +59,7 @@ Singleton { wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval" wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300 wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00" + lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : "" } } catch (e) { @@ -81,7 +83,8 @@ Singleton { "wallpaperCyclingEnabled": wallpaperCyclingEnabled, "wallpaperCyclingMode": wallpaperCyclingMode, "wallpaperCyclingInterval": wallpaperCyclingInterval, - "wallpaperCyclingTime": wallpaperCyclingTime + "wallpaperCyclingTime": wallpaperCyclingTime, + "lastBrightnessDevice": lastBrightnessDevice }, null, 2)) } @@ -196,6 +199,11 @@ Singleton { saveSettings() } + function setLastBrightnessDevice(device) { + lastBrightnessDevice = device + saveSettings() + } + FileView { id: settingsFile diff --git a/Modules/BrightnessPopup.qml b/Modules/BrightnessPopup.qml index c44849cf..fdab10ae 100644 --- a/Modules/BrightnessPopup.qml +++ b/Modules/BrightnessPopup.qml @@ -118,14 +118,11 @@ PanelWindow { name: { const deviceInfo = BrightnessService.getCurrentDeviceInfo(); - if (!deviceInfo || deviceInfo.class === "backlight") { - // Display backlight + if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") { return "brightness_medium"; } else if (deviceInfo.name.includes("kbd")) { - // Keyboard brightness return "keyboard"; } else { - // Other devices (LEDs, etc.) return "lightbulb"; } } diff --git a/Modules/ControlCenter/DisplayTab.qml b/Modules/ControlCenter/DisplayTab.qml index 68d391fb..3606c0a1 100644 --- a/Modules/ControlCenter/DisplayTab.qml +++ b/Modules/ControlCenter/DisplayTab.qml @@ -56,11 +56,18 @@ Item { } DankDropdown { + id: deviceDropdown width: parent.width height: 40 visible: BrightnessService.devices.length > 1 text: "Device" - description: "" + description: { + const deviceInfo = BrightnessService.getCurrentDeviceInfo(); + if (deviceInfo && deviceInfo.class === "ddc") { + return "DDC changes can be slow and unreliable"; + } + return ""; + } currentValue: BrightnessService.currentDevice options: BrightnessService.devices.map(function(d) { return d.name; @@ -68,6 +75,9 @@ Item { optionIcons: BrightnessService.devices.map(function(d) { if (d.class === "backlight") return "desktop_windows"; + + if (d.class === "ddc") + return "tv"; if (d.name.includes("kbd")) return "keyboard"; @@ -75,7 +85,35 @@ Item { return "lightbulb"; }) onValueChanged: function(value) { - BrightnessService.setCurrentDevice(value); + BrightnessService.setCurrentDevice(value, true); + } + + Connections { + target: BrightnessService + function onDevicesChanged() { + if (BrightnessService.currentDevice) { + deviceDropdown.currentValue = BrightnessService.currentDevice; + } + + // Check if saved device is now available + const lastDevice = SessionData.lastBrightnessDevice || ""; + if (lastDevice) { + const deviceExists = BrightnessService.devices.some(d => d.name === lastDevice); + if (deviceExists && (!BrightnessService.currentDevice || BrightnessService.currentDevice !== lastDevice)) { + BrightnessService.setCurrentDevice(lastDevice, false); + } + } + } + function onDeviceSwitched() { + // Force update the description when device switches + deviceDropdown.description = Qt.binding(function() { + const deviceInfo = BrightnessService.getCurrentDeviceInfo(); + if (deviceInfo && deviceInfo.class === "ddc") { + return "DDC changes can be slow and unreliable"; + } + return ""; + }); + } } } @@ -85,7 +123,8 @@ Item { value: BrightnessService.brightnessLevel leftIcon: "brightness_low" rightIcon: "brightness_high" - enabled: BrightnessService.brightnessAvailable + enabled: BrightnessService.brightnessAvailable && BrightnessService.isCurrentDeviceReady() + opacity: BrightnessService.isCurrentDeviceReady() ? 1.0 : 0.5 onSliderValueChanged: function(newValue) { brightnessDebounceTimer.pendingValue = newValue; brightnessDebounceTimer.restart(); @@ -230,7 +269,11 @@ Item { brightnessDebounceTimer: Timer { property int pendingValue: 0 - interval: 50 + interval: { + // Use longer interval for DDC devices since ddcutil is slow + const deviceInfo = BrightnessService.getCurrentDeviceInfo(); + return (deviceInfo && deviceInfo.class === "ddc") ? 100 : 50; + } repeat: false onTriggered: { BrightnessService.setBrightnessInternal(pendingValue, BrightnessService.currentDevice); diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 2a9b40fb..e1865b58 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -10,12 +10,28 @@ Singleton { property bool brightnessAvailable: devices.length > 0 property var devices: [] + property var ddcDevices: [] property var deviceBrightness: ({}) + property var ddcPendingInit: ({}) property string currentDevice: "" property string lastIpcDevice: "" + property bool ddcAvailable: false + property var ddcInitQueue: [] + property bool skipDdcRead: false property int brightnessLevel: { const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice); - return deviceToUse ? (deviceBrightness[deviceToUse] || 50) : 50; + if (!deviceToUse) return 50; + + const deviceInfo = getCurrentDeviceInfoByName(deviceToUse); + if (deviceInfo && deviceInfo.class === "ddc") { + if (ddcPendingInit[deviceToUse]) { + return deviceBrightness[deviceToUse] || 50; + } + return deviceBrightness[deviceToUse] || 50; + } + + // For non-DDC devices, don't use cache - they're fast to read + return deviceBrightness[deviceToUse] || 50; } property int maxBrightness: 100 property bool brightnessInitialized: false @@ -30,17 +46,26 @@ Singleton { const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice()); // Update the device brightness cache - if (actualDevice) { - var newBrightness = deviceBrightness; + const deviceInfo = getCurrentDeviceInfoByName(actualDevice); + if (actualDevice && deviceInfo && deviceInfo.class === "ddc") { + // Always cache DDC values since we never read them again + var newBrightness = Object.assign({}, deviceBrightness); newBrightness[actualDevice] = clampedValue; deviceBrightness = newBrightness; } - if (device) - brightnessSetProcess.command = ["brightnessctl", "-d", device, "set", clampedValue + "%"]; - else - brightnessSetProcess.command = ["brightnessctl", "set", clampedValue + "%"]; - brightnessSetProcess.running = true; + if (deviceInfo && deviceInfo.class === "ddc") { + // Use ddcutil for DDC devices + ddcBrightnessSetProcess.command = ["ddcutil", "setvcp", "-d", String(deviceInfo.ddcDisplay), "10", String(clampedValue)]; + ddcBrightnessSetProcess.running = true; + } else { + // Use brightnessctl for regular devices + if (device) + brightnessSetProcess.command = ["brightnessctl", "-d", device, "set", clampedValue + "%"]; + else + brightnessSetProcess.command = ["brightnessctl", "set", clampedValue + "%"]; + brightnessSetProcess.running = true; + } } function setBrightness(percentage, device) { @@ -48,33 +73,77 @@ Singleton { brightnessChanged(); } - function setCurrentDevice(deviceName) { + function setCurrentDevice(deviceName, saveToSession = false) { if (currentDevice === deviceName) return ; currentDevice = deviceName; lastIpcDevice = deviceName; + + // Only save to session if explicitly requested (user choice) + if (saveToSession) { + SessionData.setLastBrightnessDevice(deviceName); + } + deviceSwitched(); - brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"]; - brightnessGetProcess.running = true; + + // Check if this is a DDC device + const deviceInfo = getCurrentDeviceInfoByName(deviceName); + if (deviceInfo && deviceInfo.class === "ddc") { + // For DDC devices, never read after initial - just use cached values + return; + } else { + // For regular devices, use brightnessctl + brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"]; + brightnessGetProcess.running = true; + } } function refreshDevices() { deviceListProcess.running = true; } + + function refreshDevicesInternal() { + const allDevices = [...devices, ...ddcDevices]; + + allDevices.sort((a, b) => { + if (a.class === "backlight" && b.class !== "backlight") + return -1; + if (a.class !== "backlight" && b.class === "backlight") + return 1; + + if (a.class === "ddc" && b.class !== "ddc" && b.class !== "backlight") + return -1; + if (a.class !== "ddc" && b.class === "ddc" && a.class !== "backlight") + return 1; + + return a.name.localeCompare(b.name); + }); + + devices = allDevices; + + if (devices.length > 0 && !currentDevice) { + const lastDevice = SessionData.lastBrightnessDevice || ""; + const deviceExists = devices.some(d => d.name === lastDevice); + if (deviceExists) { + setCurrentDevice(lastDevice, false); + } else { + const nonKbdDevice = devices.find(d => !d.name.includes("kbd")) || devices[0]; + setCurrentDevice(nonKbdDevice.name, false); + } + } + } function getDeviceBrightness(deviceName) { return deviceBrightness[deviceName] || 50; } function getDefaultDevice() { - // Find first backlight device for (const device of devices) { if (device.class === "backlight") { return device.name; } } - // Fallback to first device if no backlight found return devices.length > 0 ? devices[0].name : ""; } @@ -90,6 +159,39 @@ Singleton { return null; } + function isCurrentDeviceReady() { + const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice); + if (!deviceToUse) return false; + + if (ddcPendingInit[deviceToUse]) { + return false; + } + + return true; + } + + function getCurrentDeviceInfoByName(deviceName) { + if (!deviceName) return null; + + for (const device of devices) { + if (device.name === deviceName) { + return device; + } + } + return null; + } + + function processNextDdcInit() { + if (ddcInitQueue.length === 0 || ddcInitialBrightnessProcess.running) { + return; + } + + const displayId = ddcInitQueue.shift(); + ddcInitialBrightnessProcess.command = ["ddcutil", "getvcp", "-d", String(displayId), "10", "--brief"]; + ddcInitialBrightnessProcess.running = true; + } + + function enableNightMode() { if (nightModeActive) return; @@ -128,7 +230,9 @@ Singleton { } } + Component.onCompleted: { + ddcDetectionProcess.running = true; refreshDevices(); // Check if night mode was enabled on startup @@ -137,6 +241,94 @@ Singleton { } } + Process { + id: ddcDetectionProcess + + command: ["which", "ddcutil"] + running: false + + onExited: function(exitCode) { + ddcAvailable = (exitCode === 0); + if (ddcAvailable) { + console.log("BrightnessService: ddcutil detected"); + ddcDisplayDetectionProcess.running = true; + } else { + console.log("BrightnessService: ddcutil not available"); + } + } + } + + Process { + id: ddcDisplayDetectionProcess + + command: ["bash", "-c", "ddcutil detect --brief 2>/dev/null | grep '^Display [0-9]' | awk '{print \"{\\\"display\\\":\" $2 \",\\\"name\\\":\\\"ddc-\" $2 \"\\\",\\\"class\\\":\\\"ddc\\\"}\"}' | tr '\\n' ',' | sed 's/,$//' | sed 's/^/[/' | sed 's/$/]/' || echo '[]'"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) { + console.log("BrightnessService: No DDC displays found"); + ddcDevices = []; + return; + } + + try { + const parsedDevices = JSON.parse(text.trim()); + const newDdcDevices = []; + + for (const device of parsedDevices) { + if (device.display && device.class === "ddc") { + newDdcDevices.push({ + "name": device.name, + "class": "ddc", + "current": 50, + "percentage": 50, + "max": 100, + "ddcDisplay": device.display + }); + } + } + + ddcDevices = newDdcDevices; + console.log("BrightnessService: Found", ddcDevices.length, "DDC displays"); + + // Queue initial brightness readings for DDC devices + ddcInitQueue = []; + for (const device of ddcDevices) { + ddcInitQueue.push(device.ddcDisplay); + // Mark DDC device as pending initialization + ddcPendingInit[device.name] = true; + } + + // Start processing the queue + processNextDdcInit(); + + // Refresh device list to include DDC devices + refreshDevicesInternal(); + + // Retry setting last device now that DDC devices are available + const lastDevice = SessionData.lastBrightnessDevice || ""; + if (lastDevice) { + const deviceExists = devices.some(d => d.name === lastDevice); + if (deviceExists && (!currentDevice || currentDevice !== lastDevice)) { + setCurrentDevice(lastDevice, false); + } + } + } catch (error) { + console.warn("BrightnessService: Failed to parse DDC devices:", error); + ddcDevices = []; + } + } + } + + onExited: function(exitCode) { + if (exitCode !== 0) { + console.warn("BrightnessService: Failed to detect DDC displays:", exitCode); + ddcDevices = []; + } + } + } + Process { id: deviceListProcess @@ -168,18 +360,24 @@ Singleton { }); } - newDevices.sort((a, b) => { - if (a.class === "backlight" && b.class !== "backlight") - return -1; - - if (a.class !== "backlight" && b.class === "backlight") - return 1; - - return a.name.localeCompare(b.name); - }); - devices = newDevices; - if (devices.length > 0 && !currentDevice) - setCurrentDevice(devices[0].name); + // Store brightnessctl devices separately, will be combined with DDC + const brightnessCtlDevices = newDevices; + devices = brightnessCtlDevices; + + // If we have DDC devices, combine them + if (ddcDevices.length > 0) { + refreshDevicesInternal(); + } else if (devices.length > 0 && !currentDevice) { + // Try to restore last selected device, fallback to first device + const lastDevice = SessionData.lastBrightnessDevice || ""; + const deviceExists = devices.some(d => d.name === lastDevice); + if (deviceExists) { + setCurrentDevice(lastDevice, false); + } else { + const nonKbdDevice = devices.find(d => !d.name.includes("kbd")) || devices[0]; + setCurrentDevice(nonKbdDevice.name, false); + } + } } } @@ -196,6 +394,58 @@ Singleton { } } + + Process { + id: ddcBrightnessSetProcess + + running: false + onExited: function(exitCode) { + if (exitCode !== 0) + console.warn("BrightnessService: Failed to set DDC brightness:", exitCode); + } + } + + Process { + id: ddcInitialBrightnessProcess + + running: false + onExited: function(exitCode) { + if (exitCode !== 0) + console.warn("BrightnessService: Failed to get initial DDC brightness:", exitCode); + + processNextDdcInit(); + } + + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) + return; + + const parts = text.trim().split(" "); + if (parts.length >= 5) { + const current = parseInt(parts[3]) || 50; + const max = parseInt(parts[4]) || 100; + const brightness = Math.round((current / max) * 100); + + const commandParts = ddcInitialBrightnessProcess.command; + if (commandParts && commandParts.length >= 4) { + const displayId = commandParts[3]; + const deviceName = "ddc-" + displayId; + + var newBrightness = Object.assign({}, deviceBrightness); + newBrightness[deviceName] = brightness; + deviceBrightness = newBrightness; + + var newPending = Object.assign({}, ddcPendingInit); + delete newPending[deviceName]; + ddcPendingInit = newPending; + + console.log("BrightnessService: Initial DDC Device", deviceName, "brightness:", brightness + "%"); + } + } + } + } + } Process { id: brightnessGetProcess @@ -234,6 +484,43 @@ Singleton { } } + + Process { + id: ddcBrightnessGetProcess + + running: false + onExited: function(exitCode) { + if (exitCode !== 0) + console.warn("BrightnessService: Failed to get DDC brightness:", exitCode); + } + + stdout: StdioCollector { + onStreamFinished: { + if (!text.trim()) + return; + + // Parse ddcutil getvcp output format: "VCP 10 C 50 100" + const parts = text.trim().split(" "); + if (parts.length >= 5) { + const current = parseInt(parts[3]) || 50; + const max = parseInt(parts[4]) || 100; + maxBrightness = max; + const brightness = Math.round((current / max) * 100); + + // Update the device brightness cache + if (currentDevice) { + var newBrightness = deviceBrightness; + newBrightness[currentDevice] = brightness; + deviceBrightness = newBrightness; + } + + brightnessInitialized = true; + console.log("BrightnessService: DDC Device", currentDevice, "brightness:", brightness + "%"); + brightnessChanged(); + } + } + } + } Process { id: gammaStepTestProcess @@ -285,7 +572,7 @@ Singleton { const targetDevice = device || ""; root.lastIpcDevice = targetDevice; if (targetDevice && targetDevice !== root.currentDevice) { - root.setCurrentDevice(targetDevice); + root.setCurrentDevice(targetDevice, false); } root.setBrightness(clampedValue, targetDevice); if (targetDevice) @@ -305,7 +592,7 @@ Singleton { const newLevel = Math.max(1, Math.min(100, currentLevel + stepValue)); root.lastIpcDevice = targetDevice; if (targetDevice && targetDevice !== root.currentDevice) { - root.setCurrentDevice(targetDevice); + root.setCurrentDevice(targetDevice, false); } root.setBrightness(newLevel, targetDevice); if (targetDevice) @@ -325,7 +612,7 @@ Singleton { const newLevel = Math.max(1, Math.min(100, currentLevel - stepValue)); root.lastIpcDevice = targetDevice; if (targetDevice && targetDevice !== root.currentDevice) { - root.setCurrentDevice(targetDevice); + root.setCurrentDevice(targetDevice, false); } root.setBrightness(newLevel, targetDevice); if (targetDevice)