Linux mobile: auto-suspend with notifications and fully functional wakelocks

2023-03-03[permalink]

The current state of the linux mobile userspace has a pretty big UX problem in my opinion.

Power management on linux mobile devices like the pinephone/pro relies upon suspend. Suspend comes with a limitation: you can't do anything in the background. This includes checking your email or chat client to provide you with notifications.

Wakelocks also don't work well on linux as a result of excessive fragmentation.

Building on some existing solutions like sleepwalk, I've arrived at a reasonable solution to this problem for my use and figured I would document it here.

TL;DR:

  • We can solve the notification detection problem with periodic wakeups.
  • We can provide user feedback while suspended by setting 'retain-state-suspended' on the notification LED in the kernel device tree and toggling the LED right before suspend.
  • We can solve the wakelock fragmentation problem with a daemon that merges the state between all interfaces.

I'm not going to be providing a complete implementation, as my thrown-together script is somewhat specific to my setup.

With my solution in place:

  • If there are notifications while sleepwalking, the LED will turn on and remain on during suspend.
  • If you're running anything that holds a wakelock - such as a music player, custom cron job, whatever: the system won't suspend.
  • The system will still spend most of the time suspended.

General routine

Pseudocode for what I'm doing:

void start() {
  while (true) {
    block_until_backlight_off_for_30s();
    block_until_there_are_no_wakelocks();
    sleepwalk();
  }
}

void sleepwalk() {
  while (backlight_is_off()) {
    schedule_wakeup(+10 minutes);

    if (is_notifications()) turn_on_led();
    else                    turn_off_led();

    suspend();

    sleep(30); // Woke up, hold wake >= 30s
  }
}

// Can be nice to fully take over the
// notification LED while awake as well to
// keep the user feedback consistent
void on_notification() { turn_on_led();  }
void on_dismiss()      { turn_off_led(); }

start();

Components

Wakelocks

The linux desktop has a large number of different wakelock interfaces. Applications and DEs tend to pick just one of those interfaces.

This means it's very difficult to obtain all of the wakelocks across the system to determine when it's a good time to sleep.

I've been working on a terrible hack for this problem: unified-inhibit.

uinhibitd will cause all of these interfaces to share the same state. We can choose any of the interfaces available to obtain all wakelocks.

One interface is particularly easy to write scripts for - linux kernel wakelocks:

echo my-lock > /sys/power/wake_lock;   # Take a lock
echo my-lock > /sys/power/wake_unlock; # Release a lock
cat /sys/power/wake_lock;              # List current locks

If we want to script our own wakelocks for any use-case (perhaps lock while there are any active ssh sessions, or lock on a UI toggle), using these sysfs wakelocks makes it easy to do so.

Obtaining notifications

We need to know if any notifications exist immediately before suspending such that we can decide if we should light an LED or not.

Thankfully, this is a substantially less fragmented problem: org.freedesktop.Notifications covers pretty much everything except for sxmo (who are doing their own thing, as always).

If doing this from a shell script, parsing the dbus-monitor output works well enough. A more correct solution would be to use a D-Bus library and calling BecomeMonitor.

Detecting idle

Generally, wakelocks are not generated for the case of 'user interacting with device'. Different desktop environments solve this detection problem differently.

Thankfully, the result of this detection tends to have one common side effect across all DEs: when the user isn't interacting with the device, the screen turns off.

We can detect this via sysfs:

$ cat /sys/class/backlight/backlight/bl_power
0

If it's 0, the backlight is on.

Retaining LED state on suspend

Depending on your hardware and kernel, the device tree might not be configured to retain LED state on suspend. We need this so we can turn the LED on during suspend if there's a notification.

Here's an example patch for the pinephone pro:

diff --git a/arch/arm64/boot/dts/rockchip/rk3399-pinephone-pro.dts b/arch/arm64/boot/dts/rockchip/rk3399-pinephone-pro.dts
index 8ae966047796..14808acade8d 100644
--- a/arch/arm64/boot/dts/rockchip/rk3399-pinephone-pro.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3399-pinephone-pro.dts
@@ -221,6 +221,7 @@ led-red {
                        function = LED_FUNCTION_INDICATOR;
                        gpios = <&gpio4 RK_PD2 GPIO_ACTIVE_HIGH>;
                        panic-indicator;
+                       retain-state-suspended;
                };
 
                led-green {
@@ -228,12 +229,14 @@ led-green {
                        function = LED_FUNCTION_INDICATOR;
                        gpios = <&gpio4 RK_PD5 GPIO_ACTIVE_HIGH>;
                        linux,default-trigger = "default-on";
+                       retain-state-suspended;
                };
 
                led-blue {
                        color = <LED_COLOR_ID_BLUE>;
                        function = LED_FUNCTION_INDICATOR;
                        gpios = <&gpio4 RK_PD6 GPIO_ACTIVE_HIGH>;
+                       retain-state-suspended;
                };
        };

Controlling the LED

# View possible LED modes. Selected mode surrounded by [].
# Your DE may have set this to 'pattern' to make it flash.
cat /sys/class/leds/blue:indicator/trigger;

# You probably want to set the mode before controlling brightness.
# I've had the most success with 'default-on' right before suspend.
echo default-on > /sys/class/leds/blue:indicator/trigger;

echo 1 > /sys/class/leds/blue:indicator/brightness; # Turn on the LED
echo 0 > /sys/class/leds/blue:indicator/brightness; # Turn off the LED

Suspending the system

echo mem > /sys/power/state can be used to suspend the system.

This is a pretty low-level way to suspend the system. This ensures:

  • No pre-suspend hooks are changing the LED state before suspend
  • The system suspends quickly

This has the downside that you might actually care about some of those pre-suspend hooks. If your DE isn't setting up a hook to turn off the LEDs, systemctl suspend may work as well and allow any hooks to run if they're configured properly with systemd (and not relying on internal codepath to schedule the hooks).

Scheduling wakeups

This one is easy: rtcwake -m no --date +600s

That's about it

Holding hope that mobile DEs implement something like this down the road.

Relying on userspace applications to schedule and manage this themselves is going to be mess through slightly varying implementations. You would also have to deal with the issue of individual application wakeups not occuring at the same time resulting in far more wakeups than needed.

Though applications should definitely tie themselves into a wakeup hook to run their checks - be it checking for a chat message or email or whatever else is needed to figure out if any notifications are needed.

For now though, this is an easy, general solution anyone can throw together with a script to make it work.

Copyright© 2023 Matthew Egeler