2.1 ESPHOME Lesson03 Adjust Screen Brightness
Lesson03---Rotating the Knob to Adjust Screen Brightness¶
1. Course Introduction¶
In the last lesson, we learned how to light up the rotary screen, making it turn on and display text. This lesson builds on the previous one to teach how to rotate the knob to adjust the screen brightness.
2. Learning Objectives¶
How to drive the rotation function on the rotary screen
Use the rotation function to control screen brightness
3. Project Demonstration¶
You can adjust the screen brightness by rotating the knob, and the current brightness value will be displayed on the screen in real time.
(Clockwise increases screen brightness, counterclockwise decreases screen brightness)
4. Case Code Download Link and Key Code Explanation¶
Click the GitHub link below to download the full code:
Next, we will explain the key parts of this code and show how to write code on the ESPHome platform to rotate the knob and control the screen brightness.
Some of the related code configurations have already been explained in detail in the last lesson, so we will not repeat them here. We will only explain the new code content in this lesson.
(1) Initialization Process (esphome block)
When the system starts, the LCD is first powered on and goes through a strict reset sequence to ensure the screen hardware is in a stable state. Then there is a delay to wait for the rotary encoder to stabilize after power-on, preventing false triggers. On this basis, the actual backlight output brightness is set to 50%, and with the knob input frozen (encoder_ready = false), the software internal brightness variable brightness_value is set to 50. At the same time, publish_state(50) is used to force-align the internal count of the rotary_encoder so that the "knob logical position, software brightness value, and actual PWM brightness" are fully consistent.
Then the backlight brightness is confirmed again, and the LVGL interface is updated synchronously to display "50%." After a short delay to ensure the state takes effect, the knob is finally unfrozen, allowing user rotation, ensuring that from the first rotation the system adjusts brightness continuously, linearly, and without jumps.
on_boot:
priority: 800
then:
- output.turn_on: lcd_power
- output.turn_on: display_reset
- delay: 100ms
- output.turn_off: display_reset
- delay: 100ms
- delay: 500ms
- light.turn_on:
id: display_backlight
brightness: 0.5
- lambda: |-
id(encoder_ready) = false;
id(brightness_value) = 50;
id(knob).publish_state(0);
- light.turn_on:
id: display_backlight
brightness: 0.5
- lambda: |-
char buf[10];
snprintf(buf, sizeof(buf), "%d%%", id(brightness_value));
lv_label_set_text(id(lbl_brightness), buf);
- delay: 200ms
- lambda: |-
id(encoder_ready) = true;
Key points of this code:
on_boot is ESPHome’s startup hook, and priority indicates the execution order of this startup task. The higher the number, the earlier it executes.
Set the LCD backlight PWM output to 50%, so the initial display brightness is medium.
encoder_ready = false: temporarily disables knob events from changing brightness.
brightness_value = 50: sets the software internal brightness variable to 50%, consistent with the PWM output.
publish_state(0): sets the internal count state of the rotary_encoder to 0, aligning the knob logic with the software variable.
The reason for this design: to prevent sudden jumps during knob initialization, which could cause brightness to jump to 0 or 100. Ensuring that "knob position, internal brightness value, and actual PWM brightness" are fully aligned is a common safety initialization strategy in many rotary screen brightness systems.
The purpose of this code is to initialize the brightness variable and the percentage text on the display to 50% at startup, display it through the LVGL label "lbl_brightness," wait 200ms to stabilize the display and internal state, and finally set "encoder_ready" to "true," unlocking the knob so it can respond to user operation. This design ensures that the screen display and internal brightness value are synchronized at startup and prevents false touches from causing brightness jumps during initialization, achieving smooth and safe alignment between interface display and operation logic.
(2) Global Variables
A reminder: many parts of the code have been explained in detail in the last lesson; this lesson only explains the code related to this lesson’s functionality.
This "globals" code defines two global variables: "brightness_value" stores the current screen brightness as an integer (0–100), with an initial value of 50, meaning the default brightness at startup is 50%; "encoder_ready" is a boolean flag controlling whether the knob can respond to user operation. Its initial value is "false," indicating that the knob is temporarily disabled during startup or initialization to prevent false touches. This design allows the system to complete brightness and display initialization first, then safely allow the user to adjust the knob.
(3) Configuring the Rotary Encoder
First, configure the encoder hardware: bind phase A (CLK) and phase B (DT) to GPIO42 and GPIO4 respectively, enable input mode with internal pull-up resistors for signal stability, set resolution to 1, add 20ms debounce filtering and integer rounding filtering to improve encoder signal stability. When the encoder value changes and triggers the on_value callback, use the encoder_ready variable to ensure the system has completed initialization. Use static variables to record the first execution state and the previous encoder value, calculate the difference (delta) between current and previous values to determine rotation direction, decrease brightness by 5% when rotating left, increase by 5% when rotating right, and clamp brightness within 0–100%. Finally, write the corresponding brightness level directly to the PWM output pin (controlling the backlight in real time), format the brightness percentage string, update the LVGL interface label, and force a screen refresh to ensure real-time display.
sensor:
- platform: rotary_encoder
id: knob
name: "Encoder"
pin_a:
number: 42
mode:
input: true
pullup: true
pin_b:
number: 4
mode:
input: true
pullup: true
filters:
- debounce: 20ms
- lambda: return round(x);
resolution: 1
internal: true
on_value:
then:
- lambda: |-
if (!id(encoder_ready)) {
return;
}
static bool first_run = true;
static int last_encoder = 0;
int current = (int) id(knob).state;
if (first_run) {
last_encoder = current;
first_run = false;
return;
}
int delta = current - last_encoder;
last_encoder = current;
if (delta == 0) {
return;
}
if (delta < 0) {
id(brightness_value) += 5;
} else {
id(brightness_value) -= 5;
}
if (id(brightness_value) < 0) id(brightness_value) = 0;
if (id(brightness_value) > 100) id(brightness_value) = 100;
float level = id(brightness_value) / 100.0f;
id(bl_pwm).set_level(level);
char buf[10];
snprintf(buf, sizeof(buf), "%d%%", id(brightness_value));
lv_label_set_text(id(lbl_brightness), buf);
lv_refr_now(NULL);
Key points in this code:
This code defines a rotary encoder sensor to read the physical knob’s rotation status, assigns it a unique ID "knob," names it "Encoder," and marks it as an internal sensor (local use only, not exposed to external platforms).
Then configure the encoder’s core hardware pins: bind phase A (CLK) to GPIO42 and phase B (DT) to GPIO4, set both as input mode to read encoder pulse signals, and enable internal pull-up resistors to prevent voltage fluctuation and false triggering due to floating pins.
Next, add two levels of signal filtering: a 20ms debounce filter to eliminate mechanical jitter noise during rotation, and an integer rounding filter to convert the encoder output from float to integer, simplifying subsequent calculations.
Finally, set the encoder resolution to 1, meaning each mechanical notch changes the value by only 1 step, ensuring accurate signal reading and simple calculation, laying the foundation for stable and precise encoder rotation signal output.
This code is the core logic of the on_value callback triggered by encoder value changes in the ESPHome framework, fully implementing the process from encoder signal parsing to backlight brightness adjustment and LVGL interface update, including multiple fault tolerance and initialization protection mechanisms. The logic can be broken down as:
First, use the global boolean variable encoder_ready for initialization protection; if the system has not completed startup initialization (variable is false), skip all encoder processing logic to avoid false operations during startup.
Next, define two static variables (first_run for first execution flag, last_encoder for previous encoder value). These variables retain their values across multiple callback executions, reading the current encoder value and converting it to an integer.
If it is the first execution of the callback function, only synchronize last_encoder with the current encoder value, disable the first_run flag, and exit without adjusting brightness, ensuring the encoder state is aligned during initialization; then calculate the difference delta between the current value and the previous value (core for determining rotation direction and steps), and update last_encoder with the current value for the next calculation. If delta is 0 (no valid rotation), exit immediately to reduce unnecessary operations;
Determine the rotation direction based on the sign of delta: when rotating left (delta < 0), decrease the global brightness variable brightness_value by 5%; when rotating right (delta > 0), increase it by 5%. At the same time, clamp the brightness value strictly within the 0–100% range using conditional checks to prevent exceeding the hardware-supported brightness. Then convert the brightness percentage to a 0.0–1.0 PWM level and write it directly to the backlight PWM output pin (bl_pwm), achieving instantaneous hardware-level brightness control without queues or delay;
Finally, create a temporary character buffer, format the brightness value as a percentage string (e.g., “50%”), update the LVGL interface brightness label text, and call lv_refr_now(NULL) to force a screen refresh, ensuring the UI display is synchronized with the actual backlight brightness in real time.
Summary: You rotate the encoder → The encoder outputs A/B phase pulse signals → The ESP32 reads the signals and converts them into digital values → The code calculates the rotation direction / step count and adjusts the brightness value → The brightness value is converted into a PWM signal to control the backlight current → The screen brightness changes in real time, and at the same time, the LVGL interface synchronously displays the percentage of brightness.
The key point of this process is: The encoder only "tells the MCU the direction and step count of rotation," while the actual control of brightness is done by the PWM signal output by the ESP32 – the encoder is an "input device," the PWM output is a "control method," and they are connected through software logic, achieving the intuitive interaction of "rotating knob → brightness change."
Here, the "lambda" usage is an inline C++ code block provided by ESPHome, allowing you to write C++ logic directly in YAML, access ESPHome-defined global variables (e.g., "id(brightness_value)") and sensor objects (e.g., "id(knob)"), and flexibly handle sensor events, calculations, conditional checks, and interface updates without writing a separate external C++ file.
(4) LVGL Configuration
This code configures the LVGL graphics library in ESPHome: it binds the "my_display" display to LVGL, allocates 50% of the buffer for drawing the interface, then creates two label controls: the first "lbl_title" is centered at the top of the screen, displaying the title “Knob Screen Brightness” with the "montserrat_28" font;
the second "lbl_brightness" is centered below, using a larger "montserrat_48" font to display the current brightness percentage, initially set to “50%,” allowing the screen to simultaneously show the interface title and the brightness value controlled by the knob, creating a user-friendly real-time feedback interface.
5. Code Logic Flowchart¶
6. Flashing Steps¶
Next, we will teach you how to write code using ESPHome, including the overall operation process. Please follow our steps one by one.
To reiterate:
The following devices need to be on the same LAN:
① Your computer
② CrowPanel HMI ESP32 Rotary Display
③ Raspberry Pi with the Home Assistant system
In our project, the Raspberry Pi with the Home Assistant system acts as the server, so accessing its IP address means entering Home Assistant.
Only after entering this page can you perform the following operations.
The following operations assume you have completed the installation steps in Lesson 1 and arrived at the ESPHome main page.
Once the installation is complete, we can start adding devices. Click on + New Device -> Continue.
Click “New Device Setup”
Enter a name and click Next.
(You can use any custom name. Do not include any strange symbols such as @, #, etc.)
Here, do not check "Use recommended settings."
Select the main chip of the CrowPanel 2.1inch-HMI ESP32 Rotary Display, ESP32-S3.
Next, choose any option (since we will replace it in the code later).
Here, click "SKIP."
Then, return to the main interface, find the 2.1_Adjust_Screen_Brightness you just created, click "EDIT," and enter the code editor.
This code is automatically generated based on the previous steps. Next, we will make replacements in it to optimize the code for more efficient operation.
Automatically generated code:
This is the complete code. You can click on the code link below to obtain it.
Next, you can replace the relevant content in ESPHome and ESP32 as needed, and add the item "PSRAM."
Then, based on your existing code, follow the format shown in the figure to add or replace these codes.
(Other configurations remain unchanged)
Remember to replace your own Wi-Fi name and password.
Note: This Wi-Fi connection must be in the same local network as your computer and Raspberry Pi!
Here, our Wi-Fi account and password are set on the ESPHome main page.
So in the code, we write it as "elecrow888" for the Wi-Fi.
Of course, you can also write the Wi-Fi account and password in detail in the code.
Note: These items are specific to the device you created; do not use the same as mine. Keep your original settings.
You can copy my other functional code.
Once the code replacement is complete, click "INSTALL" in the top right corner.
Select "Manual download."
Wait for a few minutes until the installation is complete.
Then, select "Factory format."
Once the download is complete, you will see the .bin file.
Remember the path of this .bin file.
Open the following website: https://web.esphome.io/?dashboard_wizard
After opening this website, you will arrive at this interface:
Next, we will flash this .bin file into the CrowPanel 2.1inch-HMI ESP32 Rotary Display.
Connect the CrowPanel 2.1inch-HMI ESP32 Rotary Display to your computer.
Click "Connect"
Select the COM port and connect it.
After connecting the CrowPanel 2.1inch-HMI ESP32 Rotary Display, click "Install."
Add the .bin file you just downloaded, then click "Install."
Then select the .bin file that you just downloaded.
Click "INSTALL"
Wait for a few minutes.
After the installation is complete, click "Close."
After successfully flashing the .bin file, return to the ESPHome page in Home Assistant.
Press the RESET button on the CrowPanel 2.1inch-HMI ESP32 Rotary Display.
Restart the ESP32 display, and you should see the device you created earlier show as ONLINE in the top right corner.
If you have also added the functional code we provided, you will see the current screen brightness displayed, and when you rotate the knob, the screen brightness changes in real time.
Once it shows ONLINE, in the future, any code modifications can be uploaded via the Wireless method, making code uploads much more convenient.













































