Skip to content

Instantly share code, notes, and snippets.

@lbussy
Last active August 23, 2026 14:25
Show Gist options
  • Select an option

  • Save lbussy/9e81cbcc617952f1250e353bd42e7775 to your computer and use it in GitHub Desktop.

Select an option

Save lbussy/9e81cbcc617952f1250e353bd42e7775 to your computer and use it in GitHub Desktop.
Raspberry Pi Shutdown and Wake Button

Raspberry Pi Shutdown and Wake Button

This setup adds a momentary button that requests an orderly Raspberry Pi OS shutdown and can wake a halted Pi while power remains connected. It uses the kernel gpio-shutdown device-tree overlay and systemd-logind; no continuously running Python service is required.

The default wiring uses GPIO3 because Raspberry Pi firmware supports wake from halt when GPIO3 is pulled low.

Hardware and electrical safety

Use a normally-open, momentary push button:

  • One terminal to physical pin 5, GPIO3 (BCM numbering)
  • The other terminal to physical pin 6, ground

GPIO3 has an external pull-up on supported boards. Pressing the button connects GPIO3 to ground.

Do not connect the button to 3.3 V or 5 V. Confirm the header orientation and pin numbers for the exact Raspberry Pi model before wiring. Power down and disconnect power while changing wiring.

GPIO3 is also I2C1 SCL. Do not use this button arrangement when the same pin is required by an I2C bus or attached hardware. Raspberry Pi 1 Model B revision 1 uses GPIO1 for this function and is outside this guide's default configuration.

Behavior and limitations

  • Pressing the button generates a KEY_POWER event; systemd-logind requests shutdown.
  • Pressing it after halt can wake the Pi while its power supply remains connected.
  • This is a shutdown request, not a physical mains or DC power disconnect.
  • A severely wedged kernel or blocked shutdown can prevent an orderly halt.
  • Applications must still save and flush important data correctly.
  • Button behavior can be changed by logind configuration, another input-event handler, an incompatible operating system, or conflicting overlays.
  • Raspberry Pi models with a dedicated onboard power button may not need an external GPIO3 button.

Requirements

  • A supported Raspberry Pi with a 40-pin header
  • Current Raspberry Pi OS using systemd-logind
  • /boot/firmware/config.txt
  • Root access to change the boot configuration
  • Bash, awk, curl, install, and standard core utilities for the helper

One-line install - skips review

This revision-free command installs the current helper as /usr/local/bin/rpi-shutdown-button:

curl -fsSL https://gist.githubusercontent.com/lbussy/9e81cbcc617952f1250e353bd42e7775/raw/shutdown_button.sh | sudo bash -s -- --install

This deliberately skips review and follows the current unpinned Gist revision. Anyone controlling the served content can execute code as root. Use it only when you accept that risk; otherwise use the inspect-first procedure below.

Installing the helper does not change config.txt and does not reboot the Pi.

Install: download and inspect first

This revision-free URL always retrieves the current helper:

curl -fsSL https://gist.githubusercontent.com/lbussy/9e81cbcc617952f1250e353bd42e7775/raw/shutdown_button.sh -o shutdown_button.sh
less shutdown_button.sh
bash -n shutdown_button.sh
sudo install -m 0755 shutdown_button.sh /usr/local/bin/rpi-shutdown-button
rm -f shutdown_button.sh

Because the URL is unpinned, its content can change. Inspect it before installation.

Inspect status

rpi-shutdown-button --status

The command reports the selected boot configuration, whether its managed block is absent, present, or malformed, and how many active gpio-shutdown entries it finds.

The helper fails closed when managed markers are malformed or an unmanaged gpio-shutdown entry would conflict.

Preview and enable

Preview the exact addition without writing:

rpi-shutdown-button --enable --dry-run

Enable the button:

sudo rpi-shutdown-button --enable

The helper:

  • verifies that the configuration is an existing regular, non-symlink file;
  • refuses conflicting or duplicate active gpio-shutdown entries;
  • creates a timestamped metadata-preserving backup;
  • writes through a same-directory temporary file;
  • adds an explicitly marked configuration block;
  • makes no change when its exact configuration is already present;
  • never reboots automatically.

Reboot when ready:

sudo systemctl reboot

Manual configuration

If you do not install the helper, back up /boot/firmware/config.txt and add this block:

# BEGIN rpi-shutdown-button
dtoverlay=gpio-shutdown,gpio_pin=3,active_low=1,gpio_pull=up,debounce=100
# END rpi-shutdown-button

The parameters explicitly select GPIO3, active-low behavior, pull-up bias, and 100 ms debounce. GPIO3, active-low, pull-up, and 100 ms are also the overlay defaults, but spelling them out makes the intended electrical behavior reviewable.

The Gist's config.txt contains the same sample block.

Verify after reboot

Confirm the configuration and input device:

rpi-shutdown-button --status
grep -A 8 -B 2 -i 'gpio.*key\|power button' /proc/bus/input/devices || true
sudo journalctl -b | grep -Ei 'gpio|power key|button' || true

Test during a maintenance window with local access and no important unsaved work:

  1. Press and release the button once.
  2. Confirm that Raspberry Pi OS performs an orderly shutdown.
  3. Wait until shutdown completes.
  4. Press and release the button again to test wake from halt.

Do not repeatedly press the button during shutdown or disconnect power before shutdown completes.

Disable and roll back

Preview removal:

rpi-shutdown-button --disable --dry-run

Remove only the helper-managed block:

sudo rpi-shutdown-button --disable
sudo systemctl reboot

The helper creates a timestamped backup before removal and leaves unmanaged configuration untouched. If the managed markers are damaged or incomplete, it refuses to edit the file and requires manual review.

Alternate configuration path

The helper defaults to /boot/firmware/config.txt. For an operating system that keeps its active boot configuration elsewhere, provide an absolute path explicitly:

sudo RPI_CONFIG_PATH=/path/to/config.txt rpi-shutdown-button --enable

Use this only after confirming that the selected file is the configuration actually read during boot.

Status LEDs and power-cut controllers

A simple LED driven by a normal GPIO can indicate that software has reached a chosen state, but it does not prove that writes are complete or that power is safe to remove.

The gpio-poweroff overlay is intended to signal an external circuit that physically removes power. It interferes with the normal power-down sequence, disables GPIO3 wake, and requires external power-control hardware to act on its signal. Do not use it merely to drive an indicator LED.

If a status indicator is required, design it separately around the exact meaning needed—application ready, operating system running, shutdown requested, or external power safe—and document that it is not a substitute for verified shutdown.

Remaining risks

  • Incorrect wiring can short power rails or damage the Pi.
  • GPIO3 conflicts with I2C1 SCL.
  • A stuck or electrically noisy button can cause unwanted shutdown requests.
  • Shutdown does not remove supply voltage from the board or attached hardware.
  • Overlay behavior depends on firmware, kernel, board, and operating-system support.
  • Backups of config.txt remain on the boot filesystem and should be managed appropriately.
  • Test the complete shutdown and wake behavior before relying on it remotely.

Authoritative references

# Momentary normally-open button between physical pins 5 (GPIO3) and 6 (GND)
# BEGIN rpi-shutdown-button
dtoverlay=gpio-shutdown,gpio_pin=3,active_low=1,gpio_pull=up,debounce=100
# END rpi-shutdown-button
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
readonly GIST_ID="9e81cbcc617952f1250e353bd42e7775"
readonly RAW_URL="https://gist.githubusercontent.com/lbussy/${GIST_ID}/raw/shutdown_button.sh"
readonly BEGIN_MARKER="# BEGIN rpi-shutdown-button"
readonly END_MARKER="# END rpi-shutdown-button"
readonly OVERLAY_LINE="dtoverlay=gpio-shutdown,gpio_pin=3,active_low=1,gpio_pull=up,debounce=100"
ACTION=""
DRY_RUN=false
CONFIG_PATH="${RPI_CONFIG_PATH:-}"
TEMP_FILE=""
INSTALL_TEMP=""
FILTERED_FILE=""
die() {
printf 'Error: %s\n' "$*" >&2
exit 1
}
cleanup() {
if [[ -n "$TEMP_FILE" && -e "$TEMP_FILE" ]]; then
rm -f -- "$TEMP_FILE"
fi
if [[ -n "$FILTERED_FILE" && -e "$FILTERED_FILE" ]]; then
rm -f -- "$FILTERED_FILE"
fi
if [[ -n "$INSTALL_TEMP" && -e "$INSTALL_TEMP" ]]; then
rm -f -- "$INSTALL_TEMP"
fi
}
trap cleanup EXIT
usage() {
cat <<'EOF'
Usage: rpi-shutdown-button ACTION [--dry-run]
Actions:
--enable Add the managed GPIO3 shutdown/wake overlay
--disable Remove only the managed overlay block
--status Show the current managed and active overlay state
--install Install the current Gist script in /usr/local/bin
-h, --help Show this help
Options:
--dry-run Show the intended enable/disable change without writing
The default configuration file is /boot/firmware/config.txt. Set
RPI_CONFIG_PATH to an absolute path only when the active boot configuration
is located elsewhere. A reboot is required after enable or disable.
EOF
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
parse_args() {
while (($#)); do
case "$1" in
--enable|--disable|--status|--install)
[[ -z "$ACTION" ]] || die "Choose exactly one action."
ACTION=$1
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
done
[[ -n "$ACTION" ]] || die "Choose --enable, --disable, --status, or --install."
if [[ "$ACTION" == "--status" || "$ACTION" == "--install" ]]; then
[[ "$DRY_RUN" == false ]] || die "--dry-run applies only to --enable or --disable."
fi
}
install_current() {
(( EUID == 0 )) || die "Installation requires root; use sudo."
require_command bash
require_command curl
require_command install
require_command mktemp
INSTALL_TEMP=$(mktemp)
curl -fsSL "$RAW_URL" -o "$INSTALL_TEMP"
[[ -s "$INSTALL_TEMP" ]] || die "Downloaded script was empty."
bash -n "$INSTALL_TEMP"
install -m 0755 "$INSTALL_TEMP" /usr/local/bin/rpi-shutdown-button
printf 'Installed /usr/local/bin/rpi-shutdown-button\n'
}
resolve_config() {
if [[ -z "$CONFIG_PATH" ]]; then
CONFIG_PATH="/boot/firmware/config.txt"
fi
[[ "$CONFIG_PATH" == /* ]] || die "RPI_CONFIG_PATH must be absolute."
[[ -f "$CONFIG_PATH" && ! -L "$CONFIG_PATH" ]] ||
die "Boot configuration must be an existing regular, non-symlink file: $CONFIG_PATH"
[[ -r "$CONFIG_PATH" ]] || die "Boot configuration is not readable: $CONFIG_PATH"
}
active_overlay_count() {
awk '
{
line=$0
sub(/^[[:space:]]*/, "", line)
if (line ~ /^#/) next
sub(/[[:space:]]*#.*/, "", line)
if (line ~ /^dtoverlay[[:space:]]*=[[:space:]]*gpio-shutdown([,[:space:]]|$)/) count++
}
END { print count + 0 }
' "$CONFIG_PATH"
}
managed_state() {
awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" -v overlay="$OVERLAY_LINE" '
$0 == begin {
begins++
if (managed) bad=1
managed=1
next
}
$0 == end {
ends++
if (!managed) bad=1
managed=0
next
}
managed && $0 == overlay { exact++ }
END {
if (begins == 0 && ends == 0) print "absent"
else if (begins == 1 && ends == 1 && exact == 1 && !managed && !bad) print "present"
else print "malformed"
}
' "$CONFIG_PATH"
}
show_status() {
local state active
state=$(managed_state)
active=$(active_overlay_count)
printf 'Configuration: %s\n' "$CONFIG_PATH"
printf 'Managed block: %s\n' "$state"
printf 'Active entries: %s\n' "$active"
if [[ "$state" == "malformed" ]]; then
return 2
fi
if [[ "$state" == "present" && "$active" != 1 ]]; then
printf 'Conflict: additional active gpio-shutdown entries exist.\n' >&2
return 3
fi
}
make_backup() {
local backup
backup="${CONFIG_PATH}.backup.$(date '+%Y%m%d-%H%M%S')"
[[ ! -e "$backup" ]] || backup="${backup}.$$"
cp -p -- "$CONFIG_PATH" "$backup"
printf 'Backup: %s\n' "$backup"
}
prepare_temp() {
local config_dir config_name
config_dir=$(dirname -- "$CONFIG_PATH")
config_name=$(basename -- "$CONFIG_PATH")
TEMP_FILE=$(mktemp "$config_dir/.${config_name}.tmp.XXXXXX")
cp -p -- "$CONFIG_PATH" "$TEMP_FILE"
}
enable_button() {
local state active
state=$(managed_state)
active=$(active_overlay_count)
[[ "$state" != "malformed" ]] ||
die "Managed markers are malformed; inspect $CONFIG_PATH manually."
if [[ "$state" == "present" ]]; then
(( active == 1 )) ||
die "Additional unmanaged gpio-shutdown entries exist; inspect manually."
printf 'Already enabled; no change made.\n'
return 0
fi
(( active == 0 )) ||
die "An unmanaged gpio-shutdown entry already exists; inspect it manually."
printf 'Enable GPIO3 shutdown/wake button in %s\n' "$CONFIG_PATH"
if [[ "$DRY_RUN" == true ]]; then
printf 'Would append:\n%s\n%s\n%s\n' "$BEGIN_MARKER" "$OVERLAY_LINE" "$END_MARKER"
return 0
fi
(( EUID == 0 )) || die "Enabling requires root; use sudo."
make_backup
prepare_temp
printf '\n%s\n%s\n%s\n' "$BEGIN_MARKER" "$OVERLAY_LINE" "$END_MARKER" >> "$TEMP_FILE"
mv -f -- "$TEMP_FILE" "$CONFIG_PATH"
TEMP_FILE=""
printf 'Enabled. Reboot when ready; this command did not reboot the Pi.\n'
}
disable_button() {
local state
state=$(managed_state)
[[ "$state" != "malformed" ]] ||
die "Managed markers are malformed; inspect $CONFIG_PATH manually."
if [[ "$state" == "absent" ]]; then
printf 'Managed overlay is already absent; no change made.\n'
return 0
fi
printf 'Disable the managed GPIO3 shutdown/wake button in %s\n' "$CONFIG_PATH"
if [[ "$DRY_RUN" == true ]]; then
printf 'Would remove only the block from %s through %s.\n' "$BEGIN_MARKER" "$END_MARKER"
return 0
fi
(( EUID == 0 )) || die "Disabling requires root; use sudo."
make_backup
prepare_temp
FILTERED_FILE="${TEMP_FILE}.filtered"
awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" '
$0 == begin { managed=1; next }
$0 == end && managed { managed=0; next }
!managed { print }
' "$CONFIG_PATH" > "$FILTERED_FILE"
cat -- "$FILTERED_FILE" > "$TEMP_FILE"
rm -f -- "$FILTERED_FILE"
FILTERED_FILE=""
mv -f -- "$TEMP_FILE" "$CONFIG_PATH"
TEMP_FILE=""
printf 'Disabled. Reboot when ready; this command did not reboot the Pi.\n'
}
main() {
parse_args "$@"
if [[ "$ACTION" == "--install" ]]; then
install_current
return 0
fi
require_command awk
resolve_config
case "$ACTION" in
--status) show_status ;;
--enable) enable_button ;;
--disable) disable_button ;;
esac
}
main "$@"
@lbussy

lbussy commented Jan 24, 2024

Copy link
Copy Markdown
Author

tl;dr Version works fine on my Raspberry 4. But to power off, the shutdown dialog pops up where I can chose to shutdown, reboot or logout and I have to press the button another time. Is there a way to shutdown directly without this dialog?

https://forums.raspberrypi.com/viewtopic.php?t=363725

Looks like you need to click twice. Odd, but hey.

@AgentRev

AgentRev commented Jan 24, 2024

Copy link
Copy Markdown

@Drexel2k On RPi OS, I think all the power button does is call the lxde-logout command, so you should probably be able to override it with something like alias lxde-logout='shutdown -h now' in ~/.profile or by creating a shell script at /usr/bin/lxde-logout

@davthomaspilot

Copy link
Copy Markdown

If I'm using the gpio-poweroff overlay, on a specific GPIO, can that GPIO also be driven from code?

If the GPIO is configured as an output and driven by code, will it still go to the state configured by the overlay at power off?

@lbussy

lbussy commented Feb 20, 2024

Copy link
Copy Markdown
Author

No, a pin is either an "in" (read) or an out (write.)

@davthomaspilot

Copy link
Copy Markdown

well, it would be an output--it's driven by the rpi to indicate it has powered down.

Did you think I was asking about the shutdown overlay?

So, the designated pin is always an output. My question is what happens if my code drives it? Will it change to the state that indicates the rpi is powered down when powerdown occurs?

@lbussy

lbussy commented Feb 20, 2024

Copy link
Copy Markdown
Author

Do you mean the LED? Not sure about that. I'd think the daemon would continuously drive the LED high (off) but you'd need to test.

@davthomaspilot

Copy link
Copy Markdown

No,

I am specifying a GPIO to be used to indicate the RPI has been shutdown as described in the documentation above. That GPIO isn't driving the LED

That GPIO OUTPUT is only used by hardware on a higher level card assembly. That external hardware turns off other devices in the system once the RPI has completely shutdown.

It would be useful to be able to also control the GPIO output from code running on the RPI--one use would be for manufacturing test of the higher level card assembly.

Can I do this? Where is the right place to ask?

@lbussy

lbussy commented Feb 20, 2024

Copy link
Copy Markdown
Author

Well, you can either try it or ask on the Raspberry Pi Forums. There are some knowledgeable people there (but you will have to weed through the not-so-knowledgeable people.)

@davthomaspilot

Copy link
Copy Markdown

Trying it might not be so easy. There's a note in the documentation about high power dissipation if you do something I was planning to do.

I'd like to get an answer from someone knowledgeable with the code, or, better yet, look at the code myself.

I'll try the RPI forum, but I usually don't get a good answer from there (probably 100 posts, maybe five or six answers worth the post).

@lbussy

lbussy commented Feb 21, 2024

Copy link
Copy Markdown
Author

It's all open source, of course finding what you need is a special sort of hell. Good luck, sir.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment