Fix MangoHud double-counting battery power and remaining life (#270)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-05 15:07:40 +00:00
1 parent 356208ca9e
commit 2ca051fe71
2 files changed
+76 -3

No files matched your search

@@ -14,7 +14,7 @@ import kotlin.math.abs
* The client reads `/sys/class/power_supply/BAT<n>/...` - a laptop's or a Deck's naming and files
* (`energy_now`, `power_now`, `time_to_empty_now`, a `uevent` of `POWER_SUPPLY_*` lines). Android
* names its supply `battery` and lays it out differently, and on many phones an app may not read
* it at all, so the client showed no battery. This writes a `BAT0` (and a `BAT1`, the Deck's name)
* it at all, so the client showed no battery. This writes one `BAT1` (the Deck's name)
* from Android's own battery API - the same on every device - into a directory the session binds
* over `/sys/class/power_supply`, refreshed every few seconds while the session runs.
*
@@ -31,6 +31,8 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart
private var thread: Thread? = null
override fun start() {
// Older builds exported the same aggregate battery twice; MangoHud counts both supplies.
File(dir, "BAT0").deleteRecursively()
running = true
write()
thread = Thread({
@@ -39,7 +41,7 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart
if (running) write()
}
}, "battery-sysfs").apply { isDaemon = true; start() }
Log.i(TAG, "battery: BAT0/BAT1 written to ${dir.path} every ${PERIOD_MS / 1000} s")
Log.i(TAG, "battery: BAT1 written to ${dir.path} every ${PERIOD_MS / 1000} s")
}
override fun stop() {
@@ -87,7 +89,7 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart
pct <= 15 -> "Low"
else -> "Normal"
}
for (name in listOf("BAT0", "BAT1")) {
for (name in listOf("BAT1")) {
val bat = File(dir, name).apply { mkdirs() }
val attrs = linkedMapOf(
"type" to "Battery",
@@ -0,0 +1,71 @@
package com.droiddeck.launcher.session
import android.content.Intent
import android.os.BatteryManager
import java.io.File
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], manifest = Config.NONE)
class BatteryComponentTest {
@get:Rule val tmp = TemporaryFolder()
private var battery: BatteryComponent? = null
@After fun stop() { battery?.stop() }
@Test fun oneAggregateBatteryKeepsOverlayPowerAndLifetimeConsistentWithQam() {
val supplies = tmp.newFolder("power_supply")
val vpower = tmp.newFolder("vpower")
start(supplies, vpower, -2_000_000L)
assertTelemetry(supplies, vpower)
}
@Test fun upgradingRemovesTheDuplicateRatherThanLeavingItForOverlayDiscovery() {
val supplies = tmp.newFolder("power_supply")
val vpower = tmp.newFolder("vpower")
val legacy = File(supplies, "BAT0").apply { mkdirs() }
File(legacy, "power_now").writeText("8000000\n")
File(legacy, "charge_now").writeText("4000000\n")
start(supplies, vpower, 2_000_000L)
assertFalse(legacy.exists())
assertTelemetry(supplies, vpower)
}
private fun start(supplies: File, vpower: File, currentUa: Long) {
val context = RuntimeEnvironment.getApplication()
context.sendStickyBroadcast(Intent(Intent.ACTION_BATTERY_CHANGED).apply {
putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING)
putExtra(BatteryManager.EXTRA_LEVEL, 50)
putExtra(BatteryManager.EXTRA_SCALE, 100)
putExtra(BatteryManager.EXTRA_VOLTAGE, 4000)
})
shadowOf(context.getSystemService(BatteryManager::class.java)).apply {
setLongProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER, 4_000_000L)
setLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW, currentUa)
setLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE, currentUa)
}
battery = BatteryComponent(supplies, vpower).also { it.attach(context); it.start() }
}
private fun assertTelemetry(supplies: File, vpower: File) {
val discovered = supplies.listFiles()!!.filter { it.name.contains("BAT") }
assertEquals(listOf("BAT1"), discovered.map { it.name })
fun read(name: String) = File(discovered.single(), name).readText().trim().toLong()
// One 4 Ah battery at 4 V drawing 2 A: 8 W and two hours, regardless of vendor current sign.
assertEquals(8_000_000L, read("power_now"))
assertEquals(16_000_000L, read("energy_now"))
assertEquals(7200L, read("time_to_empty_now"))
assertEquals("7200", File(vpower, "secs_until_shutdown_request").readText().trim())
assertEquals("50.00", File(vpower, "battery_percent").readText().trim())
}
}