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}