To safely turn off a Raspberry Pi 4 or Zero 2 W without corrupting the SD card, wire a momentary pushbutton to GPIO 21 (Physical Pin 40) and GND (Physical Pin 39), then run a Python gpiozero daemon that triggers sudo shutdown -h now on a 2-second long press. Unlike the Raspberry Pi 5, which features a dedicated onboard power button, the Pi 4 and Zero 2 W require a custom GPIO circuit to handle headless or embedded enclosure shutdowns safely. Pulling the power cable directly risks ext4 filesystem corruption and bricked OS images.

Hardware Spec Sheet & Pin Mapping

Before soldering, verify your components. We are using a hardware RC debounce circuit to prevent switch bounce from triggering multiple interrupt edges, which can crash the GPIO daemon. This build targets the Raspberry Pi 4 Model B (4GB/8GB) and Raspberry Pi Zero 2 W running Raspberry Pi OS (Bookworm or later).

Table 1: Bill of Materials & Electrical Specifications
Component Specification / Part Number Electrical Rating Purpose
Momentary Switch C&K PTS645 Series (6x6mm) 12VDC, 50mA max User input trigger
Decoupling Capacitor 0.1µF Ceramic (X7R) 50VDC Hardware debounce filter
Pull-up Resistor 10kΩ Carbon Film (Optional) 1/4W, 5% tolerance External pull-up (internal used by default)
Hookup Wire 24 AWG Stranded Silicone 300V, 105°C GPIO to switch routing
Table 2: GPIO Pin Mapping (BCM vs Physical)
Function BCM GPIO Physical Pin Wiring Destination
Signal (Input) GPIO 21 Pin 40 Switch Terminal A
Ground GND Pin 39 Switch Terminal B
Bench Note: We chose GPIO 21 because it is located on the very edge of the header (Pin 40), making it easy to route wires out of an enclosure without interfering with HATs. Avoid GPIO 2 (Pin 3) and GPIO 3 (Pin 5) for this build, as they have hard-wired 1.8kΩ onboard pull-up resistors for the I2C bus and can cause boot-state conflicts.

Step-by-Step Wiring & Assembly

  1. Prepare the Switch: Solder two 24 AWG silicone wires to opposite diagonal pins of the 6x6mm momentary pushbutton. This ensures you are using the normally-open (NO) contacts.
  2. Add Hardware Debounce: Solder the 0.1µF ceramic capacitor directly across the same two switch terminals. This creates a low-pass RC filter with the Pi's internal 50kΩ pull-up resistor, physically smoothing out the microsecond contact bounce that plagues cheap tactile switches.
  3. Connect to GPIO: Route the wires to the Pi header. Connect Terminal A to Physical Pin 40 (GPIO 21) and Terminal B to Physical Pin 39 (GND).
  4. Verify with a Multimeter: Set your DMM to continuity mode. Place probes on Pin 40 and Pin 39. It should read open (OL). Press the button; it should read near 0.0Ω. Release; it should return to OL.

The Python Shutdown Daemon

Modern Raspberry Pi OS relies on the gpiozero library for robust GPIO handling. The script below uses a long-press (hold) detection to prevent accidental shutdowns from a brief bump.

#!/usr/bin/env python3
"""
Raspberry Pi Safe Shutdown Daemon
Target: Raspberry Pi 4 Model B / Zero 2 W
OS: Raspberry Pi OS (Bookworm+)
"""
import sys
import logging
import subprocess
from gpiozero import Button
from signal import pause

# Configuration
SHUTDOWN_PIN = 21
HOLD_TIME = 2.0  # Seconds to hold before shutdown

# Setup logging to track daemon state
logging.basicConfig(
    filename='/var/log/pi-shutdown.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def initiate_shutdown():
    logging.info("Shutdown button held for 2s. Initiating safe shutdown.")
    try:
        # Execute system halt command
        subprocess.run(['sudo', 'shutdown', '-h', 'now'], check=True)
    except subprocess.CalledProcessError as e:
        logging.error(f"Shutdown command failed with exit code {e.returncode}")
        sys.exit(1)
    except Exception as e:
        logging.critical(f"Unexpected error during shutdown: {e}")
        sys.exit(1)

def main():
    try:
        # pull_up=True activates the internal 50k pull-up resistor.
        # bounce_time=0.05 provides a secondary software debounce layer.
        btn = Button(
            SHUTDOWN_PIN, 
            pull_up=True, 
            hold_time=HOLD_TIME, 
            bounce_time=0.05
        )
        btn.when_held = initiate_shutdown
        
        logging.info(f"Shutdown daemon started. Monitoring GPIO {SHUTDOWN_PIN}.")
        pause()  # Keep the script running
        
    except Exception as e:
        logging.critical(f"Daemon crashed during initialization: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Deployment: Save this as /opt/pi-shutdown/shutdown_daemon.py. To ensure the script can actually halt the system without prompting for a password, add this line to your sudoers file via sudo visudo:

pi ALL=(ALL) NOPASSWD: /sbin/shutdown

Finally, wrap it in a systemd service (/etc/systemd/system/pi-shutdown.service) so it starts on boot, using User=root to guarantee GPIO hardware access.

Debugging: When the Pi Refuses to Shut Down

If you press the button and nothing happens, or the script crashes on boot, check the logs at /var/log/pi-shutdown.log. Here are the exact error strings you will encounter and how to fix them.

Exact Error: RuntimeError: Failed to add edge detection

This is the classic error inherited from the legacy RPi.GPIO library, which gpiozero sometimes falls back to if the pin factory is misconfigured.

  • Cause 1 (Most Likely): The pin is already claimed by an active interface (like I2C or SPI) enabled in raspi-config.
  • Cause 2: A zombie Python process from a previous crash is still holding the GPIO file descriptor open.
  • Fix: Run sudo killall python3, then disable unused interfaces in sudo raspi-config > Interface Options.

Exact Error: gpiozero.exc.GPIOPinInUse: pin 21 is already in use

  • Cause: Another script or an overlay in /boot/firmware/config.txt has reserved GPIO 21.
  • Fix: Check config.txt for dtoverlay lines mapping to GPIO 21, or change the SHUTDOWN_PIN variable in the script to GPIO 16 (Physical Pin 36).
The First 3 Things to Check When It Fails:
  1. Pin Multiplexing Conflicts: Verify GPIO 21 isn't assigned to a custom DPI display or I2C bus in config.txt.
  2. Systemd Permissions: Ensure the systemd service file runs as root and the sudoers NOPASSWD rule is active. If the service runs as user pi, the subprocess.run call will silently fail or hang waiting for a password prompt that never arrives in a headless daemon.
  3. Hardware Bounce Overload: If the log shows the button triggering 10 times in a millisecond, your 0.1µF capacitor is missing or soldered poorly, overwhelming the software edge detector.

Extending and Simplifying the Build

Depending on your enclosure constraints and production volume, you may need to alter this design.

How to Simplify (Cost & Space Reduction)

If you are building a one-off prototype and lack the 0.1µF capacitor, you can drop the hardware debounce entirely. Rely purely on gpiozero's software filtering by increasing the bounce_time parameter in the Python script from 0.05 to 0.2 (200ms). This costs nothing but adds a slight latency to the button response, which is acceptable for a 2-second hold-to-shutdown mechanic.

How to Extend (Status Feedback & Pi 5 Migration)

Add a Status LED: Wire a 3mm LED with a 330Ω current-limiting resistor to GPIO 16 (Physical Pin 36). Modify the Python script to pulse the LED during the boot sequence using gpiozero.PWMLED, and turn it off inside the initiate_shutdown() function right before calling subprocess.run. This gives the user visual confirmation that the halt command was received.

Migrating to Raspberry Pi 5: If you upgrade to the Raspberry Pi 5, you no longer need this GPIO script. The Pi 5 features a dedicated onboard power button and a J2 header specifically for an external case power button. Simply wire your momentary switch directly to the J2 pins, and the onboard RP1 power management IC will handle the safe ACPI shutdown natively at the hardware level, completely eliminating the need for a Python daemon.