Getting the power button to work on a Jetson Orin Nano
I wired a button to J14 on my Orin Nano and nothing happened. It wasn't the wiring — GNOME was quietly swallowing the press.
I wired a button across J14 pins 11 and 12 on my Orin Nano dev kit, pressed it, and nothing happened. My first thought was that I'd got the wiring wrong. I hadn't, and it took me longer than I'd like to admit to find out what was actually eating the press — so I'm writing it down.
This is on JetPack 6 (L4T R36.x, Ubuntu 22.04).
What I checked first
Everything on the hardware and kernel side turned out to be fine, which is what made this confusing:
- The carrier board routes the button to a GPIO.
- The device tree declares it —
/proc/device-tree/gpio-keys/key-power, withlinux,code = 0x74= 116 =KEY_POWER. - The kernel exposes it as an evdev node, in my case
/dev/input/event0, namedgpio-keysand tagged by udev aspower-switch. - systemd-logind sees it. The journal literally says "Power key pressed."
So the press was getting all the way to logind and then dying in userspace.
What was actually eating it
Two things, and they stack.
GNOME claims the key. gnome-settings-daemon's media-keys plugin registers a systemd block inhibitor on handle-power-key, with the reason "GNOME handling keypresses". What caught me out is that this runs even with nobody logged in — the GDM greeter starts its own session as the user gdm. So my headless, never-logged-into board had the inhibitor sitting there the whole time.
logind always honours those inhibitors. This is the part that cost me the most time. Setting PowerKeyIgnoreInhibited=yes in logind.conf does not override it — that option only covers the shutdown, sleep and idle classes, not handle-*-key. Which means the fix I found recommended on half the forum threads I read simply cannot work here. logind logs its refusal at debug level only, so all I ever saw was "Power key pressed." and then nothing.
And having claimed the key, GNOME applies its own policy from org.gnome.settings-daemon.plugins.power power-button-action, which defaults to interactive — pop up a shutdown confirmation dialog. Headless, that dialog renders to nobody and the board just stays up.
I did try pointing that gsetting at a plain shutdown. You can't: on GNOME 42 the enum is only { nothing, suspend, hibernate, interactive }. There's no shutdown to select.
How to check if this is what you're hitting
# Is something blocking the power key?
systemd-inhibit --list --no-pager | grep handle-power-key
# Did logind see the press at all?
journalctl -b --no-pager | grep -i "power key pressed"If both come back with something, that's this problem. If the second one is empty, press the button once and run it again — if it still logs nothing, the event isn't reaching userspace at all and you're looking at wiring or device tree instead, not this.
The two options I had
Drop the display manager. If you were going headless anyway, this is the cleaner one:
sudo systemctl set-default multi-user.target
sudo systemctl disable gdm3No greeter means no inhibitor, and the stock HandlePowerKey=poweroff starts working on its own. It also gave me back somewhere between 700 MB and 1 GB of RAM, which on an 8 GB board is worth having on its own. But you lose the GUI, so it only works if you don't want one.
Go around logind. I wanted to keep the desktop, so this is what I actually did: read the gpio-keys evdev node directly and call systemctl poweroff myself. Inhibitors are a logind concept, so if you're not asking logind, they don't apply. It behaves the same whether GDM is running or not, it survives GNOME updates, and it doesn't touch any existing config. The cost is one small always-on service sitting blocked on a read() — a few MB of RSS.
The watcher I ended up with
#!/usr/bin/env python3
"""Power-button watcher for Jetson Orin Nano.
Reads the gpio-keys evdev node directly, so it works whether or not
GDM/GNOME is running. Set POWER_BUTTON_DRY_RUN=1 to log without acting.
"""
import os
import struct
import subprocess
import sys
import time
EV_KEY = 0x01
KEY_POWER = 116
# struct input_event on 64-bit:
# __kernel_ulong_t tv_sec, tv_usec; __u16 type, code; __s32 value
# On a 32-bit userspace this would be "iiHHi" instead.
EVENT_FORMAT = "llHHi"
EVENT_SIZE = struct.calcsize(EVENT_FORMAT)
# A hand-held jumper wire bounces; a real switch bounces less. Cheap insurance.
DEBOUNCE_SECONDS = 3.0
DRY_RUN = os.environ.get("POWER_BUTTON_DRY_RUN") == "1"
def find_gpio_keys_device():
"""Return /dev/input/eventN for the 'gpio-keys' device.
Looked up by name rather than hardcoded to event0, so it survives input
devices being enumerated in a different order across boots.
"""
name = None
with open("/proc/bus/input/devices") as fh:
for line in fh:
line = line.strip()
if line.startswith("N: Name="):
name = line.split("=", 1)[1].strip('"')
elif line.startswith("H: Handlers=") and name == "gpio-keys":
for handler in line.split("=", 1)[1].split():
if handler.startswith("event"):
return "/dev/input/" + handler
return None
def main():
device = find_gpio_keys_device()
if device is None:
print("ERROR: no 'gpio-keys' input device found", flush=True)
return 1
print(f"watching {device} for KEY_POWER (dry_run={DRY_RUN})", flush=True)
last_fired = 0.0
with open(device, "rb") as fh:
while True:
data = fh.read(EVENT_SIZE)
if not data or len(data) != EVENT_SIZE:
print("ERROR: short read from input device", flush=True)
return 1
_sec, _usec, etype, code, value = struct.unpack(EVENT_FORMAT, data)
if etype != EV_KEY or code != KEY_POWER:
continue
# value: 1 = press, 0 = release, 2 = autorepeat
if value != 1:
continue
now = time.monotonic()
if now - last_fired < DEBOUNCE_SECONDS:
print("power key press ignored (debounce)", flush=True)
continue
last_fired = now
if DRY_RUN:
print("power key pressed -- DRY RUN, not powering off", flush=True)
continue
print("power key pressed -- powering off", flush=True)
subprocess.run(["/bin/systemctl", "poweroff"], check=False)
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(0)I put that at /usr/local/sbin/power-button-watch.py and chmod 755'd it.
Two small things I'd point out in there. I look the device up by name instead of hardcoding event0, because input devices don't always enumerate in the same order across boots. And the three-second debounce is there because I was testing with a jumper wire held by hand, which bounces far more than a real switch — cheap insurance either way.
The unit
[Unit]
Description=Jetson J14 power button watcher (GNOME-independent poweroff)
After=multi-user.target
[Service]
Type=simple
ExecStart=/usr/local/sbin/power-button-watch.py
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now power-button-watch.service
# Watch presses without actually shutting down
sudo POWER_BUTTON_DRY_RUN=1 /usr/local/sbin/power-button-watch.pyUse the dry run first. Once the service is live the next short press really will power the board off, so do that first real test while you're sitting next to it.
The one thing I couldn't fix
A long press is a PMIC-level hard cut, handled in hardware below the OS. Nothing in userspace gets a look at it, so there was nothing I could do about it in software. That's the unclean power-off I was trying to avoid in the first place — short press only.
I've been running this on a Jetson Orin Nano Developer Kit: JetPack 6 / L4T R36.4.7, systemd 249, Ubuntu 22.04 aarch64.