Add PiSugar 3 shutdown/startup scripts and watchdog

PiSugar 3 users need graceful shutdown with e-ink feedback, boot-loop
prevention when charging from dead battery, and automatic recovery
when pisugar-server loses I2C connection after MCU wake.

- safe-shutdown.sh: stops pwnagotchi, draws sleeping face on e-ink,
  powers off. Boot-loop guard skips shutdown when battery <10% and
  charging.
- pisugar-watchdog.sh + systemd timer: detects MCU wake from deep
  sleep and restarts pisugar-server when I2C is present but daemon
  reports disconnected.
- epd-shutdown.py / epd-startup.py: e-ink display faces for
  Waveshare V4. Exit silently on non-V4 displays.

Signed-off-by: CoderFX <4704376+CoderFX@users.noreply.github.com>
This commit is contained in:
CoderFX
2026-03-10 23:33:51 +02:00
parent 6808063e9b
commit dff77e105d
7 changed files with 256 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
#!/home/pi/.pwn/bin/python3
"""Draw a sleeping face on the Waveshare V4 e-ink before shutdown.
Requires: Waveshare 2.13" V4 display (250x122).
On non-V4 displays the import will fail and the script exits silently.
Install: sudo cp epd-shutdown.py /usr/local/bin/ && sudo chmod +x /usr/local/bin/epd-shutdown.py
"""
import sys
sys.path.insert(0, '/home/pi/.pwn/lib/python3.13/site-packages')
try:
from pwnagotchi.ui.hw.libs.waveshare.epaper.v2in13_V4.epd2in13_V4 import EPD
except ImportError:
sys.exit(0) # not a V4 display — nothing to do
from PIL import Image, ImageDraw, ImageFont
WIDTH = 250
HEIGHT = 122
FONT_NAME = 'DejaVuSansMono'
FONT_BOLD = 'DejaVuSansMono-Bold'
def load_font(name, size):
try:
return ImageFont.truetype(name, size)
except (IOError, OSError):
return ImageFont.load_default()
def main():
epd = EPD()
epd.init()
img = Image.new('1', (WIDTH, HEIGHT), 255)
draw = ImageDraw.Draw(img)
# Sleeping face ( -_- ) Zzz
face_font = load_font(FONT_BOLD, 28)
face = "( -_- )"
face_bbox = draw.textbbox((0, 0), face, font=face_font)
face_w = face_bbox[2] - face_bbox[0]
face_h = face_bbox[3] - face_bbox[1]
face_x = (WIDTH - face_w) // 2 - 15
face_y = (HEIGHT - face_h) // 2 - 12
draw.text((face_x, face_y), face, font=face_font, fill=0)
z1_font = load_font(FONT_BOLD, 16)
z2_font = load_font(FONT_BOLD, 12)
z3_font = load_font(FONT_BOLD, 9)
zx = face_x + face_w + 4
draw.text((zx, face_y - 2), "Z", font=z1_font, fill=0)
draw.text((zx + 12, face_y + 12), "z", font=z2_font, fill=0)
draw.text((zx + 20, face_y + 22), "z", font=z3_font, fill=0)
sm_font = load_font(FONT_NAME, 10)
off_text = "powered off"
off_bbox = draw.textbbox((0, 0), off_text, font=sm_font)
off_w = off_bbox[2] - off_bbox[0]
off_x = (WIDTH - off_w) // 2
draw.text((off_x, face_y + face_h + 16), off_text, font=sm_font, fill=0)
# Full refresh persists after power cut
img = img.rotate(180)
epd.display(epd.getbuffer(img))
epd.sleep()
if __name__ == '__main__':
try:
main()
except Exception as e:
print(f"epd-shutdown: {e}", file=sys.stderr)

View File

@@ -0,0 +1,68 @@
#!/home/pi/.pwn/bin/python3
"""Draw a boot indicator on the Waveshare V4 e-ink at startup.
Requires: Waveshare 2.13" V4 display (250x122).
On non-V4 displays the import will fail and the script exits silently.
Install: sudo cp epd-startup.py /usr/local/bin/ && sudo chmod +x /usr/local/bin/epd-startup.py
"""
import sys
sys.path.insert(0, '/home/pi/.pwn/lib/python3.13/site-packages')
try:
from pwnagotchi.ui.hw.libs.waveshare.epaper.v2in13_V4.epd2in13_V4 import EPD
except ImportError:
sys.exit(0) # not a V4 display
from PIL import Image, ImageDraw, ImageFont
WIDTH = 250
HEIGHT = 122
FONT_NAME = 'DejaVuSansMono'
FONT_BOLD = 'DejaVuSansMono-Bold'
def load_font(name, size):
try:
return ImageFont.truetype(name, size)
except (IOError, OSError):
return ImageFont.load_default()
def main():
epd = EPD()
epd.init()
img = Image.new('1', (WIDTH, HEIGHT), 255)
draw = ImageDraw.Draw(img)
# Waking face ( O_O )
face_font = load_font(FONT_BOLD, 28)
face = "( O_O )"
face_bbox = draw.textbbox((0, 0), face, font=face_font)
face_w = face_bbox[2] - face_bbox[0]
face_h = face_bbox[3] - face_bbox[1]
face_x = (WIDTH - face_w) // 2
face_y = (HEIGHT - face_h) // 2 - 12
draw.text((face_x, face_y), face, font=face_font, fill=0)
sm_font = load_font(FONT_NAME, 10)
boot_text = "booting ..."
boot_bbox = draw.textbbox((0, 0), boot_text, font=sm_font)
boot_w = boot_bbox[2] - boot_bbox[0]
boot_x = (WIDTH - boot_w) // 2
draw.text((boot_x, face_y + face_h + 16), boot_text, font=sm_font, fill=0)
img = img.rotate(180)
epd.display(epd.getbuffer(img))
epd.sleep()
if __name__ == '__main__':
try:
main()
except Exception as e:
print(f"epd-startup: {e}", file=sys.stderr)

View File

@@ -0,0 +1,38 @@
#!/bin/bash
# PiSugar 3 MCU watchdog — detects when the MCU wakes from deep sleep
# and restarts pisugar-server so it reconnects via I2C.
#
# Requires: PiSugar 3 (I2C address 0x57 on bus 1, pisugar-server daemon).
# On non-PiSugar setups the I2C probe fails — script does nothing.
#
# Install:
# sudo cp pisugar-watchdog.sh /usr/local/bin/ && sudo chmod +x /usr/local/bin/pisugar-watchdog.sh
# sudo cp systemd/pisugar-watchdog.service /etc/systemd/system/
# sudo cp systemd/pisugar-watchdog.timer /etc/systemd/system/
# sudo systemctl daemon-reload && sudo systemctl enable --now pisugar-watchdog.timer
PISUGAR_ADDR=0x57
i2c_found() {
python3 -c "
import fcntl, os
try:
bus = os.open('/dev/i2c-1', os.O_RDWR)
fcntl.ioctl(bus, 0x0703, $PISUGAR_ADDR)
os.read(bus, 1)
os.close(bus)
exit(0)
except:
exit(1)
" 2>/dev/null
}
pisugar_connected() {
result=$(echo "get battery" | nc -q 1 127.0.0.1 8423 2>/dev/null)
! echo "$result" | grep -q "not connected"
}
if i2c_found && ! pisugar_connected; then
logger "pisugar-watchdog: PiSugar MCU detected on I2C, restarting pisugar-server"
systemctl restart pisugar-server
fi

View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Show shutdown face on e-ink display, then power off.
# Called by PiSugar soft_poweroff_shell (power button or battery protect).
#
# Requires: PiSugar 3 (pisugar-server on port 8423).
# On non-PiSugar setups the nc commands return empty — shutdown proceeds normally.
#
# Boot-loop guard: if battery is low AND charging (USB-C plugged in),
# skip shutdown — the Pi just auto-started to charge, don't kill it.
#
# Install:
# sudo cp safe-shutdown.sh /usr/local/bin/ && sudo chmod +x /usr/local/bin/safe-shutdown.sh
# Then set in PiSugar config: "soft_poweroff_shell": "sudo /usr/local/bin/safe-shutdown.sh"
PISUGAR="127.0.0.1 8423"
# Query PiSugar battery level and charging status
BATTERY=$(echo "get battery" | nc -q 1 $PISUGAR 2>/dev/null | grep -oP '[\d.]+' | head -1)
CHARGING=$(echo "get battery_power_plugged" | nc -q 1 $PISUGAR 2>/dev/null)
# Guard: low battery + charging = skip shutdown (charging from dead)
if echo "$CHARGING" | grep -q "true"; then
BAT_INT=${BATTERY%.*}
BAT_INT=${BAT_INT:-0}
if [ "$BAT_INT" -lt 10 ] 2>/dev/null; then
echo "Charging at ${BAT_INT}% — skipping shutdown" > /tmp/.pwnagotchi-button-msg
exit 0
fi
fi
# Normal shutdown flow
echo "Shutting down..." > /tmp/.pwnagotchi-button-msg
# Stop pwnagotchi first to release the SPI bus
systemctl stop pwnagotchi 2>/dev/null
sleep 2
# Draw sleeping face on e-ink (full refresh, persists after power cut)
/home/pi/.pwn/bin/python3 /usr/local/bin/epd-shutdown.py 2>/dev/null
# Power off
shutdown -h now

View File

@@ -0,0 +1,14 @@
[Unit]
Description=E-ink boot indicator (Waveshare V4)
DefaultDependencies=no
After=local-fs.target
Before=pwnagotchi.service
[Service]
Type=oneshot
ExecStart=/home/pi/.pwn/bin/python3 /usr/local/bin/epd-startup.py
TimeoutStartSec=30
RemainAfterExit=no
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,7 @@
[Unit]
Description=PiSugar 3 MCU watchdog
After=pisugar-server.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/pisugar-watchdog.sh

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Run PiSugar 3 MCU watchdog every 15 seconds
[Timer]
OnBootSec=30s
OnUnitActiveSec=15s
Unit=pisugar-watchdog.service
[Install]
WantedBy=timers.target