# I made a monitor brightness controller using an Arduino-powered light sensor

Whenever I would adjust my desktop monitor's brightness as the day progressed, I opened a window, or I turned on/off a light in the room, I would think: if only this happened automatically... I'm manually changing the monitor brightness to a value directly related to the room's level of light!

I was already changing my monitor's brightness via its [DDC/CI](https://en.wikipedia.org/wiki/Display_Data_Channel) interface using [ddcutil](https://www.ddcutil.com/) on [Fedora](https://getfedora.org/), so I figured this could be achieved. I set out to build a software monitor brightness controller in Python that uses data from an Arduino-powered light sensor to correlate ambient light and backlight brightness. Now, as the system regularly reads the sensor data, my monitor will auto*magically* change its brightness to reflect the room's level of ambient light—I call it: **lighten**. 💡

Here I will share how to go about setting up the hardware and software for this project, how it works, and what I learned along the way.

# Setup

## The hardware

> For any of this to work, a [ddcutil](https://www.ddcutil.com/)\-controllable monitor must be used.

The hardware side of this project is composed of an Arduino board, a light sensor, and a means of connecting them.

I chose these components:

* [Adafruit QT Py ESP32-S2](https://www.adafruit.com/product/5325)
    
    * This board has built-in support for the **TinyUSB** library
        
* [Adafruit TSL2591 Light Sensor](https://www.adafruit.com/product/1980)
    
    * This sensor is precise, allowing for exact lux calculations
        
* [STEMMA QT Cable](https://www.adafruit.com/product/5384)
    
    * This cable is super convenient!
        

Simply connect the QT Py board to the light sensor with the STEMMA QT cable and position them behind your monitor with masking tape!

Behold the beautiful hack:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1676242318259/381b4b0f-0958-49bc-9370-d9dc39f9c203.jpeg align="left")

Remember to keep the light sensor itself exposed:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1676242328243/4c2aacbf-9234-4616-9f85-f63dbc847245.jpeg align="left")

Power and receive data from the Arduino with a USB-C to USB cable plugged into your computer.

## The software

The Arduino code to read the light sensor data and the system-side controller are open-source.

> This process requires a working Arduino IDE, basic knowledge of uploading sketches, and experience with the Linux command line.

### Arduino

The Arduino code can be found here: [arduino-lighten](https://github.com/jcrd/arduino-lighten). It requires the following libraries:

* [Adafruit TSL2591 library](https://github.com/adafruit/Adafruit_TSL2591_Library)
    
* [Adafruit Unified Sensor](https://github.com/adafruit/Adafruit_Sensor)
    
* [Adafruit TinyUSB](https://github.com/adafruit/Adafruit_TinyUSB_Arduino) (The board used should have [built-in support](https://github.com/adafruit/Adafruit_TinyUSB_Arduino#cores-with-built-in-support) for this library)
    

[This tutorial](https://learn.adafruit.com/adafruit-tsl2591/wiring-and-test#install-adafruit-tsl2591-library-2980796) explains how to set up the first two.

The **TinyUSB** library can be installed similarly:

> In the Arduino IDE menus, go to Sketch -&gt; Include Library -&gt; Manage Libraries, then search for and install Adafruit TinyUSB.

Now clone the **arduino-lighten** repo:

```sh
git clone https://github.com/jcrd/arduino-lighten.git
```

Open the `arduino-lighten.ino` file in the Arduino IDE and upload it to the board.

Next, appropriate **udev** rules must be configured to access the HID device.

Create a `.rules` file in `/etc/udev/rules.d` that looks like:

```txt
SUBSYSTEM=="usb", ATTR{idVendor}=="239a", ATTR{idProduct}=="8111", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"
KERNEL=="hidraw*", ATTRS{idVendor}=="239a", ATTRS{idProduct}=="8111", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"
```

where the `idVendor` and `idProduct` attributes reflect the values found using `lsusb`:

```plaintext
 > lsusb
 Bus 005 Device 002: ID 239a:8111 Adafruit QT Py ESP32-S2
```

Here the vendor ID is `239a` and the product ID is `8111`.

See [this section](https://pypi.org/project/hidapi/#udev-rules) of the [hidapi Python library](https://github.com/trezor/cython-hidapi) documentation for more information.

### lighten

[lighten](https://github.com/jcrd/lighten) runs on your Linux computer and controls your monitor's brightness with [ddcutil](https://www.ddcutil.com/).

This tool requires read/write access to `/dev/i2c` video card devices. In order to use it without root permissions:

1. Add user to `i2c` group:
    
    ```sh
    sudo usermod <user-name> -aG i2c
    ```
    
2. Copy ddcutil's udev rule into place:
    
    ```sh
    sudo cp /usr/share/ddcutil/data/45-ddcutil-i2c.rules /etc/udev/rules.d
    ```
    
3. Reload and trigger the new rule:
    
    ```sh
    sudo udevadm control --reload
    sudo udevadm trigger
    ```
    

See [this document](https://www.ddcutil.com/i2c_permissions/) for more information.

Now, let's set up **lighten**. It's currently available as an RPM package on Fedora, but it should be compatible with any Linux distro if installed from source!

1. Install with [copr](https://copr.fedorainfracloud.org/coprs/jcrd/lighten/):
    
    ```sh
    dnf copr enable jcrd/lighten
    dnf install lighten
    ```
    
2. lighten requires the product and vendor ID of the Arduino HID device. These are the same values we found previously using `lsusb`, i.e. `239a` and `8111`. Create a new file at `~/.config/lighten/lightend.conf` with this content:
    
    ```txt
    [sensor]
    vendor_id=239a
    product_id=8111
    ```
    
3. Enable the daemon's `systemd` service:
    
    ```sh
    systemctl --user enable --now lightend
    ```
    
    If the command above succeeds, everything should be operational!
    

# How it works

**lighten** makes no assumptions about what monitor brightness value corresponds to an ambient light reading. The daemon, `lightend`, runs in the background and records manual changes to monitor brightness made with the client, `lighten`, until it's able to guess which values are appropriate.

`lighten` is used to adjust monitor brightness like this:

```sh
lighten set - 10 # decrease brightness by 10
lighten set + 20 # increase brightness by 20
lighten set = 100 # set brightness to max
```

Over time, **lighten** builds up a database of the ideal monitor brightness in relation to the ambient light level based on your adjustments, and restores brightness:

* when ambient light changes significantly
    
* on demand
    
* at startup
    
* upon wakeup from sleep
    
* at regular intervals as time passes
    

**Update**: If appropriate, lighten can set the monitor brightness to the value detected by the sensor, so that a sensor value of 100 corresponds to 100 monitor brightness. This is enabled by setting the `normalize_mode` configuration option to `true`.

# What I learned

I learned how to implement and interface with an [HID](https://en.wikipedia.org/wiki/Human_interface_device) device using **TinyUSB** and [python-hid](https://pypi.org/project/hid/) after trying and failing to maintain a serial connection to the Arduino board upon the computer waking up from suspension.

I also learned how to use **GLib** via [PyGObject](https://pygobject.readthedocs.io/en/latest/) to run a main loop with custom GSources alongside a DBus server all in Python. I found only scattered documentation about this, so I wrote about building a D-Bus service in Python [here](https://twiddlingbits.net/building-a-d-bus-service-in-python)!
