feat: add kernel ACPI support for querying battery percentage, add battery indicator to desktop, move log state to 0:/state directory
This commit is contained in:
1 parent
0a18b2cf2d
commit
e641500b40
34 files changed
+2461
-100
No files matched your search
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* Battery.cpp
|
||||
* ACPI Control Method Battery: _STA / _BIF / _BIX / _BST, plus _PSR on the
|
||||
* AC adapter
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Battery.hpp"
|
||||
#include "AcpiDevices.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace Battery {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
// ACPI reports an unreadable field as all-ones rather than zero.
|
||||
static constexpr uint32_t Unknown32 = 0xFFFFFFFFu;
|
||||
|
||||
// The EC transactions behind _BST take a few milliseconds, and the
|
||||
// value only moves on a scale of minutes, so a short cache keeps a
|
||||
// one-second panel repaint from becoming a bus workload.
|
||||
static constexpr uint64_t CacheMs = 2000;
|
||||
|
||||
static bool g_initialized = false;
|
||||
static bool g_haveBattery = false;
|
||||
static bool g_haveAdapter = false;
|
||||
static char g_batteryPath[128];
|
||||
static char g_adapterPath[128];
|
||||
static int32_t g_batteryNode = -1;
|
||||
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
// Set for the boot-path probe only, so the per-method tracing does not
|
||||
// repeat on every poll once the system is up.
|
||||
static bool g_tracing = false;
|
||||
|
||||
static bool Refresh(Status& out);
|
||||
static void Probe();
|
||||
static Status g_cached{};
|
||||
static bool g_cacheValid = false;
|
||||
static uint64_t g_cacheStamp = 0;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Path helpers
|
||||
|
||||
// ============================================================================
|
||||
static void AppendMethod(const char* devicePath, const char* method, char* out, int maxLen) {
|
||||
int i = 0;
|
||||
while (devicePath[i] && i < maxLen - 6) {
|
||||
out[i] = devicePath[i];
|
||||
i++;
|
||||
}
|
||||
// The root scope is written "\" with no separator before the first
|
||||
// segment; every other path takes a dot.
|
||||
if (!(i == 1 && out[0] == '\\'))
|
||||
out[i++] = '.';
|
||||
for (int j = 0; j < 4 && i < maxLen - 1; j++)
|
||||
out[i++] = method[j];
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
static void CopyPath(char* dst, const char* src, int maxLen) {
|
||||
int i = 0;
|
||||
while (src[i] && i < maxLen - 1) {
|
||||
dst[i] = src[i];
|
||||
i++;
|
||||
}
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Initialize
|
||||
|
||||
// ============================================================================
|
||||
void Initialize() {
|
||||
if (g_initialized) return;
|
||||
g_initialized = true;
|
||||
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return;
|
||||
|
||||
// Searched by HID rather than off an enumerated list: a laptop
|
||||
// namespace runs well past the list's fixed size, and the battery
|
||||
// sits deep under the EC where it would be cut off.
|
||||
AcpiDevices::DeviceInfo bat{};
|
||||
if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_BATTERY, bat)) {
|
||||
CopyPath(g_batteryPath, bat.Path, sizeof(g_batteryPath));
|
||||
g_batteryNode = bat.NodeIndex;
|
||||
g_haveBattery = true;
|
||||
}
|
||||
|
||||
AcpiDevices::DeviceInfo ac{};
|
||||
if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_AC_ADAPTER, ac)) {
|
||||
CopyPath(g_adapterPath, ac.Path, sizeof(g_adapterPath));
|
||||
g_haveAdapter = true;
|
||||
}
|
||||
|
||||
if (!g_haveBattery) {
|
||||
// Distinguish "this machine has no battery" from "the namespace
|
||||
// walk found nothing at all", which would point at the walk
|
||||
// rather than the hardware.
|
||||
auto& ns = interp.GetNamespace();
|
||||
int deviceCount = 0;
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||
[&](int32_t, const AML::NamespaceNode*) { deviceCount++; });
|
||||
|
||||
KernelLogStream(INFO, "Battery") << "No ACPI battery device (PNP0C0A) among "
|
||||
<< base::dec << (uint64_t)deviceCount << " ACPI devices";
|
||||
return;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "Battery") << "ACPI battery at " << g_batteryPath
|
||||
<< (g_haveAdapter ? " (AC adapter present)" : " (no AC adapter device)");
|
||||
|
||||
Probe();
|
||||
}
|
||||
|
||||
// Evaluate the battery methods once, here on the boot path, announcing
|
||||
// each before it runs. These are the first firmware methods this kernel
|
||||
// executes that reach the embedded controller, and if one of them takes
|
||||
// the machine down the last line printed says which -- far better than
|
||||
// discovering it when the desktop makes its first query.
|
||||
static void Probe() {
|
||||
KernelLogStream(INFO, "Battery") << "Probing battery methods";
|
||||
|
||||
g_tracing = true;
|
||||
Status s{};
|
||||
bool ok = Refresh(s);
|
||||
g_tracing = false;
|
||||
|
||||
if (!ok) {
|
||||
KernelLogStream(WARNING, "Battery") << "Probe failed; battery status unavailable";
|
||||
g_haveBattery = false;
|
||||
return;
|
||||
}
|
||||
|
||||
g_cached = s;
|
||||
g_cacheStamp = 0; // let the first real query refresh
|
||||
g_cacheValid = true;
|
||||
|
||||
if (!s.Present) {
|
||||
KernelLogStream(INFO, "Battery") << "Battery slot empty"
|
||||
<< (s.AcOnline ? " (on AC)" : "");
|
||||
return;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "Battery") << "Battery at " << base::dec
|
||||
<< (uint64_t)s.Percent << "% ("
|
||||
<< (s.Charging ? "charging" : (s.Discharging ? "discharging" : "idle"))
|
||||
<< ", " << (uint64_t)s.RemainingCapacity << " of "
|
||||
<< (uint64_t)s.FullCapacity << (s.PowerUnitMilliAmps ? " mAh)" : " mWh)");
|
||||
}
|
||||
|
||||
bool IsPresent() {
|
||||
return g_haveBattery;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Method evaluation
|
||||
|
||||
// ============================================================================
|
||||
static const char* TypeName(AML::ObjectType t) {
|
||||
switch (t) {
|
||||
case AML::ObjectType::None: return "none";
|
||||
case AML::ObjectType::Integer: return "integer";
|
||||
case AML::ObjectType::String: return "string";
|
||||
case AML::ObjectType::Buffer: return "buffer";
|
||||
case AML::ObjectType::Package: return "package";
|
||||
case AML::ObjectType::Device: return "device";
|
||||
case AML::ObjectType::Method: return "method";
|
||||
case AML::ObjectType::OperationRegion: return "opregion";
|
||||
case AML::ObjectType::Field: return "field";
|
||||
default: return "other";
|
||||
}
|
||||
}
|
||||
|
||||
static bool EvalPackage(const char* devicePath, const char* method, AML::Object& out) {
|
||||
char path[160];
|
||||
AppendMethod(devicePath, method, path, sizeof(path));
|
||||
|
||||
auto& interp = AML::GetInterpreter();
|
||||
|
||||
// Separate "the method is not there" from "it ran and gave us
|
||||
// something that is not a package": those need opposite fixes, and
|
||||
// the failure looks identical from the caller.
|
||||
int32_t node = interp.GetNamespace().FindNode(path);
|
||||
if (node < 0) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " " << path << ": absent";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trace the opcodes of the one method whose behaviour we cannot
|
||||
// otherwise explain: it runs to completion, trips no limit, and
|
||||
// still yields no value.
|
||||
bool traceThis = g_tracing && method[1] == 'B';
|
||||
if (traceThis) interp.SetTrace(120);
|
||||
|
||||
bool ok = interp.EvaluateObject(path, out);
|
||||
|
||||
if (traceThis) interp.SetTrace(0);
|
||||
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " " << path << ": "
|
||||
<< (ok ? "ok" : "FAILED") << ", returned " << TypeName(out.Type)
|
||||
<< " with " << base::dec
|
||||
<< (uint64_t)interp.GetPackageCount(out) << " element(s)";
|
||||
}
|
||||
|
||||
return ok && out.Type == AML::ObjectType::Package;
|
||||
}
|
||||
|
||||
static uint32_t ElementInteger(const AML::Object& pkg, int index) {
|
||||
AML::Object elem{};
|
||||
if (!AML::GetInterpreter().GetPackageElement(pkg, index, elem)) return Unknown32;
|
||||
if (elem.Type != AML::ObjectType::Integer) return Unknown32;
|
||||
return (uint32_t)elem.Integer;
|
||||
}
|
||||
|
||||
// _BIX supersedes _BIF and prepends a Revision field, shifting every
|
||||
// shared field by one. Both are read into the same three numbers.
|
||||
struct BatteryInfo {
|
||||
bool Valid;
|
||||
bool PowerUnitMilliAmps;
|
||||
uint32_t DesignCapacity;
|
||||
uint32_t FullCapacity;
|
||||
};
|
||||
|
||||
static bool ReadInfo(BatteryInfo& info) {
|
||||
info = BatteryInfo{};
|
||||
|
||||
AML::Object pkg{};
|
||||
int base = 0;
|
||||
|
||||
if (EvalPackage(g_batteryPath, "_BIX", pkg)) {
|
||||
// _BIX revision 0 starts with Revision; later revisions keep it
|
||||
// in the same place, so the offset is fixed at one.
|
||||
base = 1;
|
||||
} else if (EvalPackage(g_batteryPath, "_BIF", pkg)) {
|
||||
base = 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t unit = ElementInteger(pkg, base + 0);
|
||||
uint32_t design = ElementInteger(pkg, base + 1);
|
||||
uint32_t full = ElementInteger(pkg, base + 2);
|
||||
|
||||
info.PowerUnitMilliAmps = (unit != Unknown32) && (unit & 1);
|
||||
info.DesignCapacity = (design == Unknown32) ? 0 : design;
|
||||
info.FullCapacity = (full == Unknown32) ? 0 : full;
|
||||
info.Valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool AcOnline() {
|
||||
if (!g_haveAdapter) return false;
|
||||
|
||||
char path[160];
|
||||
AppendMethod(g_adapterPath, "_PSR", path, sizeof(path));
|
||||
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " evaluating " << path;
|
||||
|
||||
AML::Object result{};
|
||||
if (!AML::GetInterpreter().EvaluateObject(path, result)) return false;
|
||||
return result.Type == AML::ObjectType::Integer && result.Integer != 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// GetStatus
|
||||
|
||||
// ============================================================================
|
||||
static bool Refresh(Status& out) {
|
||||
out = Status{};
|
||||
out.MinutesRemaining = -1;
|
||||
out.HasAcAdapter = g_haveAdapter;
|
||||
|
||||
// _STA bit 4 says whether a battery is actually in the slot. A
|
||||
// laptop with the pack removed still has the device.
|
||||
uint32_t sta = (g_batteryNode >= 0)
|
||||
? AcpiDevices::EvaluateSta(g_batteryNode)
|
||||
: AcpiDevices::STA_DEFAULT;
|
||||
|
||||
// Bit 4 is the one that decides everything below, and firmware
|
||||
// clears it for reasons that have nothing to do with the pack
|
||||
// (an EC that has not been told its region handler is live, most
|
||||
// often), so log the raw value rather than just the verdict.
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " _STA = " << base::hex
|
||||
<< (uint64_t)sta << (((sta & AcpiDevices::STA_BATTERY) != 0)
|
||||
? " (battery present)" : " (battery bit clear)");
|
||||
}
|
||||
|
||||
out.AcOnline = AcOnline();
|
||||
|
||||
// _STA bit 4 is the spec's answer for "is a pack installed", but
|
||||
// it is only as good as the firmware path behind it. Read the
|
||||
// battery methods either way and let the capacity data have the
|
||||
// final say -- see the staSaysAbsent handling below.
|
||||
bool staSaysAbsent = (sta & AcpiDevices::STA_BATTERY) == 0;
|
||||
|
||||
BatteryInfo info{};
|
||||
if (!ReadInfo(info)) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " no _BIX/_BIF package";
|
||||
return true;
|
||||
}
|
||||
|
||||
AML::Object bst{};
|
||||
if (!EvalPackage(g_batteryPath, "_BST", bst)) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " no _BST package";
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t state = ElementInteger(bst, 0);
|
||||
uint32_t rate = ElementInteger(bst, 1);
|
||||
uint32_t remaining = ElementInteger(bst, 2);
|
||||
uint32_t voltage = ElementInteger(bst, 3);
|
||||
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " _BIF/_BIX: design="
|
||||
<< base::dec << (uint64_t)info.DesignCapacity
|
||||
<< " lastFull=" << (uint64_t)info.FullCapacity
|
||||
<< (info.PowerUnitMilliAmps ? " (mAh)" : " (mWh)");
|
||||
KernelLogStream(INFO, "Battery") << " _BST: state=" << base::hex
|
||||
<< (uint64_t)state << " rate=" << base::dec << (uint64_t)rate
|
||||
<< " remaining=" << (uint64_t)remaining
|
||||
<< " voltage=" << (uint64_t)voltage << "mV";
|
||||
}
|
||||
|
||||
// Trust real capacity data over a status bit that says otherwise:
|
||||
// firmware that reports a pack's charge is describing a pack that
|
||||
// is there.
|
||||
if (staSaysAbsent) {
|
||||
bool looksReal = info.FullCapacity > 0 && info.FullCapacity != Unknown32
|
||||
&& remaining != Unknown32;
|
||||
if (!looksReal) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery")
|
||||
<< " _STA says absent and the methods agree";
|
||||
return true;
|
||||
}
|
||||
if (g_tracing)
|
||||
KernelLogStream(WARNING, "Battery")
|
||||
<< " _STA says absent but _BST reports charge; trusting _BST";
|
||||
}
|
||||
|
||||
if (state == Unknown32) state = 0;
|
||||
|
||||
out.Present = true;
|
||||
out.Charging = (state & StateCharging) != 0;
|
||||
out.Discharging = (state & StateDischarging) != 0;
|
||||
out.Critical = (state & StateCritical) != 0;
|
||||
|
||||
out.PowerUnitMilliAmps = info.PowerUnitMilliAmps;
|
||||
out.DesignCapacity = info.DesignCapacity;
|
||||
out.FullCapacity = info.FullCapacity;
|
||||
out.RemainingCapacity = (remaining == Unknown32) ? 0 : remaining;
|
||||
out.PresentRate = (rate == Unknown32) ? 0 : rate;
|
||||
out.VoltageMv = (voltage == Unknown32) ? 0 : voltage;
|
||||
|
||||
// Some firmware leaves last-full at zero or below the current
|
||||
// reading after a swap; fall back to the design capacity, and
|
||||
// clamp so the panel can never show 143%.
|
||||
uint32_t full = out.FullCapacity;
|
||||
if (full == 0) full = out.DesignCapacity;
|
||||
|
||||
if (full > 0 && out.RemainingCapacity > 0) {
|
||||
uint64_t pct = ((uint64_t)out.RemainingCapacity * 100) / full;
|
||||
out.Percent = (pct > 100) ? 100 : (uint8_t)pct;
|
||||
}
|
||||
|
||||
if (out.PresentRate > 0 && out.Discharging) {
|
||||
out.MinutesRemaining =
|
||||
(int32_t)(((uint64_t)out.RemainingCapacity * 60) / out.PresentRate);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetStatus(Status& out) {
|
||||
if (!g_haveBattery) return false;
|
||||
|
||||
g_lock.Acquire();
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_cacheValid && now - g_cacheStamp < CacheMs) {
|
||||
out = g_cached;
|
||||
g_lock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
Status fresh{};
|
||||
bool ok = Refresh(fresh);
|
||||
if (ok) {
|
||||
g_cached = fresh;
|
||||
g_cacheStamp = now;
|
||||
g_cacheValid = true;
|
||||
}
|
||||
|
||||
out = ok ? fresh : g_cached;
|
||||
g_lock.Release();
|
||||
return ok || g_cacheValid;
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user