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).
| 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 |
| Function | BCM GPIO | Physical Pin | Wiring Destination |
|---|---|---|---|
| Signal (Input) | GPIO 21 | Pin 40 | Switch Terminal A |
| Ground | GND | Pin 39 | Switch Terminal B |
Step-by-Step Wiring & Assembly
- 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.
- 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.
- 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).
- 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 insudo 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.txthas reserved GPIO 21. - Fix: Check
config.txtfordtoverlaylines mapping to GPIO 21, or change theSHUTDOWN_PINvariable in the script to GPIO 16 (Physical Pin 36).
- Pin Multiplexing Conflicts: Verify GPIO 21 isn't assigned to a custom DPI display or I2C bus in
config.txt. - Systemd Permissions: Ensure the
systemdservice file runs asrootand thesudoersNOPASSWD rule is active. If the service runs as userpi, thesubprocess.runcall will silently fail or hang waiting for a password prompt that never arrives in a headless daemon. - 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.






