Proximity and Ambient Light Sensor

The Librem 5 development board has a proximity sensor (PS) and ambient light sensor (ALS) provided by the VCNL4040M3OE-H5 module. These sensors can be accessed programmatically via messages sent to the relevant addresses on the appropriate I2C bus. Applications can also receive sensor information by accessing the appropriate system D-Bus interface.

This guide provides a basic overview of the commands that can be used to obtain ambient light and proximity information from the device; first via the D-Bus interface, then using the I2C bus.

D-Bus Interface

Note

Support for this interface is only present in recent kernels. Please update the kernel on your device if you experience problems starting the service.

The ambient light and proximity sensors are exposed to applications via the net.hadess.SensorProxy service on the system D-Bus. This is provided by the iio-sensor-proxy service.

Information about the light level and proximity of nearby objects can be obtained by querying the interface provided by the D-Bus service, calling methods to access devices and reading their properties.

Preparations

If the iio-sensor-proxy daemon is not already running, start it from the command line in the following way:

systemctl start iio-sensor-proxy

Check that it is running with this command:

systemctl status iio-sensor-proxy

This should include useful information to help diagnose problems if the service does not start.

Introspecting the D-Bus Object

It is useful to get an overview of the properties and methods that the service exposes via D-Bus. Use the gdbus tool from the command line to discover these:

gdbus introspect --system --dest net.hadess.SensorProxy \
                          --object-path /net/hadess/SensorProxy

Reading the Ambient Light Level

Using the gdbus tool again, first claim the light:

gdbus call --system --dest net.hadess.SensorProxy \
                    --object-path /net/hadess/SensorProxy \
                    --method net.hadess.SensorProxy.ClaimLight

This will cause the LightLevel property to be updated:

gdbus call --system --dest net.hadess.SensorProxy \
                    --object-path /net/hadess/SensorProxy \
                    --method org.freedesktop.DBus.Properties.Get \
                    net.hadess.SensorProxy LightLevel

Finally, release the light to stop the service from polling the device:

gdbus call --system --dest net.hadess.SensorProxy \
                    --object-path /net/hadess/SensorProxy \
                    --method net.hadess.SensorProxy.ClaimLight

This can also be performed using the Gio.DBusProxy class – see the GNOME API Reference for further information.

I2C Bus

Note

This section contains advice that only works with older Linux kernels that do not have built-in support for the proximity and ambient light sensors. If the i2cdetect tool does not produce the expected output then the devices are being managed by the kernel and their I2C interfaces are not accessible to user space programs.

The proximity and ambient light sensors are located at the top edge of the development board below the LS1601 label.

The location of the proximity and ambient light sensors (click to enlarge)

The location of the proximity and ambient light sensors

The VCNL4040M3OE-H5 module is accessed via an I2C bus address and exposes a set of registers that can be accessed by their addresses in the device. This means that we need two addresses – a bus address and a device address – to access a register.

The kernel provides abstractions that allow the sensors to be accessed fairly simply without requiring knowledge of the message protocol used for I2C devices.

Preparations

Before trying to access the sensors as the purism user, you need to install the i2c-tools package on the development board:

sudo apt install i2c-tools

Then add the purism user to the i2c group:

sudo usermod -a -G i2c purism

This user should now have permission to read and write the device files for the I2C devices and run the tools to access them.

Accessing the Module

The module is available via address 0x60 on I2C bus 2. This can be verified by running the i2cdetect tool:

purism@pureos:~$ /usr/sbin/i2cdetect -y 2

This should produce output like the following:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- UU -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 1e --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- UU -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- UU -- --
60: 60 -- -- -- -- -- -- -- -- -- UU -- -- -- -- --
70: -- -- -- -- -- -- -- --

It should be clear that there is a device at address 0x60 on the I2C bus – this should be the proximity and ambient light sensor.

Reading the Device ID

We check that the device at address 0x60 on I2C bus 2 is the sensor by querying its ID_L and ID_M registers, which should be readable at address 0x0c on the device. We can do this with the i2cget tool:

/usr/sbin/i2cget -y 2 0x60 0x0c w

The expected value is 0x0186 for the sensor module. If a different value is returned then the device is not the one we were expecting.

Powering the Ambient Light Sensor

Initially, the device will be powered down. To determine its state we read its ALS_CONF register at address 0x00 on the device:

/usr/sbin/i2cget -y 2 0x60 0x00 w

This should return 0x0001, indicating that the shut down bit is enabled. Set it to 0x0000 with the i2cset tool:

/usr/sbin/i2cset -y 2 0x60 0x00 0 w

When we no longer need to read from the device, we set the shut down bit again:

/usr/sbin/i2cset -y 2 0x60 0x00 1 w

We can read the state of the device again to verify that it has powered down, if necessary.

Reading the Ambient Light Level

With the device powered, ambient light sensor readings can be obtained by reading from the ALS_Data_L and ALS_Data_H registers at address 0x09 on the device:

/usr/sbin/i2cget -y 2 0x60 0x09 w

White channel values can be read from the White_Data_L and White_Data_H registers at 0x0a on the device:

/usr/sbin/i2cget -y 2 0x60 0x0a w

These should vary as the ambient light level at the sensor changes.

Powering the Proximity Sensor

The state of the proximity sensor can be obtained by reading the PS_CONF1 and PS_CONF2 registers at address 0x03 on the device:

/usr/sbin/i2cget -y 2 0x60 0x03 w

The lowest bit (PS_SD) of the value returned is 1 if the sensor is shut down. Set it to 0 to power it up. In the simplest case, we can set both the registers to 0x00:

/usr/sbin/i2cset -y 2 0x60 0x03 0 w

Setting the lowest bit to 1 again will shut down the sensor.

Reading Proximity Data

With the proximity sensor enabled, values can be obtained for the proximity of object to the sensor by reading from the PS_Data_L and PS_Data_M registers at address 0x08 on the device:

/usr/sbin/i2cget -y 2 0x60 0x08 w

Closer objects should result in higher values, up to the maximum value set in bit 3 (PS_HD) of the PS_CONF2 register. The default resolution is 12 bits.

Using Python to Access the Sensors

The python3-smbus package can be used to read these levels. Install it on the development board Python script with apt:

sudo apt install python3-smbus

The following script will show the ambient light levels every second until it is terminated: ambient.py

#!/usr/bin/env python3

import smbus
import time

# Bus number and i2c address:
BUS = 2
ADDRESS = 0x60

# Register addresses:
ALS_CONF = 0x00
ALS_Data = 0x09
White_Data = 0x0a

# Masks and values:
ALS_SD = 1

# Create an object to represent the appropriate bus.
bus = smbus.SMBus(BUS)

# Power up the ambient light sensor if not already powered.
state = bus.read_word_data(ADDRESS, ALS_CONF)
if state & ALS_SD == ALS_SD:
    bus.write_word_data(ADDRESS, ALS_CONF, state ^ ALS_SD)

try:
    while True:
        print('Ambient:', bus.read_word_data(ADDRESS, ALS_Data))
        print('White:  ', bus.read_word_data(ADDRESS, White_Data))
        time.sleep(1)
finally:
    # Restore the initial state.
    bus.write_word_data(ADDRESS, ALS_CONF, state)

Similarly, this script will show the proximity sensor’s response to close objects four times a second: proximity.py

#!/usr/bin/env python3

import smbus
import time

# Bus number and i2c address:
BUS = 2
ADDRESS = 0x60

# Register addresses:
PS_CONF1 = 0x03
PS_Data = 0x08

# Masks and values:
PS_SD = 1

# Create an object to represent the appropriate bus.
bus = smbus.SMBus(BUS)

# Power up the proximity sensor if not already powered.
state = bus.read_word_data(ADDRESS, PS_CONF1)
if state & PS_SD == PS_SD:
    bus.write_word_data(ADDRESS, PS_CONF1, state ^ PS_SD)

try:
    while True:
        print('Proximity:', bus.read_word_data(ADDRESS, PS_Data))
        time.sleep(0.25)
finally:
    # Restore the initial state.
    bus.write_word_data(ADDRESS, PS_CONF1, state)