Skip to content

Build the TFT pictures and theme TFTs without the Editor, and check them in CI - #407

Open
jamesmulcahy wants to merge 8 commits into
NSPManager:develfrom
jamesmulcahy:feat/headless-tft-pictures
Open

jamesmulcahy wants to merge 8 commits into
NSPManager:develfrom
jamesmulcahy:feat/headless-tft-pictures

Conversation

@jamesmulcahy

@jamesmulcahy jamesmulcahy commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Building the panel GUIs today means driving the Nextion Editor on Windows with AutoHotkey, once per theme: delete every picture, import the theme's pictures, compile. Picture rendering needs a desktop GIMP, and image progress bars need their pictures drawn by hand for every theme.

This makes everything except one Editor compile per region scriptable and headless, and checks it in CI:

  • make_pictures.sh renders every theme's pictures with GIMP in Docker, with no display.
  • make_tfts.sh builds all four themes' TFTs from the one TFT the Editor compiled, by swapping pictures and recomputing the checksums. It also builds us_horizontal_mirrored from the EU TFTs.
  • progress_bars.txt lists image progress bars with their colours, and their pictures are generated from the HMI. Nothing to draw, and nothing to redraw when a bar moves.
  • hmi_tool.py reads an HMI's pages, components, event code and fonts, and edits numeric attributes (position, size, picture IDs) without the Editor.
  • check_build.py and a CI workflow fail if the committed HMIs, pictures and TFTs don't agree.

docker/HMI_files/tft_automation/README.md documents the workflow: what to do after changing an HMI, each script, and the checks.

Byte-for-byte with what's committed

  • Pictures: make_pictures.sh reproduces every committed picture byte for byte. The container is Ubuntu 24.04 for GIMP 2.10, which the Script-Fu needs, and ImageMagick 6.9.12. Debian bookworm's 6.9.11 writes different colour-space endpoints into the BMP headers (metadata only, the pixels are the same).
  • TFTs: six of the eight committed TFTs come out byte-identical to the Editor's compiles. In EU theme 2 and US theme 3, the Editor had stored one picture compressed, in a format not yet known. This rebuilds those two, and the mirrored theme 2 made from EU's, with that picture stored raw, so every committed TFT is what make_tfts.sh builds. A rebuilt TFT has run on a panel.
  • us_horizontal_mirrored: upstream's mirrored TFTs differ from the EU TFTs only in the display orientation byte and the checksums, so they're now made from the EU TFTs. Its AutoHotkey script and orientation reminder are removed. A test checks this against the committed files.

Commits

  1. Render the TFT pictures in Docker without a display.
  2. Read and edit Nextion HMI files without the Editor. This adds the nextion package the scripts share.
  3. Build the theme TFTs without the Editor. The TFT checksum algorithm comes from UNUF/TFTTool.
  4. Generate the progress bar pictures from the HMI. The committed HMIs have no image progress bars yet, so this generates nothing until a bar is added. The media player page will add the first.
  5. Check in CI that the committed HMIs, pictures and TFTs agree.
  6. Document the TFT build.
  7. Use nextion-tools for the Nextion file formats (see below).
  8. Use nextion-tools 0.1.1. It knows the Editor's scrolling text component, which the media player page (Add the media player page to the EU and US HMIs and TFTs #408) uses.

The Nextion file formats live in nextion-tools

Commit 7 moves the generic code (reading and editing HMIs, fonts, pictures, TFT checksums) out of this repo into nextion-tools, a small library on PyPI. requirements.txt pins nextion-tools[images]==0.1.1, and the Docker image installs that same version. What is particular to this build (regions, themes, progress bars) stays here, in tft_build/, with the tests that check this project's own files.

Why a separate library:

  • It has its own tests, format notes and CI, and it's pure Python under the MIT licence.
  • It also holds a Nextion display emulator, which runs the HMI's own pages, scripts and touch handling. The firmware's QEMU tests use it, and it can show a panel in a browser.
  • It reads more of the HMI than the old package did: each component's event code and ids, and the fonts. hmi_tool.py gains events, fonts and dump to show them.

The cost is a dependency on a package published from my GitHub account. It's pinned to an exact version, so nothing changes here until someone bumps it. If you'd rather keep the code in this repo, commit 7 can be dropped, and the first six commits stand on their own.

Limits

  • Compile in the Editor: each region's template TFT still has to be compiled in the Nextion Editor once after any HMI change. The page code in a TFT is encrypted.
  • Compressed pictures: make_tfts.sh refuses a template in which a picture that differs between themes is stored compressed. It asks for a new template.
  • HMI edits:
    • hmi_tool.py set changes numeric attributes only.
    • The Editor once rejected an edited file for a reason not yet found. If it reports "Version Mismatch", a Save As in the Editor, then editing that copy, gets past it.
  • What CI can't see: CI can't tell whether a TFT was compiled from a page or code change that leaves the pictures alone.

Testing

  • Unit tests and check_build.py pass locally at every commit (46 tests after commit 7, installing nextion-tools from PyPI).
  • After commit 7, every TFT still rebuilds byte-identical.
  • make_pictures.sh followed by check_build.py --rendered passes for both regions.
  • The CI workflow runs for the first time on this PR.
  • A US TFT built by make_tfts.sh has run on panels.

🤖 Generated with Claude Code

@tpanajott

Copy link
Copy Markdown
Collaborator

Hey, this is fricking awesome! Do you happen to know if GIMP runs on docker for all our supported architectures? If so it should be possible to allow a user to upload any background image of the choice and rebuild the TFT, right?

@jamesmulcahy

Copy link
Copy Markdown
Contributor Author

Hey, this is fricking awesome! Do you happen to know if GIMP runs on docker for all our supported architectures? If so it should be possible to allow a user to upload any background image of the choice and rebuild the TFT, right?

Oh, that's an interesting idea. I'm not 100% sure whether it's supported across all the NSPM architectures, but in principal, it could be used to do that for any where there is support.

I think the gap here would be the existing, non-"all_numbered" pages which have been manually created, and I think include elements of the underlying background in them. They'd need something similar to what I did for the new p_pos and p_volume progress bars, to make them compatible with this.

@tpanajott

Copy link
Copy Markdown
Collaborator

@cablesandcoffee do you have anything to add regarding @jamesmulcahy answer above. As I'm not all the familiar with the Nextion editor it doesn't really tell me much. Is this something that is possible and should be further explored? Ie. take a look if we can get gimp included in all builds of NSPanelManager in order to recompile the .tfts with the background of users wish?

@cablesandcoffee

Copy link
Copy Markdown
Collaborator

Don't fully understand how this changes the workflow yet when adding new stuff to the tft but I guess it gets clearer when I have to use Nextion Editor again. It was a while since we did something there.

But to the answer your question @tpanajott I need to ask @jamesmulcahy: What do you mean with "non-"all_numbered" pages which have been manually created"

It's a while since I thought about the HMI files and the gimp scripts and everything so sorry for not really following. :)

jamesmulcahy and others added 7 commits October 28, 2026 18:12
make_pictures.sh runs create_pictures_{eu,us}.sh in an Ubuntu 24.04
container. It has GIMP 2.10, which the Script-Fu needs, and ImageMagick
6.9.12, which made the committed pictures: 6.9.11 (Debian bookworm)
writes different colour-space endpoints into every BMP header. A gimp
shim runs gimp-console, so no X server is needed; --xvfb runs it under
Xvfb instead. Each run starts GIMP with a fresh profile, so the
container creates the folders and empty tag cache GIMP otherwise
complains about.

The container checks that every picture the Script-Fu saves was written,
since the scripts ignore GIMP's exit status.

Both regions reproduce every committed picture byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the nextion package, which the TFT build scripts share, starting
with nextion.hmi: it reads an HMI project's pages and component
attributes, skipping event code, and edits numeric attributes in place.
An edit keeps endx/endy in step with x/y/w/h and re-stamps the page, so
the Editor opens the result.

The page stamp is CRC-32/MPEG-2 over the page with each byte widened to
a 32-bit word, XORed with a constant that depends only on the page
length; nextion.crc holds that CRC, which TFT checksums use too.

hmi_tool.py lists pages and components and sets attributes from the
command line. The Editor has opened its edits, including to files that
still hold stale blocks from earlier saves, but once rejected one for a
reason not yet found; a Save As in the Editor, then editing that copy,
got past it, so set says so when a file holds stale blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The four themes differ only in pictures, so make_tfts.sh builds every
theme's TFT from the one the Editor compiled into output_tft1/: each
theme's pictures from all_numbered/ are converted to RGB565 and swapped
into the template's picture slots, and the checksums recomputed. Slots
keep their size, since header 2 is encrypted and cannot be rewritten. It
refuses pictures that do not fit their slot, and theme pictures the
Editor stored compressed, whose codec is not known.

The checksum algorithm comes from https://github.com/UNUF/TFTTool,
reimplemented in nextion.crc. Pictures are read with Pillow and numpy,
listed in requirements.txt.

The display orientation is a byte in header 1, so make_tfts.sh checks
that a template has its region's (EU 270, US 180), and makes
us_horizontal_mirrored from the EU TFTs by changing only that byte.
Upstream compiled those in the Editor from the EU HMI at orientation 90,
and they differ from the EU TFTs in exactly that byte and its checksums,
so the AutoHotkey script and the orientation reminder for them go.

Six of the eight committed TFTs come out byte-identical to the Editor's.
In EU theme 2 and US theme 3, the Editor had stored one picture
compressed; those two, and the mirrored theme 2 made from EU's, are
rebuilt here with that picture stored raw, so the committed TFTs are
what make_tfts.sh builds. A rebuilt TFT ran correctly on a panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An image progress bar needs empty and full pictures exactly the bar's
size, matching the page background in every theme. Rather than drawing
and redrawing them by hand, a region's progress_bars.txt lists its bars,
each with its colour, and make_pictures.sh makes their pictures after
GIMP's: the empty bar is the page background cropped to the bar, from
each theme's dithered pictures, and the full bar a fill in the bar's
colour. Every bar must name its colour; there is no default. The fill
is the same in every theme, since the Editor compresses flat pictures
and the themes may differ only in pictures it stores raw.

Pages not yet in gui.HMI are skipped with a note, and pictures that
would overwrite GIMP's are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check_build.py fails if a theme's all_numbered/ has gaps, if a progress
bar's pictures are not what its bar in the HMI makes, if no theme's TFT
holds exactly the HMI's pictures (the HMI stores each picture record as
the TFT does), if a TFT has the wrong display orientation, or if a TFT
is not what make_tfts.sh builds, us_horizontal_mirrored included.
--rendered also compares freshly rendered pictures with the committed
ones, by pixels; make_pictures.sh runs the picture checks after
rendering.

The TFT build workflow runs the unit tests and check_build.py on changes
under docker/HMI_files/, then renders everything in Docker and runs
check_build.py --rendered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README.md covers what to do after changing an HMI, make_pictures.sh,
progress_bars.txt, make_tfts.sh, hmi_tool.py, the tests and checks, the
nextion package, and how us_horizontal_mirrored is made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generic half of the nextion/ package (hmi, fonts, images, tft, crc)
now lives in nextion-tools (https://github.com/jamesmulcahy/nextion-tools,
on PyPI), with its own tests, format notes and CI, alongside a Nextion
display emulator for testing firmware. requirements.txt pins
nextion-tools 0.1.0, and make_pictures.sh passes the same requirement to
the picture-rendering image.

What is particular to this build (region, themes, bars) stays here, as
tft_build/. So do the tests that check this project's own files: the
page and component ids, fonts and event code the firmware relies on,
and that the committed TFTs are what the build makes. The decoder, CRC
and image tests moved with the code.

The library also reads what the old package skipped: each component's
event code, its explicit id and the page id, and the .zi fonts.
hmi_tool.py gains `events`, `fonts` and `dump` subcommands to show
them.

Every TFT still rebuilds byte-identical, and check_build.py passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamesmulcahy
jamesmulcahy force-pushed the feat/headless-tft-pictures branch from 5bdb0dc to 6372e31 Compare October 29, 2026 01:12
0.1.1 names the Editor's scrolling text component (type 55), which the
media player page uses for the song and artist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants