How to Control Chipsee PC Backlight with Python and Browser

By Printfinn, last updated at 2023-04-13

Today let's see how we can adjust the screen backlight of a Chipsee industrial Pi PC.

So, a Chipsee engineer told me adjusting backlight is just writing some text file, and I thought, oh we have a Debian Linux operating system, writing text file is not hard. So I started to think about which file I should write to. And then luckily I found Chipsee has a document website, I then looked up if my model is in the document website. It turns out they have many docs online and my Industrial Pi model is among one of those.

So I opened the docs web page, and skimmed a little bit, then found the backlight part. It says I can get or set the brightness from these two files. So I went to the folder and checked what files are there, and if there are other files there which might be of interest to me.

Initial Attempt by Reading/Writing Linux Files

Luckily, there is a pwm-backlight folder in the backlight directory, there are files named max_brightness and brightness and some other files, according to the file names, which happen to be the same as those in the document.

So I cat a file, at first I tried the max_brightness, from its name, I assume it should list the maximum available brightness of this device, it returns a 99. It makes sense, the minimum be zero, the maximum be 100, or sometimes 99, so we have a 100 level pwm backlight.

Then I cat the brightness and actual_brightness files, they all returned 90 for me. I'm a little bit confused now, which one should I read to get the actual brightness? They all seem to be the same. My first assumption is they might have different Linux file permissions, or to avoid read/write conflicts on the same file. I'm not an expert on Linux drivers, so I just put it aside and proceeded. So I went back to the official document again, and the example Chipsee gives is writing to the brightness file to adjust the brightness. Let's try it, let's copy and paste to the terminal and OK, I can see from my Chipsee Pi, the screen is a lot darker now. Can I write something else? I then write 90 again to see if I can get it brighter. Cool. So I understand if I write to this file, then I can adjust the screen brightness of this device with any programming language.

Although not documented, I'd still love to see what's the difference between the actual_brightness and brightness file. I think they might not want me to write to the actual_brightness file, so I checked the file permission with ls -la, it gives only read permission, OK cool, so my assumption might be correct. But what about the brightness file, it has both read and write permission. But whatever, I'll just read the actual_brightness file to get the current value, and only write the brightness file to set a new value. I don't know the mechanism behind the files, maybe to avoid transaction problems? In the end it's just the backlight, so not a big deal, since it gives me the option, and the values are the same, then I'd pick a safe way to read.

Start Python Flask Server: Hello World

So now the first step investigation is done, let's write the actual GUI application.

Our aim is now to show the actual brightness value on a browser, and then try to modify this value in the browser, so we can adjust the brightness of our Chipsee industrial Pi.

I will use Python, Flask, Javascript, HTML, CSS and a web browser here. They are all very basic knowledge of web development. If you don't understand some steps, you can search for the keywords I mentioned on the Internet. These technologies are not limited to Chipsee PC, they're all very popular solutions in software engineering.

I use Flask as a lightweight web server, because it uses Python, and can understand HTML and HTTP, we don't need to know everything about Flask to start, we will only use a tiny small part of it, I also try to avoid the framework specific conventions, and try only to use the plain JS and HTML in the browser, so you can reuse the code on your favorite frameworks.

$ mkdir backlight
$ cd backlight
$ python3 -m venv venv
$ . venv/bin/activate
$ pip install Flask

Let's create a default Flask app. Then let's run flask, and see if our Chipsee Pi can display a hello world. Cool.

$ touch app.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"
$ flask run --host=0.0.0.0 --debug

On our PC we can visit the http://IP:5000 (change IP to your Chipsee PC IP address, you can get this IP by entering ifconfig in your Chipsee PC terminal and look around) address and see Hello World. It means our Flask demo app is running on the Chipsee PC.

A Web Page to Show Dummy Brightness

Then let's create a brightness.html file in the templates folder. The templates folder is Flask's convention of storing html template files, this is one of a few things about Flask we need to memorize for building this app. An HTML file has some markup structures, you can refer to the Mozilla web doc to learn more, but for our use case, we don't need too much, will just copy from our demo code, they're the html, head, body, and that's it.

To add some CSS styles, let's also copy the CSS files from the static folder of our demo. Otherwise our page will look too tedious.

So let's change our code, we will return a brightness.html template, with a actual_brightness=actual_b, max_brightness=max_b, let assign a new variable named actual_b and give it a value, say 80. And also a max_b, say 100. And make sure that there is an actual_brightness, a max_brightness with double curly brackets in our HTML template file. I'll explain later what this means, let's first check what it will display in the browser.

The brightness.html should look like this:
<!doctype html>
<html>

<head>
    <title>Brightness | Chipsee Industrial PC Demo</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>

<body class="dark-bg sticky-body">
    <div class="row w-100">
        <div class="col col-9 vh-100">
            <div class="brightness-text d-flex flex-column align-items-center justify-content-center"
                style="height:95vh;">
                <div class="brightness-desc">Brightness</div>
                <div class="brightness-value">{{ actual_brightness }}</div>
            </div>
        </div>
        <div class="col position-relative">
            <input type="range" class="brightness-slider brightness-slider-position"
                id="brightness-slider" name="brightness" min="0" max="{{ max_brightness }}" step="1"
                value="{{ actual_brightness }}">
        </div>
    </div>
</body>

</html>
The app.py should look like this:
from flask import Flask, render_template
from flask import request

app = Flask(__name__)

@app.route('/brightness', methods=['GET'])
def brightness():
    if request.method == 'GET':
        actual_b = "80"
        max_b = "100"
        return render_template('brightness.html', actual_brightness=actual_b, max_brightness=max_b)

Let's visit the http://IP:5000/brightness page of the browser, and see there is a 80 displaying alongside the slider. This 80 comes from the brightness inside curly brackets, and, the value is passed by the same name variable when we return the render template, see there is a actual_brightness=actual_b, it's the 80 that is passed to the actual_brightness inside the curly brackets in the brightness.html. Now you can play around and change this value.

For the max_brightness=max_b, this informs the slider the max value it can approach. Some Chipsee industrial PCs have 255 levels of backlight, and some 100 levels. This value should be obtained from the OS files we looked at in the beginning, remember the pwm-backlight folder? There is a max_brightness file.

So, things get easier now. If we can grep the actual brightness value from Chipsee operating system files, then we can display the actual brightness in our GUI application right? It's even easier, let's just read the file with Python.

Get the Actual Brightness from File with Python

Let's create a function to read this file. I'll just copy paste the file from our demo code. And import it in our app.py.

The models/brightness.py file should look like this:

import os
import subprocess

class Brightness:
    def __init__(self):
        self.device = "/sys/class/backlight/pwm-backlight"
        if self.device is None:
            print("Brightness: cannot find brightness device in config file, brightness model not initialized.")
            return
        self.max_brightness_f = os.path.join(self.device, "max_brightness")
        self.actual_brightness_f = os.path.join(self.device, "actual_brightness")
        self.brightness_f = os.path.join(self.device, "brightness")
        self.give_linux_permission()
        self.init_max_brightness()

    def get_actual_brightness(self):
        if self.device is None:
            return 0

        with open(self.actual_brightness_f, 'r') as f:
            return int(f.read())

    def set_brightness(self, brightness):
        if self.device is None:
            return 0

        _b = int(brightness)
        if _b < 1:
            _b = 1
        if _b > self.max_brightness:
            _b = self.max_brightness
        brightness = str(_b)
        with open(self.brightness_f, 'w') as f:
            return f.write(brightness)

    def init_max_brightness(self):
        with open(self.max_brightness_f, 'r') as f:
            self.max_brightness = int(f.read())

    def give_linux_permission(self):
        """
        Some Industrial PC doesn't allow write permission of brightness file, like PX30.
        This method gives write permission of backlight brightness Linux file to the user running this program.
        """
        if self.device is None:
            return
        subprocess.run(["sudo", "chmod", "a+w", self.brightness_f])

After importing the Brightness class, the app.py file should look like this:

from flask import Flask, render_template
from flask import request

app = Flask(__name__)

from models.brightness import Brightness
dev_brightness = Brightness()

@app.route('/brightness', methods=['GET'])
def brightness():
    if request.method == 'GET':
        actual_b = dev_brightness.get_actual_brightness()
        max_b = dev_brightness.max_brightness or "100"
        return render_template('brightness.html', actual_brightness=actual_b, max_brightness=max_b)

Then when we refresh the page, we should see on the webpage that the value is the current brightness value of our Chipsee industrial PC. And the slider should be in a place where it represents the same value.

The models/brightness.py does a few things: It gives Linux permission to the backlight driver file, and reads values from them. For example to get the maximum available brightness or current actual brightness. It also has a method to set brightness to the Chipsee PC. We will try this method now, and see if we can set a new brightness to this Chipsee PC.

Setting a New Brightness from Browser

So to set a value we should use a POST http request. Let's first ask our Flask server to listen to POST request, we can write a method like this in the app.py file:

@app.route('/api/brightness', methods=['POST'])
def api_brightness():
    new_brightness = request.form["brightness"]
    dev_brightness.set_brightness(brightness=new_brightness)
    actual_brightness = dev_brightness.get_actual_brightness()
    return {"brightness": actual_brightness}

And in our templates/brightness.html, we can add a Javascript function, to trigger a POST request whenever the slider's value changes:

<script>
    const brightnessSlider = document.querySelector('#brightness-slider');
    const curr_brightness = document.querySelector('.brightness-value');
    document.addEventListener("DOMContentLoaded", () => {
        brightnessSlider.value = `${curr_brightness.textContent.trim()}`;
    });
    brightnessSlider.addEventListener('input', (event) => {
        const xhr = new XMLHttpRequest();
        xhr.open("POST", '/api/brightness', true);
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xhr.onreadystatechange = () => { // Call a function when the state changes.
            if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                // Request finished. Do processing here.
            }
        }
        xhr.send(`brightness=${brightnessSlider.value}`);
        curr_brightness.textContent = `${event.target.value}`;
    });
</script>

These two parts of code do something like this: whenever the slider value changes, for example a user touches or slides the slider, then Javascript can listen to the change and perform a XML http request to our Flask server. The JS will send a POST request to /api/brightness endpoint, sending the slider's value together with this request. In the meantime, JS will also change the value displayed on the browser to the slider's value.

And when Flask server receives this XML http request that the browser's JS sends, it will read the value this request carries with. And then call a Python method to write to a file, which is the file of Chipsee PC's brightness controller.

Then the screen backlight brightness will change to the value the slider indicates, thus setting a new brightness.

Not too complicated hah?

Now we can set our Chipsee PC's brightness through a browser GUI, woo hoo!

Final Code

In the end, your files should look like this: First, you have an app.py, which is the entrypoint of your Flask server app:

from flask import Flask, render_template
from flask import request

app = Flask(__name__)

from models.brightness import Brightness
dev_brightness = Brightness()

@app.route('/brightness', methods=['GET'])
def brightness():
    if request.method == 'GET':
        actual_b = dev_brightness.get_actual_brightness()
        max_b = dev_brightness.max_brightness or "100"
        return render_template('brightness.html', actual_brightness=actual_b, max_brightness=max_b)
    
@app.route('/api/brightness', methods=['POST'])
def api_brightness():
    new_brightness = request.form["brightness"]
    dev_brightness.set_brightness(brightness=new_brightness)
    actual_brightness = dev_brightness.get_actual_brightness()
    return {"brightness": actual_brightness}

Second, you have a templates/brightness.html, which is how your GUI looks like in the browser:

<!doctype html>
<html>

<head>
    <title>Brightness | Chipsee Industrial PC Demo</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>

<body class="dark-bg sticky-body">
    <div class="row w-100">
        <div class="col col-9 vh-100">
            <div class="brightness-text d-flex flex-column align-items-center justify-content-center"
                style="height:95vh;">
                <div class="brightness-desc">Brightness</div>
                <div class="brightness-value">{{ actual_brightness }}</div>
            </div>
        </div>
        <div class="col position-relative">
            <input type="range" class="brightness-slider brightness-slider-position"
                id="brightness-slider" name="brightness" min="0" max="{{ max_brightness }}" step="1"
                value="{{ actual_brightness }}">
        </div>
    </div>
</body>

</html>

<script>
    const brightnessSlider = document.querySelector('#brightness-slider');
    const curr_brightness = document.querySelector('.brightness-value');
    document.addEventListener("DOMContentLoaded", () => {
        brightnessSlider.value = `${curr_brightness.textContent.trim()}`;
    });
    brightnessSlider.addEventListener('input', (event) => {
        const xhr = new XMLHttpRequest();
        xhr.open("POST", '/api/brightness', true);
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xhr.onreadystatechange = () => { // Call a function when the state changes.
            if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                // Request finished. Do processing here.
            }
        }
        xhr.send(`brightness=${brightnessSlider.value}`);
        curr_brightness.textContent = `${event.target.value}`;
    });
</script>

Third, you have a models/brightness.py, which deals with the reading and writing of Chipsee pwm-backlight:

import os
import subprocess

class Brightness:
    def __init__(self):
        self.device = "/sys/class/backlight/pwm-backlight"
        if self.device is None:
            print("Brightness: cannot find brightness device in config file, brightness model not initialized.")
            return
        self.max_brightness_f = os.path.join(self.device, "max_brightness")
        self.actual_brightness_f = os.path.join(self.device, "actual_brightness")
        self.brightness_f = os.path.join(self.device, "brightness")
        self.give_linux_permission()
        self.init_max_brightness()

    def get_actual_brightness(self):
        if self.device is None:
            return 0

        with open(self.actual_brightness_f, 'r') as f:
            return int(f.read())

    def set_brightness(self, brightness):
        if self.device is None:
            return 0

        _b = int(brightness)
        if _b < 1:
            _b = 1
        if _b > self.max_brightness:
            _b = self.max_brightness
        brightness = str(_b)
        with open(self.brightness_f, 'w') as f:
            return f.write(brightness)

    def init_max_brightness(self):
        with open(self.max_brightness_f, 'r') as f:
            self.max_brightness = int(f.read())

    def give_linux_permission(self):
        """
        Some Industrial PCs don't allow write permission for brightness file, like the PX30.
        This method gives write permission of the backlight brightness Linux file to the user running this program.
        """
        if self.device is None:
            return
        subprocess.run(["sudo", "chmod", "a+w", self.brightness_f])

Also, you should have static/css/style.css and bootstrap.min.css, which sets how things look like in the browser, you should copy them from the demo app. If you find your brightness web page looks weird, you should remember to check these two CSS files, see if you copied them currently.

Conclusion

To sum up, to get the actual brightness, we ask Python to read a file, and to set a new brightness, we touch the slider, which triggers a POST request to our Flask backend, which will then write to a Linux file. This way, we're able to get or set the Chipsee PC backlight brightness by reading or writing to Linux files.

So that's it, now you can set your Chipsee PC's brightness with an HMI!