1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#ifndef SDL_POWER_DISABLED
24#ifdef SDL_POWER_LINUX
25
26#include <stdio.h>
27#include <unistd.h>
28
29#include <sys/types.h>
30#include <sys/stat.h>
31#include <dirent.h>
32#include <fcntl.h>
33
34#include "../SDL_syspower.h"
35
36#include "../../core/linux/SDL_dbus.h"
37
38static const char *proc_apm_path = "/proc/apm";
39static const char *proc_acpi_battery_path = "/proc/acpi/battery";
40static const char *proc_acpi_ac_adapter_path = "/proc/acpi/ac_adapter";
41static const char *sys_class_power_supply_path = "/sys/class/power_supply";
42
43static int open_power_file(const char *base, const char *node, const char *key)
44{
45 int fd;
46 const size_t pathlen = SDL_strlen(base) + SDL_strlen(node) + SDL_strlen(key) + 3;
47 char *path = SDL_stack_alloc(char, pathlen);
48 if (!path) {
49 return -1; // oh well.
50 }
51
52 (void)SDL_snprintf(path, pathlen, "%s/%s/%s", base, node, key);
53 fd = open(path, O_RDONLY | O_CLOEXEC);
54 SDL_stack_free(path);
55 return fd;
56}
57
58static bool read_power_file(const char *base, const char *node, const char *key,
59 char *buf, size_t buflen)
60{
61 ssize_t br = 0;
62 const int fd = open_power_file(base, node, key);
63 if (fd == -1) {
64 return false;
65 }
66 br = read(fd, buf, buflen - 1);
67 close(fd);
68 if (br < 0) {
69 return false;
70 }
71 buf[br] = '\0'; // null-terminate the string.
72 return true;
73}
74
75static bool make_proc_acpi_key_val(char **_ptr, char **_key, char **_val)
76{
77 char *ptr = *_ptr;
78
79 while (*ptr == ' ') {
80 ptr++; // skip whitespace.
81 }
82
83 if (*ptr == '\0') {
84 return false; // EOF.
85 }
86
87 *_key = ptr;
88
89 while ((*ptr != ':') && (*ptr != '\0')) {
90 ptr++;
91 }
92
93 if (*ptr == '\0') {
94 return false; // (unexpected) EOF.
95 }
96
97 *(ptr++) = '\0'; // terminate the key.
98
99 while (*ptr == ' ') {
100 ptr++; // skip whitespace.
101 }
102
103 if (*ptr == '\0') {
104 return false; // (unexpected) EOF.
105 }
106
107 *_val = ptr;
108
109 while ((*ptr != '\n') && (*ptr != '\0')) {
110 ptr++;
111 }
112
113 if (*ptr != '\0') {
114 *(ptr++) = '\0'; // terminate the value.
115 }
116
117 *_ptr = ptr; // store for next time.
118 return true;
119}
120
121static void check_proc_acpi_battery(const char *node, bool *have_battery,
122 bool *charging, int *seconds, int *percent)
123{
124 const char *base = proc_acpi_battery_path;
125 char info[1024];
126 char state[1024];
127 char *ptr = NULL;
128 char *key = NULL;
129 char *val = NULL;
130 bool charge = false;
131 bool choose = false;
132 int maximum = -1;
133 int remaining = -1;
134 int secs = -1;
135 int pct = -1;
136
137 if (!read_power_file(base, node, "state", state, sizeof(state))) {
138 return;
139 } else if (!read_power_file(base, node, "info", info, sizeof(info))) {
140 return;
141 }
142
143 ptr = &state[0];
144 while (make_proc_acpi_key_val(&ptr, &key, &val)) {
145 if (SDL_strcasecmp(key, "present") == 0) {
146 if (SDL_strcasecmp(val, "yes") == 0) {
147 *have_battery = true;
148 }
149 } else if (SDL_strcasecmp(key, "charging state") == 0) {
150 // !!! FIXME: what exactly _does_ charging/discharging mean?
151 if (SDL_strcasecmp(val, "charging/discharging") == 0) {
152 charge = true;
153 } else if (SDL_strcasecmp(val, "charging") == 0) {
154 charge = true;
155 }
156 } else if (SDL_strcasecmp(key, "remaining capacity") == 0) {
157 char *endptr = NULL;
158 const int cvt = (int)SDL_strtol(val, &endptr, 10);
159 if (*endptr == ' ') {
160 remaining = cvt;
161 }
162 }
163 }
164
165 ptr = &info[0];
166 while (make_proc_acpi_key_val(&ptr, &key, &val)) {
167 if (SDL_strcasecmp(key, "design capacity") == 0) {
168 char *endptr = NULL;
169 const int cvt = (int)SDL_strtol(val, &endptr, 10);
170 if (*endptr == ' ') {
171 maximum = cvt;
172 }
173 }
174 }
175
176 if ((maximum >= 0) && (remaining >= 0)) {
177 pct = (int)((((float)remaining) / ((float)maximum)) * 100.0f);
178 if (pct < 0) {
179 pct = 0;
180 } else if (pct > 100) {
181 pct = 100;
182 }
183 }
184
185 // !!! FIXME: calculate (secs).
186
187 /*
188 * We pick the battery that claims to have the most minutes left.
189 * (failing a report of minutes, we'll take the highest percent.)
190 */
191 if ((secs < 0) && (*seconds < 0)) {
192 if ((pct < 0) && (*percent < 0)) {
193 choose = true; // at least we know there's a battery.
194 }
195 if (pct > *percent) {
196 choose = true;
197 }
198 } else if (secs > *seconds) {
199 choose = true;
200 }
201
202 if (choose) {
203 *seconds = secs;
204 *percent = pct;
205 *charging = charge;
206 }
207}
208
209static void check_proc_acpi_ac_adapter(const char *node, bool *have_ac)
210{
211 const char *base = proc_acpi_ac_adapter_path;
212 char state[256];
213 char *ptr = NULL;
214 char *key = NULL;
215 char *val = NULL;
216
217 if (!read_power_file(base, node, "state", state, sizeof(state))) {
218 return;
219 }
220
221 ptr = &state[0];
222 while (make_proc_acpi_key_val(&ptr, &key, &val)) {
223 if (SDL_strcasecmp(key, "state") == 0) {
224 if (SDL_strcasecmp(val, "on-line") == 0) {
225 *have_ac = true;
226 }
227 }
228 }
229}
230
231bool SDL_GetPowerInfo_Linux_proc_acpi(SDL_PowerState *state, int *seconds, int *percent)
232{
233 struct dirent *dent = NULL;
234 DIR *dirp = NULL;
235 bool have_battery = false;
236 bool have_ac = false;
237 bool charging = false;
238
239 *seconds = -1;
240 *percent = -1;
241 *state = SDL_POWERSTATE_UNKNOWN;
242
243 dirp = opendir(proc_acpi_battery_path);
244 if (!dirp) {
245 return false; // can't use this interface.
246 } else {
247 while ((dent = readdir(dirp)) != NULL) {
248 const char *node = dent->d_name;
249 check_proc_acpi_battery(node, &have_battery, &charging,
250 seconds, percent);
251 }
252 closedir(dirp);
253 }
254
255 dirp = opendir(proc_acpi_ac_adapter_path);
256 if (!dirp) {
257 return false; // can't use this interface.
258 } else {
259 while ((dent = readdir(dirp)) != NULL) {
260 const char *node = dent->d_name;
261 check_proc_acpi_ac_adapter(node, &have_ac);
262 }
263 closedir(dirp);
264 }
265
266 if (!have_battery) {
267 *state = SDL_POWERSTATE_NO_BATTERY;
268 } else if (charging) {
269 *state = SDL_POWERSTATE_CHARGING;
270 } else if (have_ac) {
271 *state = SDL_POWERSTATE_CHARGED;
272 } else {
273 *state = SDL_POWERSTATE_ON_BATTERY;
274 }
275
276 return true; // definitive answer.
277}
278
279static bool next_string(char **_ptr, char **_str)
280{
281 char *ptr = *_ptr;
282 char *str;
283
284 while (*ptr == ' ') { // skip any spaces...
285 ptr++;
286 }
287
288 if (*ptr == '\0') {
289 return false;
290 }
291
292 str = ptr;
293 while ((*ptr != ' ') && (*ptr != '\n') && (*ptr != '\0')) {
294 ptr++;
295 }
296
297 if (*ptr != '\0') {
298 *(ptr++) = '\0';
299 }
300
301 *_str = str;
302 *_ptr = ptr;
303 return true;
304}
305
306static bool int_string(char *str, int *val)
307{
308 char *endptr = NULL;
309 *val = (int)SDL_strtol(str, &endptr, 0);
310 return (*str != '\0') && (*endptr == '\0');
311}
312
313// http://lxr.linux.no/linux+v2.6.29/drivers/char/apm-emulation.c
314bool SDL_GetPowerInfo_Linux_proc_apm(SDL_PowerState *state, int *seconds, int *percent)
315{
316 bool need_details = false;
317 int ac_status = 0;
318 int battery_status = 0;
319 int battery_flag = 0;
320 int battery_percent = 0;
321 int battery_time = 0;
322 const int fd = open(proc_apm_path, O_RDONLY | O_CLOEXEC);
323 char buf[128];
324 char *ptr = &buf[0];
325 char *str = NULL;
326 ssize_t br;
327
328 if (fd == -1) {
329 return false; // can't use this interface.
330 }
331
332 br = read(fd, buf, sizeof(buf) - 1);
333 close(fd);
334
335 if (br < 0) {
336 return false;
337 }
338
339 buf[br] = '\0'; // null-terminate the string.
340 if (!next_string(&ptr, &str)) { // driver version
341 return false;
342 }
343 if (!next_string(&ptr, &str)) { // BIOS version
344 return false;
345 }
346 if (!next_string(&ptr, &str)) { // APM flags
347 return false;
348 }
349
350 if (!next_string(&ptr, &str)) { // AC line status
351 return false;
352 } else if (!int_string(str, &ac_status)) {
353 return false;
354 }
355
356 if (!next_string(&ptr, &str)) { // battery status
357 return false;
358 } else if (!int_string(str, &battery_status)) {
359 return false;
360 }
361 if (!next_string(&ptr, &str)) { // battery flag
362 return false;
363 } else if (!int_string(str, &battery_flag)) {
364 return false;
365 }
366 if (!next_string(&ptr, &str)) { // remaining battery life percent
367 return false;
368 }
369 if (str[SDL_strlen(str) - 1] == '%') {
370 str[SDL_strlen(str) - 1] = '\0';
371 }
372 if (!int_string(str, &battery_percent)) {
373 return false;
374 }
375
376 if (!next_string(&ptr, &str)) { // remaining battery life time
377 return false;
378 } else if (!int_string(str, &battery_time)) {
379 return false;
380 }
381
382 if (!next_string(&ptr, &str)) { // remaining battery life time units
383 return false;
384 } else if (SDL_strcasecmp(str, "min") == 0) {
385 battery_time *= 60;
386 }
387
388 if (battery_flag == 0xFF) { // unknown state
389 *state = SDL_POWERSTATE_UNKNOWN;
390 } else if (battery_flag & (1 << 7)) { // no battery
391 *state = SDL_POWERSTATE_NO_BATTERY;
392 } else if (battery_flag & (1 << 3)) { // charging
393 *state = SDL_POWERSTATE_CHARGING;
394 need_details = true;
395 } else if (ac_status == 1) {
396 *state = SDL_POWERSTATE_CHARGED; // on AC, not charging.
397 need_details = true;
398 } else {
399 *state = SDL_POWERSTATE_ON_BATTERY;
400 need_details = true;
401 }
402
403 *percent = -1;
404 *seconds = -1;
405 if (need_details) {
406 const int pct = battery_percent;
407 const int secs = battery_time;
408
409 if (pct >= 0) { // -1 == unknown
410 *percent = (pct > 100) ? 100 : pct; // clamp between 0%, 100%
411 }
412 if (secs >= 0) { // -1 == unknown
413 *seconds = secs;
414 }
415 }
416
417 return true;
418}
419
420bool SDL_GetPowerInfo_Linux_sys_class_power_supply(SDL_PowerState *state, int *seconds, int *percent)
421{
422 const char *base = sys_class_power_supply_path;
423 struct dirent *dent;
424 DIR *dirp;
425
426 dirp = opendir(base);
427 if (!dirp) {
428 return false;
429 }
430
431 *state = SDL_POWERSTATE_NO_BATTERY; // assume we're just plugged in.
432 *seconds = -1;
433 *percent = -1;
434
435 while ((dent = readdir(dirp)) != NULL) {
436 const char *name = dent->d_name;
437 bool choose = false;
438 char str[64];
439 SDL_PowerState st;
440 int secs;
441 int pct;
442 int energy;
443 int power;
444
445 if ((SDL_strcmp(name, ".") == 0) || (SDL_strcmp(name, "..") == 0)) {
446 continue; // skip these, of course.
447 } else if (!read_power_file(base, name, "type", str, sizeof(str))) {
448 continue; // Don't know _what_ we're looking at. Give up on it.
449 } else if (SDL_strcasecmp(str, "Battery\n") != 0) {
450 continue; // we don't care about UPS and such.
451 }
452
453 /* if the scope is "device," it might be something like a PS4
454 controller reporting its own battery, and not something that powers
455 the system. Most system batteries don't list a scope at all; we
456 assume it's a system battery if not specified. */
457 if (read_power_file(base, name, "scope", str, sizeof(str))) {
458 if (SDL_strcasecmp(str, "Device\n") == 0) {
459 continue; // skip external devices with their own batteries.
460 }
461 }
462
463 // some drivers don't offer this, so if it's not explicitly reported assume it's present.
464 if (read_power_file(base, name, "present", str, sizeof(str)) && (SDL_strcmp(str, "0\n") == 0)) {
465 st = SDL_POWERSTATE_NO_BATTERY;
466 } else if (!read_power_file(base, name, "status", str, sizeof(str))) {
467 st = SDL_POWERSTATE_UNKNOWN; // uh oh
468 } else if (SDL_strcasecmp(str, "Charging\n") == 0) {
469 st = SDL_POWERSTATE_CHARGING;
470 } else if (SDL_strcasecmp(str, "Discharging\n") == 0) {
471 st = SDL_POWERSTATE_ON_BATTERY;
472 } else if ((SDL_strcasecmp(str, "Full\n") == 0) || (SDL_strcasecmp(str, "Not charging\n") == 0)) {
473 st = SDL_POWERSTATE_CHARGED;
474 } else {
475 st = SDL_POWERSTATE_UNKNOWN; // uh oh
476 }
477
478 if (!read_power_file(base, name, "capacity", str, sizeof(str))) {
479 pct = -1;
480 } else {
481 pct = SDL_atoi(str);
482 pct = (pct > 100) ? 100 : pct; // clamp between 0%, 100%
483 }
484
485 if (read_power_file(base, name, "time_to_empty_now", str, sizeof(str))) {
486 secs = SDL_atoi(str);
487 secs = (secs <= 0) ? -1 : secs; // 0 == unknown
488 } else if (st == SDL_POWERSTATE_ON_BATTERY) {
489 /* energy is Watt*hours and power is Watts */
490 energy = (read_power_file(base, name, "energy_now", str, sizeof(str))) ? SDL_atoi(str) : -1;
491 power = (read_power_file(base, name, "power_now", str, sizeof(str))) ? SDL_atoi(str) : -1;
492 secs = (energy >= 0 && power > 0) ? (3600LL * energy) / power : -1;
493 } else {
494 secs = -1;
495 }
496
497 /*
498 * We pick the battery that claims to have the most minutes left.
499 * (failing a report of minutes, we'll take the highest percent.)
500 */
501 if ((secs < 0) && (*seconds < 0)) {
502 if ((pct < 0) && (*percent < 0)) {
503 choose = true; // at least we know there's a battery.
504 } else if (pct > *percent) {
505 choose = true;
506 }
507 } else if (secs > *seconds) {
508 choose = true;
509 }
510
511 if (choose) {
512 *seconds = secs;
513 *percent = pct;
514 *state = st;
515 }
516 }
517
518 closedir(dirp);
519 return true; // don't look any further.
520}
521
522// d-bus queries to org.freedesktop.UPower.
523#ifdef SDL_USE_LIBDBUS
524#define UPOWER_DBUS_NODE "org.freedesktop.UPower"
525#define UPOWER_DBUS_PATH "/org/freedesktop/UPower"
526#define UPOWER_DBUS_INTERFACE "org.freedesktop.UPower"
527#define UPOWER_DEVICE_DBUS_INTERFACE "org.freedesktop.UPower.Device"
528
529static void check_upower_device(DBusConnection *conn, const char *path, SDL_PowerState *state, int *seconds, int *percent)
530{
531 bool choose = false;
532 SDL_PowerState st;
533 int secs;
534 int pct;
535 Uint32 ui32 = 0;
536 Sint64 si64 = 0;
537 double d = 0.0;
538
539 if (!SDL_DBus_QueryPropertyOnConnection(conn, UPOWER_DBUS_NODE, path, UPOWER_DEVICE_DBUS_INTERFACE, "Type", DBUS_TYPE_UINT32, &ui32)) {
540 return; // Don't know _what_ we're looking at. Give up on it.
541 } else if (ui32 != 2) { // 2==Battery
542 return; // we don't care about UPS and such.
543 } else if (!SDL_DBus_QueryPropertyOnConnection(conn, UPOWER_DBUS_NODE, path, UPOWER_DEVICE_DBUS_INTERFACE, "PowerSupply", DBUS_TYPE_BOOLEAN, &ui32)) {
544 return;
545 } else if (!ui32) {
546 return; // we don't care about random devices with batteries, like wireless controllers, etc
547 }
548
549 if (!SDL_DBus_QueryPropertyOnConnection(conn, UPOWER_DBUS_NODE, path, UPOWER_DEVICE_DBUS_INTERFACE, "IsPresent", DBUS_TYPE_BOOLEAN, &ui32)) {
550 return;
551 }
552 if (!ui32) {
553 st = SDL_POWERSTATE_NO_BATTERY;
554 } else {
555 /* Get updated information on the battery status
556 * This can occasionally fail, and we'll just return slightly stale data in that case
557 */
558 SDL_DBus_CallMethodOnConnection(conn, UPOWER_DBUS_NODE, path, UPOWER_DEVICE_DBUS_INTERFACE, "Refresh", DBUS_TYPE_INVALID, DBUS_TYPE_INVALID);
559
560 if (!SDL_DBus_QueryPropertyOnConnection(conn, UPOWER_DBUS_NODE, path, UPOWER_DEVICE_DBUS_INTERFACE, "State", DBUS_TYPE_UINT32, &ui32)) {
561 st = SDL_POWERSTATE_UNKNOWN; // uh oh
562 } else if (ui32 == 1) { // 1 == charging
563 st = SDL_POWERSTATE_CHARGING;
564 } else if ((ui32 == 2) || (ui32 == 3) || (ui32 == 6)) {
565 /* 2 == discharging;
566 * 3 == empty;
567 * 6 == "pending discharge" which GNOME interprets as equivalent
568 * to discharging */
569 st = SDL_POWERSTATE_ON_BATTERY;
570 } else if ((ui32 == 4) || (ui32 == 5)) {
571 /* 4 == full;
572 * 5 == "pending charge" which GNOME shows as "Not charging",
573 * used when a battery is configured to stop charging at a
574 * lower than 100% threshold */
575 st = SDL_POWERSTATE_CHARGED;
576 } else {
577 st = SDL_POWERSTATE_UNKNOWN; // uh oh
578 }
579 }
580
581 if (!SDL_DBus_QueryPropertyOnConnection(conn, UPOWER_DBUS_NODE, path, UPOWER_DEVICE_DBUS_INTERFACE, "Percentage", DBUS_TYPE_DOUBLE, &d)) {
582 pct = -1; // some old/cheap batteries don't set this property.
583 } else {
584 pct = (int)d;
585 pct = (pct > 100) ? 100 : pct; // clamp between 0%, 100%
586 }
587
588 if (!SDL_DBus_QueryPropertyOnConnection(conn, UPOWER_DBUS_NODE, path, UPOWER_DEVICE_DBUS_INTERFACE, "TimeToEmpty", DBUS_TYPE_INT64, &si64)) {
589 secs = -1;
590 } else {
591 secs = (int)si64;
592 secs = (secs <= 0) ? -1 : secs; // 0 == unknown
593 }
594
595 /*
596 * We pick the battery that claims to have the most minutes left.
597 * (failing a report of minutes, we'll take the highest percent.)
598 */
599 if ((secs < 0) && (*seconds < 0)) {
600 if ((pct < 0) && (*percent < 0)) {
601 choose = true; // at least we know there's a battery.
602 } else if (pct > *percent) {
603 choose = true;
604 }
605 } else if (secs > *seconds) {
606 choose = true;
607 }
608
609 if (choose) {
610 *seconds = secs;
611 *percent = pct;
612 *state = st;
613 }
614}
615#endif
616
617bool SDL_GetPowerInfo_Linux_org_freedesktop_upower(SDL_PowerState *state, int *seconds, int *percent)
618{
619 bool result = false;
620
621#ifdef SDL_USE_LIBDBUS
622 SDL_DBusContext *dbus = SDL_DBus_GetContext();
623 char **paths = NULL;
624 int i, numpaths = 0;
625
626 if (!dbus || !SDL_DBus_CallMethodOnConnection(dbus->system_conn, UPOWER_DBUS_NODE, UPOWER_DBUS_PATH, UPOWER_DBUS_INTERFACE, "EnumerateDevices",
627 DBUS_TYPE_INVALID,
628 DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &paths, &numpaths, DBUS_TYPE_INVALID)) {
629 return false; // try a different approach than UPower.
630 }
631
632 result = true; // Clearly we can use this interface.
633 *state = SDL_POWERSTATE_NO_BATTERY; // assume we're just plugged in.
634 *seconds = -1;
635 *percent = -1;
636
637 for (i = 0; i < numpaths; i++) {
638 check_upower_device(dbus->system_conn, paths[i], state, seconds, percent);
639 }
640
641 dbus->free_string_array(paths);
642#endif // SDL_USE_LIBDBUS
643
644 return result;
645}
646
647#endif // SDL_POWER_LINUX
648#endif // SDL_POWER_DISABLED
649