Exposing a D-Bus Interface in Linux — Part 2
From basic concepts to code
This is the 2nd part of a series about D-Bus, so please take a look at the 1st one if you haven’t yet. This 2nd part was co-written by me and Thiago Cardoso.
Since D-Bus is an IPC / RPC mechanism, as explained on the first post, there are at least two processes involved: One that consumes APIs (aka client) and another one that provides those APIs (aka server).
We’re going to start by the server, which usually is a service that manages something on the system and exposes objects and interfaces to clients interact with. By the way, objects/interfaces are some of the keywords from D-Bus protocol that worths explaining before one can fully understand what it takes to implement server and client sides. Here they are:
Note: The following concepts explanation were taken from Avichal Pandey’s great post and then rewritten/organized differently or just complemented to better fitting this text.
API Definition
The following keywords are used to mention server available APIs and to reference them in code:
- Object Path: Client uses the object path to reference an object exposed by the Service. For example,
org/bluez/hci0
is the path of a remote object exposed by BlueZ representing a Bluetooth adapter. - Object: All objects exposed by the service implement Interfaces and have a unique object path.
- Interface: The API itself (and its members). It should answer: “What could I use from this service?”. Interfaces names are namespace strings much like Java package names, for example,
org.freedesktop.NetworkManager.
Interface Members
Interfaces are composed of three types of members: methods, properties, and signals — the typical members provided in other object-oriented languages, such as C#. Each member defines how clients are going to communicate with the exposed objects:
- Method: It is an example of a request-response model of communication and it is used for one-to-one communication. A client calls a method exposed by a service optionally passing inputs and receiving an output. It is always initiated by the client and can be used in a blocking or an async way.
- Signal: It is a notification that something of interest has happened. A signal is an example of a publish-subscribe model of communication and it is generally used for one-to-many type of communication. A client listens to a signal exposed by a service which at some point of time emits the signal. In contrast with methods, signals are a one-way form of communication, i.e. don’t have an output.
- Property: A variable that an object exposes. Its value can be accessed and modified via a getter and a setter, respectively.
Defining an interface
At the KNoT platform (powered by CESAR) we have a daemon that manages devices communicating using nRF24 radios. This daemon exposes a D-Bus interface that allows other components to interact with these devices. In this example, we’ll use a subset of it.
There is an XML format for defining D-Bus interfaces. The XML file can be constructed by hand, but to avoid errors most frameworks or libraries provide means of generating this file by parsing the code. When the XML file is provided by the code, D-Bus specification says that the objects were introspected at runtime.
Interface name
The first step is to define a name for the interface. As we are representing a device exposed by the nRF daemon from the KNoT platform, let’s call it br.org.cesar.knot.nrf.Device1.
Note: The “1” at the end of interface name stands for the API version. For more information please check API versioning.
<interface name='br.org.cesar.knot.nrf.Device1'>
Properties
Our device has two properties: address and a flag telling whether it is paired or not with the gateway. The address will be represented as a string and the paired flag will be represented as a boolean.
<property name='Address' type='s' access='read'>
<property name='Paired' type='b' access='read'>
We don’t want the user to change the address or to directly change the pairing, so these two properties are read-only, i.e. have only getters.
Methods
There are two operations that can be executed on our device: pair and forget. Pair will allow the device to communicate with the gateway and forget will do the inverse.
<method name='Pair'>
<arg name='keys' type='a{ss}' direction='in'/>
<!-- Possible errors:
br.org.cesar.knot.nrf.Error.AlreadyExists
br.org.cesar.knot.nrf.Error.InvalidArguments
-->
</method><method name='Forget'>
<!-- Possible errors:
br.org.cesar.knot.nrf.Error.InProgress
-->
</method>
When pairing, we might want to specify a public-private key pair to protect the communication. This will be passed as a dictionary containing the PublicKey
and PrivateKey
keys.
Both methods don’t return a value, but if they did it would be something like that:
<arg name="result" type="s" direction="out"/>
In case of error, a couple of errors messages can be returned. Error messages have names defined in the same way as interface names, but they are not included in interface XML, instead, a textual API documentation is provided.
Signals
When a device is paired or forgotten, we want to notify other interested parties. In this case, we’re going to use a pre-defined signal on D-Bus, by just adding an annotation on property:
<property name='Paired' type='b' access='read'>
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
</property>
But we could define a custom signal too as follows:
<signal name="Connected">
<arg type="b" name="connected" />
</signal>
Now let’s see how this can be implemented. (Finally!)
Server’s code
For convenience, we’re going to write server’s code using Python and pydbus library, but a similar result can be achieved with Node.js and node-dbus (example) or with C and ELL (example).
For running our server’s code you will need to install pydbus library using the following command:pip install 'pydbus'
. You may need also to (re)install some Python APIs for GObject Introspection and D-Bus (as discussed here):
sudo apt install --reinstall python-gi
sudo apt install python-gobject python-dbus
Please create a file named server.py
and begin defining a method main
and an empty DeviceInterface
class:
The code above obtains system bus with bus = SystemBus()
and exposes an interface (empty for now) with name br.org.cesar.knot.nrf.Device1
by calling bus.publish(service_name, ("Device1", DeviceInterface())
.
Another thing that above code does is setting up an event loop that is going to handle all source of events (including D-Bus ones) and then call the right callbacks for us:
loop = GLib.MainLoop()
try:
loop.run()
except KeyboardInterrupt:
loop.quit()
Now, let’s properly define the interface and its methods:
The class begins with the introspection data (mentioned on the previous section) that represents interface XML and then implements methods, properties, and signal handling.
Above implementation is obviously just an example. The Pair
method always set the paired
property as True
and emits PropertiesChanged
after half of a second. The Forget
method always returns the InProgress
error. The full code can be found here.
Gaining system access for our service
The service needs its own <busconfig>
file, as we did on first post, to get access to the system bus. Please download nrf24.conf file from KNoT’s GitHub, put it into /etc/dbus-1/system.d/
folder and reload the machine.
Run python server.py
as root and no org.freedesktop.DBus.Error.AccessDenied
error should be raised.
Sending Messages from Terminal
Now that the server is up and running, it is possible to send messages to him from another terminal without the need to write any client’s code.
The mdbus2
command helps us on checking if our interface was exposed right. Just pass the service name and object path and we should see something like this (The org.freedesktop.DBus
entries were omitted):
~$ sudo mdbus2 -s br.org.cesar.knot.nrf /br/org/cesar/knot/nrf/Device1[METHOD] br.org.cesar.knot.nrf.Device1.Pair(a{ss}:keys) -> ()
[METHOD] br.org.cesar.knot.nrf.Device1.Forget() -> ()
[PROPERTY] br.org.cesar.knot.nrf.Device1.Address(Address:s)
[PROPERTY] br.org.cesar.knot.nrf.Device1.Paired(Paired:b)
Everything is good, let’s send some messages with the dbus-send
command. Like dbus-monitor
, we should indicate on which bus and for whom we want to send messages, so include --system
option and specify destination with --dest
option. It is also important to include --print-reply
option to see what happened after our call.
Forget
method call example:
~$ sudo dbus-send --system --print-reply --dest=br.org.cesar.knot.nrf /br/org/cesar/knot/nrf/Device1
br.org.cesar.knot.nrf.Device1.ForgetError unknown.Error: pygi-error: br.org.cesar.knot.nrf.Error.InProgress (0)
Pair
method call example:
~$ sudo dbus-send --system --print-reply --dest=br.org.cesar.knot.nrf /br/org/cesar/knot/nrf/Device1 br.org.cesar.knot.nrf.Device1.Pair dict:string:string:"public","key"method return time=1522329445.167334 sender=:1.15 -> destination=:1.17 serial=5 reply_serial=2
Since Pair
method doesn’t return a value and emits a signal after some time, using dbus-monitor
on another terminal should show our emitted signal:
~$ sudo dbus-monitor --system "sender='br.org.cesar.knot.nrf'"signal time=1522329445.166423 sender=:1.15 -> destination=(null destination) serial=4 path=/br/org/cesar/knot/nrf/Device1; interface=org.freedesktop.DBus.Properties; member=PropertiesChanged
string "br.org.cesar.knot.nrf.Device1"
array [
dict entry(
string "Paired"
variant boolean true
)
]
array [
]
That’s it. Our server is ready to accept calls from clients. This part took more time (and words!) than I expected, so we will cover client’s side in a future post.
Special thanks to @cktakahasi who always raises some good points (and topics to cover in future) and Paulo Serra Filho who made himself available to test our code.
References
- KNoT Platform (Github)
- An Introduction to DBus by Avichal Pandey
- D-Bus API Design Guidelines
- D-Bus Specification
- BlueZ — Bluetooth protocol stack for Linux (Github)
- Python DBus Library Tutorial
- Embedded Linux Library
- GLib: The Main Event Loop
- Mickey’s DBus Introspection and Interaction Utility V2
- Playing with D-Bus interface of Spotify for Linux by Fran Diéguez (from where I found
dbus-send
for the first time)