I had a cool HP Spectre x360 for a long time, it is a pretty good laptop that has not made me regret not choosing a Dell XPS. Despite how much I love it, I never fully embraced the tablet modality. Not until a couple of weeks ago.
Since its inception, I have always been fascinated with the Enlightenment project. It is a Desktop Environment based on EFL, the same powerful UI toolkit powering Tizen, light, fast and pretty customizable. Long story short, it suits me more than other well-known DE.
Obviously, I am able to rotate the screen through the Screen section in the Enlightenment Settings panel and, in theory, I could have created a script to rotate the screen using the xrandr
binary and then set some key bindings to easily switch between normal mode and tablet mode, but this ain't what I was hoping to get.
By the way, the setting panel about screens is quite comprehensive.
Adventure
I thought, wouldn't it be cooler if I can have a gadget (in enlightenment) that can do that for me? So, given these premises, there is only one possible question that I could have asked myself: Can I auto-rotate my screen in E?
So, I started digging in E development documentation, reading the specs and going through the APIs doc.
My action plan consisted of the following steps:
- Figure out where the logs were being saved and how to access them
- Study the E gadget architecture
- Creating a testbed where to run my little experiment
- Find out if I have to read raw data from the gyroscope or there is a higher-level layer on which I could rely on
- Study the E APIs to manipulate the screen
- Study the EFL APIs to manipulate the input (touchscreen)
- Finalise the gadget
Logs
The first thing I planned to do, was to be able to access the logs that were being generated. Following the documentation available on enlightenment Website it was easy to figure out what I needed to do.
EINA_LOG_LEVELS=my_awesome_module:4 ./{application}
But where? Where should I do that? Well, the enlightenment
process is run, and monitored, by another process named enlightenment_start
. So, we can edit that file to set the log level and redirects all the output to a file of our choice.
The main hassle was that I did not know where my session file was.
Enlightenment starting process
Usually, the enlightenment_start
file should be in /usr/bin but you may go for:
sudo vim $(which enlightenment_start)
Of course, instead of modifying the enlightenment_start
I would rather copy it to, let's say, enlightenment_start_logging
and alter the latter. The main reason to do that, for me, is that I recompile the E package quite often and I would like to avoid applying this change every time I compile.
You should end up with something like this:
export EINA_LOG_LEVELS=convertible:4
LOG_FILE=/tmp/enlightenment.log
/usr/bin/enlightenment_start >> $LOG_FILE 2>&1
The last step is to make sure that your Desktop Manager runs enlightenment_start_logging
instead of enlightenment_start
.
E Gadget Architecture
There is quite a lot of information on the sandbox gadget on the E site, but I was not able to find other tutorials or guides for regular gadgets. On the other hand, people on IRC are absolutely fantastic. Raster in primis, followed by basically everybody there were kind and gave me insightful comments and tips.
The overall idea is to first have a Module by declaring a E_Module_Api
. You then need to write your own code to load and destroy the module. In these two functions named respectively e_modapi_init
and e_modapi_shutdown
you can register your gadget and de-register it.
Usually, your e_modapi_init
will look more or less like this:
E_API void *
e_modapi_init(E_Module *m)
{
// Initialise the logger
_log_dom = eina_log_domain_register("gadget_name", EINA_COLOR_LIGHTBLUE);
convertible_module = m;
// Register callback for gadget creation
e_gadget_type_add("gadget_name", convertible_create, NULL);
return m;
}
Note that convertible_module is a variable of type E_Module
.
Testbed environment
In the enlightenment repository, you can find a great utility named xdebug.sh that will run another enlightenment process using Xephyr.
Reading data
LuckiIy, there is a driver for my sensor. I used hid_sensor_accel_3d
from the kernel as reported on the Gentoo Wiki page.
Now I had two options:
- Read raw data
- Use something on a slightly high level, like IIO-sensor-proxy
The people working on IIO-sensor-proxy have a large range of supported devices, including mine. Thus, it seemed the best option for me. IIO-sensor-proxy creates a Sensor Proxy service and exposes sensors data through the D-Bus interface. More information about the Proxy can be found here. It is advised to go through the D-Bus Documentation first to get a good grasp of what it is and how it works.
The best place to read about it is the page on freedesktop.
D-Bus is a system for interprocess communication (IPC) allowing multiple applications to exchange messages. A good representation of how this works is depicted in the figure below, taken from freedesktop.
Going back to IIO-sensor-proxy, you can get a list of available properties by running
gdbus introspect --system --dest net.hadess.SensorProxy --object-path /net/hadess/SensorProxy
The property of interest is AccelerometerOrientation and it is of type string. Moreover, the property can only assume one of the following values: - undefined - normal - bottom-up - left-up - right-up.
According to the Sensor Proxy documentation, the first thing to keep in mind is that our code needs to claim the accelerometer first and release it when it has done.
Of course, there are many bindings for D-Bus and, likewise, many libraries to use it. Writing a gadget for E, my answer was Eldbus. I spent some time getting familiar with it by reading the official documentation.
First I created a struct to contains D-Bus related information:
#define EFL_DBUS_ACC_BUS "net.hadess.SensorProxy"
#define EFL_DBUS_ACC_PATH "/net/hadess/SensorProxy"
#define EFL_DBUS_ACC_IFACE "net.hadess.SensorProxy"
typedef struct _DbusAccelerometer DbusAccelerometer;
struct _DbusAccelerometer
{
Eina_Bool has_accelerometer;
char *orientation;
Eina_Bool monitoring;
Eina_Bool acquired;
Eldbus_Proxy *sensor_proxy, *sensor_proxy_properties;
Eldbus_Pending *pending_has_orientation, *pending_orientation, *pending_acc_claim, *pending_acc_crelease;
};
Then, the accelerator must be claimed and finally, we are able to read the AccelerometerOrientation property.
// Initialise DBUS component
int initialization = eldbus_init();
inst->accelerometer->sensor_proxy = get_dbus_interface(EFL_DBUS_ACC_IFACE);
inst->accelerometer->sensor_proxy_properties = get_dbus_interface(ELDBUS_FDO_INTERFACE_PROPERTIES);
inst->accelerometer->pending_has_orientation = eldbus_proxy_property_get(inst->accelerometer->sensor_proxy,
"HasAccelerometer", on_has_accelerometer,
inst);
inst->accelerometer->pending_orientation = eldbus_proxy_property_get(inst->accelerometer->sensor_proxy,
"AccelerometerOrientation",
on_accelerometer_orientation, inst);
// Claim the accelerometer
inst->accelerometer->pending_acc_claim = eldbus_proxy_call(inst->accelerometer->sensor_proxy, "ClaimAccelerometer",
on_accelerometer_claimed, NULL, -1, "");
The get_dbus_interface is a convenient function to access a proxy interface.
Eldbus_Proxy *get_dbus_interface(const char *IFACE)
{
Eldbus_Connection *conn;
Eldbus_Object *obj;
Eldbus_Proxy *sensor_proxy;
conn = eldbus_connection_get(ELDBUS_CONNECTION_TYPE_SYSTEM);
if (!conn)
{
ERR("Error: could not get system bus");
}
obj = eldbus_object_get(conn, EFL_DBUS_ACC_BUS, EFL_DBUS_ACC_PATH);
if (!obj)
{
ERR("Error: could not get object");
}
else
{
INF("Object fetched.");
}
sensor_proxy = eldbus_proxy_get(obj, IFACE);
if (!sensor_proxy)
{
ERR("Error: could not get proxy for interface %s", IFACE);
}
else
{
INF("Proxy fetched");
}
return sensor_proxy;
}
The last bit is to sign up for changes in the property of interest. The eldbus_proxy_signal_handler_add function will do this for us. It needs the proxy object, the signal, the callback and (optionally) some data to pass. Note that the PropertiesChanged signal is emitted if any property changes on the object in question. From the official documentation:
If one or more properties change on an object, the org.freedesktop.DBus.Properties.PropertiesChanged signal may be emitted (this signal was added in 0.14):
So, when the callback for PropertiesChanged is triggered, we request again the value of the AccelerometerOrientation property.
/**
* Prepare to fetch the new value for the DBUS property that has changed
* */
static void
_cb_properties_changed(void *data, const Eldbus_Message *msg)
{
Instance *inst = (Instance *) data;
Eldbus_Message_Iter *array, *invalidate;
char *iface;
if (!eldbus_message_arguments_get(msg, "sa{sv}as", &iface, &array, &invalidate))
{
ERR("Error getting data from properties changed signal.");
}
// Given that the property changed, let's get the new value
eldbus_proxy_property_get(inst->accelerometer->sensor_proxy, "AccelerometerOrientation",
on_accelerometer_orientation, inst);
}
Eldbus_Signal_Handler *sh = eldbus_proxy_signal_handler_add(inst->accelerometer->sensor_proxy_properties,
"PropertiesChanged",
_cb_properties_changed, inst);
Rotating the screen
In E, there is a component for Randr (The X Resize, Rotate and Reflect Extension) that is meant to manipulate X through Xrandr.
The most simple solution was to directly use it in order to rotate the screen. You can see the details in the header file of e_randr2. In my code, I only had to import it and loop through the items of the screens field of the _ERandr2 type.
A better (in progress) solution would be to rely on Zones. In E, the canvas is split into so-called zones and E deals with zones for basically everything (wallpapers, shelves and windows). Moreover, there is a 1:1 mapping between zones and screens at the time.
More information about how to deal with them can be inferred from the e_zones.h header file. Note that each zone has a field to get the randr2_id. So, it is possible to go back and forth between zones and screens.
The idea is to use e_comp (available when you import e.h) to get the list of zones and, from there, get the actual screen(s) to rotate.
Input
Of course, in a convertible laptop, rotating the screen(s) using randr is not enough. Input devices like the touchscreen(s) are not handled by randr, that focuses on output. For that, other tools are needed. Although things may be easier for wayland, they are not that easy for X. In E, and specifically in ecore_x_xi2.c, there is no code to handle devices through xinput.
That module will need a little bit of extension, that may probably end up in another post.
Final consideration
Although the missing input rotation is a huge problem for problem a proper and complete convertible laptop experience, it is enough for me at the moment. I can read long articles or watch movies like on a pad.
My main goal was to get familiar with writing a gadget for E and see how still good my C skill is.
You can watch this recording of how the gadget work and if you would like to contribute, please check out my repo: rafspiny/convertible. Please do not hesitate to contact me if you wish to know more or contribute.