Here’s a new version I have been using for a while. This one will turn off bluetooth + WIFI and will also emit a resume.sh
file in the same folder as the script that can be ran on startup to recover bluetooth + WIFI in case the device died during sleep. Comment those lines out if turning off bluetooth or wifi is not desirable, but it may save a bit on battery life. Also the system has an issue with clock sync when wifi is off during sleep so I have also tried to address that.
Last but not least, I have found that sleep in the previous scripts can take a bit of time (approx. 3-4 seconds) due to trying to put many processes to sleep. Thus waking can be ignored if a previous sleep script was still in progress. This one will save the PID of the previous script and then will kill it if user initiated wake while a previous sleep script was still in progress. So in short: it wakes faster than before.
Please note that my setup is default Bookworm image without a login prompt or sudo password so this most likely won’t work with a login prompt or sudo password. Please look at @dotsony’s work for reference on how to address that.
#!/bin/bash
# Run below command in terminal first to set things up
# sudo apt install cpulimit sysstat wlr-randr procps
# Properly use filename for exclusion instead of predefined
BASE_NAME=$(basename "$0" | awk -F'.' '{print $1}')
# Base directory for script (can be anywhere)
BASE_DIR=$(dirname "$0")
# Add any process that needs to run in background during "sleep"
EXCLUDED_PROCS="systemd|pipewire|wireplumber|wayfire|labwc|cpulimit|$BASE_NAME"
# Automatically lock to min and max reported CPU clock on sleep
CPU_MAX_FREQ=$(cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq)
CPU_MIN_FREQ=$(cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq)
# File names can be changed here but they should be all default
# Please add absolute path to resume.sh on startup to recover network functions
PID_FILE=$BASE_DIR/sleep.pid
WAKE_FILE=$BASE_DIR/resume.sh
LOG_FILE=$BASE_DIR/log.txt
# Create wakefile for resume from sleep/bootup
CreateWakefile ()
{
echo '#!/bin/bash' | tee $WAKE_FILE
# Turn on bluetooth
echo 'bluetoothctl power on' | tee -a $WAKE_FILE
# Turn on wifi
echo 'nmcli radio all on' | tee -a $WAKE_FILE
# Turn on time sync (also will retrigger time sync when network is connected)
echo "sudo timedatectl set-ntp on" | tee -a $WAKE_FILE
# Cleanup once called
echo "rm -rf $WAKE_FILE" | tee -a $WAKE_FILE
echo "rm -rf $PID_FILE" | tee -a $WAKE_FILE
chmod +x $WAKE_FILE
}
# Set CPU clock
SetCPU ()
{
echo $1 | sudo tee /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
echo $1 | sudo tee /sys/devices/system/cpu/cpu1/cpufreq/scaling_max_freq
}
Log ()
{
echo "$1: $2" | tee -a $LOG_FILE
}
# Check if wake file exists
if [ ! -f $WAKE_FILE ];
# Enter sleep if wake file does not exist
then
# Write PID file
echo "$$" | tee $PID_FILE
# Make wakefile
CreateWakefile
# Turn off display
wlr-randr --output DSI-1 --off
# Turn off bluetooth
bluetoothctl power off
# Turn off wifi
nmcli radio all off
# Disable time sync (will re-enable on wake/boot)
sudo timedatectl set-ntp off
# Stop all current running processes
ps -A | egrep -v $EXCLUDED_PROCS | awk '{print $1}' | while read pid
do
# will not stop system processes
kill -STOP $pid
done
# Limit performance of existing processes
nohup bash -c '
while true; do
for pid in $(pidstat 1 1 | grep "^Average:" | awk "BEGIN {FS=\"[[:space:]]+\"} \$8 > 5 {print \$3}"); do
cpulimit -z -b -l 1 --pid=$pid;
done;
exec -a qsleep_bg sleep 60;
done' > /dev/null 2>&1 &
disown
# Reduce CPU to min freq
SetCPU $CPU_MIN_FREQ
# Otherwise wake up using wake file
else
# Stop previous script
kill -9 $(<"$PID_FILE")
# Log current frequencies & voltage for debug purposes
rm -rf $LOG_FILE
Log "CPU" "$(echo "scale=2; $(vcgencmd measure_clock arm | awk -F'=' '{print $2}') / 1000000" | bc -l)MHz"
Log "GPU" "$(echo "scale=2; $(vcgencmd measure_clock core | awk -F'=' '{print $2}') / 1000000" | bc -l)MHz"
Log "Voltage" "$(vcgencmd measure_volts core | awk -F'=' '{print $2}')"
# Wake CPU to max freq
SetCPU $CPU_MAX_FREQ
# Clean up previous scripts
killall pidstat
killall cpulimit
pkill -f qsleep_bg
# Launch wake file
. $WAKE_FILE &
# Resume all stopped processes
kill -CONT -1
# Turn on display and apply rotation (needed by Wayfire)
wlr-randr --output DSI-1 --on --transform 270
fi
exit 0
I’m using wayfire, so to set this up on wayfire, add this extra line to wayfire.ini
:
[autostart]
resume=/path/to/resume.sh
For labwc, I’d guess the script can also be added to /etc/rc.local
or some other script location on boot.
In the same folder as the script, a new log.txt
file will also be emitted on every wake that has this content basically:
CPU: 200.00MHz
GPU: 100.00MHz
Voltage: 0.8950V
The clocks above can be some really big value if wake was initiated while another sleep script was in progress. So this was basically how I caught that situation. I left this in for debugging purposes since different setups can behave differently.
Bonus: here’s how to reduce GPU and CPU clocks on idle. Open up /boot/firmware/config.txt
and add the following:
over_voltage_min=-5
arm_freq_min=200
gpu_freq_min=100
I have found these values to be generally stable with my CM4. If you experience instability, either remove or raise over_voltage_min
. These lines can be added in conjunction with over_voltage
, arm_freq
and gpu_freq
that overclock the GPU. Those will change the min/max clocks for CPU and GPU. Personally I run with a mild overclock to 1.6GHz CPU and have found no issues at all.
Even with the extreme underclock and slight undervolt, I have found there to be not much benefit to battery life (roughly 8-10 hours of use with/without this script to sleep) so this may or may not be needed. But still, it at least reduces temperature during idle, too.