Adventures with my Pinephone - A Matter of Time (Part 2)

Posted on 25 April 2021 by vkraven

Note: If you don’t care enough about my blabbering and you’re just looking for the code, you can find it on Github. And who can blame you? I blabber a lot =P

This is the second part of the Adventures with my Pinephone - A Matter of Time series. Part 1 may be found at Adventures with my Pinephone - A Matter of Time (Part 1)

The Problem

In our quest to enable the PostmarketOS Pinephone’s ability to wake itself from suspend to ring a timer or an alarm, we ended previously with the realisation that our two options weren’t going to work. The options were:

  1. Grant CAP_WAKE_ALARM to the Pinephone user during login, thereby having it be inherited by the PowerDevil process
    • NO: because Alpine does not ship pam_cap.so
  2. Setting the CAP_WAKE_ALARM capability on the org_kde_powerdevil binary itself.
    • NO: because CAP_WAKE_ALARM “breaks dbus” and Alpine actually explicitly removes the capability before shipping KDE.

But why would DBus be broken with extra permissions granted to the binary? This is quite an interesting mystery which I hope to solve someday.

Nonetheless, it seems like our journey is heading towards a disappointing end - this issue seems to be something that should be sorted out between the distro and the KDE project.

That said, if all I wanted out of a project was to beg/coerce/whine/induce somebody else to do the work of implementing features and resolving bugs for me, I would just use an iPhone. My beautiful Linux phone has all the compiler tools. Plus the relevant components’ source code is readily available - so there’s no reasonable excuse on my part for not solving it myself!

Designing my Solution

If I were to implement a solution of my own, here are some goals that would be important to me:

  • Lightweight:
    • The solution should be extremely lightweight, since the Pinephone is not a particularly capable device and it runs on a battery
  • Simple Interface:
    • Part of the problem with the also (somewhat) simple rtcwake application is that it puts the system to sleep when setting a wakeup time! I want my solution to be “pluggable” and usable for more than just my own purposes.
    • As such, my solution should do only one thing - wake the system up if it is asleep. Nothing more.
  • Avoid root privs:
    • As far as possible, avoid the need for root privileges. Ideally, the program should be invokable entirely as a low-privilege user.

Implementing the Waker

On first glance, the Waker does not seem particularly tricky. In fact, we already have the trickiest part (creating a CLOCK_REALTIME_ALARM) solved in our proof of concept in the last post.

All that remains is to follow the manpage’s guidance and set an itimerspec containing our desired wake time. Nicely, timerfd supports the TFD_TIMER_ABSTIME parameter which allows you to pass in an absolute timestamp when calling timerfd_settime, instead of having to calculate the time difference on your own.

...

#include <stdlib.h>
#include <time.h>
...

int main(int argc, char** argv) {
	...
	struct itimerspec its;
	struct itimerspec new_its;

	long int newtime = atol(argv[1]);
	new_its.it_value.tv_sec = newtime;
	new_its.it_value.tv_nsec = 0;
	new_its.it_interval.tv_sec = 0;
	new_its.it_interval.tv_nsec = 0;

	int res;
	res = timerfd_settime(timer, TFD_TIMER_ABSTIME, &new_its, &its);
	...

}

We pull in stdlib.h for the atol function to convert our first argument to a long int. We also pull in time.h for the itimerspec struct. We then set the new itimerspec struct’s seconds value to our desired time, and set the interval (used for repeating timers, not for us) to 0.

Finally, we call timerfd_settime on the timer we initially created, with the TFD_TIMER_ABSTIME property.

Very nice. Now, we’ll create an event loop to read the timerfd timer, and wait for it to return. Because we’re doing this just for the side effect of the timerfd waking the system up from sleep, we don’t really have to care about what it returns. I picked epoll for this task.

#include <sys/epoll.h>
...

int main(int argc, char** argv) {
	...

	int epoller;
	struct epoll_event epollEvent;
	struct epoll_event newEvents;

	epoller = epoll_create1(0);
	epollEvent.events = EPOLLIN;
	epollEvent.data.fd = timer;
	epoll_ctl(epoller, EPOLL_CTL_ADD, timer, &epollEvent);

	printf("Entering epoll event loop\n");

	while (1) {
		int numEvents = epoll_wait(epoller, &newEvents, 1, -1);
		if (numEvents > 0) {
			printf("Picked up an epoll event\n");
			break;
		}
	}

	printf("Exiting\n");
	close(epoller);
	close(timer);

}

After we pick up an event, we break out of the event loop and close the two fds. Notice that I set epoll_wait’s timeout to -1: this allows our waker program to block perpetually until an event is returned from timer - thereby saving us as much CPU processing power as possible on our relatively resource-starved Pinephone.

So now our waker program will take in a Unix timestamp in argv, set up a timer, and block until the timer fires. Simple!

Implementing the Sync-er

With rtcwaker now capable of waking the Pinephone up at any arbitrary timestamp, we now need some way of programmatically figuring out the timestamps that correspond to the alarms and timers we set in KClock.

Nicely enough, KClock exposes a number of fantastic DBus methods that will provide us just the very timestamps we need (for Alarms, at least). Execute this most convenient command and see for yourself:

$ qdbus org.kde.kclockd
/
/Alarms
/Alarms/[SOME_HEXADECIMAL_VALUE1]
/Alarms/[SOME_HEXADECIMAL_VALUE2]
...
/MainApplication
/Settings
/Timers
/Timers/[SOME_HEXADECIMAL_VALUE...]
...
/org
/org/kde
/org/kde/kclockd

The /Alarms/ca98bf604945408184657b498f8ac673 and /Timers/8132e4badc0543599cb9ba01c5c1bb73 (example values) are actually UUID strings with their - characters removed, and they correspond to unique timers and alarms set in the KClock app.

Particularly interesting for us is the getNextAlarm Method in /Alarms:

$ qdbus org.kde.kclockd /Alarms getNextAlarm
1619378700

It looks like getNextAlarm actually returns the very information we need - a timestamp for the next upcoming alarm for KClock to ring. Awesome!

In pursuit of being lightweight, I wanted my program to run compiled binary code that fetched these values from DBus through these Methods. If it could be done in the same binary, we’d add a little “fetch_next_kclock_alarm_from_dbus” function into rtcwaker, and have rtcwaker call it and pass its value in timerfd_settime. This would all be done in one binary.

To make sure that I was on the right track, I pulled in qmake and created this sample QDBus test program:

#include <iostream>
#include <QtCore/QDebug>
#include <QtDBus/QtDBus>

int main() {
	// Try to connect to session bus
	if (!QDBusConnection::sessionBus().isConnected()) {
		std::cout << "Cannot connect to the session bus" << std::endl;
		exit(1);
	}

	QDBusConnection bus = QDBusConnection::sessionBus();
	QDBusInterface interface("org.kde.kclockd", "/Alarms", "org.kde.kclock.AlarmModel");
	QDBusReply<qulonglong> reply = interface.call("getNextAlarm");
	qDebug() << reply;

	return 0;
}

It connects to the session bus, opens an interface to org.kde.kclockd’s /Alarms path, and invokes getNextAlarm and prints the response out through QDebug. Basically, it performs exactly what we did with the qdbus program above.

$ qmake
$ make
$ ./dbustest
1619379700

Very nice! This is precisely what we needed, and our program is shaping up well indeed. All that remains is to modify rtcwaker.c to reconcile any C/C++ syntax differences, and convert the qulonglong into the long int that itimerspec requires.

But wait.

This is all very exciting and promising. Still, before we go ahead, let’s just make sure that the program will absolutely run in the conditions we want it to run - namely, with the CAP_WAKE_ALARM capability assigned.

$ sudo setcap cap_wake_alarm+ep /home/user/dbustest/dbustest
$ getcap -v /home/user/dbustest/dbustest
/home/user/dbustest/dbustest cap_wake_alarm=ep

$ ./dbustest
Cannot connect to the session bus

“Cannot connect to the session bus”, it said.

WHATEVER.

I’ll do it in the shell.

Implementing the Sync-er (In the Shell)

After my intended binary failed me due to some dbus breakage (if anybody knows why this happens in pmOS, please do reach out), I figured the next best approach would be to pass the next alarm timer in through the shell. This was definitely not my first choice, as I anticipated the syncing script would have to be called relatively frequently, and a shell script is interpreted at runtime.

Nonetheless, I figured that the performance cost would be tolerable at this point in time. I set about spinning up a quick shell script in the busybox ash shell to perform this task. This was a pretty fun experience, as I had to unlearn a number of “bashisms” that I’d gotten used to over the years. Without array types, I had to consider using ugly variables and loops to handle a number of conditions.

But eventually I had a simple script that would fetch the dbus output of getNextAlarm, and start rtcwaker in the background with it.

It was time for a test.

I set an alarm in KClock for around 10 minutes later (since I know PowerDevil suspends the Pinephone after around 5 minutes).

I unplugged my power cable to put the phone into the Battery profile.

I started the script, and the logs suggested that it had correctly picked up the next alarm time and passed it to rtcwaker. I checked ps -a and rtcwaker was running. Good good.

I locked the Pinephone’s screen and left it on my desk. After 5 minutes, I tapped the screen twice. The phone did not respond. Good - this meant that the phone was suspended.

I waited for a touch longer…

BEEP BEEP, BEEP BEEP

IT WORKS!!

BEEP BEEP, BEEP BEEP

The wonderful Pinephone wakes on KDE Alarms!!! AWESOME!!!

And I didn’t need to sudo anything throughout this process at all!

Adding More Features

With that proof of concept working, I quickly added in support for KClock timers as well as Alarms. The timer interface is not as ideal as the Alarms one, but it does expose the elapsed and length properties through DBus. Calculating the next timestamp would therefore just be a matter of computing

NEXT_TIMESTAMP = NOW - elapsed + length

And, a matter of adding a loop over the running timers in order to get the earliest expiring timer to pass to rtcwaker. After that, we compare the earliest expiring timer timestamp to getNextAlarm’s output and pick the earlier of the two.

And finally, I created an OpenRC init script and set it to run at the default runlevel.

$ sudo rc-update add rtcsyncwake default

And now I get to use KClock Alarms and Timers, and my Pinephone will always wake the phone from suspend when it’s time for it to ring. And all this without sudo-ing or SUID-ing or su-ing.

Time for a Conclusion

We started with wake-mobile’s GTK-based application with a root-owned SUID binary that sets timers in systemd, and we ended with a user-owned binary that relies on Linux to handle our relationship with the real-time clock. This was quite a journey!

I don’t write application code for a living, so do pardon the quality of my code. But if you’re on a Pinephone and you think this program could be useful for you, I’ve left instructions on getting it running in this Github repo. It would be great if my rtcwaker program eventually proves useful to somebody else as well.

Ah, open-source is simply wonderful.