Add read-only Linux system battery reader

Reads the device's own battery from /sys/class/power_supply, skipping
peripheral (scope=Device) supplies and implausible values. Offline tested
against a fake power_supply tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
baketnkandClaude Opus 5.5 committed 2026-09-25 10:47:11 -04:00
1 parent 94b747bb04
commit 59f4f9cff8
4 files changed
+93 -1

No files matched your search

+7 -1
View File
@@ -62,6 +62,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_include_directories(frameyap_mount PUBLIC src)
add_library(frameyap_worker src/worker.cpp)
target_include_directories(frameyap_worker PUBLIC src)
add_library(frameyap_battery src/battery.cpp)
target_include_directories(frameyap_battery PUBLIC src)
endif()
if(FRAMEYAP_NATIVE)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
@@ -100,7 +102,7 @@ if(FRAMEYAP_NATIVE)
target_sources(frameyap PRIVATE src/runtime.cpp src/controller.cpp src/backend_manager.cpp src/overlay.cpp src/overlay_texture.cpp src/audio.cpp src/text_input.cpp src/focus_guard.cpp
"${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method.c" "${CMAKE_CURRENT_BINARY_DIR}/gamescope-input-method-client.h")
target_include_directories(frameyap PRIVATE "${OPENVR_INCLUDE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(frameyap PRIVATE frameyap_paced frameyap_worker frameyap_mount frameyap_panel PkgConfig::SDL3 PkgConfig::WAYLAND PkgConfig::XCB Vulkan::Vulkan "${OPENVR_LIBRARY}")
target_link_libraries(frameyap PRIVATE frameyap_paced frameyap_worker frameyap_mount frameyap_battery frameyap_panel PkgConfig::SDL3 PkgConfig::WAYLAND PkgConfig::XCB Vulkan::Vulkan "${OPENVR_LIBRARY}")
target_compile_definitions(frameyap PRIVATE FRAMEYAP_NATIVE=1)
set_target_properties(frameyap PROPERTIES INSTALL_RPATH "$ORIGIN/../lib")
endif()
@@ -176,6 +178,10 @@ if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
add_executable(frameyap_lock_test tests/lock_test.cpp)
target_include_directories(frameyap_lock_test PRIVATE src)
add_test(NAME frameyap.instance_lock COMMAND frameyap_lock_test)
add_executable(frameyap_battery_test tests/battery_test.cpp)
target_link_libraries(frameyap_battery_test PRIVATE frameyap_battery)
target_compile_options(frameyap_battery_test PRIVATE -UNDEBUG)
add_test(NAME frameyap.battery COMMAND frameyap_battery_test)
endif()
add_executable(frameyap_panel_drag_test tests/panel_drag_test.cpp)
target_include_directories(frameyap_panel_drag_test PRIVATE src)
+35
View File
@@ -0,0 +1,35 @@
#include "battery.hpp"
#include <algorithm>
#include <fstream>
#include <string>
#include <system_error>
#include <vector>
namespace frameyap {
namespace {
std::string first_line(const std::filesystem::path& path) {
std::ifstream in(path);
std::string line;
if (in) std::getline(in, line);
return line;
}
} // namespace
std::optional<BatteryLevel> system_battery(const std::filesystem::path& root) {
std::error_code error;
std::vector<std::filesystem::path> supplies;
for (const auto& entry : std::filesystem::directory_iterator(root, error)) supplies.push_back(entry.path());
std::sort(supplies.begin(), supplies.end()); // deterministic choice among several
for (const auto& supply : supplies) {
if (first_line(supply / "type") != "Battery" || first_line(supply / "scope") == "Device") continue;
const auto capacity = first_line(supply / "capacity");
if (capacity.empty() || capacity.size() > 3 ||
!std::all_of(capacity.begin(), capacity.end(), [](char c) { return c >= '0' && c <= '9'; }))
continue;
const int percent = std::stoi(capacity);
if (percent > 100) continue;
return BatteryLevel{percent, first_line(supply / "status") == "Charging"};
}
return std::nullopt;
}
} // namespace frameyap
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <filesystem>
#include <optional>
namespace frameyap {
struct BatteryLevel {
int percent = 0; // 0..100
bool charging = false;
bool operator==(const BatteryLevel&) const = default;
};
// The device's own battery from a Linux power_supply tree. Peripheral batteries
// (scope=Device, such as paired controllers) are skipped. Read-only; no sudo.
std::optional<BatteryLevel> system_battery(const std::filesystem::path& root = "/sys/class/power_supply");
} // namespace frameyap
+37
View File
@@ -0,0 +1,37 @@
#include "battery.hpp"
#include <cassert>
#include <fstream>
#include <string>
#include <unistd.h>
using namespace frameyap;
namespace fs = std::filesystem;
namespace {
void put(const fs::path& path, const std::string& value) {
fs::create_directories(path.parent_path());
std::ofstream(path) << value << '\n';
}
}
int main() {
const auto root = fs::temp_directory_path() / ("frameyap-battery-" + std::to_string(::getpid()));
fs::remove_all(root);
assert(!system_battery(root)); // missing tree
fs::create_directories(root);
put(root / "AC/type", "Mains");
put(root / "AC/online", "1");
put(root / "hid-controller/type", "Battery"); // paired peripheral, not the headset
put(root / "hid-controller/scope", "Device");
put(root / "hid-controller/capacity", "12");
assert(!system_battery(root));
put(root / "BAT0/type", "Battery");
put(root / "BAT0/capacity", "250"); // implausible values are ignored
assert(!system_battery(root));
put(root / "BAT0/capacity", "64");
put(root / "BAT0/status", "Discharging");
assert((system_battery(root) == BatteryLevel{64, false}));
put(root / "BAT0/status", "Charging");
assert((system_battery(root) == BatteryLevel{64, true}));
put(root / "BAT0/capacity", "x1");
assert(!system_battery(root));
fs::remove_all(root);
}