Soft-Fixing a Kindle 8th Gen with a Dead Fuel Gauge

 

TL;DR: The bd7181x fuel gauge IC on a Kindle 8th gen (model SY69JL) can fail in a way that makes the device believe the battery is always empty, even when it's fully charged. Since the replacement chip is difficult to find and replace without breaking the screen, this guide documents how to work around it entirely using a jailbreak and spoofing the battery level indicator via software.

You can also view the script used in Github.


Background and Problem Description

This is the story of a Kindle Paperwhite 8th generation (model SY69JL) that refused to stay on. The symptom was clear, even with a fully charged battery (measured at ~4.2 V with a multimeter), the device would report a low battery, show 0–7% capacity, and shut down within minutes of use. However this verifies the charging circuit working fine as the battery stays within its operating voltage range.

The usual suspects were ruled out first:

  • Hard resets (40-second power button hold, with and without USB connected) had no lasting effect.
  • The battery itself was healthy — voltage confirmed at 4.20 V.
  • Bridging the RSENS resistor (marked R030 on the board, which is the current-sense shunt used by the fuel gauge) made things briefly better, but the problem always returned.

Root Cause: The Coulomb Counter

After reviewing the bd7181x datasheet (a ROHM PMIC that integrates five buck converters and a battery management unit), the actual culprit became clear.

The chip's coulomb counter — the circuit that tracks how much charge has been consumed — was reporting a constant ~1354 mA discharge current (current_now = -1354) regardless of actual consumption. A Kindle at idle draws a fraction of that. The chip was lying, and because it's the authoritative source for capacity in the Linux kernel's power_supply subsystem, the OS believed it.

/sys/class/power_supply/bd7181x_bat/capacity   = 7
/sys/class/power_supply/bd7181x_bat/voltage_now = 4188   ← ~4.19 V, nearly full
/sys/class/power_supply/bd7181x_bat/status      = Charging

The voltage was perfectly fine. The reported capacity was not. The fault was self-evident, a broken coulomb counter. The battery and the IC-reported state-of-charge completely different.

Replacing the chip was not an option. The bd7181x is not available on AliExpress, eBay, or any accessible market. I could either,

  1. live with it, therefore stashing the kindle on a drawer because it stayed on for around 30 minutes before powering off, after disconecting the battery and connecting again.
  2. Buy another broken kindle with a working charging chip and swap it. Risking damaging the screen because e-ink as i have read is prone to permanently damage it with heat.
  3. Bypass it, it seemed like the most appropiate option at the time, and i think it was the right option.

The Fix: Jailbreak + Virtual Fuel Gauge

The approach is conceptually simple:

  1. Jailbreak the Kindle (using WinterBreak).
  2. Access the device over SSH via USB, using USBNET.
  3. Create a shell script that reads the real battery voltage and maps it to the estimated capacity percentage.
  4. Use bind mounts to shadow the broken sysfs files (capacity, battery_mah) with the calculated values.
  5. Register the script as an Upstart service that launches before powerd, so the OS never sees the bad values.

No hardware modification is needed beyond what you may have already attempted. The Kindle runs normally now, the battery indicator just reflects voltage-based estimation rather than the broken coulomb counter.


Prerequisites

  • Kindle 8th gen (tested on model SY69JL)
  • Jailbreak applied (WinterBreak or equivalent that gives root SSH access)
  • SSHd running on the device
  • A Linux machine (or WSL) with a micro-usb cable

Step 1 — Connect via SSH over USB

When you plug the Kindle into a Linux machine, it exposes a USB network interface (RNDIS/CDC-ECM). The Kindle assigns itself a static IP in the 192.168.15.0/24 range. You need to configure your host interface to match.

First, identify the interface name:

ip addr

Look for an interface that got an address in 169.254.x.x — that's the Kindle's USB interface. In this case it was enp0s20f0u4i1.

Reconfigure it:

sudo ip addr flush dev enp0s20f0u4i1
sudo ip addr add 192.168.15.201/24 dev enp0s20f0u4i1
sudo ip link set enp0s20f0u4i1 up

The Kindle should be reachable at 192.168.15.244:

ping 192.168.15.244
ssh root@192.168.15.244

When prompted for a password, just press Enter — the default root password is empty on jailbroken Kindles.

You'll be greeted with:

Welcome to Kindle!
Rootfs is mounted read-only. Invoke mntroot rw to switch back to a writable rootfs.

Step 2 — Inspect the Battery State

Before doing anything permanent, verify what the kernel is actually seeing:

for d in /sys/class/power_supply/*; do
    echo "== $d =="
    for f in capacity battery_mah status voltage_now present online health temp charge_now current_now; do
        [ -f "$d/$f" ] && echo "$f = $(cat "$d/$f" 2>/dev/null)"
    done
done

On a device with this failure, you'll see something like:

== /sys/class/power_supply/bd7181x_bat ==
capacity     = 7          ← should be ~95–100%
status       = Charging
voltage_now  = 4188       ← 4.19 V, battery is fine
present      = 1
health       = Good
temp         = 30
charge_now   = 68
current_now  = -1354      ← ~1.35 A drain reported — clearly wrong

The voltage is healthy. The capacity is nonsense. This confirms the diagnosis.


Step 3 — Install the Virtual Fuel Gauge Script

Remount the root filesystem as writable:

mntroot rw

If mntroot is not available:

mount -o remount,rw /

Now create the script. This version maps voltage to percentage using a lookup table, then uses bind mounts to shadow the two sysfs files the Kindle's power daemon (powerd) reads for battery level. The mount is only applied once; subsequent loop iterations just update the file contents, avoiding mount stack accumulation.

cat > /usr/local/bin/virtual-fuelgauge.sh <<'EOF'
#!/bin/sh

BAT="/sys/class/power_supply/bd7181x_bat"
FAKE_DIR="/tmp/battery-spoof"
FAKE_CAP="$FAKE_DIR/capacity"
FAKE_MAH="$FAKE_DIR/battery_mah"

DISABLE1="/var/local/DISABLE_BATTERY_SPOOF"
DISABLE2="/mnt/us/DISABLE_BATTERY_SPOOF"

mkdir -p "$FAKE_DIR"

voltage_to_percent() {
    mv="$1"
    # Normalize microvolt readings if needed
    if [ "$mv" -gt 100000 ]; then
        mv=$((mv / 1000))
    fi

    if   [ "$mv" -ge 4180 ]; then echo 100
    elif [ "$mv" -ge 4138 ]; then echo 95
    elif [ "$mv" -ge 4096 ]; then echo 90
    elif [ "$mv" -ge 4053 ]; then echo 85
    elif [ "$mv" -ge 4011 ]; then echo 80
    elif [ "$mv" -ge 3969 ]; then echo 75
    elif [ "$mv" -ge 3927 ]; then echo 70
    elif [ "$mv" -ge 3884 ]; then echo 65
    elif [ "$mv" -ge 3842 ]; then echo 60
    elif [ "$mv" -ge 3800 ]; then echo 55
    elif [ "$mv" -ge 3757 ]; then echo 50
    elif [ "$mv" -ge 3715 ]; then echo 45
    elif [ "$mv" -ge 3673 ]; then echo 40
    elif [ "$mv" -ge 3630 ]; then echo 35
    elif [ "$mv" -ge 3588 ]; then echo 30
    elif [ "$mv" -ge 3545 ]; then echo 25
    elif [ "$mv" -ge 3500 ]; then echo 20
    elif [ "$mv" -ge 3400 ]; then echo 15
    elif [ "$mv" -ge 3300 ]; then echo 10
    elif [ "$mv" -ge 3200 ]; then echo 5
    elif [ "$mv" -ge 3100 ]; then echo 1
    else echo 0
    fi
}

is_mounted() { grep -q " $1 " /proc/mounts; }

unmount_all() {
    while grep -q " $1 " /proc/mounts; do
        umount "$1" 2>/dev/null || break
    done
}

mount_fake_once() {
    real="$(readlink -f "$1" 2>/dev/null)"
    fake="$2"
    [ -n "$real" ] && [ -f "$real" ] && [ -f "$fake" ] || return 0
    is_mounted "$real" || mount --bind "$fake" "$real"
}

update_values_only() {
    [ -d "$BAT" ] || exit 0
    voltage="$(cat "$BAT/voltage_now" 2>/dev/null)"
    [ -n "$voltage" ] || voltage="4000"
    percent="$(voltage_to_percent "$voltage")"
    echo "$percent" > "$FAKE_CAP"
    echo "1000"    > "$FAKE_MAH"
}

mount_all_once() {
    update_values_only
    mount_fake_once "$BAT/capacity"    "$FAKE_CAP"
    mount_fake_once "$BAT/battery_mah" "$FAKE_MAH"
}

disabled_forever() {
    unmount_all "$(readlink -f "$BAT/capacity"    2>/dev/null)"
    unmount_all "$(readlink -f "$BAT/battery_mah" 2>/dev/null)"
    while true; do sleep 3600; done
}

case "$1" in
    once)
        [ -f "$DISABLE1" ] || [ -f "$DISABLE2" ] && exit 0
        mount_all_once; exit 0 ;;

    loop)
        { [ -f "$DISABLE1" ] || [ -f "$DISABLE2" ]; } && disabled_forever
        mount_all_once
        while true; do
            { [ -f "$DISABLE1" ] || [ -f "$DISABLE2" ]; } && disabled_forever
            update_values_only
            sleep 30
        done ;;

    *)
        echo "Usage: $0 once|loop"; exit 1 ;;
esac
EOF

chmod +x /usr/local/bin/virtual-fuelgauge.sh

How the script works

The script has two modes:

  • once — applies the bind mounts immediately and exits. Used by the Upstart service's pre-start hook so the spoofed values are in place before powerd reads them.
  • loop — applies the bind mounts once, then re-reads the real voltage every 30 seconds and updates the file content (without mounting again). This keeps the displayed percentage tracking the actual battery voltage throughout use.

The bind mount trick works because /sys files are just kernel interfaces. Mounting a tmpfs-backed file on top of them makes the kernel return our file's content whenever anything reads that path — including powerd.


Step 4 — Register the Upstart Service

The service must start before powerd reads the battery state. Upstart's start on starting powerd directive guarantees this ordering.

cat > /etc/upstart/battery-spoof.conf <<'EOF'
description "Virtual battery fuel gauge spoof"

start on starting powerd
stop on stopping powerd

respawn
respawn limit unlimited

pre-start exec /usr/local/bin/virtual-fuelgauge.sh once
exec /usr/local/bin/virtual-fuelgauge.sh loop
EOF

Reload Upstart and start the service immediately:

initctl reload-configuration
initctl start battery-spoof

Verify everything looks correct:

initctl status battery-spoof
cat /sys/class/power_supply/bd7181x_bat/voltage_now
cat /sys/class/power_supply/bd7181x_bat/capacity
cat /sys/class/power_supply/bd7181x_bat/charge_now
mount | grep bd7181x_bat

Expected output for mount | grep bd7181x_bat — exactly two lines, nothing more:

tmpfs on .../bd7181x_bat/capacity type tmpfs (rw,relatime,size=65536k)
tmpfs on .../bd7181x_bat/battery_mah type tmpfs (rw,relatime,size=65536k)

If you see more than two lines, the earlier version of the script (which mounted on every loop iteration) was running. Stop the service, clean up the stacked mounts, and restart:

initctl stop battery-spoof

BAT="$(readlink -f /sys/class/power_supply/bd7181x_bat)"
while grep -q "$BAT/capacity" /proc/mounts; do
    umount "$BAT/capacity" 2>/dev/null || break
done
while grep -q "$BAT/battery_mah" /proc/mounts; do
    umount "$BAT/battery_mah" 2>/dev/null || break
done

initctl start battery-spoof

Step 5 — Seal and Reboot (Warning)

(Warning!) Theorically you should do this, however i couldn't mount as ro and, as rebooting the kindle could lead to it not powering on again for a while or after i replugged it, i decided against it, but i leave the steps here to documentate it properly.

It should mount itself as ro after rebooting anyway so it isn't really necesary, but here we go. 

Remount the root filesystem as read-only:

mntroot ro

Then reboot and reconnect over SSH to confirm the service started automatically:

reboot
# ... reconnect after boot ...
initctl status battery-spoof
cat /sys/class/power_supply/bd7181x_bat/capacity

If capacity reflects a value that is multiple of 5 (ends in either 5 or 0) it is working.


Kill Switch

If something goes wrong or you need to disable the spoof, you don't need to touch the filesystem. Create an empty file at either of these paths and reboot:

# From the Kindle shell
mntroot rw
touch /var/local/DISABLE_BATTERY_SPOOF

# Or from the USB storage visible on your computer
touch /mnt/us/DISABLE_BATTERY_SPOOF

To re-enable:

mntroot rw
rm -f /var/local/DISABLE_BATTERY_SPOOF
reboot

Caveats and Limitations

  • (IMPORTANT) The spoof only works while the Kindle is on. If the device powers off due to genuinely depleted voltage, the spoof can't help — it requires the kernel to be running.
  • Voltage-based estimation is not perfect. Li-ion discharge curves are non-linear and load-dependent. The lookup table here is a reasonable approximation for typical Kindle usage, but don't treat the percentage as a precise fuel gauge, it is just an estimation.
  • current_now is not spoofed. The reported current draw in sysfs will still show the bogus ~1.35 A reading if you check it directly. Only capacity and battery_mah are overridden, which is what powerd uses for its shutdown logic.
  • This was tested on an 8th gen Kindle (SY69JL). Other models may use different PMIC drivers, sysfs paths, or Upstart configurations.

References

Comentarios