/* * 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 #include #include #include #include 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; } }; };