Your own code
Code you write yourself, beside the code LCD Wizard writes: where it goes, how it starts, and how it reaches the screens, the variables and the buses.
An export is written fresh every time, every file in it. So your own code goes in a file of your own, beside the sketch, with a name no export uses. Unzipping a new export over the folder then replaces LCD Wizard’s files and leaves yours alone. Three things connect that file to the design: the setup and loop hooks, Call function events, and the names the generated headers give every widget and variable.
Setup and loop hooks
Switch them on in File › Project settings... › Your own code, with My own code: setup and loop hooks. They are off in a new design, and a design with them off exports exactly as it did before.
With them on, the generated code calls two functions of yours:
| Function | Called |
|---|---|
user_setup() | Once, at the end of start-up: after the panel, the touch panel, the buses and devices, the screens, the network and the variables are all up, just before the serial monitor says UI ready. |
user_loop() | On every pass of the loop, after the devices have been read and the variables worked out, just before LVGL draws. |
Neither has to exist. The export writes lw_user.cpp, which holds an empty
user_setup() and user_loop() marked weak: a design with the setting on and
no file of yours still builds and runs. When your file has the same function, the linker takes
yours.
// my_code.cpp - beside the .ino. No export contains this file.
#include <Arduino.h>
void user_setup(void)
{
Serial.println("my code is running");
}
void user_loop(void)
{
// Every pass, so never wait here: no delay(), no loop that blocks.
}
user_loop() runs on the same task as LVGL, so it can call LVGL directly. It runs
every few milliseconds, so anything slow belongs behind a time check:
if (millis() - last >= 1000) { last = millis(); ... }.
Your own file, and libraries
Make it a .cpp file in the sketch folder, beside the .ino:
my_code.cpp, or any name that is not one of the export’s. Arduino libraries are
C++, and a .cpp can include them. The Arduino IDE shows the file as a tab beside the
sketch and builds it with it; PlatformIO builds every file in the sketch folder
(src_dir = .).
A library of your own choosing is included at the top of that file, as in any sketch:
#include <Wire.h>, #include <DHT.h>. LCD Wizard does not
install it for you:
- Arduino IDE. Install it once with Tools › Manage Libraries... (the Library Manager). It stays installed, so every later export builds with it.
- PlatformIO. Add it to
lib_depsinplatformio.ini, with a version:adafruit/DHT sensor library@1.4.6.platformio.iniis written fresh by every export, so add the line again after each one. A library copied intolib/in the sketch folder is built without alib_depsline, and no export touches that folder.
The buses LCD Wizard starts
The buses on the Buses tab, and the touch panel’s, are started before
user_setup() runs. Use them; do not begin them again. A second
Wire.begin() on other pins moves the bus away from the devices the design reads.
- Each bus’s heading on the Buses tab shows the object it became. The first I2C
bus is
Wireand the secondWire1, except that an I2C touch controller ownsWire, so a bus on other pins starts atWire1. SPI buses areSPI, thenSPI1. - A device of yours on a bus the design already has goes on that object:
Wire1.beginTransmission(0x40). - The ESP32-C3, C5 and C6 have one I2C controller. On those chips there is no second bus to start, and a device of yours shares the one the design has.
Setting a widget from your code
Every widget is a global in src/ui/ui.h, named ui_ and the name in
its Inspector: a label called Temp is ui_Temp. A screen’s widgets exist
once the screen has been built, so check for NULL where a screen is built later or
freed.
#include <stdio.h>
#include <string.h>
#include "src/ui/ui.h"
static void show_temp(float c)
{
char buf[16];
snprintf(buf, sizeof buf, "%.1f C", c);
if (ui_Temp == NULL) return;
const char * now = lv_label_get_text(ui_Temp);
if (!now || strcmp(now, buf) != 0) lv_label_set_text(ui_Temp, buf);
}
- Compare before you set.
lv_label_set_text()redraws the label, and restarts a scrolling label’s scroll, every time it is called. Fromuser_loop()that is hundreds of times a second unless the text has changed. - Format with
snprintf, notlv_label_set_text_fmt. LVGL’s own printf leaves%fout unless float printing is switched on, and the generatedlv_conf.hleaves it off, as LVGL 8.3 and 9.4 do by default. A%fthere prints nothing useful. - With the code prefix changed in Project settings, the file is
src/<prefix>/<prefix>.hand the names start with it.
Setting a variable
Every variable on the Data tab has a getter and a setter in app_logic.h:
a variable called level is var_level_get() and var_level_set().
They are declared extern "C", so a .c file can call them as well as a
.cpp.
#include "app_logic.h"
void user_loop(void)
{
static uint32_t last = 0;
if (millis() - last < 500) return;
last = millis();
var_level_set(analogRead(4) / 40.95f); // 0-100
}
Every widget bound to the variable follows on the next pass, and so does every automation that watches it. Give the variable the Source (set by events only). A variable with a source of its own, a sensor or a pin, is written from it on every pass and would overwrite yours.
Call function, and Do not export to code
A Call function action calls a function by name: from an event on a widget or a screen,
or from an automation. It is declared in
ui_events.h as void my_callback(lv_event_t * e). An event hands it the
LVGL event; an automation has none and hands it NULL.
By default the export writes an empty stub for it in ui_events.c. Write the
function in your own file and tick Do not export to code on the action: the stub is left
out, the declaration and the call stay, and the export never writes an empty one beside yours.
One tick is enough where several events call the same function.
// in my_code.cpp
#include "src/ui/ui.h"
extern "C" void my_callback(lv_event_t * e)
{
if (e == NULL) return; // called by an automation
// called by an event
}
ui_events.h declares it inside extern "C", so in a .cpp
file include the header, or write extern "C" before the function as above. In a
.c file neither is needed.
What happens to ui_events.c
ui_events.c is written fresh with every export, like every other file in the zip.
Fill the stubs in there and unzip a new export over the folder, and your code is replaced with
empty stubs again. There are two ways to keep it:
- Keep your own copy of
ui_events.coutside the folder, and copy it back after each export. When the new export has a new stub, take its line from the freshui_events.h. - Put your functions in a file of your own, which no export contains, and tick Do not export to code on each. Nothing then needs copying back.
Which exports
| Export | Setup and loop hooks | Your file |
|---|---|---|
| Arduino | Yes | A .cpp beside the .ino. |
| ESP-IDF | Yes. user_setup() runs at the end of start-up in LVGL’s task, user_loop() in that task’s loop. | A .c file in main/src/, such as main/src/my_code.c, which main/CMakeLists.txt builds; run idf.py reconfigure once after adding it. In a .cpp there, write both functions inside extern "C" { }. An Arduino library does not build here: add an ESP-IDF component to main/idf_component.yml instead. |
| MicroPython | No: the export refuses a design with them on and says so. | Call function goes to ui_events.py, which is written fresh too: keep your own copy. |
| ESPHome | No: the export refuses a design with them on and says so. | A Call function is a script of that name in a file of your own. |