05 – Software¶
This is the single place for the knowledge needed to build and change the firmware: toolchain, code layout, conventions, how the hardware is driven, how the numbers are computed and how the screen is built.
What lives elsewhere, and is only linked from here:
| Where | What |
|---|---|
| 01 – Overview | Purpose, architecture, project-wide conventions, way of working, status |
| 02 – Component list | Components and their key data |
| Electrical drawing | Wiring, pins, levels, supply (hardware data) |
| 03 – Check list | All open items Ö1, Ö2 … (they are never kept in this file) |
Status: the firmware is built with the ESP32 toolchain, uploaded and tested on the board, and works (display, touch and HX711; confirmed by the user 2026-10-16, closes Ö3). Before that it was checked on a PC: the C++ was compiled against the LVGL 9.3.0 sources, and the HX711 logic was run against a simulated HX711 (decoding, tare, calibration, peaks, offline detection). What is still not verified on the board is tracked as open items in 03 – Check list (section 9 lists the software-related ones).
1. Toolchain and build¶
| Item | Value |
|---|---|
| IDE / build | PlatformIO (VS Code extension), configuration in platformio.ini |
| Platform | pioarduino fork of espressif32 (platform-espressif32.zip, stable). The official PlatformIO platform has no ESP32-P4 support yet |
| Framework | Arduino, as bundled with the pioarduino platform (arduino-esp32 3.x; 3.3.12 was resolved when this was written) |
| Board | esp32-p4-evboard (ES pre rev.300). Fallback if the DSI display does not start: esp32-p4_r3 |
| Memory | 32 MB flash and 32 MB PSRAM: default_32MB.csv, -DBOARD_HAS_PSRAM, QIO flash at 80 MHz |
| Build flags | -DBOARD_HAS_PSRAM -DLV_CONF_INCLUDE_SIMPLE -Iinclude |
| Libraries | Arduino_GFX (from GitHub) and lvgl/lvgl@^9.3.0. The HX711 needs no library |
| Serial | 115200 baud; upload_port and monitor_port are set to COM4 and must be changed to the actual port |
Notes:
main.cppstops at compile time ifBOARD_HAS_PSRAMis missing.- The decisions pioarduino + Arduino come from the working project Boxter83/Flip-HMI. This repository has been built and uploaded with them (Ö3, closed 2026-10-16).
- Fonts are generated, not hand-made:
tools/generate_fonts.shneeds node/npm (see section 7.5).
2. Repository layout¶
| File | Role |
|---|---|
src/main.cpp |
Starts the subsystems and runs the loop. Display, touch and LVGL start-up taken from Boxter83/Flip-HMI |
include/load_cell_config.h |
Fixed constants: HX711 pins and rate, load cell data, sign, factory scale estimate, filter settings |
include/hx711_manager.h, src/hx711_manager.cpp |
Reads the HX711 (bit-banged, non-blocking), signal plausibility checks, moving average, tare, calibration, rate and sensor range (stored in NVS), peak values |
include/ui_screen.h, src/ui_screen.cpp |
The screen, its calibration, settings, force alarm and overload popups |
include/ui_fonts.h, src/fonts/font_force_96.c |
96 px digit font for the force readout |
include/backlight.h, src/backlight.cpp |
Screen brightness (10–100 %): PWM on GPIO26 and registers 0x86 and 0x96 of the I²C bridge 0x45, stored in NVS |
include/displays_config.h |
Panel timing and the JD9365 initialisation table for the 10.1" display, backlight and I²C bridge start-up |
include/i2c.h, src/i2c.cpp |
I²C master set-up (shared by the display bridge and the touch controller) |
include/gt911.h, src/gt911.cpp, include/touch.h, src/touch.cpp |
Touch driver |
include/serial_log.h |
Serial output helper |
include/lv_conf.h |
LVGL configuration |
tools/generate_fonts.sh |
Regenerates src/fonts/font_force_96.c |
tools/render_hmi_screens.py |
Draws the HMI screens as SVG pictures for 06 – User manual (docs/images/hmi/). Mirrors the layout in ui_screen.cpp; update it when the screen changes |
The display, I²C and touch files are copied from Boxter83/Flip-HMI (identical to Boxter83/Flip_HMI_Test) and are known to bring up this display. Only the 10.1" panel was kept from the Waveshare example, which also had 7" and 8" variants. Change these files as little as possible.
3. Conventions¶
- Modular firmware. Each subsystem has its own
.h/.cpppair following a manager pattern (a namespace withbegin()orbuild()and a non-blockingpoll()orupdate()), andmain.cppis changed as little as possible. - Language. All code, comments, log text and all text on the screen is in English (see 01 – Overview).
- Constants that describe the hardware or the load cell go in
include/load_cell_config.h, not in the managers. - No blocking in
loop().hx711_manager::poll()returns at once if no conversion is ready; the screen limits its own update rate. - Branch. Ask which GitHub branch to work from before starting on new code (rule in 01 – Overview, Way of working).
- Adding a subsystem. Create
include/<name>_manager.handsrc/<name>_manager.cpp, callbegin()fromsetup()andpoll()fromloop(), then record new facts here and new items in 03 – Check list. - When wiring or I/O changes, update the files listed in section 9 of
Electrical drawing, including the pin definitions in
include/load_cell_config.h.
4. Program structure¶
Load → bridge (mV) → HX711 (24-bit raw) ─ hx711_manager ─ (tare, scale) → kg ─ ui_screen → display
▲ │
└──── TARE / CALIBRATE / RESET ◄───┘ touch (GT911)
4.1 setup() order and why¶
hx711_manager::begin()first. GPIO20 (PD_SCK) and GPIO22 (RATE) have no pull resistor at reset and float until driven.begin()sets RATE, drives PD_SCK low and gives a reset pulse (high > 60 µs, then low). The HX711 then needs its settling time, which the display start-up provides.display_cfg_prepare(): I²C bridge sequence on device 0x45 (must run beforegfx->begin()), thendisplay_cfg_backlight(true)and a 300 ms delay.gfx->begin(), clear the screen.lv_init(), log callback, tick callback (millis), draw buffer, display.- Touch:
DEV_I2C_Init()reuses the bus from step 2, thentouch_gt911_init(). ui_screen::build().
4.2 loop()¶
hx711_manager::poll() → ui_screen::update() → lv_timer_handler() →
delay(2). At 80 SPS a conversion is ready every 12.5 ms; a conversion that is
not read in time is simply replaced by the next one, which the averaging
tolerates.
4.3 Memory¶
| Item | Where | Size |
|---|---|---|
| LVGL draw buffer (partial render, 100 lines × 800 px, RGB565) | PSRAM, 64-byte aligned | 160 kB |
LVGL objects (built-in TLSF allocator, LV_MEM_SIZE) |
Internal RAM (static) | 128 kB |
| Trend history (600 floats) | Internal RAM | 2.4 kB |
The LVGL draw buffer is allocated as width × lines × sizeof(uint16_t) and the
display colour format is set explicitly to RGB565, because lv_color_t is 3
bytes in LVGL 9 and must not be used for sizing an RGB565 buffer.
5. HX711¶
5.1 Pins¶
Defined in include/load_cell_config.h (Ö8, closed 2026-10-16: works on the board;
the hardware reasoning is in Electrical drawing,
section 5.2).
| Signal | ESP32-P4 GPIO | P6 pin | Direction | Constant |
|---|---|---|---|---|
| CLK / PD_SCK | GPIO20 | 14 | Output | HX711_PIN_CLK |
| DO / DOUT | GPIO21 | 12 | Input | HX711_PIN_DOUT |
| RTE / RATE | GPIO22 | 11 | Output | HX711_PIN_RATE |
Output rate: HX711_RATE_80SPS (default false = 10 SPS) is only the value used until a rate has been stored; the rate is changed on the SETTINGS screen (section 8.4) and then kept in NVS. Gain: channel A,
gain 128 (HX711_GAIN_PULSES = 1, i.e. 25 clock pulses per conversion).
5.2 Serial interface¶
Moved here from the electrical drawing (was section 4.5).
| Item | Value |
|---|---|
| Data ready | DOUT goes low |
| Clock | 25–27 pulses on PD_SCK per conversion: 25 = ch A gain 128, 26 = ch B gain 32, 27 = ch A gain 64 |
| PD_SCK high time | 0.2–50 µs (keep pulses short; disable interrupts while clocking) |
| PD_SCK low time | ≥ 0.2 µs |
| Power down | PD_SCK high > 60 µs; returning low resets the chip (default ch A, gain 128) |
| Data format | 24-bit two's complement, MSB first; saturates at 800000h / 7FFFFFh |
| Output rate | RATE = 0: 10 SPS, RATE = 1: 80 SPS (internal oscillator) |
| Settling time | 400 ms at 10 SPS, 50 ms at 80 SPS (after power-up, reset, channel/gain change) |
Start-up behaviour (moved from section 5.2 of the electrical drawing): GPIO20 and GPIO22 float until firmware drives them. If PD_SCK floated high long enough the HX711 enters power-down; it resets when PD_SCK goes low, so this is harmless, but the first readings after start-up must be discarded.
5.3 How hx711_manager reads¶
poll()checks DOUT. Only if it is low, one conversion is clocked out: 24 data bits (DOUT sampled after each rising edge, about 1 µs high and 1 µs low) plus one gain pulse. The 25 pulses run inside a critical section so that no pulse can be stretched beyond 60 µs.decode24()sign-extends the 24-bit value.0x7FFFFFand0x800000are reported as saturated (input outside the ADC range).- The first
kWarmupSamples(4) conversions after reset are discarded: about 400 ms at 10 SPS and 50 ms at 80 SPS, matching the settling time. - A moving average over
kAverageSamples(8) conversions is used for the displayed value, about 0.8 s at 10 SPS. - Is a real HX711 answering? A stuck-low DOUT (nothing connected, a floating
or shorted line) reads as an endless stream of zeros at several hundred
conversions per second and used to be shown as SENSOR OK. The sensor now counts
as online (
fault() == Fault::None) only if all of these hold: - After the 25th clock pulse DOUT is high again (the datasheet says the
25th pulse pulls it high). Three reads in a row that leave it low =
StuckLine; the clocking is abandoned and the chip is reset. - The conversion rate, measured over one second, lies between 0.2 and 1.6
times the nominal rate (10 or 80 SPS) = otherwise
BadRate(kRateLowFactor,kRateHighFactor). - The value is not the same non-saturated number 16 times in a row
(
kStuckValueSamples) = otherwiseStuckValue. A real 24-bit conversion always has some noise in the low bits. - A conversion arrived within
kOfflineAfterMs(1500 ms) = otherwiseNoData.Settlingis reported for the first second after start or restart. The reason is shown on the SETTINGS screen (SIGNAL card,faultText()); the header shows NO SIGNAL for every fault. - A DOUT that floats and happens to toggle cannot be told from a real reading with certainty, but fails at least one of the checks above in practice.
6. Measurement and calibration¶
6.1 Formula¶
force [kg] = sign × (filtered_raw − tare_offset) / counts_per_kg
force [N] = force [kg] × N_per_kg (default 9.80665, setting "Newton per kg")
The sensor measures kilograms. The display unit (g, kg, N, kN) and the N-per-kg factor only affect what is shown (section 8.4); all internal values, the limits, the calibration weight and the settings values stay in kg.
- Sign. The screen shows compression positive and tension negative
(
LOAD_CELL_COMPRESSION_POSITIVE,true; requirement of 2026-10-14). The raw counts are assumed to rise with tension, whichLOAD_CELL_INVERT_SIGN(defaultfalse) corrects if the wire polarity turns out to be the other way round (Ö1). The two flags are independent: the wire correction is applied first, the display convention second. A swapped pair only inverts the reading and damages nothing. Record in the check list which fix was used (swap the wires or set the flag). - Sensor range and limits. The measuring range of the connected load cell is
a setting (SETTINGS, "Sensor range", 0.1–2000 kg; default
kRatedKg= 200 kg for the Gefran TU-K2C). Gauge, bar, alarm limit and the maximum permitted force may be set up tokPermittedFactor(1.3) times the range, the permitted static load of the Gefran TU. The maximum permitted force (default = the range) triggers the red OVERLOAD popup (section 8.4).
6.2 Factory estimate of the scale¶
Until a calibration is stored the scale is the ideal value 2²³ × 0.512 / range ≈
21 475 counts/kg for the default 200 kg (defaultCountsPerKg(), kDefaultCountsPerKg). It follows from the ratiometric
HX711 input: full scale at gain 128 is ±0.5 × AVDD / 128, and the bridge gives
2 mV/V × AVDD at 200 kg, so the supply voltage cancels (AVDD ≈ 4.30 V on the
click: 8.6 mV of 16.8 mV, about 51 % of the range; see section 4.4 of the
electrical drawing). Real cells and the click's excitation network differ from this by a few
percent, so the screen shows UNCALIBRATED until a calibration has been run (Ö12).
6.3 Tare¶
TARE stores the present filtered raw value as tare_offset and clears the peaks.
It is refused while the sensor is offline or the moving average is not yet full
(about a second after start-up).
6.4 Calibration¶
- Remove the load and press TARE.
- Load the cell with a known weight (either direction) and open CALIBRATE.
- Enter the weight (0.1 kg steps, 1.0 kg up to the sensor range) and press APPLY.
counts_per_kg = |filtered_raw − tare_offset| / known_kg. It is refused if the
sensor is offline, the weight is below 1 kg, or the signal is smaller than
kMinCalDeltaCounts (1000 counts, i.e. no load). Only the magnitude is stored;
the direction comes from the two sign flags above. FACTORY in the popup restores
the factory estimate and clears the tare.
6.5 Stored values (NVS)¶
Namespace loadcell, written when TARE or CALIBRATE succeeds and when the
calibration is cleared:
| Key | Type | Content |
|---|---|---|
tare |
int32 | Raw value at zero load |
scale |
float | Counts per kg (values ≤ 100 or non-finite are ignored at start-up) |
cal |
bool | A real calibration has been stored (the stored scale is used only if this is true) |
rate80 |
bool | HX711 rate: true = 80 SPS, false = 10 SPS (SETTINGS) |
rated |
float | Measuring range of the connected load cell in kg (SETTINGS) |
stab |
int | Stabilisation level 0 off, 1 medium, 2 high (SETTINGS) |
Changing the sensor range means another load cell, so it clears the tare and the calibration (back to the factory estimate for the new range). Changing the rate restarts the HX711 and the filter. The screen settings (gauge, limits) are stored separately, see section 8.4.
6.6 Stabilisation of the displayed force¶
The last digits of a 24-bit reading always move with the ADC noise (the HX711 has roughly ±100–250 counts of noise after the 8-sample average; 0.01 kg is about 215 counts on a 200 kg cell), so with 2–3 decimals the last digit flickers. Two stages calm it:
- Filter in
hx711_manager(forceStableKg()): after the moving average, a low-pass filter (time constant 1.0 s on MED, 2.5 s on HIGH) is applied as long as the value stays within a band around the filtered value (400 counts on MED, 800 on HIGH,kStabBandCounts,kStabTauMsinload_cell_config.h). A change larger than the band is followed at once, so a real load change is not delayed. The band is in counts, not kg, because the noise is about the same number of counts for any load cell range. OFF = no filter (forceStableKg()equalsforceKg()). The peak values are tracked on the stabilised value, so noise does not push them upward. - Hold in the display (
holdForDisplay()inui_screen.cpp): the shown number only changes when the stabilised value has moved by at least 0.8 (MED) or 1.0 (HIGH) of the last digit; with OFF it is plain rounding (0.5).
All numbers, the gauge, the bar, the trend and the arrows show the stabilised value.
The limits (FORCE ALARM, OVERLOAD) are judged on the fast value forceKg(), so they
react without the filter delay. Host test on a simulated noisy HX711 (±250 counts,
2 decimals, 300 samples): the displayed value changed 37 times with the filter off,
7 times on MED and once on HIGH. Residual noise on HIGH is about ±0.003 kg, so the
third decimal still moves a little at this noise level.
7. Display, touch and LVGL¶
7.1 Panel¶
| Item | Value |
|---|---|
| Panel | JD9365, 800 × 1280 (portrait), RGB565, MIPI-DSI 2 lanes, 1500 Mbit/s per lane |
| Timing | HSYNC pulse/back/front porch 20/20/40, VSYNC 4/12/30, prefer_speed 80 MHz |
| Initialisation | Command table kJd9365Lcd10Commands in include/displays_config.h (from the Waveshare example) |
| Driver | Arduino_GFX: Arduino_ESP32DSIPanel and Arduino_DSI_Display |
| LCD reset | GPIO27 (lcd_rst) |
| Output to LVGL | gfx->draw16bitRGBBitmap() from the flush callback |
7.2 Backlight and the I²C bridge (Ö11)¶
Two mechanisms are executed at start-up, exactly as in Flip-HMI, because it is not known which one is needed on this display:
display_cfg_prepare()writes to device 0x45 on the I²C bus: register 0x95 ← 0x11, 0x95 ← 0x17, 0x96 ← 0x00, 100 ms pause, 0x96 ← 0xFF, then waits 1 s. The sequence was written for the 7" panel and kept because it is part of the start-up that works in Flip-HMI.display_cfg_backlight(true)sets up PWM on GPIO26 (5 kHz, 10 bit) at full duty.
Brightness control (works on the board, confirmed 2026-10-14). The brightness
is set from the SETTINGS screen (slider and − / + buttons, 10–100 %) by
backlight::apply() in src/backlight.cpp. To make it work, the firmware must:
- Have the I²C bus up (
DEV_I2C_Init(), GPIO7 / GPIO8) and the bridge start-up sequence above executed insetup()beforegfx->begin(). - For every change, add device 0x45 on the shared bus (7-bit address, 100 kHz),
write the value
255 × percent / 100to register 0x86 and the same value to register 0x96 (two writes of two bytes: register, value), and remove the device again. Register 0x86 is the one given by the Waveshare wiki for the 10.1-DSI-TOUCH-A (0x00–0xFF); 0x96 is the register the start-up sequence uses for full brightness (0xFF). Both are written because it was not tested which one the display needs; with both written the brightness follows the slider. - Never go below 10 % (
backlight::kMinPercent), so the screen cannot be turned off by mistake. - Also set the GPIO26 PWM duty (
ledcWrite), as in the inherited start-up. It is harmless but probably has no effect, because GPIO26 is a USB pin on this board. - Keep the live preview while the slider moves; SAVE stores the value in NVS
(namespace
display, keybright), CANCEL restores the stored one.backlight::begin()applies the stored value at start-up, right afterdisplay_cfg_backlight(true).
Every change prints a line on the serial monitor, for example
Backlight 60 %: PWM set, I2C 0x45 reg 0x86 and 0x96 written; FAILED means that
the I²C write was not acknowledged.
Still open (Ö11): which of 0x86, 0x96 and the GPIO26 PWM the display really needs.
To find out, write only one of them at a time (and later remove
display_cfg_backlight(true)) and see whether the brightness still follows the
slider. Note also that GPIO24–27 are listed as USB pins on the
ESP32-P4-WIFI6-POE-ETH (electrical drawing, section 5.2), while the inherited
firmware uses GPIO26 for the backlight PWM and GPIO27 for the LCD reset; those
numbers come from the Waveshare LCD-X example code.
7.3 Touch (Ö10)¶
- Driver:
gt911.cpp, I²C bus on GPIO7 (SDA) and GPIO8 (SCL),I2C_NUM_1, 400 kHz (i2c.h). The bus is initialised once byDEV_I2C_Init()and reused. - Address: probes 0x5D first, then 0x14. The product ID is read from
register 0x8140 and printed as
TouchPad_ID:0x..,0x..,0x..; the bytes are ASCII "911" for a GT911 and "927" for a GT9271. The driver never checks the value. - The wiki and the component list say GT9271; the code is the GT911 driver from
Flip-HMI, which works there. No touch reset or interrupt GPIO is used
(
GPIO_NUM_NC). touchpad_read_cb()inmain.cppreturns the first touch point.
7.4 LVGL configuration (include/lv_conf.h)¶
| Setting | Value | Why |
|---|---|---|
LV_COLOR_DEPTH |
16 | RGB565 to match the panel |
LV_USE_STDLIB_MALLOC |
LV_STDLIB_BUILTIN, LV_MEM_SIZE 128 kB |
One screen plus a popup fits with a wide margin |
LV_USE_LOG |
1 (level WARN), printed to the serial monitor | Must stay on. Without it LVGL is silent when lv_malloc() fails: the memory assert just loops and the screen stays black |
LV_USE_PERF_MONITOR |
0 | The FPS box would cover the UI |
LV_USE_PPA |
not enabled | The ESP32-P4 PPA driver does not build in a PlatformIO project (Flip-HMI found it needs a Kconfig symbol that only exists in an ESP-IDF component build) |
LV_USE_FLOAT |
0 | LVGL's own printf has no %f; numbers are formatted with snprintf |
| Fonts | Montserrat 14, 16, 18, 20, 22, 24, 26, 28, 32, 36, 40, 48 enabled | The screen uses 14–48, plus the custom 96 px digit font |
If a network connection (Ethernet, MQTT) is ever added, read the comments in
Flip-HMI's lv_conf.h and lvgl_pool.cpp first. LVGL allocating small objects
from the normal heap (LV_STDLIB_CLIB) used up the internal, DMA-capable memory
that lwIP needs for every new socket: Ethernet had link and IP, but MQTT never
connected. Flip-HMI therefore uses a fixed-size LVGL pool from lvgl_pool_alloc() and
takes internal RAM for the pool and the draw buffer only if enough is left for the
network stack, otherwise PSRAM.
7.5 Fonts¶
font_force_96 is Montserrat Medium at 96 px, 4 bpp, limited to space, +, -,
. and 0–9, so it has no letters and no symbols. Use
lv_font_montserrat_XX for text and for LV_SYMBOL_* icons. To change the size
or the character set, edit tools/generate_fonts.sh and run it (requires
node/npm; the script fetches lv_font_conv through npx). The generated file is
committed, so building does not need node.
8. The screen¶
Portrait 800 × 1280, dark blue palette from Flip-HMI. Implemented in
src/ui_screen.cpp; geometry and colours are constants at the top of the file.
Layout rules (binding for all screens)¶
The 10.1" panel has about 5.9 px/mm; the code rounds this to 6 px/mm
(kPxPerMm).
| Rule | Value | Constant |
|---|---|---|
| Header bar | Always present at the top of the screen, fixed (does not scroll). Height about 15 mm (90 px). Full width with no margin to the screen edges and no corner radius | kHeaderH |
| Edge margin | Cards and buttons keep about 1 mm (6 px) to the left, right and bottom screen edges | kMargin |
| Popups | Every popup / modal is drawn on lv_layer_top() over a dimmed backdrop (black, LV_OPA_70) that covers the whole screen, including the header, and blocks touches to everything behind it |
– |
Colour rules (binding for all screens)¶
Every colour that carries a meaning comes from this palette and is used only for that meaning. Background, card and border colours are neutral and not part of it.
| Colour | Hex | Meaning |
|---|---|---|
| Turquoise | #00D4E8 |
Active measured values, focus and selected functions |
| Green | #20D979 |
Machine started and confirmed operation |
| Yellow/orange | #FFB547 |
Warnings and values close to a limit |
| Red | #FF4057 |
Stop, faults and critical alarms |
| Magenta | #C65BFF |
Setpoints and reference lines in graphs |
| Grey-blue | #8291A5 |
Inactive functions and secondary information |
8.1 Layout¶
| Area | y [px] | Content |
|---|---|---|
| Header | 0–90 | Full-width bar. Inside, with 6 px to the edges and one common height (78 px), radius (14 px) and border: title LOAD CELL, FORCE ALARM switch button (fills the free width), two stacked status chips of equal size (sensor: SENSOR OK / NO SIGNAL / INPUT OVER RANGE / OVERLOAD / LIMIT EXCEEDED; calibration: CALIBRATED / UNCALIBRATED), square gear at the far right (opens SETTINGS) |
| Force card | 96–681 | Vertical bar on the right (compression only, 0 … gauge maximum, red mark = max permitted force, magenta mark = alarm limit), 96 px readout in the chosen unit, second line with the value in the other unit family (N when g or kg is shown, kg when N or kN is shown), 270° gauge arc (560 px) with an adjustable range, scale labels, alarm limit text and markers, compression / tension / no-load chip |
| Sensor card | 687–887 | Load cell drawing with arrows, peak compression, peak tension |
| Trend card | 893–1150 | Force over the last 60 s (takes the height left above the buttons). Dark card like the others; turquoise curve and grid (grid at 30 % opacity), white text |
| Buttons | 1156–1274 | TARE, RESET PEAKS (CALIBRATE is on the SETTINGS page) |
| Message pill | floats at y 1090 | Short feedback after a button press (3 s), drawn on lv_layer_top() |
Margins and header follow the layout rules above; the cards are 6 px apart
(kGap). The popups (calibration, settings, force alarm, overload) follow the
popup rule; the overload popup is created last and therefore lies on top of the
others, also on top of the settings page. The raw value and the conversion rate are
not on the home screen; they are on the SETTINGS page (SIGNAL card), as are CALIBRATE and the brightness.
8.2 Meaning of the graphics¶
- Colours. Only the palette in "Colour rules" is used: turquoise for the active measured force (value, gauge, bar, curve, load cell outline and arrows, the COMPRESSION / TENSION chip), red above the maximum permitted force, grey-blue when there is no signal or no load. The direction is shown by the chip text and the arrow directions, not by colour. Green: SENSOR OK, CALIBRATED and confirmations. Yellow/orange: UNCALIBRATED, LIMIT EXCEEDED and the force alarm popup. Red: NO SIGNAL, OVERLOAD, INPUT OVER RANGE and the overload popup. Magenta: the alarm limit markers on the gauge and the bar. Grey-blue: FORCE ALARM while off, secondary text. Selected functions (the FORCE ALARM button when on, the selected setting, the selected rate, the main buttons TARE / APPLY / SAVE) have a turquoise border on the neutral button face.
- Gauge. 270° arc over the range set in SETTINGS (default −range … +range, i.e. −200 … +200 kg). The indicator is drawn from the zero point to the value, so positive values (compression) fill clockwise and negative values (tension) counter-clockwise, also when the range is not symmetric. The scale labels are the minimum, half of it, 0, half of the maximum and the maximum.
- Load cell drawing. Tension (negative) pulls the studs apart (arrows point away from the body), compression (positive) pushes them together (arrows point towards the body). The arrow length grows with the force; the outline and glow of the body follow the force colour and size. Arrows are hidden below 0.6 kg.
- Unit. The unit selected in SETTINGS (g, kg, N or kN) is used for the large readout and its unit label, the second line, the gauge and bar scale labels (compact: 0–2 decimals depending on size, grams of 10 000 and more as
100k), the unit text under the bar, the peak values, the trend read-out bubble, the alarm and overload popups and the alarm limit text. Not converted (always kg): the settings values and their step buttons, the calibration popup, the trend chart axis (labelledscale +-N kg). - Sign display. Values that round to zero show as
0,0.0or0.00(depending on the decimals setting), without a sign and never as-0.0.formatForce()is used for every force value on the screen. - Trend. 600 points, one every 100 ms (the HX711 delivers 10 SPS at the default rate). The vertical range steps through ±1, 2, 5, 10, 20, 50, 100, 200, 500, 1000 and 2000 kg so that the curve fits without constantly rescaling; it is chosen once a second, or at once if the value would leave the chart. The middle grid line is zero and positive (compression) is up. The curve is stored in 0.01 kg units.
- Touch read-out on the trend. While a finger is held on the chart, a white
vertical line follows it, a turquoise dot marks the point on the curve and a
bubble at the top of the chart shows the value (with the decimals setting) and how
long ago it was measured, for example
-54.01 kg -3.8 s;no signalwhere the sensor was offline. The values come from the ring buffertrend_hist(full float precision), not from the chart. Line, dot and bubble are drawn onlv_layer_top()and disappear when the finger is lifted. - Gauge position. The round gauge is centred on the screen width
(
kArcCx = kContentW / 2); the vertical bar sits at the right edge. - Vertical bar. Shows the same value as the gauge, but only the positive (compression) part: it starts at 0 at the bottom and its top is the gauge maximum, so it follows every change of the gauge maximum. It turns red above the maximum permitted force.
8.3 Update behaviour¶
ui_screen::update()runs at most every 50 ms (kUiPeriodMs).- Widgets are only touched when a value changes (text compared first, colours and arrow geometry cached), which keeps LVGL's redraw area small.
- Peaks are tracked in
hx711_manageron the filtered force and are cleared by RESET PEAKS, TARE and CALIBRATE.peakPositiveKg()is the largest value (compression),peakNegativeKg()the most negative (tension). - The moving average (
kAverageSamples, 8) is unchanged, so at 10 SPS the shown value lags by about 0.4 s. 80 SPS makes it react faster; it has been tested on the board and works (Ö8), and is selected on the SETTINGS screen. The default stays 10 SPS (calmer readings).
8.4 Settings, force alarm and overload¶
The gear in the header opens the SETTINGS page. Touch a value to select it, then change it with the step buttons (−100, −10, −1, −0.1, +0.1, +1, +10, +100 kg):
| Setting | Range | Meaning |
|---|---|---|
| Unit | g / kg / N / kN | Unit of the displayed force, see section 8.2 (Unit). Default kg. Stored with SAVE (uiset/unit). The sensor range, gauge, limits and calibration weight are still entered in kg |
| Newton per kg | 1.00000 … 20.00000 | Factor for N and kN and for the second line (default 9.80665 = standard gravity). Buttons −0.1, −0.01, −0.001, +0.001, +0.01, +0.1 and STD (back to 9.80665). Stored with SAVE as an integer in units of 1e-5 (uiset/npk) |
| Decimals | 0 / 1 / 2 / 3 | Number of decimals of the large force value in the gauge, of the peak compression / tension values, and of the values in the alarm and overload popups and in the trend read-out, in the chosen unit (0 decimals in g is 1 g, in kN it is 1 kN). Default 1. Selected with four buttons, stored with SAVE. The settings values themselves are not affected. With 3 decimals (0.001 kg is about 21 counts on a 200 kg cell) the last digit moves with the ADC noise unless Stabilise is on (section 6.6) |
| Stabilise | OFF / MED / HIGH | Calms the last digits of the displayed force (section 6.6). Default MED; stored in NVS by hx711_manager |
| HX711 RATE | 10 SPS / 80 SPS | Conversions per second (RATE pin). Saved with SAVE; restarts the HX711 and the filter |
| Sensor range | 0.1 … 2000 kg | Measuring range of the connected load cell. A new range resets gauge and max force to ±range / range and clears tare and calibration (press TARE with no load afterwards) |
| Gauge minimum | −1.3 × range … 0 | Tension end of the round gauge |
| Gauge maximum | 1 kg … 1.3 × range | Compression end of the round gauge; also the top of the vertical bar |
| Max permitted force | 1 kg … 1.3 × range | Largest force allowed for the connected sensor (default = range) |
| Alarm limit | 0 … 1.3 × range | Limit of the FORCE ALARM; 0 = not set |
| Screen brightness | 10 … 100 % | Slider (20 px high with a 26 px larger touch area), applied live; SAVE stores it, CANCEL restores the stored value. Also has − / + buttons (10 % steps). Written to the GPIO26 PWM and to registers 0x86 (Waveshare wiki) and 0x96 (start-up sequence from Flip-HMI) of device 0x45; Ö11: not yet known which one lights the backlight. Every change prints a Backlight … line on the serial monitor, with FAILED if the I²C write was not acknowledged |
| CALIBRATE | – | Button on the page; opens the calibration popup (section 6.4) above the settings page |
The CALIBRATE button, the SIGNAL card and the rest of the page are on one screen. The SIGNAL card shows the RAW value, the measured conversions per second,
the sensor status text (faultText()) and the counts per kg, live while the page is
open. SAVE refuses a gauge that spans less than 1.0 kg; DEFAULTS restores range
200 kg, gauge ±200, max force 200 kg, alarm limit not set, unit kg, 9.80665 N/kg and the default rate;
CANCEL discards the changes.
Overload popup (always on). When |force| exceeds the maximum permitted force a modal popup (OVERLOAD, dimmed background) flashes red (bright and dark red, 350 ms each) with the live force and the limit. It is latched and stays until ACKNOWLEDGE is pressed; it reopens only after the force has dropped below the limit (hysteresis: the larger of 0.2 kg and 5 % of the limit) and exceeds it again. The header chip reads OVERLOAD while the force is above the limit. Nothing is raised while the sensor is offline.
Force alarm (optional). The FORCE ALARM button in the header switches an additional alarm on and off (amber when on). When it is on and the alarm limit is set, reaching |force| ≥ limit opens an amber modal popup with the same latching and hysteresis rules; the header chip reads LIMIT EXCEEDED and the alarm text on the force card turns red. Amber markers on the arc and the bar show the limit. Changing a limit or switching the alarm re-arms it.
Stored values (NVS). Namespace uiset, written by SAVE and by the FORCE ALARM
button; forces in units of 0.1 kg:
| Key | Content |
|---|---|
gmin |
Gauge minimum (≤ 0) |
gmax |
Gauge maximum (≥ 1 kg) |
pmax |
Maximum permitted force |
alim |
Alarm limit (0 = not set) |
aon |
FORCE ALARM button state (if missing, on when alim > 0, for older firmware) |
dec |
Decimals of the displayed force (0 … 3; default 1) |
unit |
Display unit: 0 g, 1 kg (default), 2 N, 3 kN |
npk |
Newton per kg × 1e5 (default 980665; allowed 100000 … 2000000) |
The brightness is stored in namespace display, key bright (percent), by backlight::save().
The sensor range and the HX711 rate are stored by hx711_manager (rated,
rate80, section 6.5). Values outside the allowed ranges are ignored at start-up
and the defaults for the stored range are used.
9. Software-related open items¶
The items themselves are kept in 03 – Check list.
| # | Concerns |
|---|---|
| Ö1 | Sign of the reading (LOAD_CELL_INVERT_SIGN) |
| Ö4 | DOUT level (3.3 V logic) towards GPIO21 |
| Ö10 | Touch IC (GT9271 or GT911) and driver |
| Ö11 | Backlight path (I²C 0x45 or GPIO26 PWM) and the use of GPIO26/27 |
| Ö12 | Real counts per kg versus the factory estimate |
First start-up checklist for the serial monitor (115200 baud):
HX711: 10 SPS, tare …, … counts/kg (factory estimate)— set-up values.GT911 touch initialisedand theTouchPad_IDline — touch (Ö10).UI ready— LVGL and the screen built. Any[LVGL] …line means a problem.- Open SETTINGS (gear): RAW and SPS show that the HX711 answers (about 10 SPS) and the status reads Sensor OK. With nothing connected the header shows NO SIGNAL and the status gives the reason (section 5.3).