diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e6d3d2..df1c6bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/battery.cpp b/src/battery.cpp new file mode 100644 index 0000000..d629a67 --- /dev/null +++ b/src/battery.cpp @@ -0,0 +1,35 @@ +#include "battery.hpp" +#include +#include +#include +#include +#include + +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 system_battery(const std::filesystem::path& root) { + std::error_code error; + std::vector 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 diff --git a/src/battery.hpp b/src/battery.hpp new file mode 100644 index 0000000..46a7e12 --- /dev/null +++ b/src/battery.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +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 system_battery(const std::filesystem::path& root = "/sys/class/power_supply"); +} // namespace frameyap diff --git a/tests/battery_test.cpp b/tests/battery_test.cpp new file mode 100644 index 0000000..f2c1e0c --- /dev/null +++ b/tests/battery_test.cpp @@ -0,0 +1,37 @@ +#include "battery.hpp" +#include +#include +#include +#include + +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); +}