master ohmu/fastfetch-openbsd / src / detection / brightness / brightness_bsd.c
ohmu  ·  2026-10-15
  1#include "brightness.h"
  2#include "common/arrutil.h"
  3#include "common/io.h"
  4#include "common/kmod.h"
  5#include "common/debug.h"
  6#include "common/time.h"
  7#include "common/edidHelper.h"
  8
  9#include <ctype.h>
 10#include <errno.h>
 11#include <string.h>
 12#include <sys/ioctl.h>
 13#include <sys/fcntl.h>
 14#include <unistd.h>
 15
 16#if __has_include(<dev/iicbus/iic.h>)
 17    #include <dev/iicbus/iic.h>
 18
 19    // Try to load cache from file
 20    static bool loadCacheFromFile(FFBrightnessDdcCache* cache) {
 21        cache->count = 0;
 22        FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath();
 23        FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate();
 24        bool valid = false;
 25        FF_DEBUG("Brightness DDC/CI: loading I2C cache from %s", cacheDir.chars);
 26        if (ffReadFileBuffer(cacheDir.chars, &cacheContent)) {
 27            // Parse cache: first number is count, rest are device numbers
 28            char* ptr = cacheContent.chars;
 29            char* end = ptr + cacheContent.length;
 30
 31            // Read count
 32            char* next;
 33            long count = strtol(ptr, &next, 10);
 34            if (next != ptr && count > 0 && count <= FF_I2C_DEVICE_CACHE_MAX) {
 35                ptr = next;
 36
 37                // Read device numbers and reject truncated or trailing data.
 38                size_t parsedCount = 0;
 39                for (; parsedCount < (size_t) count && ptr < end; ++parsedCount) {
 40                    long dev = strtol(ptr, &next, 10);
 41                    if (next == ptr || dev < 0 || dev >= 100) {
 42                        break;
 43                    }
 44                    cache->devices[parsedCount] = (int) dev;
 45                    ptr = next;
 46                }
 47
 48                while (ptr < end && isspace((unsigned char) *ptr)) {
 49                    ++ptr;
 50                }
 51
 52                if (parsedCount == (size_t) count && ptr == end) {
 53                    cache->count = parsedCount;
 54                    valid = true;
 55                    for (size_t i = 0; i < cache->count; ++i) {
 56                        char path[32];
 57                        snprintf(path, sizeof(path), "/dev/iic%d", cache->devices[i]);
 58                        FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC);
 59                        if (fd < 0) {
 60                            int error = errno;
 61                            FF_DEBUG("Brightness DDC/CI: cached bus %d is unavailable (errno=%d: %s)", cache->devices[i], error, strerror(error));
 62                            valid = false;
 63                            break;
 64                        }
 65                    }
 66                }
 67            }
 68        }
 69
 70        if (!valid) {
 71            FF_DEBUG("Brightness DDC/CI: I2C cache is missing or invalid");
 72            cache->count = 0;
 73            return false;
 74        }
 75
 76        FF_DEBUG("Brightness DDC/CI: loaded %zu bus(es) from cache", cache->count);
 77        return true;
 78    }
 79
 80    // Save cache to file
 81    static void saveCacheToFile(const FFBrightnessDdcCache* cache) {
 82        if (cache->count == 0) {
 83            return;
 84        }
 85
 86        FF_STRBUF_AUTO_DESTROY cacheDir = ffBrightnessDdcCachePath();
 87        FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreateF("%zu", cache->count);
 88        for (size_t i = 0; i < cache->count; i++) {
 89            ffStrbufAppendF(&cacheContent, " %d", cache->devices[i]);
 90        }
 91
 92        if (ffWriteFileBuffer(cacheDir.chars, &cacheContent)) {
 93            FF_DEBUG("Brightness DDC/CI: saved %zu bus(es) to %s", cache->count, cacheDir.chars);
 94        } else {
 95            FF_DEBUG("Brightness DDC/CI: failed to save cache to %s", cacheDir.chars);
 96        }
 97    }
 98
 99    static bool readDdcCiResponse(int fd, FFBrightnessOptions* options, uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE]) {
100        uint8_t request[5];
101        ffDdcCiBuildGetVcpRequest(request);
102
103        struct iic_msg msg = {
104            .slave = FF_DDC_CI_WRITE_ADDR,
105            .flags = IIC_M_WR,
106            .len = sizeof(request),
107            .buf = request
108        };
109
110        struct iic_rdwr_data data = {
111            .msgs = &msg,
112            .nmsgs = 1
113        };
114
115        int ioctlResult = ioctl(fd, I2CRDWR, &data);
116        if (ioctlResult < 0) {
117            int error = errno;
118            FF_DEBUG("Brightness DDC/CI: write request failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, error, strerror(error));
119            return false;
120        }
121
122        ffTimeSleep(options->ddcciSleep);
123
124        msg.slave = FF_DDC_CI_WRITE_ADDR;
125        msg.flags = IIC_M_RD;
126        msg.len = FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE;
127        msg.buf = response;
128        ioctlResult = ioctl(fd, I2CRDWR, &data);
129        if (ioctlResult < 0) {
130            int error = errno;
131            FF_DEBUG("Brightness DDC/CI: read response failed (fd=%d, result=%d, errno=%d: %s)", fd, ioctlResult, error, strerror(error));
132            return false;
133        }
134
135        if (!ffDdcCiValidateGetVcpResponse(response)) {
136            FF_DEBUG("Brightness DDC/CI: invalid response (addr=0x%02x, header=0x%02x, type=0x%02x, opcode=0x%02x, checksum=0x%02x)",
137                (unsigned) response[0], (unsigned) response[1], (unsigned) response[2], (unsigned) response[4], (unsigned) response[10]);
138            return false;
139        }
140
141        FF_DEBUG("Brightness DDC/CI: valid response (fd=%d, current=%u, max=%u)", fd,
142            ((unsigned) response[8] << 8u) | (unsigned) response[9],
143            ((unsigned) response[6] << 8u) | (unsigned) response[7]);
144        return true;
145    }
146
147    static bool scanI2cDevices(FFBrightnessOptions* options, FFBrightnessDdcCache* cache) {
148        FF_DEBUG("Brightness DDC/CI: scanning FreeBSD I2C buses");
149        cache->count = 0;
150
151        for (int i = 0; i < 100 && cache->count < FF_I2C_DEVICE_CACHE_MAX; i++) {
152            char path[32];
153            snprintf(path, sizeof(path), "/dev/iic%d", i);
154
155            FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC);
156            if (fd < 0) {
157                continue;
158            }
159
160            FF_DEBUG("Brightness DDC/CI: probing %s", path);
161            uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE];
162            if (!readDdcCiResponse(fd, options, response)) {
163                continue;
164            }
165
166            cache->devices[cache->count++] = i;
167            FF_DEBUG("Brightness DDC/CI: found compatible display on %s", path);
168        }
169
170        FF_DEBUG("Brightness DDC/CI: scan finished with %zu compatible bus(es)", cache->count);
171        return cache->count > 0;
172    }
173
174    static bool readEdid(int fd, uint8_t* edidData) {
175        uint8_t offset = 0;
176
177        struct iic_msg msgs[2];
178        msgs[0].slave = FF_DDC_EDID_WRITE_ADDR;
179        msgs[0].flags = IIC_M_WR;
180        msgs[0].len = 1;
181        msgs[0].buf = &offset;
182
183        msgs[1].slave = FF_DDC_EDID_WRITE_ADDR;
184        msgs[1].flags = IIC_M_RD;
185        msgs[1].len = 128;
186        msgs[1].buf = edidData;
187
188        struct iic_rdwr_data data = {
189            .msgs = msgs,
190            .nmsgs = 2
191        };
192
193        return ioctl(fd, I2CRDWR, &data) >= 0;
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/iic%d", device);
206
207            FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC);
208            if (fd < 0) {
209                int error = errno;
210                FF_DEBUG("Brightness DDC/CI: failed to open cached bus %d (errno=%d: %s)", device, error, strerror(error));
211                continue;
212            }
213
214            uint8_t response[FF_DDC_CI_VCP_RESPONSE_BUFFER_SIZE];
215            if (!readDdcCiResponse(fd, options, response)) {
216                continue;
217            }
218
219            uint32_t current = ((uint32_t) response[8] << 8u) + (uint32_t) response[9];
220            uint32_t max = ((uint32_t) response[6] << 8u) + (uint32_t) response[7];
221            if (max == 0 || current > max) {
222                FF_DEBUG("Brightness DDC/CI: ignoring bus %d with invalid brightness range %u/%u", device, current, max);
223                continue;
224            }
225
226            cache->devices[validCount++] = device;
227
228            // Try to read EDID for monitor name
229            uint8_t edidData[128];
230            FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(32);
231            if (readEdid(fd, edidData)) {
232                ffEdidGetName(edidData, &name);
233            }
234            if (name.length == 0) {
235                ffStrbufSetF(&name, "I2C-%d", device);
236            }
237
238            FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
239            brightness->max = max;
240            brightness->min = 0;
241            brightness->current = current;
242            brightness->builtin = false;
243            ffStrbufInitMove(&brightness->name, &name);
244            FF_DEBUG("Brightness DDC/CI: added display '%s' from bus %d (%u/%u)", brightness->name.chars, device, current, max);
245        }
246
247        cache->count = validCount;
248        FF_DEBUG("Brightness DDC/CI: cache validation kept %zu bus(es), result count is %u", validCount, result->length);
249        return result->length > resultLength;
250    }
251
252    const char* detectWithDdcci(FFBrightnessOptions* options, FFlist* result) {
253        FFBrightnessDdcCache cache = {0};
254        FF_DEBUG("Brightness DDC/CI: detection started (result count=%u)", result->length);
255        if (!loadCacheFromFile(&cache)) {
256            if (!scanI2cDevices(options, &cache)) {
257                FF_DEBUG("Brightness DDC/CI: no compatible I2C device found during initial scan");
258                return "No I2C devices found";
259            }
260        }
261
262        if (!detectCachedDevices(options, result, &cache)) {
263            FF_DEBUG("Brightness DDC/CI: cached detection found no usable display; rescanning");
264            if (!scanI2cDevices(options, &cache) || !detectCachedDevices(options, result, &cache)) {
265                FF_DEBUG("Brightness DDC/CI: rescan found no compatible display");
266                return "No DDC/CI compatible displays found";
267            }
268        }
269
270        saveCacheToFile(&cache);
271        FF_DEBUG("Brightness DDC/CI: detection finished with %u result(s)", result->length);
272        return nullptr;
273    }
274
275#else
276
277const char* detectWithDdcci([[maybe_unused]] FFBrightnessOptions* options, [[maybe_unused]] FFlist* result) {
278    FF_DEBUG("DDC/CI support is not available on this system");
279    return "DDC/CI is supported only on FreeBSD";
280}
281
282#endif
283
284#if __has_include(<sys/backlight.h>)
285    #include <sys/backlight.h>
286
287const char* detectWithBacklight([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) {
288    // https://man.freebsd.org/cgi/man.cgi?query=backlight&sektion=9
289    char path[] = "/dev/backlight/backlight0";
290
291    for (char i = '0'; i <= '9'; ++i) {
292        path[ARRAY_SIZE(path) - 2] = i;
293
294        FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC);
295        if (fd < 0) {
296            int err = errno;
297            if (err == ENOENT) {
298                FF_DEBUG("No more backlight devices");
299                break;
300            } else if (i == '0' && !ffKmodLoaded("backlight")) {
301                FF_DEBUG("backlight module is not loaded");
302                return "No backlight devices found and backlight module is not loaded";
303            }
304            FF_DEBUG("open(/dev/backlight/backlight%c) failed: %s", i, strerror(errno));
305            continue;
306        }
307
308        struct backlight_props status;
309        if (ioctl(fd, BACKLIGHTGETSTATUS, &status) < 0) {
310            continue;
311        }
312
313        FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result);
314        ffStrbufInit(&brightness->name);
315
316        brightness->max = BACKLIGHTMAXLEVELS;
317        brightness->min = 0;
318        brightness->current = status.brightness;
319        brightness->builtin = true;
320
321        struct backlight_info info;
322        if (ioctl(fd, BACKLIGHTGETINFO, &info) == 0) {
323            ffStrbufAppendS(&brightness->name, info.name);
324        } else {
325            ffStrbufAppendS(&brightness->name, path + strlen("/dev/backlight/"));
326        }
327    }
328    return nullptr;
329}
330
331#else
332
333const char* detectWithBacklight([[maybe_unused]] FFBrightnessOptions* options, [[maybe_unused]] FFlist* result) {
334    FF_DEBUG("Backlight support is not available on this system");
335    return "Backlight is supported only on FreeBSD 13 and newer";
336}
337
338#endif
339
340const char* ffDetectBrightness([[maybe_unused]] FFBrightnessOptions* options, FFlist* result) {
341    FF_DEBUG("Brightness: detection started (result count=%u)", result->length);
342    detectWithBacklight(options, result);
343    FF_DEBUG("Brightness: backlight detection finished (result count=%u)", result->length);
344
345    if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && result->length == 0) {
346        FF_DEBUG("Brightness: trying DDC/CI because no backlight result was found");
347        detectWithDdcci(options, result);
348    } else if (options->ddcciSleep == FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) {
349        FF_DEBUG("Brightness: skipping DDC/CI because it is disabled");
350    } else {
351        FF_DEBUG("Brightness: skipping DDC/CI because backlight detection already found %u result(s)", result->length);
352    }
353    FF_DEBUG("Brightness: detection finished (result count=%u)", result->length);
354    return nullptr;
355}