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}