diff --git a/scripts/pisugar/epd-shutdown.py b/scripts/pisugar/epd-shutdown.py new file mode 100644 index 00000000..387ed574 --- /dev/null +++ b/scripts/pisugar/epd-shutdown.py @@ -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) diff --git a/scripts/pisugar/epd-startup.py b/scripts/pisugar/epd-startup.py new file mode 100644 index 00000000..6ca372d7 --- /dev/null +++ b/scripts/pisugar/epd-startup.py @@ -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) diff --git a/scripts/pisugar/pisugar-watchdog.sh b/scripts/pisugar/pisugar-watchdog.sh new file mode 100644 index 00000000..22b74bb1 --- /dev/null +++ b/scripts/pisugar/pisugar-watchdog.sh @@ -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 diff --git a/scripts/pisugar/safe-shutdown.sh b/scripts/pisugar/safe-shutdown.sh new file mode 100644 index 00000000..1a96446c --- /dev/null +++ b/scripts/pisugar/safe-shutdown.sh @@ -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 diff --git a/scripts/pisugar/systemd/epd-startup.service b/scripts/pisugar/systemd/epd-startup.service new file mode 100644 index 00000000..32e762a0 --- /dev/null +++ b/scripts/pisugar/systemd/epd-startup.service @@ -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 diff --git a/scripts/pisugar/systemd/pisugar-watchdog.service b/scripts/pisugar/systemd/pisugar-watchdog.service new file mode 100644 index 00000000..3fb92fbf --- /dev/null +++ b/scripts/pisugar/systemd/pisugar-watchdog.service @@ -0,0 +1,7 @@ +[Unit] +Description=PiSugar 3 MCU watchdog +After=pisugar-server.service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/pisugar-watchdog.sh diff --git a/scripts/pisugar/systemd/pisugar-watchdog.timer b/scripts/pisugar/systemd/pisugar-watchdog.timer new file mode 100644 index 00000000..98041c1a --- /dev/null +++ b/scripts/pisugar/systemd/pisugar-watchdog.timer @@ -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