Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,26 @@ desktop. The **Bazaar** app store and the **Armada Installer**

### Power button and sleep

Pressing the power button does a "fake suspend" (inspired by ROCKNIX) rather than
real S3 sleep: it blanks the screen and freezes the session, and the same press
wakes it. Because the device does not truly sleep, idle battery drain is higher
than it would be with real suspend.
Pressing the power button suspends the device. Armada automatically selects
the best suspend mode for your hardware:

- **s2idle (suspend-to-idle):** If the kernel supports it, the device enters a
true low-power state — CPUs enter hardware idle, drivers are suspended, and
wake is instant via the power button IRQ. Battery drain is significantly lower
than fake suspend.
- **Fake suspend (fallback):** On hardware where s2idle hangs, the device blanks
the screen, freezes the session, offlines secondary CPU cores, and waits for a
power button press. Battery drain is higher than real suspend but the device
remains stable.

The mode can be forced via `suspend_mode` in
`/etc/armada/power-profiles.conf` under `[general]`:

| Value | Behavior |
|----------|-------------------------------------------------------|
| `auto` | Use s2idle if available, otherwise fake (default) |
| `s2idle` | Always use s2idle |
| `fake` | Always use fake suspend |

## Updating

Expand Down
4 changes: 2 additions & 2 deletions build_files/40-vendor-system-files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ systemctl enable armada-flatpak-setup.service
# `systemctl unmask --now bootc-fetch-apply-updates.timer`.
systemctl mask bootc-fetch-apply-updates.timer

# systemd-suspend.service is overridden (drop-in) to run fake-suspend; mask the
# other sleep ops so nothing reaches real suspend (it hangs this SoC).
# systemd-suspend.service is overridden (drop-in) to use s2idle when available,
# falling back to fake-suspend. Mask other sleep ops to keep a single path.
systemctl mask systemd-hibernate.service systemd-hybrid-sleep.service systemd-suspend-then-hibernate.service
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
[Unit]
Description=Armada Fake Suspend
Description=Armada Suspend (s2idle with fake-suspend fallback)

[Service]
# Real suspend hangs; run fake-suspend, which blocks until woken.
ExecStart=
ExecStart=/usr/libexec/armada/fake-suspend sleep
TimeoutStartSec=infinity
120 changes: 95 additions & 25 deletions system_files/usr/libexec/armada/fake-suspend
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
#!/bin/bash
# Real suspend hangs this SoC. Runs as systemd-suspend.service and blocks until
# woken, so logind's sleep lifecycle (and Steam's blank) lasts the whole sleep.
set -uo pipefail

STATE_FLAG="/run/armada/fake-suspend.active"
Expand All @@ -15,15 +13,33 @@ SESSION_USER="${ARMADA_SESSION_USER:-armada}"
SESSION_UID="$(id -u "$SESSION_USER" 2>/dev/null || echo 1000)"
SESSION_RUNTIME="/run/user/${SESSION_UID}"

resolve_suspend_mode() {
local mode="" f
for f in /etc/armada/power-profiles.conf /usr/share/armada/power-profiles.conf; do
[[ -r "$f" ]] || continue
mode=$(sed -n '/^\[general\]/,/^\[/{s/^suspend_mode[[:space:]]*=[[:space:]]*//p;}' "$f" | tail -1)
[[ -n "$mode" ]] && break
done
case "${mode:-auto}" in
s2idle) echo s2idle ;;
fake) echo fake ;;
*)
if grep -q s2idle /sys/power/mem_sleep 2>/dev/null; then
echo s2idle
else
echo fake
fi
;;
esac
}

as_session() {
local s b sock=""
for s in "${SESSION_RUNTIME}"/gamescope-*; do
[[ -S "$s" ]] || continue
b="$(basename "$s")"
# gamescope-N, not the -ei input socket.
[[ "$b" =~ ^gamescope-[0-9]+$ ]] && { sock="$b"; break; }
done
# A wedged gamescope must not hang suspend/resume.
timeout 8 runuser -u "$SESSION_USER" -- env \
XDG_RUNTIME_DIR="$SESSION_RUNTIME" \
${sock:+GAMESCOPE_WAYLAND_DISPLAY="$sock"} \
Expand Down Expand Up @@ -83,8 +99,6 @@ resume_radios() {

runtime_pm_devices() {
local dev
# Leave GPU runtime PM alone here; the GMU is known to wedge under
# rapid idle/resume churn on current devices.
for dev in /sys/devices/platform/soc@0/*.usb; do
[[ -e "$dev" ]] && printf '%s\n' "$dev"
done
Expand Down Expand Up @@ -120,7 +134,6 @@ resume_runtime_pm() {
done < <(tac "$SAVE_DIR/power-controls")
}

# The power key is left ungrabbed by block_input; wait_for_wake grabs it instead.
INPUT_WHITELIST='pmic_pwrkey'

power_device() {
Expand All @@ -137,7 +150,6 @@ block_input() {
local dev name
for dev in /dev/input/event*; do
[[ -e "$dev" ]] || continue
# Never grab the lid switch — logind/resume need its events to flow.
armada_dev_has_lid "$dev" && continue
name="$(cat "/sys/class/input/$(basename "$dev")/device/name" 2>/dev/null)" || continue
echo "$name" | grep -qE "^(${INPUT_WHITELIST})$" && continue
Expand All @@ -146,12 +158,9 @@ block_input() {
}
unblock_input() { pkill -9 -f 'evtest --grab' 2>/dev/null || true; }

# Grab the power key and block until woken, so the wake press resumes us instead
# of reaching logind/Steam.
wait_for_wake() {
rm -f "$WAKE_FLAG"
local pdev t0 line lidwasclosed; pdev="$(power_device || true)"; t0=$SECONDS
# Clamshell: a closed lid blocks the power button (no waking in a bag); lid-open wakes.
lidwasclosed=0; armada_lid_closed 2>/dev/null && lidwasclosed=1
if [[ -n "$pdev" ]]; then
log "waiting for wake (power key ${pdev})"
Expand All @@ -168,7 +177,7 @@ wait_for_wake() {
if (( SECONDS - t0 >= 1 )) && ! armada_lid_closed 2>/dev/null; then log "wake (power key)"; break; fi ;;
esac
elif (( $? <= 128 )); then
sleep 1 # evtest ended (not a read timeout); avoid a busy loop
sleep 1
fi
done < <(evtest --grab "$pdev" 2>/dev/null)
else
Expand All @@ -183,7 +192,6 @@ wait_for_wake() {
rm -f "$WAKE_FLAG"
}

# Values are the 15-char /proc comm.
SESSION_KEEP_COMMS='gamescope-wl|gamescope-sessi|Xwayland|mangoapp|steam_notif_dae'

session_procs_file() {
Expand All @@ -198,7 +206,6 @@ session_procs_file() {
freeze_processes() {
local procs p c

# fake-suspend and armada-powerd run as system services, outside user.slice.
if systemctl freeze user.slice; then
touch "$SAVE_DIR/user-slice"
log "froze user.slice"
Expand Down Expand Up @@ -230,23 +237,76 @@ unfreeze_processes() {
kill -CONT -1 2>/dev/null || true
}

do_suspend() {
offline_secondary_cpus() {
: > "$SAVE_DIR/cpus-online"
local cpu count=0
for cpu in /sys/devices/system/cpu/cpu[1-9]*/online; do
[[ -w "$cpu" ]] || continue
[[ "$(cat "$cpu" 2>/dev/null)" == "1" ]] || continue
printf '%s\n' "$cpu" >> "$SAVE_DIR/cpus-online"
echo 0 > "$cpu" 2>/dev/null && count=$((count + 1))
done
log "offlined ${count} secondary CPUs"
}

online_saved_cpus() {
[[ -r "$SAVE_DIR/cpus-online" ]] || return 0
local cpu
while IFS= read -r cpu; do
[[ -w "$cpu" ]] && echo 1 > "$cpu" 2>/dev/null || true
done < <(tac "$SAVE_DIR/cpus-online")
}

enable_wakeup_sources() {
local wakeup
for wakeup in /sys/devices/platform/soc@0/*/pon@*/pwrkey/input/input*/device/power/wakeup \
/sys/devices/platform/soc@0/*spmi*/*/*/pon@*/pwrkey/input/input*/device/power/wakeup; do
[[ -w "$wakeup" ]] && echo enabled > "$wakeup" 2>/dev/null
done
}

do_s2idle() {
log "entering s2idle"
mkdir -p "$SAVE_DIR"
touch "$STATE_FLAG"

mute_audio
display_off
timeout 10 /usr/bin/armada-power suspend || log "armada-power suspend timed out"
enable_wakeup_sources
echo s2idle > /sys/power/mem_sleep 2>/dev/null

if echo freeze > /sys/power/state 2>/dev/null; then
log "resumed from s2idle"
else
log "s2idle failed (rc=$?), system did not enter suspend"
fi

timeout 10 /usr/bin/armada-power resume || log "armada-power resume timed out"
unmute_audio
display_on
rm -f "$STATE_FLAG"
rm -rf "$SAVE_DIR"
}

do_fake_suspend() {
log "entering fake suspend"
mkdir -p "$SAVE_DIR"
touch "$STATE_FLAG"
mute_audio
block_input
display_off # gamescope drm_sleep (game) / bl_power (desktop); after Steam's animation
display_off
freeze_processes
suspend_radios
timeout 10 /usr/bin/armada-power suspend || log "armada-power suspend timed out"
# Do this after the final GPU devfreq writes in armada-power suspend.
suspend_runtime_pm
offline_secondary_cpus
}
do_resume() {
# Idempotent: a trap-fired resume after the explicit one is a no-op.

do_fake_resume() {
[[ -f "$STATE_FLAG" ]] || return 0
log "leaving fake suspend"
online_saved_cpus
resume_runtime_pm
timeout 10 /usr/bin/armada-power resume || log "armada-power resume timed out"
resume_radios
Expand All @@ -261,12 +321,22 @@ do_resume() {
case "${1:-sleep}" in
sleep|suspend)
[[ -f "$STATE_FLAG" ]] && exit 0
# If killed (e.g. unit stopped / timeout), always resume so nothing is stranded.
trap 'do_resume; exit 0' INT TERM
do_suspend
wait_for_wake
do_resume
trap - INT TERM
mode="$(resolve_suspend_mode)"
log "suspend mode: ${mode}"
case "$mode" in
s2idle)
trap 'rm -f "$STATE_FLAG"; rm -rf "$SAVE_DIR"; exit 0' INT TERM
do_s2idle
trap - INT TERM
;;
*)
trap 'do_fake_resume; exit 0' INT TERM
do_fake_suspend
wait_for_wake
do_fake_resume
trap - INT TERM
;;
esac
;;
wake|resume) touch "$WAKE_FLAG" ;;
toggle|power) if [[ -f "$STATE_FLAG" ]]; then touch "$WAKE_FLAG"; else exec "$0" sleep; fi ;;
Expand Down
1 change: 1 addition & 0 deletions system_files/usr/share/armada/power-profiles.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[general]
default_profile=balanced
suspend_mode=auto

[profile.eco]
label=Eco
Expand Down