Added a SystemStatus class to poll Bluetooth, Wi-Fi, cellular and battery information from the operating system

This commit is contained in:
Leon Styhre 2025-02-22 20:59:30 +01:00
parent b50b4d9d0a
commit e59f19c2ba
8 changed files with 577 additions and 1 deletions

View file

@ -123,7 +123,9 @@ if(IOS)
elseif(APPLE)
include_directories(${COMMON_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/src)
add_executable(ES-DE ${ES_SOURCES} ${ES_HEADERS})
target_link_libraries(ES-DE ${COMMON_LIBRARIES} es-core)
target_link_libraries(ES-DE ${COMMON_LIBRARIES} "-framework CoreFoundation -framework IOKit"
"-framework SystemConfiguration"
"-framework IOBluetooth" es-core)
set_target_properties(ES-DE PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE)
if(CMAKE_CXX_COMPILER_VERSION GREATER_EQUAL 15.0.0)
target_link_options(ES-DE PRIVATE LINKER:-no_warn_duplicate_libraries)

View file

@ -26,6 +26,7 @@
#include "Settings.h"
#include "Sound.h"
#include "SystemData.h"
#include "SystemStatus.h"
#include "guis/GuiDetectDevice.h"
#include "guis/GuiLaunchScreen.h"
#include "utils/FileSystemUtil.h"
@ -1165,6 +1166,7 @@ int main(int argc, char* argv[])
}
#endif
SystemStatus::getInstance();
MameNames::getInstance();
ThemeData::populateThemes();
loadSystemsReturnCode loadSystemsStatus {loadSystemConfigFile()};

View file

@ -22,6 +22,7 @@ set(CORE_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/MameNames.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Settings.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemStatus.h
${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Window.h
@ -110,6 +111,7 @@ set(CORE_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Settings.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemStatus.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Window.cpp
@ -179,6 +181,10 @@ if(ANDROID)
set(CORE_SOURCES ${CORE_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/PlatformUtilAndroid.cpp)
endif()
if(APPLE AND NOT IOS)
set(CORE_HEADERS ${CORE_HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/src/BluetoothStatusApple.m)
endif()
if(IOS)
set(CORE_HEADERS ${CORE_HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/src/InputOverlay.h)
set(CORE_HEADERS ${CORE_HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/PlatformUtilIOS.h)

View file

@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
//
// ES-DE Frontend
// BluetoothStatusApple.h
//
// Gets the Bluetooth adapter status on macOS.
//
#ifndef ES_CORE_BLUETOOTH_STATUS_APPLE_H
#define ES_CORE_BLUETOOTH_STATUS_APPLE_H
#ifdef __cplusplus
extern "C" {
int getBluetoothStatus();
}
#endif
#endif // ES_CORE_BLUETOOTH_STATUS_APPLE_H

View file

@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
//
// ES-DE Frontend
// BluetoothStatusApple.m
//
// Gets the Bluetooth adapter status on macOS.
//
#import "BluetoothStatusApple.h"
#import <IOBluetooth/IOBluetooth.h>
int getBluetoothStatus()
{
IOBluetoothHostController* hciController = [IOBluetoothHostController defaultController];
if (hciController != NULL && hciController.powerState)
return 1;
else
return 0;
}

View file

@ -221,6 +221,12 @@ void Settings::setDefaults()
#endif
mBoolMap["ScreensaverVideoBlur"] = {false, false};
mBoolMap["SystemStatusDisplayAll"] = {false, false};
mBoolMap["SystemStatusBluetooth"] = {true, true};
mBoolMap["SystemStatusWifi"] = {true, true};
mBoolMap["SystemStatusCellular"] = {true, true};
mBoolMap["SystemStatusBattery"] = {true, true};
mBoolMap["SystemStatusBatteryPercentage"] = {true, true};
mBoolMap["ThemeVariantTriggers"] = {true, true};
mBoolMap["DisplayClock"] = {false, false};
mBoolMap["MenuBlurBackground"] = {true, true};

View file

@ -0,0 +1,444 @@
// SPDX-License-Identifier: MIT
//
// ES-DE Frontend
// SystemStatus.cpp
//
// Queries system status information from the operating system.
// This includes Bluetooth, Wi-Fi, cellular and battery.
//
#include "SystemStatus.h"
#include "Log.h"
#include "Settings.h"
#include "utils/FileSystemUtil.h"
#include "utils/StringUtil.h"
#include <SDL2/SDL_timer.h>
#include <algorithm>
#if defined(__APPLE__) && !defined(__IOS__)
#include "BluetoothStatusApple.h"
#include <IOKit/ps/IOPSKeys.h>
#include <IOKit/ps/IOPowerSources.h>
#include <SystemConfiguration/SCNetworkConfiguration.h>
#endif
#if defined(_WIN64)
// clang-format off
// Because of course building fails if the files are included in the "wrong" order.
#include <windows.h>
#include <iphlpapi.h>
#include <bluetoothapis.h>
// clang-format on
#endif
#if defined(__ANDROID__)
#include "utils/PlatformUtilAndroid.h"
#endif
#define DEBUG_SYSTEM_STATUS false
SystemStatus::SystemStatus() noexcept
: mExitPolling {false}
, mPollImmediately {false}
, mHasBluetooth {false}
, mHasWifi {false}
, mHasCellular {false}
, mHasBattery {false}
, mBatteryCharging {false}
, mBatteryCapacity {0}
{
setCheckFlags();
mPollThread = std::make_unique<std::thread>(&SystemStatus::pollStatus, this);
}
SystemStatus::~SystemStatus()
{
mExitPolling = true;
if (mPollThread != nullptr && mPollThread->joinable()) {
mPollThread->join();
mPollThread.reset();
}
}
SystemStatus& SystemStatus::getInstance()
{
static SystemStatus instance;
return instance;
}
void SystemStatus::setCheckFlags()
{
std::unique_lock<std::mutex> statusLock {mStatusMutex};
mCheckBluetooth = Settings::getInstance()->getBool("SystemStatusBluetooth");
mCheckWifi = Settings::getInstance()->getBool("SystemStatusWifi");
mCheckCellular = Settings::getInstance()->getBool("SystemStatusCellular");
mCheckBattery = Settings::getInstance()->getBool("SystemStatusBattery");
}
void SystemStatus::setPolling(const bool state)
{
if (state == false) {
mExitPolling = true;
if (mPollThread != nullptr && mPollThread->joinable()) {
mPollThread->join();
mPollThread.reset();
}
}
else if (mPollThread == nullptr) {
mExitPolling = false;
mPollThread = std::make_unique<std::thread>(&SystemStatus::pollStatus, this);
}
}
SystemStatus::Status SystemStatus::getStatus()
{
mStatus.hasBluetooth = mHasBluetooth;
mStatus.hasWifi = mHasWifi;
mStatus.hasCellular = mHasCellular;
mStatus.hasBattery = mHasBattery;
mStatus.batteryCharging = mBatteryCharging;
mStatus.batteryCapacity = mBatteryCapacity;
return mStatus;
}
void SystemStatus::pollStatus()
{
while (!mExitPolling) {
std::unique_lock<std::mutex> statusLock {mStatusMutex};
getStatusBluetooth();
getStatusWifi();
getStatusCellular();
getStatusBattery();
statusLock.unlock();
#if (DEBUG_SYSTEM_STATUS)
std::string status {"Bluetooth "};
status.append(mHasBluetooth ? "enabled" : "disabled")
.append(", Wi-Fi ")
.append(mHasBluetooth ? "enabled" : "disabled")
.append(", cellular ")
.append(mHasCellular ? "enabled" : "disabled")
.append(", battery ")
.append(mHasBattery ? "enabled" : "disabled");
if (mHasBattery) {
status.append(" (")
.append(mBatteryCharging ? "charging" : "not charging")
.append(" and at ")
.append(std::to_string(mBatteryCapacity))
.append("% capacity)");
}
LOG(LogDebug) << "SystemStatus::pollStatus(): " << status;
#endif
int delayValue {0};
while (!mPollImmediately && !mExitPolling && delayValue < 3000) {
delayValue += 100;
SDL_Delay(100);
}
mPollImmediately = false;
}
}
void SystemStatus::getStatusBluetooth()
{
if (!mCheckBluetooth)
return;
bool hasBluetooth {false};
#if defined(__APPLE__) && !defined(__IOS__)
if (getBluetoothStatus() == 1)
hasBluetooth = true;
#elif defined(_WIN64)
BLUETOOTH_FIND_RADIO_PARAMS btFindRadio {sizeof(BLUETOOTH_FIND_RADIO_PARAMS)};
HANDLE btRadio {nullptr};
BLUETOOTH_RADIO_INFO btInfo {sizeof(BLUETOOTH_RADIO_INFO), 0};
if (BluetoothFindFirstRadio(&btFindRadio, &btRadio) != nullptr) {
if (BluetoothGetRadioInfo(btRadio, &btInfo) == ERROR_SUCCESS)
hasBluetooth = true;
}
#elif defined(__ANDROID__)
if (Utils::Platform::Android::getBluetoothStatus())
hasBluetooth = true;
#elif defined(__linux__)
const std::string sysEntry {"/sys/class/rfkill"};
auto entries {Utils::FileSystem::getDirContent(sysEntry, false)};
for (auto& entry : entries) {
if (Utils::FileSystem::exists(entry + "/type")) {
std::string type;
std::ifstream fileStream;
fileStream.open(entry + "/type");
getline(fileStream, type);
fileStream.close();
if (Utils::String::toLower(type) == "bluetooth") {
std::string state;
fileStream.open(entry + "/state");
getline(fileStream, state);
fileStream.close();
if (std::stoi(state) == 1)
hasBluetooth = true;
break;
}
}
}
#endif
mHasBluetooth = hasBluetooth;
}
void SystemStatus::getStatusWifi()
{
if (!mCheckWifi)
return;
bool hasWifi {false};
#if defined(__APPLE__) && !defined(__IOS__)
const CFArrayRef interfaces {SCNetworkInterfaceCopyAll()};
if (interfaces != nullptr) {
for (CFIndex i {0}; i < CFArrayGetCount(interfaces); ++i) {
SCNetworkInterfaceRef interface {
static_cast<SCNetworkInterfaceRef>(CFArrayGetValueAtIndex(interfaces, i))};
if (SCNetworkInterfaceGetInterfaceType(interface) == kSCNetworkInterfaceTypeIEEE80211) {
const CFStringRef bsdName {SCNetworkInterfaceGetBSDName(interface)};
const SCDynamicStoreRef session {
SCDynamicStoreCreate(nullptr, CFSTR("Custom"), nullptr, nullptr)};
const CFStringRef resolvedQuery {CFStringCreateWithFormat(
nullptr, nullptr, CFSTR("State:/Network/Interface/%@/IPv4"), bsdName)};
const CFDictionaryRef dict {
static_cast<CFDictionaryRef>(SCDynamicStoreCopyValue(session, resolvedQuery))};
if (dict != nullptr) {
hasWifi = true;
CFRelease(dict);
CFRelease(resolvedQuery);
CFRelease(session);
break;
}
else {
CFRelease(resolvedQuery);
CFRelease(session);
}
}
}
CFRelease(interfaces);
}
#elif defined(_WIN64)
PIP_ADAPTER_INFO pAdapterInfo {nullptr};
PIP_ADAPTER_INFO pAdapter {nullptr};
ULONG ulOutBufLen {sizeof(IP_ADAPTER_INFO)};
pAdapterInfo = reinterpret_cast<IP_ADAPTER_INFO*>(malloc(sizeof(IP_ADAPTER_INFO)));
if (pAdapterInfo != nullptr) {
// Make an initial call to GetAdaptersInfo to get the necessary size into the
// ulOutBufLen variable, which may or may not be big enough.
if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) {
free(pAdapterInfo);
pAdapterInfo = reinterpret_cast<IP_ADAPTER_INFO*>(malloc(ulOutBufLen));
}
if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == NO_ERROR) {
pAdapter = pAdapterInfo;
while (pAdapter) {
if (pAdapter->Type == IF_TYPE_IEEE80211) {
// Checking whether the interface has an IP address is crude but
// it seems to get the job done. And there is no other obvious
// way to query the interface status without using additional
// convoluted API calls.
if (const std::string {pAdapter->IpAddressList.IpAddress.String} != "0.0.0.0") {
hasWifi = true;
break;
}
}
pAdapter = pAdapter->Next;
}
}
if (pAdapterInfo)
free(pAdapterInfo);
}
#elif defined(__ANDROID__)
if (Utils::Platform::Android::getWifiStatus() == 1)
hasWifi = true;
#elif defined(__linux__)
const std::string sysEntry {"/sys/class/net"};
auto entries {Utils::FileSystem::getDirContent(sysEntry, false)};
for (auto& entry : entries) {
if (Utils::FileSystem::exists(entry + "/wireless") &&
Utils::FileSystem::exists(entry + "/operstate")) {
std::string wifiState;
std::ifstream fileStream;
fileStream.open(entry + "/operstate");
getline(fileStream, wifiState);
fileStream.close();
if (Utils::String::toLower(wifiState) == "up")
hasWifi = true;
}
}
#endif
mHasWifi = hasWifi;
}
void SystemStatus::getStatusCellular()
{
if (!mCheckCellular)
return;
bool hasCellular {false};
#if defined(__ANDROID__)
if (Utils::Platform::Android::getCellularStatus() >= 1)
hasCellular = true;
#endif
mHasCellular = hasCellular;
}
void SystemStatus::getStatusBattery()
{
if (!mCheckBattery)
return;
bool hasBattery {false};
bool batteryCharging {false};
int batteryCapacity {0};
#if defined(__APPLE__) && !defined(__IOS__)
CFTypeRef sourceInfo {IOPSCopyPowerSourcesInfo()};
CFArrayRef sourceList {IOPSCopyPowerSourcesList(sourceInfo)};
if (sourceList != nullptr && CFArrayGetCount(sourceList) > 0) {
CFDictionaryRef source {nullptr};
for (CFIndex i {0}; i < CFArrayGetCount(sourceList); ++i) {
source =
IOPSGetPowerSourceDescription(sourceInfo, CFArrayGetValueAtIndex(sourceList, i));
// Check if this is a battery.
const CFStringRef type {static_cast<CFStringRef>(
CFDictionaryGetValue(source, CFSTR(kIOPSTransportTypeKey)))};
if (kCFCompareEqualTo == CFStringCompare(type, CFSTR(kIOPSInternalType), 0))
break;
else
source = nullptr;
}
if (source != nullptr) {
hasBattery = true;
if (CFDictionaryGetValue(source, CFSTR(kIOPSIsChargingKey)) != nullptr) {
batteryCharging = CFBooleanGetValue(static_cast<CFBooleanRef>(
CFDictionaryGetValue(source, CFSTR(kIOPSIsChargingKey))));
}
int curCapacity {0};
const CFNumberRef curCapacityNum {static_cast<CFNumberRef>(
CFDictionaryGetValue(source, CFSTR(kIOPSCurrentCapacityKey)))};
CFNumberGetValue(curCapacityNum, kCFNumberIntType, &curCapacity);
int maxCapacity {0};
const CFNumberRef maxCapacityNum {
static_cast<CFNumberRef>(CFDictionaryGetValue(source, CFSTR(kIOPSMaxCapacityKey)))};
CFNumberGetValue(maxCapacityNum, kCFNumberIntType, &maxCapacity);
if (maxCapacity > 0)
batteryCapacity = curCapacity / maxCapacity * 100;
}
}
if (sourceInfo != nullptr)
CFRelease(sourceInfo);
if (sourceList != nullptr)
CFRelease(sourceList);
#elif defined(_WIN64)
SYSTEM_POWER_STATUS powerStatus;
if (GetSystemPowerStatus(&powerStatus)) {
if (powerStatus.BatteryFlag != 128 && powerStatus.BatteryFlag != 255) {
hasBattery = true;
if (powerStatus.ACLineStatus == 1)
atteryCharging = true;
batteryCapacity = powerStatus.BatteryLifePercent;
}
else {
hasBattery = false;
}
}
#elif defined(__ANDROID__)
std::pair<int, int> batteryStatus {Utils::Platform::Android::getBatteryStatus()};
hasBattery = static_cast<bool>(batteryStatus.first);
if (batteryStatus.first == -1 && batteryStatus.second == -1) {
hasBattery = false;
}
else {
hasBattery = true;
if (batteryStatus.first == 1)
batteryCharging = true;
}
batteryCapacity = batteryStatus.second;
#elif defined(__linux__)
const std::string sysEntry {"/sys/class/power_supply"};
std::string batteryDir;
auto entries {Utils::FileSystem::getDirContent(sysEntry, false)};
if (std::find(entries.cbegin(), entries.cend(), sysEntry + "/BAT0") != entries.cend())
batteryDir = sysEntry + "/BAT0";
else if (std::find(entries.cbegin(), entries.cend(), sysEntry + "/BAT1") != entries.cend())
batteryDir = sysEntry + "/BAT1";
else if (std::find(entries.cbegin(), entries.cend(), sysEntry + "/battery") != entries.cend())
batteryDir = sysEntry + "/battery";
if (!Utils::FileSystem::exists(batteryDir + "/status"))
hasBattery = false;
if (!Utils::FileSystem::exists(batteryDir + "/capacity"))
hasBattery = false;
if (hasBattery) {
std::string batteryStatusValue;
std::string batteryCapacityValue;
std::ifstream fileStream;
fileStream.open(batteryDir + "/status");
getline(fileStream, batteryStatusValue);
batteryStatusValue = Utils::String::toLower(batteryStatusValue);
fileStream.close();
if (batteryStatusValue != "discharging")
batteryCharging = true;
fileStream.open(batteryDir + "/capacity");
getline(fileStream, batteryCapacityValue);
fileStream.close();
batteryCapacity = std::stoi(batteryCapacityValue);
}
#endif
mHasBattery = hasBattery;
mBatteryCharging = batteryCharging;
mBatteryCapacity = batteryCapacity;
}

View file

@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
//
// ES-DE Frontend
// SystemStatus.h
//
// Queries system status information from the operating system.
// This includes Bluetooth, Wi-Fi, cellular and battery.
//
#ifndef ES_CORE_SYSTEM_STATUS_H
#define ES_CORE_SYSTEM_STATUS_H
#include <atomic>
#include <mutex>
#include <thread>
class SystemStatus
{
public:
~SystemStatus();
static SystemStatus& getInstance();
void setCheckFlags();
void setPolling(const bool state);
void pollImmediately() { mPollImmediately = true; }
struct Status {
bool hasBluetooth;
bool hasWifi;
bool hasCellular;
bool hasBattery;
bool batteryCharging;
int batteryCapacity;
Status()
: hasBluetooth {false}
, hasWifi {false}
, hasCellular {false}
, hasBattery {false}
, batteryCharging {false}
, batteryCapacity {0}
{
}
};
Status getStatus();
private:
SystemStatus() noexcept;
void pollStatus();
void getStatusBluetooth();
void getStatusWifi();
void getStatusCellular();
void getStatusBattery();
bool mCheckBluetooth;
bool mCheckWifi;
bool mCheckCellular;
bool mCheckBattery;
std::unique_ptr<std::thread> mPollThread;
Status mStatus;
std::mutex mStatusMutex;
std::atomic<bool> mExitPolling;
std::atomic<bool> mPollImmediately;
std::atomic<bool> mHasBluetooth;
std::atomic<bool> mHasWifi;
std::atomic<bool> mHasCellular;
std::atomic<bool> mHasBattery;
std::atomic<bool> mBatteryCharging;
std::atomic<int> mBatteryCapacity;
};
#endif // ES_CORE_SYSTEM_STATUS_H