Source Files

Most of the code for the application is included in a single main.py file which contains an Application class to manage the running of the application and a Window class to describe the user interface.

Much of the code is very similar to other examples and tutorials. We will focus on the parts that are specific to this example.

Relevant Modules

In addition to the usual modules for accessing command line arguments and GTK widgets, we use a custom sensor module to enable us to access the sensors.

import sys
import gi
from .sensor import Proximity

gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gtk

The sensor module is described in more detail below.

Setting up the Application

The application provides an Application class with the usual methods to set up the application and perform tasks when it is run. In the __init__ method we set up the application title, ID and program name to ensure that it integrates into the environment:

class Application(Gtk.Application):

    def __init__(self):
        super().__init__(application_id='com.example.proximity')
        GLib.set_application_name(_('Proximity'))
        GLib.set_prgname('com.example.proximity')

In the do_activate method, we create the application window and show it, but we also create a Proximity object from the sensor module:

    def do_activate(self):

        window = Window(application=self)
        window.set_default_size(320, 512)
        window.show_all()

        self.proximity = Proximity()
        self.proximity.connect('changed', window.update_info)
        self.proximity.claim()

The Proximity object has a changed signal that we connect to the window’s update_info method.

The do_shutdown method of the application is called just before the application exits. In this method we call the release method of the Proximity object to release the sensor for other applications:

    def do_shutdown(self):
        self.proximity.release()

Applications should usually only claim the sensor when required and release it as soon as possible.

Creating the User Interface

The Window class is used to display a simple user interface that updates when new sensor data is available. We begin by a Gtk.Label widget to show the status of the sensor:

class Window(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.label = Gtk.Label(halign='center')
        self.label.set_markup(
            _('<span size="large">Waiting for changes</span>'))

        self.add(self.label)

The label will be updated when the sensor’s state changes due to the signal connection in the application’s do_activate method.

Responding to Updates

As described above, the application contains a Proximity object that will emit a changed signal when the sensor’s state changes. When this occurs the update_info method is called:

    def update_info(self, sensor, near):

        if near:
            self.label.set_markup(
                _('<span size="large"><b>Near</b></span>'))
        else:
            self.label.set_markup(
                _('<span size="large">Far</span>'))

The signal delivers two values in the sensor and near parameters of the method. Since there is only one proximity object in use, we simply check the value of near to determine which string to show in the label.

The information provided by the changed signal is intended to be simple, so that the GUI part of the application only needs to include code to respond to proximity sensor changes. The Proximity class hides any complexity from the main application.

The Proximity Class

The sensor module provides the Proximity class that is used to access sensor data, using the Gio module to communicate with a D-Bus service:

import gi

gi.require_version('Gtk', '3.0')
from gi.repository import Gio, GObject

Since the Proximity class needs to emit signals, the GObject class from the GObject module is also needed. The class is derived from GObject and declares the changed signal to inform the rest of the application about sensor changes:

class Proximity(GObject.GObject):

    __gsignals__ = {
        'changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,))
    }

In the __init__ method of the class, we call the base class’s __init__ method to properly initialize it, before creating a proxy object that communicates with the net.hadess.SensorProxy service on the system D-Bus:

    def __init__(self):
        GObject.GObject.__init__(self)

        # Request a proxy for accessing the sensor service.
        self.proxy = Gio.DBusProxy.new_for_bus_sync(
            Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None,
            'net.hadess.SensorProxy',
            '/net/hadess/SensorProxy',
            'net.hadess.SensorProxy',
            None)

        # Track D-Bus property changes.
        self.proxy.connect('g-properties-changed',
                           self.properties_changed, None)

This proxy object emits the g-properties-changed signal when any of the properties exposed by the service change. We connect this signal to the properties_changed method in this class:

    def properties_changed(self, proxy, changed, invalidated, user_data):

        near = proxy.get_cached_property('ProximityNear').get_boolean()
        self.emit('changed', near)

In the properties_changed method, we respond to changes from the D-Bus service by reading the state of the proximity sensor and emitting the changed signal. This provides only the basic information to any methods that receive it, keeping the details of the D-Bus communication separate from other components.

We also provide the claim and release methods to allow the application to start and stop using the sensor. Each of these methods check for the presence of the proximity sensor by reading the HasProximity property of the D-Bus service:

    def claim(self):

        if self.proxy.get_cached_property('HasProximity').get_boolean():
            self.proxy.call_sync('ClaimProximity', None, Gio.DBusCallFlags.NONE,
                                 -1, None)

In the claim method, we call the ClaimProximity method if the sensor is available.

    def release(self):

        if self.proxy.get_cached_property('HasProximity').get_boolean():
            self.proxy.call_sync('ReleaseProximity', None, Gio.DBusCallFlags.NONE,
                                 -1, None)

In the release method, we call the ReleaseProximity method to release it for other applications.

Summary

Handling sensor data involves requesting information from the net.hadess.SensorProxy service on the system D-Bus. We create a module containing the Proximity class to communicate with the service, allowing the main application to subscribe to sensor changes by connecting to a changed signal.

The Proximity class itself performs a few tasks to make this possible, but users of this class can simply

  • connect its changed signal to a callback method,
  • call its claim method to start receiving updates, and
  • call its release method to stop receiving updates.

Applications do not need to provide an abstraction like this one, but it can make it easier to separate sensor-handling code from GUI code.