master ohmu/fastfetch-openbsd / src / detection / brightness / brightness_linux.c
ohmu  ·  2026-10-15
  1#include "brightness.h"
  2#include "common/io.h"
  3#include "common/edidHelper.h"
  4#include "common/strutil.h"
  5#include "common/debug.h"
  6#include "common/time.h"
  7
  8#include <ctype.h>
  9#include <dirent.h>
 10#include <errno.h>
 11#include <limits.h>
 12#include <string.h>
 13
 14#if defined(__linux__)
 15    #include <linux/i2c.h>
 16    #include <linux/i2c-dev.h>
 17    #include <sys/ioctl.h>
 18    #include <fcntl.h>
 19    #include <unistd.h>
 20    #include <sys/stat.h>
 21
 22    // Try to load cache from file
 23    static bool loadCacheFromFile(FFBrightnessDdcCache* cache) {
 24        cache->count = 0;
 25        FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath();
 26        FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate();
 27        bool valid = false;
 28        FF_DEBUG("Brightness DDC/CI: loading I2C cache from %s", cacheDir.chars);
 29        if (ffReadFileBuffer(cacheDir.chars, &cacheContent)) {
 30            // Parse cache: first number is count, rest are device numbers
 31            char* ptr = cacheContent.chars;
 32            char* end = ptr + cacheContent.length;
 33
 34            // Read count
 35            char* next;
 36            long count = strtol(ptr, &next, 10);
 37            if (next != ptr && count > 0 && count <= FF_I2C_DEVICE_CACHE_MAX) {
 38                ptr = next;
 39
 40                // Read device numbers and reject truncated or trailing data.
 41                size_t parsedCount = 0;
 42                for (; parsedCount < (size_t) count && ptr < end; ++parsedCount) {
 43                    long dev = strtol(ptr, &next, 10);
 44                    if (next == ptr || dev < 0 || dev >= 100) {
 45                        break;
 46                    }
 47                    cache->devices[parsedCount] = (int) dev;
 48                    ptr = next;
 49                }
 50
 51                while (ptr < end && isspace((unsigned char) *ptr)) {
 52                    ++ptr;
 53                }
 54
 55                if (parsedCount == (size_t) count && ptr == end) {
 56                    cache->count = parsedCount;
 57                    valid = true;
 58                    for (size_t i = 0; i < cache->count; ++i) {
 59                        char path[32];
 60                        snprintf(path, sizeof(path), "/dev/i2c-%d", cache->devices[i]);
 61                        FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC);
 62                        if (fd < 0) {
 63                            FF_DEBUG("Brightness DDC/CI: cached bus %d is unavailable (errno=%d: %s)", cache->devices[i], errno, strerror(errno));
 64                            valid = false;
 65                            break;
 66                        }
 67                    }
 68                }
 69            }
 70        }
 71
 72        if (!valid) {
 73            FF_DEBUG("Brightness DDC/CI: I2C cache is missing or invalid");
 74            cache->count = 0;
 75            return false;
 76        }
 77
 78        FF_DEBUG("Brightness DDC/CI: loaded %zu bus(es) from cache", cache->count);
 79        return true;
 80    }
 81
 82    // Save cache to file
 83    static void saveCacheToFile(const FFBrightnessDdcCache* cache) {
 84        if (cache->count == 0) {
 85            return;
 86        }
 87
 88        FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath();
 89        FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreateF("%zu", cache->count);
 90        for (size_t i = 0; i < cache->count; i++) {
 91            ffStrbufAppendF(&cacheContent, " %d", cache->devices[i]);
 92        }
 93
 94        if (ffWriteFileBuffer(cacheDir.chars, &cacheContent)) {
 95            FF_DEBUG("Brightness DDC/CI: saved %zu bus(es) to %s", cache->count, cacheDir.chars);
 96        } else {
 97            FF_DEBUG("Brightness DDC/CI: failed to save cache to %s", cacheDir.chars);
 98        }
 99    }
100
101    static bool readDdcCiResponse(int fd, FFBrightnessOptions* options, uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]) {
102        uint8_t request[5];
103        ffDdcCiBuildGetVcpRequest(request);
104
105        // i2c_msg.addr is a 7-bit address; the 8-bit write/read addresses are
106        // used only for the DDC/CI checksum.
107        struct i2c_msg msg = {
108            .addr = FF_DDC_CI_ADDR,
109            .flags = 0,
110            .len = sizeof(request),
111            .buf = request
112        };
113
114        struct i2c_rdwr_ioctl_data data = {
115            .msgs = &msg,
116            .nmsgs = 1
117        };
118
119        int ioctlResult = ioctl(fd, I2C_RDWR, &data);
120        if (ioctlResult != 1) {
121            FF_DEBUG("Brightness DDC/CI: write request failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, errno, strerror(errno));
122            return false;
123        }
124
125        ffTimeSleep(options->ddcciSleep);
126
127        msg.flags = I2C_M_RD;
128        msg.len = FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE;
129        msg.buf = response;
130        ioctlResult = ioctl(fd, I2C_RDWR, &data);
131        if (ioctlResult != 1) {
132            FF_DEBUG("Brightness DDC/CI: read response failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, errno, strerror(errno));
133            return false;
134        }
135
136        if (!ffDdcCiValidateGetVcpResponse(response)) {
137            FF_DEBUG("Brightness DDC/CI: invalid response (addr=0x%02x, header=0x%02x, type=0x%02x, opcode=0x%02x, checksum=0x%02x)",
138                (unsigned) response[0], (unsigned) response[1], (unsigned) response[2], (unsigned) response[4], (unsigned) response[10]);
139            return false;
140        }
141
142        FF_DEBUG("Brightness DDC/CI: valid response (fd=%d, current=%u, max=%u)", fd,
143            ((unsigned) response[8] << 8u) | (unsigned) response[9],
144            ((unsigned) response[6] << 8u) | (unsigned) response[7]);
145        return true;
146    }
147
148    static bool scanI2cDevices(FFBrightnessOptions* options, FFBrightnessDdcCache* cache) {
149        FF_DEBUG("Brightness DDC/CI: scanning Linux I2C buses");
150        cache->count = 0;
151
152        for (int i = 0; i < 100 && cache->count < FF_I2C_DEVICE_CACHE_MAX; i++) {
153            char path[32];
154            snprintf(path, sizeof(path), "/dev/i2c-%d", i);
155
156            FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC);
157            if (fd < 0) {
158                continue;
159            }
160
161            FF_DEBUG("Brightness DDC/CI: probing %s", path);
162            uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE];
163            if (!readDdcCiResponse(fd, options, response)) {
164                continue;
165            }
166
167            cache->devices[cache->count++] = i;
168            FF_DEBUG("Brightness DDC/CI: found compatible display on %s", path);
169        }
170
171        FF_DEBUG("Brightness DDC/CI: scan finished with %zu compatible bus(es)", cache->count);
172        return cache->count > 0;
173    }
174
175    static bool readEdid(int fd, uint8_t* edidData) {
176        uint8_t offset = 0;
177
178        struct i2c_msg msgs[2];
179        msgs[0].addr = FF_DDC_EDID_ADDR;
180        msgs[0].flags = 0;
181        msgs[0].len = 1;
182        msgs[0].buf = &offset;
183
184        msgs[1].addr = FF_DDC_EDID_ADDR;
185        msgs[1].flags = I2C_M_RD;
186        msgs[1].len = 128;
187        msgs[1].buf = edidData;
188
189        struct i2c_rdwr_ioctl_data data;
190        data.msgs = msgs;
191        data.nmsgs = 2;
192
193        return ioctl(fd, I2C_RDWR, &data) == 2;
194    }
195
196    static bool detectCachedDevices(FFBrightnessOptions* options, FFlist* result, FFBrightnessDdcCache* cache) {
197        const size_t cachedCount = cache->count;
198        const uint32_t resultLength = result->length;
199        size_t validCount = 0;
200
201        FF_DEBUG("Brightness DDC/CI: validating %zu cached bus(es)", cachedCount);
202        for (size_t i = 0; i < cachedCount; ++i) {
203            const int device = cache->devices[i];
204            char path[32];
205            snprintf(path, sizeof(path), "/dev/i2c-%d", device);
206
207            FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC);
208            if (fd < 0) {
209                FF_DEBUG("Brightness DDC/CI: failed to open cached bus %d (errno=%d: %s)", device, errno, strerror(errno));
210                continue;
211            }
212
213            uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE];
214            if (!readDdcCiResponse(fd, options, response)) {
215                continue;
216            }
217
218            uint32_t current = ((uint32_t) response[8] << 8u) + (uint32_t) response[9];
219            uint32_t max = ((uint32_t) response[6] << 8u) + (uint32_t) response[7];
220            if (max == 0 || current > max) {
221                FF_DEBUG("Brightness DDC/CI: ignoring bus %d with invalid brightness range %u/%u", device, current, max);
222                continue;
223            }
224
225            cache->devices[validCount++] = device;
226
227            // Try to read EDID for monitor name
228            uint8_t edidData[128];
229            FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(32);
230            if (readEdid(fd, edidData)) {
231                ffEdidGetName(edidData, &name);
232            }
233            if (name.length == 0) {
234                ffStrbufSetF(&name, "I2C-%d", device);
235            }
236
237            FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
238            brightness->max = max;
239            brightness->min = 0;
240            brightness->current = current;
241            brightness->builtin = false;
242            ffStrbufInitMove(&brightness->name, &name);
243            FF_DEBUG("Brightness DDC/CI: added display '%s' from bus %d (%u/%u)", brightness->name.chars, device, current, max);
244        }
245
246        cache->count = validCount;
247        FF_DEBUG("Brightness DDC/CI: cache validation kept %zu bus(es), result count is %u", validCount, result->length);
248        return result->length > resultLength;
249    }
250
251        static const char* detectWithDdcciNative(FFBrightnessOptions* options, FFlist* result) {
252        FFBrightnessDdcCache cache = {0};
253        FF_DEBUG("Brightness DDC/CI: native detection started (result count=%u)", result->length);
254        if (!loadCacheFromFile(&cache)) {
255            if (!scanI2cDevices(options, &cache)) {
256                FF_DEBUG("Brightness DDC/CI: no compatible I2C device found during initial scan");
257                return "No I2C devices found";
258            }
259        }
260
261        if (!detectCachedDevices(options, result, &cache)) {
262            FF_DEBUG("Brightness DDC/CI: cached detection found no usable display; rescanning");
263            // A cached bus can remain present after its monitor is unplugged or
264            // its number is reused. Rebuild the cache when no valid reply arrives.
265            if (!scanI2cDevices(options, &cache) || !detectCachedDevices(options, result, &cache)) {
266                FF_DEBUG("Brightness DDC/CI: rescan found no compatible display");
267                return "No DDC/CI compatible displays found";
268            }
269        }
270
271        saveCacheToFile(&cache);
272        FF_DEBUG("Brightness DDC/CI: native detection finished with %u result(s)", result->length);
273        return nullptr;
274    }
275#endif
276
277static const char* detectWithBacklight(FFlist* result) {
278    // https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-backlight
279    const char* backlightDirPath = "/sys/class/backlight/";
280
281    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(backlightDirPath);
282    if (dirp == nullptr) {
283        FF_DEBUG("opendir(\"%s\") failed: %s", backlightDirPath, strerror(errno));
284        return "Failed to open `/sys/class/backlight/`";
285    }
286
287    FF_STRBUF_AUTO_DESTROY backlightDir = ffStrbufCreateA(64);
288    ffStrbufAppendS(&backlightDir, backlightDirPath);
289
290    uint32_t backlightDirLength = backlightDir.length;
291
292    FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
293
294    struct dirent* entry;
295    while ((entry = readdir(dirp)) != nullptr) {
296        if (entry->d_name[0] == '.') {
297            continue;
298        }
299
300        ffStrbufAppendS(&backlightDir, entry->d_name);
301        ffStrbufAppendS(&backlightDir, "/brightness");
302        if (ffReadFileBuffer(backlightDir.chars, &buffer)) {
303            double actualBrightness = ffStrbufToDouble(&buffer, 0);
304            ffStrbufSubstrBefore(&backlightDir, backlightDirLength);
305            ffStrbufAppendS(&backlightDir, entry->d_name);
306            ffStrbufAppendS(&backlightDir, "/max_brightness");
307            if (ffReadFileBuffer(backlightDir.chars, &buffer)) {
308                FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
309                ffStrbufSubstrBeforeLastC(&backlightDir, '/');
310                ffStrbufAppendS(&backlightDir, "/device");
311                ffStrbufInitA(&brightness->name, PATH_MAX);
312                if (realpath(backlightDir.chars, brightness->name.chars)) {
313                    ffStrbufRecalculateLength(&brightness->name);
314                    // if we managed to get edid, use it
315                    ffStrbufAppendS(&brightness->name, "/edid");
316                    uint8_t edidData[128];
317                    if (ffReadFileData(brightness->name.chars, ARRAY_SIZE(edidData), edidData) == ARRAY_SIZE(edidData)) {
318                        ffStrbufClear(&brightness->name);
319                        ffEdidGetName(edidData, &brightness->name);
320                    } else {
321                        ffStrbufSubstrBeforeLastC(&brightness->name, '/'); // remove "/edid"
322                        ffStrbufSubstrAfterLastC(&brightness->name, '/');  // try getting DRM connector name
323                        if (ffCharIsDigit(brightness->name.chars[0])) {
324                            // PCI address or some unknown path, give up
325                            ffStrbufSetS(&brightness->name, entry->d_name);
326                        } else {
327                            if (ffStrbufStartsWithS(&brightness->name, "card") && ffCharIsDigit(brightness->name.chars[4])) {
328                                ffStrbufSubstrAfterFirstC(&brightness->name, '-');
329                            }
330                        }
331                    }
332                } else {
333                    ffStrbufInitS(&brightness->name, entry->d_name);
334                }
335                brightness->max = ffStrbufToDouble(&buffer, 0);
336                brightness->min = 0;
337                brightness->current = actualBrightness;
338                brightness->builtin = true;
339            }
340        }
341        ffStrbufSubstrBefore(&backlightDir, backlightDirLength);
342    }
343
344    return nullptr;
345}
346
347#include "detection/displayserver/displayserver.h"
348
349const char* ffDetectBrightness([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) {
350    FF_DEBUG("Brightness: detection started (result count=%u)", result->length);
351    detectWithBacklight(result);
352    FF_DEBUG("Brightness: backlight detection finished (result count=%u)", result->length);
353
354    if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) {
355        const FFDisplayServerResult* displayServer = ffConnectDisplayServer();
356        if (result->length < displayServer->displays.length) {
357            FF_DEBUG("Brightness: trying native DDC/CI (result count=%u, display count=%u)", result->length, displayServer->displays.length);
358#if defined(__linux__)
359            const char* nativeError = detectWithDdcciNative(options, result);
360            if (nativeError != nullptr) {
361                FF_DEBUG("Native I2C DDC/CI failed: %s", nativeError);
362            }
363#endif
364        } else {
365            FF_DEBUG("Brightness: skipping DDC/CI because result count=%u meets display count=%u", result->length, displayServer->displays.length);
366        }
367    } else {
368        FF_DEBUG("Brightness: skipping DDC/CI because it is disabled");
369    }
370
371    FF_DEBUG("Brightness: detection finished (result count=%u)", result->length);
372    return nullptr;
373}