More and more devices and equipment are being equipped with LCD displays for displaying values or entering input parameters. One of the basic disadvantages is the lack of automatic brightness adjustment. Multiple screens in close proximity are disruptive if they do not have approximately the same brightness level. The designed solution for the Raspberry Pi 3b+ and 7" DSI display is described in the following post.
The solution was originally created for an internet radio with VolumioOS, but it is suitable for any device running Python. The algorithm is simple. An ambient light sensor measures its intensity, which controls the LCD display backlight. The following describes the solution as implemented for LCD backlight self-regulation for Volumio, or other similar embedded systemsLCD backlight self-regulation for Volumio, or other similar embedded systems.
Required Features:
Automatic brightness adjustment based on ambient light
Logarithmic brightness curve for natural perception
Smooth transitions to prevent flickering
Configurable parameters via external files
Automatic start at boot via systemd service
Real-time monitoring with detailed logging
Hardware Components:
| Component | Model/Type | Description |
|---|---|---|
| Main Unit | Raspberry Pi 3B+ | Control Unit |
| Display | 7" LCD DPI (OFI009) | Touch display connected via DPI interface |
| Encoder | KY-040 | Rotary encoder for volume control |
| Light Sensor | VEML7700 (BH-014PA) | 16-bit I2C ambient light sensor |
note: For the correct function of brightness control, the use of the KY-040 encoder is not necessary, I mention it for a better understanding of the GPIO input selection and due to the complexity of the implementation.
Wiring Diagram:
Pin 3 (GPIO 2) ──────── Pin 2 (SDA)
Pin 5 (GPIO 3) ──────── Pin 1 (SCL)
Pin 6 (GND) ──────── Pin 4 (GND)
Rotary Encoder (KY-040):
CLK: GPIO pin (BCM numbering from gpio readall)
DT: GPIO pin (BCM numbering)
SW: GPIO pin (button)
POWER+: 3.3V
GND: Ground
Note: Configure the encoder pins in the Volumio Rotary Encoder plugin using BCM pin numbers. For the correct function of brightness control, the use of the KY-040 encoder is not necessary.
Display Connection:
Power: +5V and GND from connector X1
DPI signals: Connected according to DPI configuration in /boot/config.txt
I mounted the VEML7700 light sensor on a plastic holder printed on a 3D printer on the back of the display. I removed the color coating on the front glass mask of the display in the place where the VEML7700 sensor is located behind the glass, as is evident from the picture.
Software Requirements:
Operating System: Linux system like Raspi, Armbian, VolumioOS...
Python: 3.x
Installation:
example installation for VolumioOS, for other OS the procedure is similar and hardly needs a comment for a user with basic knowledge. For a running Linux system, you can use the prepared installation (or uninstallation) script install.sh (uninstall.sh), which you can find in the download section.
Step 1: SD Card Preparation
Use Balena Etcher to flash the Volumio image, or another e.g.:
Volumio-3.832-2025-07-26-pi.zip
Step 2: APT Repository Configuration
After the first boot, edit the APT sources:
sudo nano /etc/apt/sources.list
Replace the content:
deb http://archive.raspbian.org/raspbian/ buster main contrib non-free rpi
Enable SSH: systemctl enable sshd.service
Step 3: Installation of System Dependencies
sudo apt update sudo apt upgrade sudo apt install python3-pip i2c-tools
Step 4: Installation of Python Libraries
sudo pip3 install adafruit-circuitpython-veml7700 sudo pip3 install RPi.GPIO sudo pip3 install smbus
Step 6: Creating the Python Script
Create the file /home/volumio/backlight_control.py:
nano /home/volumio/backlight_control.py
Insert the script content (see backlight_control.py in this LCD backlight repository).
#!/usr/bin/env python3
"""
LCD Backlight Control based on Ambient Light Sensor (VEML7700)
Automatically adjusts display brightness based on surrounding light conditions
"""import smbus
import time
import os
import glob
from typing import Optional# ==================== DEFAULT CONFIGURATION ====================
INT_TIME = 1 # Interval for light measurement in seconds
MIN_BACKLIGHT = 12 # Minimum backlight value (0-255)
MAX_BACKLIGHT = 255 # Maximum backlight value
SMOOTHING_FACTOR = 0.3 # Smoothing factor for brightness changes (0.0-1.0)
LUX_MULTIPLIER = 0.75 # For gain=1/8, IT=100ms# I2C Configuration
I2C_BUS = 1
VEML7700_ADDR = 0x10# VEML7700 Registers
REG_ALS_CONF = 0x00
REG_ALS_WH = 0x01
REG_ALS_WL = 0x02
REG_POW_SAV = 0x03
REG_ALS = 0x04
REG_WHITE = 0x05
REG_INTERRUPT = 0x06# Sensor configuration for max range (0-120Klx), lowest precision
CONF_VALUES = [0x00, 0x00] # Max gain, 100ms integration time
INTERRUPT_HIGH = [0x00, 0x00]
INTERRUPT_LOW = [0x00, 0x00]
POWER_SAVE_MODE = [0x00, 0x00]# Configuration directory
CONFIG_DIR = "/etc/lcd_backlight/"
class BacklightController:
def __init__(self):
try:
print("=== Initializing Backlight Controller ===")# Initialize basic attributes
self.current_brightness = MIN_BACKLIGHT
self.file_handle = None
self.config_mtime = 0 # Track config file modification time
self.enabled = True # Service enabled/disabled flag
self.config_exists = os.path.exists(CONFIG_DIR)# Find backlight sysfs path
print("Searching for backlight device...")
backlight_paths = glob.glob("/sys/class/backlight/*/brightness")
if not backlight_paths:
raise FileNotFoundError("No backlight device found in /sys/class/backlight/")
self.backlight_path = backlight_paths[0]
print(f"Found backlight: {self.backlight_path}")# Initialize I2C bus
print("Initializing I2C bus...")
self.bus = smbus.SMBus(I2C_BUS)# Load configuration
print("Loading configuration...")
self._load_configuration()print(f"Backlight control initialized - {time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Config source: {'FILES' if self.config_exists else 'DEFAULT VALUES'}")
print(f"Config: ENABLED={self.enabled}, MIN={self.min_backlight}, MAX={self.max_backlight}, INT_TIME={self.int_time}")# Initialize sensor
self._init_sensor()# Set initial brightness only if enabled
if self.enabled:
print("Setting initial brightness...")
self._update_brightness(force=True)
else:
print("Service is disabled, skipping initial brightness setup")print("=== Backlight Controller Initialized Successfully ===")except Exception as e:
print(f"Error during initialization: {e}")
raisedef _get_config_mtime(self) -> float:
"""Get the latest modification time of all config files"""
try:
if not os.path.exists(CONFIG_DIR):
return 0files = [
'lcd_enabled',
'lcd_min_backlight',
'lcd_max_backlight',
'lcd_int_time',
'lcd_lux_multiplier',
'lcd_smoothing_factor'
]mtimes = []
for filename in files:
filepath = os.path.join(CONFIG_DIR, filename)
if os.path.exists(filepath):
mtimes.append(os.path.getmtime(filepath))return max(mtimes) if mtimes else 0except Exception as e:
print(f"Error getting config mtime: {e}")
return 0def _check_config_changed(self) -> bool:
"""Check if configuration files have been modified or appeared/disappeared"""
try:
# Check if config directory existence changed
config_exists_now = os.path.exists(CONFIG_DIR)if config_exists_now != self.config_exists:
# Config directory appeared or disappeared
self.config_exists = config_exists_now
if config_exists_now:
print(f"\n[{time.strftime('%H:%M:%S')}] Configuration directory appeared, loading from files...")
else:
print(f"\n[{time.strftime('%H:%M:%S')}] Configuration directory removed, using default values...")
return True# If config exists, check for file modifications
if config_exists_now:
current_mtime = self._get_config_mtime()
if current_mtime > self.config_mtime:
print(f"\n[{time.strftime('%H:%M:%S')}] Configuration files changed, reloading...")
self.config_mtime = current_mtime
return Truereturn Falseexcept Exception as e:
print(f"Error checking config changes: {e}")
return Falsedef _read_config_value(self, filename: str, default_value, value_type=str):
"""Read a single config value from file or return default"""
try:
if not os.path.exists(CONFIG_DIR):
return default_valuefilepath = os.path.join(CONFIG_DIR, filename)
if not os.path.exists(filepath):
return default_valuewith open(filepath, "r") as f:
value = f.read().strip()if value_type == bool:
return bool(int(value))
elif value_type == int:
return int(value)
elif value_type == float:
return float(value)
else:
return valueexcept Exception as e:
print(f"Error reading {filename}, using default: {e}")
return default_valuedef _load_configuration(self):
"""Load configuration from files if they exist, otherwise use defaults"""if os.path.exists(CONFIG_DIR):
print(f"Loading configuration from: {CONFIG_DIR}")
self.config_mtime = self._get_config_mtime()# Read all config values from files
self.enabled = self._read_config_value('lcd_enabled', True, bool)
self.min_backlight = self._read_config_value('lcd_min_backlight', MIN_BACKLIGHT, int)
self.max_backlight = self._read_config_value('lcd_max_backlight', MAX_BACKLIGHT, int)
self.int_time = self._read_config_value('lcd_int_time', INT_TIME, int)
self.lux_multiplier = self._read_config_value('lcd_lux_multiplier', LUX_MULTIPLIER, float)
self.smoothing_factor = self._read_config_value('lcd_smoothing_factor', SMOOTHING_FACTOR, float)print(f"Loaded from files: enabled={self.enabled}, min={self.min_backlight}, max={self.max_backlight}")
print(f" int_time={self.int_time}, lux_mult={self.lux_multiplier}, smooth={self.smoothing_factor}")
else:
# Use default values from constants
print(f"Config directory not found, using default values")
self.config_mtime = 0
self.enabled = True
self.min_backlight = MIN_BACKLIGHT
self.max_backlight = MAX_BACKLIGHT
self.int_time = INT_TIME
self.lux_multiplier = LUX_MULTIPLIER
self.smoothing_factor = SMOOTHING_FACTORprint(f"Defaults: enabled={self.enabled}, min={self.min_backlight}, max={self.max_backlight}")
print(f" int_time={self.int_time}, lux_mult={self.lux_multiplier}, smooth={self.smoothing_factor}")def _init_sensor(self):
"""Initialize VEML7700 sensor with configuration"""
try:
self.bus.write_i2c_block_data(VEML7700_ADDR, REG_ALS_CONF, CONF_VALUES)
self.bus.write_i2c_block_data(VEML7700_ADDR, REG_ALS_WH, INTERRUPT_HIGH)
self.bus.write_i2c_block_data(VEML7700_ADDR, REG_ALS_WL, INTERRUPT_LOW)
self.bus.write_i2c_block_data(VEML7700_ADDR, REG_POW_SAV, POWER_SAVE_MODE)
time.sleep(0.1) # Wait for sensor to stabilize
print("VEML7700 sensor initialized successfully")
except Exception as e:
print(f"Error initializing sensor: {e}")
raisedef _read_lux(self) -> Optional[float]:
"""Read ambient light value from sensor in lux"""
try:
raw_value = self.bus.read_word_data(VEML7700_ADDR, REG_ALS)
lux = raw_value * self.lux_multiplier
return lux
except Exception as e:
print(f"Error reading sensor data: {e}")
return Nonedef _lux_to_brightness(self, lux: float) -> int:
"""
Convert lux value to brightness level (min_backlight to max_backlight)
Uses logarithmic curve for more natural perception
"""
if lux <= 0:
return self.min_backlight# Logarithmic mapping: 0-10000 lux -> min_backlight-max_backlight
import math
max_lux = 10000 # Maximum expected lux# Logarithmic scale feels more natural to human perception
brightness = self.min_backlight + (self.max_backlight - self.min_backlight) * (
math.log10(lux + 1) / math.log10(max_lux + 1)
)return int(max(self.min_backlight, min(self.max_backlight, brightness)))def _write_brightness(self, value: int) -> bool:
"""Write brightness value to sysfs with optimized file handling"""
try:
if self.file_handle is None:
self.file_handle = os.open(self.backlight_path, os.O_WRONLY)os.lseek(self.file_handle, 0, os.SEEK_SET)
os.write(self.file_handle, str(value).encode())
return Trueexcept OSError as e:
print(f"Error writing brightness to {self.backlight_path}: {e}")
self._close_file_handle()
return Falsedef _close_file_handle(self):
"""Safely close file handle"""
if self.file_handle is not None:
try:
os.close(self.file_handle)
except:
pass
self.file_handle = Nonedef _update_brightness(self, force: bool = False):
"""Read sensor and update backlight brightness"""
# Skip if disabled
if not self.enabled:
returnlux = self._read_lux()if lux is None:
return # Skip update on sensor errortarget_brightness = self._lux_to_brightness(lux)# Smooth brightness changes to avoid flickering
if not force:
self.current_brightness = int(
self.current_brightness * (1 - self.smoothing_factor) +
target_brightness * self.smoothing_factor
)
else:
self.current_brightness = target_brightnesssuccess = self._write_brightness(self.current_brightness)# Uncomment for debug
# if success:
# print(f"[{time.strftime('%H:%M:%S')}] Lux: {lux:6.1f} | Brightness: {self.current_brightness:3d}/{self.max_backlight}")def run(self):
"""Main control loop"""
try:
print("\n=== Starting main control loop ===")
print("Monitoring for configuration changes...")while True:
# Check for configuration changes
if self._check_config_changed():
self._load_configuration()
print(f"Active config: ENABLED={self.enabled}, MIN={self.min_backlight}, MAX={self.max_backlight}, INT_TIME={self.int_time}")# Update brightness if enabled
if self.enabled:
self._update_brightness()
time.sleep(self.int_time)
else:
# Check for config changes more frequently when disabled
time.sleep(1)except KeyboardInterrupt:
print(f"\n\nBacklight control stopped - {time.strftime('%Y-%m-%d %H:%M:%S')}")
finally:
self.cleanup()def cleanup(self):
"""Cleanup resources"""
print("Cleaning up resources...")
self._close_file_handle()
try:
self.bus.close()
except:
pass
if __name__ == "__main__":
try:
controller = BacklightController()
controller.run()
except Exception as e:
print(f"Fatal error: {e}")
import traceback
traceback.print_exc()
Set permissions:
chmod +x /home/volumio/backlight_control.py
Step 7: Creating the Systemd Service
sudo nano /etc/systemd/system/backlight.service
Content:
[Unit] Description=Backlight Control Service After=multi-user.target [Service] Type=simple ExecStart=/usr/bin/python3 /home/volumio/backlight_control.py WorkingDirectory=/home/volumio User=volumio Group=volumio Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
Step 8: Enabling and Starting the Service
sudo systemctl daemon-reload sudo systemctl enable backlight.service sudo systemctl start backlight.service
Step 9: Verifying Service Status
sudo systemctl status backlight.service
Monitoring logs in real time:
sudo journalctl -u backlight.service -f
Configuration:
Location of Configuration Files
Create a configuration directory:
sudo mkdir -p /etc/lcd_backlight
Available Configuration Parameters:
Minimum Brightness (0-255)
echo "12" | sudo tee /etc/lcd_backlight/lcd_min_backlight
Maximum Brightness (0-255)
echo "255" | sudo tee /etc/lcd_backlight/lcd_max_backlight
Measurement Interval (seconds)
echo "1" | sudo tee /etc/lcd_backlight/lcd_int_time
Lux Multiplier (calibration)
echo "0.75" | sudo tee /etc/lcd_backlight/lcd_lux_multiplier
Smoothing Factor (0.0-1.0)
Lower values = smoother transitions
echo "0.3" | sudo tee /etc/lcd_backlight/lcd_smoothing_factor
Applying Configuration Changes
sudo systemctl restart backlight.service
Default Values:
| Parameter | Default Value | Description |
|---|---|---|
| MIN_BACKLIGHT | 12 | Minimum brightness (dark) |
| MAX_BACKLIGHT | 255 | Maximum brightness (light) |
| INT_TIME | 1 | Measurement interval (s) |
| LUX_MULTIPLIER | 0.75 | Lux calibration coefficient |
| SMOOTHING_FACTOR | 0.3 | Transition smoothing |
Usage:
Automatic Mode
The service starts automatically at system boot and runs continuously.
Manual Service Control
# Service status sudo systemctl status backlight.service # Stop service sudo systemctl stop backlight.service # Start service sudo systemctl start backlight.service # Restart service sudo systemctl restart backlight.service # Disable automatic start sudo systemctl disable backlight.service # Enable automatic start sudo systemctl enable backlight.service
Viewing Logs
# Last 50 entries sudo journalctl -u backlight.service -n 50 # Real-time monitoring sudo journalctl -u backlight.service -f # Today's logs only sudo journalctl -u backlight.service --since today
Example Log Output
[12:34:56] Lux: 245.3 | Brightness: 145/255 [12:34:57] Lux: 248.1 | Brightness: 147/255 [12:34:58] Lux: 251.7 | Brightness: 149/255
Troubleshooting:
Sensor Not Detected
i2cdetect -y 1
Enable I2C interface:
sudo nano /boot/config.txt # Add or uncomment: dtparam=i2c_arm=on
Restart after change.
Service Fails to Start
sudo journalctl -u backlight.service -n 50 --no-pager
Verify Python dependencies:
python3 -c "import adafruit_veml7700; print('VEML7700: OK')"
python3 -c "import RPi.GPIO; print('GPIO: OK')"
python3 -c "import smbus; print('SMBus: OK')"
Check script syntax:
python3 -m py_compile /home/volumio/backlight_control.py
Display Brightness Does Not Change
ls -la /sys/class/backlight/*/brightness
Test manual brightness control:
echo 128 | sudo tee /sys/class/backlight/*/brightness echo 255 | sudo tee /sys/class/backlight/*/brightness
Check file permissions:
ls -la /home/volumio/backlight_control.py # Should be: -rwxr-xr-x (executable)
Brightness Change Too Sensitive
echo "0.5" | sudo tee /etc/lcd_backlight/lcd_smoothing_factor sudo systemctl restart backlight.service
Display Too Dark/Bright
# Adjust minimum brightness echo "5" | sudo tee /etc/lcd_backlight/lcd_min_backlight # Adjust maximum brightness echo "200" | sudo tee /etc/lcd_backlight/lcd_max_backlight # Apply changes sudo systemctl restart backlight.service
Incorrect Sensor Values
# For higher sensitivity echo "1.0" | sudo tee /etc/lcd_backlight/lcd_lux_multiplier # For lower sensitivity echo "0.5" | sudo tee /etc/lcd_backlight/lcd_lux_multiplier sudo systemctl restart backlight.service
Monitoring:
Real-time Monitoring
# Monitor brightness changes sudo journalctl -u backlight.service -f
Performance Statistics
# Uptime and service status sudo systemctl status backlight.service # Recent logs with timestamps sudo journalctl -u backlight.service -n 100 --no-pager
Advanced Configuration:
Custom Brightness Curve
Edit /home/volumio/backlight_control.py and modify the _lux_to_brightness() method to implement custom brightness curves.
Multiple Sensors
The script can be extended to support multiple VEML7700 sensors for different zones.
File Structure:
/home/volumio/ └── backlight_control.py # Main Python script /etc/systemd/system/ └── backlight.service # Systemd service /etc/lcd_backlight/ ├── lcd_min_backlight # Minimum brightness value ├── lcd_max_backlight # Maximum brightness value ├── lcd_int_time # Measurement interval ├── lcd_lux_multiplier # Lux calibration └── lcd_smoothing_factor # Smoothing factor
/usr/local/bin/ └── backlight_control.py # Hlavný Python skript


