Visual TFT Screen Designer · C/C++ Code Generator for ESP32-S3
- What is TFT Designer?
- Feature List
- Requirements
- Quick Start — Local (Python)
- Quick Start — Docker
- esp32-tft-container.sh — Complete Script Reference
- User Interface Walkthrough
- Drawing Tools Reference
- Layers Panel & Element Properties
- Grid & Magnetic Snap
- Keyboard Shortcuts
- Scene Format (JSON)
- REST API Reference
- Integrating Generated Code
- Project Structure
- Hardware Wiring
- Troubleshooting
ESP32 TFT Designer is a browser-based visual editor that lets you design graphical layouts for TFT displays and instantly export ready-to-compile C or C++ source code for your embedded project.
You draw on a virtual canvas that represents the exact resolution of your target screen. Every element — rectangles, circles, text, icons — is instantly converted to function calls for one of the two supported libraries.
┌─────────────────────────────────────────────────────────┐
│ Browser (TFT Designer) │
│ ┌──────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ Tools │ │ Canvas 480×320 │ │ Layers │ │
│ │ Colors │ │ (live preview) │ │ Code C │ │
│ │ Grid │ │ │ │ Export │ │
│ └──────────┘ └──────────────────┘ └──────────────┘ │
└────────────────────────┬────────────────────────────────┘
│ HTTP POST /api/generate
▼
┌────────────────────────┐
│ Flask + codegen.py │
│ JSON scene → C code │
└────────────────────────┘
│
┌──────────────┴───────────────┐
│ │
tft_screen.c tft_screen.cpp
(st7796s / ESP-IDF) (LovyanGFX / Arduino)
Target hardware:
| Component | Specification |
|---|---|
| MCU | ESP32-S3 DevKit — 16 MB Flash + OPI PSRAM |
| Display | MSP4020 — ST7796S controller |
| Resolution | 480 × 320 pixels |
| Color format | RGB565 (16 bits/pixel) |
| Interface | SPI — SPI2_HOST — 40 MHz |
Supported output libraries:
| Library | Language | Build system | Notes |
|---|---|---|---|
st7796s |
C | ESP-IDF v6.0 | Pure driver, no external dependency |
LovyanGFX |
C++ | Arduino / PlatformIO / ESP-IDF | Full-featured, native arc support |
| Tool | Description | Parameters |
|---|---|---|
| Pixel | Place a single pixel anywhere on the canvas | Color |
| Line | Draw a straight line between two points | Color, Thickness (1–20 px) |
| Rect □ | Draw an outlined rectangle | Color, Thickness (1–20 px) |
| Rect ■ | Draw a filled rectangle | Color |
| Circle ○ | Draw an outlined circle | Color, Thickness (1–20 px) |
| Circle ● | Draw a filled circle | Color |
| Checkerboard | Draw an alternating-color checkerboard pattern | Color, Cell size (via Thickness slider, 2–20 px) |
| WiFi icon | Draw a WiFi signal icon (3 concentric arcs + dot) | Color, Thickness (1–10 px) |
| Text | Place text using a bitmap font | Text content, Font, Scale (×1–×4), Color |
| Feature | Description |
|---|---|
| Drag & drop | Select an element with the Select tool and drag it to a new position |
| Resize handles | When an element is selected, handles appear at corners and edge midpoints; drag to resize |
| Keyboard nudge | Arrow keys move the selected element by 1 px; Shift+Arrow moves by 10 px |
| Copy / Paste | Ctrl+C / Ctrl+V — each paste offsets the clone by +8 px |
| Cut | Ctrl+X — removes the element and places it in the clipboard |
| Duplicate | Ctrl+D — instant clone offset by +12 px, clipboard unchanged |
| Delete | Delete key — removes the selected element permanently |
| Feature | Description |
|---|---|
| Grid overlay | Blue reference grid drawn on a separate canvas layer, always sharp regardless of zoom |
| Independent H/V steps | Horizontal and vertical grid spacing set independently (2–128 px each) |
| Sub-grid | When zoom × step > 48 px, a finer sub-grid appears automatically |
| Magnetic snap | All element creation and drag positions snap to the nearest grid intersection |
| Snap indicator | A ⊕ symbol appears in the coordinate display when snap is active |
| Feature | Description |
|---|---|
| Zoom | Range 10%–800%; use +/− buttons, Ctrl+scroll, or type a value directly in the input field |
| Fit to screen | "Fit" button scales the canvas to fill the available area |
| Pixelated rendering | image-rendering: pixelated — each native pixel becomes a sharp block, matching the real TFT output |
| Smooth rendering | Toggle off pixelated mode for anti-aliased browser rendering (easier to read text at low zoom) |
| Resizable panels | Drag the vertical dividers to make the left or right panel wider or narrower |
| Feature | Description |
|---|---|
| Layer list | Every element appears as a named entry in the right panel, in drawing order |
| Inline property editor | Click an element in the layer list to expand a property grid (x, y, w, h, r, color, etc.) |
| Layer reordering | ▲ / ▼ buttons change z-order (later elements draw on top) |
| Undo / Redo | 80-state history — Ctrl+Z undoes, Ctrl+Y redoes |
| Selection indicator | Selected element highlighted with a dashed blue bounding box + white handles |
| Feature | Description |
|---|---|
| Save scene | Downloads the current design as a JSON file (scene.json) |
| Load scene | Imports a previously saved JSON file and restores the full design |
| Generate code | Converts the scene to C or C++ via the API and displays it with syntax highlighting |
| Copy code | Copies the displayed code to the clipboard |
| Export ZIP | Downloads a single ZIP archive containing .c, .cpp, scene.json, and README.txt |
| Dependency | Minimum version | Purpose |
|---|---|---|
| Python | 3.10 | Runtime |
| pip | Any recent | Package installer |
| Flask | 3.0.0 | Web framework |
| Gunicorn | 21.2.0 | Production WSGI server (optional for local dev) |
| Web browser | Chrome 90+, Firefox 88+, Edge 90+, Safari 15+ | Canvas/ES2020 support required |
| Dependency | Notes |
|---|---|
| Docker Engine | 24.0+ recommended |
| Docker Compose | V2 (docker compose, not docker-compose) |
unzip esp32-tft-designer.zip
cd esp32-tft-designer# Linux / macOS
python3 -m venv venv
source venv/bin/activate
# Windows (Command Prompt)
python -m venv venv
venv\Scripts\activate.bat
# Windows (PowerShell)
python -m venv venv
venv\Scripts\Activate.ps1pip install -r requirements.txtpython app.pyExpected output:
╔══════════════════════════════════════════╗
║ TFT Designer — ST7796S / LovyanGFX ║
║ http://127.0.0.1:5100 ║
╚══════════════════════════════════════════╝
Navigate to http://127.0.0.1:5100
# Extract, install, run, design, export
unzip esp32-tft-designer.zip && cd esp32-tft-designer
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python app.py
# → Browser opens http://127.0.0.1:5100
# → Draw a rectangle, add "Hello!" text, click "Generate C", click "Export ZIP"
# → tft_screen_YYYYMMDD_HHMMSS.zip downloaded — ready for ESP-IDFdocker compose up -d --build
# or with Podman:
RUNTIME=podman ./esp32-tft-container.sh build && ./esp32-tft-container.sh start
# → http://localhost:5100# Make the script executable (first time only)
chmod +x esp32-tft-container.sh
# Full workflow
./esp32-tft-container.sh build # Build the Docker image
./esp32-tft-container.sh start # Start the container in the background
./esp32-tft-container.sh status # Check that it is running
# → Open http://localhost:5100 in your browser
./esp32-tft-container.sh logs # Watch live logs (Ctrl+C to exit)
./esp32-tft-container.sh stop # Stop and remove the containerPORT=8080 ./esp32-tft-container.sh start
# → http://localhost:8080esp32-tft-container.sh is a Bash helper that wraps common Docker / Docker Compose commands. It automatically detects whether docker-compose.yml is present and uses Compose when available; otherwise it falls back to plain docker commands.
./esp32-tft-container.sh <command> [environment variables]
| Variable | Default | Description |
|---|---|---|
PORT |
5100 |
Host port mapped to the container's internal port 5100. Override with PORT=xxxx ./esp32-tft-container.sh start. |
./esp32-tft-container.sh buildWhat it does:
- Runs
docker compose build --no-cache(ordocker build --no-cachewithout Compose). - Forces a complete rebuild from scratch — no layer cache is used.
- Should be run after any modification to
app.py,codegen.py,requirements.txt, or theDockerfile.
When to use:
- First time setup.
- After changing Python source files or dependencies.
- After updating the Dockerfile.
Example output:
[TFT] Building image / Construction de l'image : tft-designer…
[+] Building 45.2s (12/12) FINISHED
[OK ] Image built successfully / Image construite avec succès : tft-designer
./esp32-tft-container.sh start
# or with a custom host port:
PORT=8080 ./esp32-tft-container.sh startWhat it does:
- Runs
docker compose up -d(detached mode). - The container starts in the background and restarts automatically on crash or system reboot (
restart: unless-stopped). - Displays the container status and the access URL after startup.
When to use:
- Normal production startup after
build. - Restarting after a
stop.
Example output:
[TFT] Starting container / Démarrage du container : tft-designer…
[OK ] Container started / Container démarré → http://localhost:5100
Status / État : Up 3 seconds | Ports : 0.0.0.0:5100->5100/tcp
./esp32-tft-container.sh stopWhat it does:
- Runs
docker compose down. - Stops the running container and removes it (and its network).
- The Docker image is kept — you can
startagain without rebuilding.
When to use:
- Planned shutdown.
- Before rebuilding with new code.
./esp32-tft-container.sh restartWhat it does:
- Runs
docker compose restart(ordocker restart). - Stops and restarts the container without rebuilding the image.
- Faster than
stop+start; useful to pick up environment variable changes.
When to use:
- Applying a configuration change in
docker-compose.yml. - Recovering from an unresponsive but not crashed container.
⚠️ If you modified Python source files, usebuild+startinstead.
./esp32-tft-container.sh logsWhat it does:
- Runs
docker compose logs -f --tail=100. - Streams the last 100 log lines then follows new output in real time.
- Press Ctrl+C to stop following.
Log content:
- Gunicorn startup and worker messages
- HTTP access log:
GET / 200/POST /api/generate 200 - Flask application logs (
print()andloggingcalls) - Health check results
Example output:
[TFT] Logs from / Logs du container tft-designer (Ctrl+C to quit / pour quitter) :
tft-designer | [INFO] Starting gunicorn 21.2.0
tft-designer | [INFO] Listening at: http://0.0.0.0:5100
tft-designer | [INFO] Worker booted (pid: 8)
tft-designer | 172.17.0.1 - - [05/May/2026] "GET / HTTP/1.1" 200 18432
tft-designer | 172.17.0.1 - - [05/May/2026] "POST /api/generate HTTP/1.1" 200 4821
./esp32-tft-container.sh statusWhat it does:
- Inspects the container and prints its state, image, start time, port mapping, CPU and memory limits.
- Distinguishes three states:
- RUNNING — container is healthy and serving requests
- STOPPED — container exists but is not running
- NOT FOUND — container has never been created or was cleaned
Example output (running):
═══ TFT Designer — Container Status / Statut container ═══
[OK ] Container RUNNING / EN COURS D'EXÉCUTION
Image : tft-designer
Started : 2026-05-05T22:00:00Z
Port : 5100 → 5100
CPU : 1000000000 nano-CPU
Memory : 268435456 bytes
→ Open / Ouvrir : http://localhost:5100
./esp32-tft-container.sh shellWhat it does:
- Runs
docker exec -it tft-designer /bin/bash(falls back to/bin/sh). - Opens an interactive terminal inside the running container.
- You are logged in as
appuser(non-root, uid 1001).
Useful for:
- Inspecting the application files inside the container.
- Running Python commands for debugging:
python -c "from codegen import *; ...". - Checking environment variables:
env | grep FLASK.
Example session:
./esp32-tft-container.sh shell
appuser@abc123:/app$ ls
app.py codegen.py static/ templates/
appuser@abc123:/app$ python -c "from codegen import hex_to_rgb; print(hex_to_rgb('#FF8000'))"
(255, 128, 0)
appuser@abc123:/app$ exit./esp32-tft-container.sh cleanWhat it does:
- Asks for explicit confirmation (
y/o). - Runs
docker compose down --rmi all --volumes. - Removes: the running container, the Docker network, and the built image.
- After
clean, a fullbuildis required before the nextstart.
When to use:
- Freeing disk space.
- Starting completely fresh after major changes.
- Switching to a different version of the project.
⚠️ This action is irreversible. The image must be rebuilt from scratch afterward.
Example:
[WRN] This will PERMANENTLY remove the container AND image : tft-designer
Confirm / Confirmer ? [y/o/N] y
[OK ] Cleanup complete / Nettoyage terminé.
./esp32-tft-container.sh dev
# or on a custom port:
PORT=8080 ./esp32-tft-container.sh devWhat it does:
- Starts the container with
--rm -it(removed on exit, interactive). - Mounts local source files as read-only volumes inside the container:
./app.py→/app/app.py./codegen.py→/app/codegen.py./templates/→/app/templates/./static/→/app/static/
- Launches
python app.pywithFLASK_DEBUG=1(Werkzeug auto-reloader). - Any change to a local source file is automatically picked up without rebuilding the image.
When to use:
- Developing and testing changes to
app.py,codegen.py, templates, or CSS/JS. - The image must have been built at least once with
./esp32-tft-container.sh build.
⚠️ Never use in production.FLASK_DEBUG=1exposes an interactive debugger in the browser.
Exit: Press Ctrl+C to stop the dev server and remove the container.
./esp32-tft-container.sh help
# or
./esp32-tft-container.sh --help
./esp32-tft-container.sh -h
./esp32-tft-container.sh # (no argument also shows help)| Command | Rebuilds image | Container state after | Use case |
|---|---|---|---|
build |
✅ Yes | Unchanged | After code changes |
start |
❌ No | Running (detached) | Normal startup |
stop |
❌ No | Stopped & removed | Planned shutdown |
restart |
❌ No | Running (detached) | Config change |
logs |
❌ No | Unchanged | Monitoring |
status |
❌ No | Unchanged | Check health |
shell |
❌ No | Unchanged | Debug inside container |
clean |
Removes image | Removed | Full reset |
dev |
❌ No | Running (interactive, auto-removed) | Development |
help |
❌ No | Unchanged | Display help |
╔═══════════════════════════════════════════════════════════════════════════╗
║ [←] TFT Designer ST7796S · LovyanGFX [Undo][Redo][Clear][Scene] [Generate C][Export ZIP] ║
╠═══════════╦═══╦══════════════════════════════════════╦═══╦════════════════╣
║ ║ ◄ ║ ║ ► ║ ║
║ LEFT ║ ║ CANVAS AREA ║ ║ RIGHT PANEL ║
║ PANEL ║ ║ [Zoom − 100% + Fit] [Pixel] x:— y:—║ ║ [Layers|Code] ║
║ ║ ║ ┌───────────────────────────────┐ ║ ║ ║
║ Resolution║ ║ │ │ ║ ║ Layer list ║
║ Tools ║ ║ │ 480 × 320 canvas │ ║ ║ + properties ║
║ Color ║ ║ │ │ ║ ║ ║
║ Thickness ║ ║ └───────────────────────────────┘ ║ ║ Code viewer ║
║ Grid ║ ║ ║ ║ + copy btn ║
║ ║ ◄ ║ ║ ► ║ ║
╚═══════════╩═══╩══════════════════════════════════════╩═══╩════════════════╝
↑ dividers — drag to resize panels
| Section | Controls |
|---|---|
| Resolution | Width, Height (canvas dimensions in px), Background color, Apply button |
| Tools | 10 tool buttons in a 2-column grid (see §8) |
| Color | Color picker + 25-color quick palette |
| Thickness / Cell | Slider 1–20 px — controls stroke thickness and checkerboard cell size |
| Grid | Grid / Snap toggle buttons + H step / V step inputs |
| Tab | Content |
|---|---|
| Layers | Toolbar (▲▼ Copy Cut Paste Duplicate Delete) + scrollable element list |
| Code C | Library selector + Copy button + code preview with syntax highlighting |
- Select a tool in the left panel.
- For most tools: click and drag on the canvas — release to place the element.
- For Pixel: a single click places a pixel.
- For Text: a single click opens the text modal.
- Click anywhere on the canvas to place a 1×1 pixel.
- Color: set in the Color section.
- No thickness applies.
- Click the start point, drag to the end point, release.
- Thickness 1–20 px (slider).
- Horizontal/vertical lines generate optimized
tft_draw_hline/tft_draw_vlinecalls. - Diagonal lines generate a Bresenham loop using
tft_fill_rect. - Resize: drag either endpoint handle.
- Click and drag to define the bounding box.
- Thickness 1–20 px — generates concentric rectangle calls for thick borders.
- Resize: 8 handles (4 corners + 4 edge midpoints).
- Same as Rect □ but generates a single
tft_fill_rectcall. - Resize: 8 handles.
- Click the center, drag outward — release when the radius is correct.
- Thickness: concentric circles with decreasing radius.
- Resize: 4 handles (N, E, S, W) — all adjust the radius.
- Same as Circle ○ but generates
tft_fill_circle. - Resize: 4 handles.
- Click and drag to define the pattern area.
- Cell size is controlled by the Thickness slider (2–20 px).
- The cell size can be fine-tuned in the Layer properties panel after placement.
- Generates a nested for-loop in the output code.
- Resize: 8 handles.
- Click the signal origin point (base of the icon), drag outward — release to set the outer arc radius.
- Three concentric arcs at 100%, 65%, 33% of the radius, opening upward.
- Center dot radius = radius / 10.
- Resize: 4 handles adjust the radius.
- Click anywhere on the canvas — a modal dialog opens.
- Fill in: text content (max 58 chars), font, scale (×1–×4), color.
- Click Place — the text appears at the click position.
- Move/resize by editing X, Y, Scale in the layer properties.
Available fonts:
| Font name | Description | Approximate size at scale ×1 |
|---|---|---|
8×16 (default) |
Standard monospace bitmap | 8 px wide × 16 px tall |
6×8 |
Small monospace bitmap | 6 px wide × 8 px tall |
12×16 |
Medium proportional | 12 px wide × 16 px tall |
bold |
Bold proportional | ~12 px wide × 24 px tall |
large |
Large proportional | ~14 px wide × 32 px tall |
7seg |
7-segment LCD style | Variable |
When an element is selected with the Select / Move tool, white square handles appear:
Rect and Checkerboard (8 handles):
●───●───●
│ │
● ●
│ │
●───●───●
- Corner handles (NW, NE, SW, SE): resize both dimensions simultaneously.
- Edge handles (N, S, E, W): resize a single dimension.
Circle and WiFi (4 handles):
● ← N
● (c) ●
W E
● ← S
- All 4 handles adjust the radius only. The center does not move.
Line (2 handles):
- One handle per endpoint. Drag either to move that endpoint independently.
Each element appears as a row:
[icon] [color swatch] [label]
- Click a row to select the element (highlighted on canvas).
- Selected row expands an inline property editor.
Fields shown depend on element type:
| Element | Editable properties |
|---|---|
| Pixel | x, y, color |
| Line | x1, y1, x2, y2, thickness, color |
| Rect | x, y, w, h, thickness (if outline), color |
| Checkerboard | x, y, w, h, cell size, color |
| Circle | cx, cy, r, thickness (if outline), color |
| WiFi | cx, cy, r, thickness, color |
| Text | x, y, scale, text content, color |
All numeric fields update the canvas in real time as you type.
| Button | Shortcut | Action |
|---|---|---|
| ▲ | — | Move element one step up in draw order |
| ▼ | — | Move element one step down in draw order |
| ⎘ Copy | Ctrl+C | Copy to internal clipboard |
| ✂ Cut | Ctrl+X | Cut to internal clipboard |
| ⎗ Paste | Ctrl+V | Paste from clipboard (+8 px offset per paste) |
| ⧉ Duplicate | Ctrl+D | Instant clone (+12 px offset, clipboard unchanged) |
| 🗑 Delete | Delete | Remove selected element |
- Click the Grid button in the left panel, or press
G. - The grid appears as a blue overlay on a dedicated canvas layer — it is always pixel-sharp at any zoom level.
- Set Pas H (horizontal step) and Pas V (vertical step) independently (2–128 px).
When zoom × step > 48 px, a finer semi-transparent sub-grid appears between the main lines automatically.
- Requires the grid to be visible first.
- Click Aimantation or press
S. - When active, all positions snap to the nearest grid intersection:
- Element creation start and end points
- Drag positions (anchor point snaps)
- Resize handle positions
- The coordinate display shows
x:nn y:nn ⊕when snap is active.
- Pressing
Gagain hides the grid and automatically disables snap. - Pressing
Stoggles snap independently (only when grid is visible).
| Shortcut | Action |
|---|---|
Ctrl+Z |
Undo (up to 80 states) |
Ctrl+Y or Ctrl+Shift+Z |
Redo |
G |
Toggle grid overlay |
S |
Toggle magnetic snap (requires grid) |
P |
Toggle pixelated / smooth rendering |
Escape |
Deselect current element |
| Shortcut | Action |
|---|---|
Ctrl+C |
Copy selected element to clipboard |
Ctrl+X |
Cut selected element (copy + delete) |
Ctrl+V |
Paste from clipboard (+8 px offset per paste) |
Ctrl+D |
Duplicate selected element (+12 px offset) |
Delete |
Delete selected element |
↑ ↓ ← → |
Move element by 1 px |
Shift + ↑ ↓ ← → |
Move element by 10 px |
| Shortcut | Action |
|---|---|
Ctrl + scroll wheel |
Zoom in / out |
Type in zoom field + Enter |
Set zoom to exact value (e.g. 150% or 150) |
Note: Keyboard shortcuts are disabled when focus is in a text input or textarea to avoid conflicts with typing.
The scene file (scene.json) is the project file format. It can be saved, shared, version-controlled, and reloaded.
{
"width": 480,
"height": 320,
"bg_color": "#000000",
"elements": [ ... ]
}| Field | Type | Description |
|---|---|---|
width |
integer | Canvas width in pixels (matches target display width) |
height |
integer | Canvas height in pixels |
bg_color |
string | Background color in #RRGGBB hex format |
elements |
array | Ordered list of graphical elements (first = drawn first = bottom layer) |
{ "type": "pixel", "x": 10, "y": 20, "color": "#ff0000" }{
"type": "line",
"x1": 0, "y1": 0, "x2": 200, "y2": 100,
"color": "#00ff00",
"thickness": 2
}{
"type": "rect",
"x": 20, "y": 20, "w": 160, "h": 80,
"color": "#0000ff",
"filled": false,
"thickness": 1
}{
"type": "rect",
"x": 20, "y": 20, "w": 160, "h": 80,
"color": "#0000ff",
"filled": true,
"thickness": 1
}{
"type": "circle",
"cx": 240, "cy": 160, "r": 60,
"color": "#ffff00",
"filled": false,
"thickness": 2
}{
"type": "circle",
"cx": 240, "cy": 160, "r": 60,
"color": "#ffff00",
"filled": true,
"thickness": 1
}{
"type": "checkerboard",
"x": 0, "y": 0, "w": 120, "h": 120,
"cell": 10,
"color": "#ffffff"
}{
"type": "wifi",
"cx": 400, "cy": 160, "r": 40,
"color": "#00ffff",
"thickness": 2
}{
"type": "text",
"x": 10, "y": 10,
"text": "Hello World!",
"color": "#ffffff",
"font": "8x16",
"scale": 2,
"bg_color": "#000000"
}{
"width": 480,
"height": 320,
"bg_color": "#001020",
"elements": [
{
"type": "rect",
"x": 10, "y": 10, "w": 460, "h": 300,
"color": "#58a6ff",
"filled": false,
"thickness": 2
},
{
"type": "text",
"x": 30, "y": 140,
"text": "Hello ESP32!",
"color": "#ffffff",
"font": "8x16",
"scale": 3,
"bg_color": "#001020"
},
{
"type": "wifi",
"cx": 420, "cy": 280, "r": 30,
"color": "#3fb950",
"thickness": 2
}
]
}The Flask server exposes a minimal REST API used by the browser editor. You can also call it directly for automation or integration purposes.
Returns the HTML editor page.
curl http://localhost:5100/
# → 200 OK, Content-Type: text/htmlConverts a JSON scene to C and/or C++ source code.
Request body:
{
"scene": { ... },
"library": "st7796s"
}| Field | Type | Required | Values |
|---|---|---|---|
scene |
object | Yes | Full scene JSON (see §12) |
library |
string | No (default: "st7796s") |
"st7796s" · "lovyangfx" · "both" |
Response:
{
"st7796s": "/* tft_screen_st7796s.c */\n..."
}When library = "both":
{
"st7796s": "...",
"lovyangfx": "..."
}curl example:
curl -s -X POST http://localhost:5100/api/generate \
-H "Content-Type: application/json" \
-d '{
"library": "both",
"scene": {
"width": 480, "height": 320, "bg_color": "#000000",
"elements": [
{"type": "text", "x": 100, "y": 140,
"text": "Hello!", "color": "#ffffff",
"font": "8x16", "scale": 3, "bg_color": "#000000"}
]
}
}' | python3 -m json.toolError response:
{ "error": "description of the error" }HTTP status 400 (bad request) or 500 (generation error).
Generates and returns a ZIP archive containing both source files, the scene JSON, and a README.
Request body:
{ "scene": { ... } }Response: application/zip file download, filename tft_screen_YYYYMMDD_HHMMSS.zip.
Archive contents:
| File | Description |
|---|---|
tft_screen_st7796s_TIMESTAMP.c |
C source for st7796s (ESP-IDF) |
tft_screen_lovyangfx_TIMESTAMP.cpp |
C++ source for LovyanGFX |
scene.json |
The source scene (reloadable in the editor) |
README.txt |
Integration instructions |
curl example:
curl -X POST http://localhost:5100/api/export_zip \
-H "Content-Type: application/json" \
-d '{"scene": {"width":480,"height":320,"bg_color":"#000000","elements":[]}}' \
-o output.zipValidates a scene JSON object and re-returns it. Used by the editor when importing a file.
Request body:
{ "scene": { ... } }Response:
{ "scene": { ... } }Step 1: Copy the generated .c file into main/ of your ESP-IDF project.
Step 2: Create the header file main/tft_screen_st7796s.h:
#pragma once
#include "st7796s.h"
void tft_draw_screen(tft_handle_t *tft);Step 3: Add to main/CMakeLists.txt:
idf_component_register(
SRCS "main.c" "tft_screen_st7796s.c"
INCLUDE_DIRS "."
REQUIRES st7796s
)Step 4: Call from main.c:
#include "st7796s.h"
#include "tft_screen_st7796s.h"
void app_main(void) {
tft_handle_t tft;
ESP_ERROR_CHECK(tft_init(&tft)); // Initialize SPI + display
tft_draw_screen(&tft); // Render the designed layout
// ... rest of your application
}Build:
idf.py set-target esp32s3
idf.py build
idf.py -p /dev/ttyUSB0 flash monitorStep 1: Copy the generated .cpp file into your sketch folder.
Step 2: Create the header file tft_screen_lovyangfx.h:
#pragma once
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
void draw_screen(LGFX_Device& lcd);Step 3: Use in your sketch:
#include <LovyanGFX.hpp>
#include "tft_screen_lovyangfx.h"
// The LGFX_ST7796 class is defined inside the generated .cpp file
LGFX_ST7796 lcd;
void setup() {
lcd.begin();
lcd.setRotation(0); // Landscape: 480×320
draw_screen(lcd); // Render the designed layout
}
void loop() {
// your application code
}PlatformIO platformio.ini:
[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
lib_deps = lovyan03/LovyanGFX @ ^1.1.0esp32-tft-designer/
│
├── app.py ← Flask web server
│ Routes: GET /, POST /api/generate,
│ POST /api/export_zip, POST /api/load_scene
│
├── codegen.py ← C/C++ code generator
│ Functions: generate_st7796s(scene), generate_lovyangfx(scene)
│ Utilities: hex_to_rgb(), rgb_to_565(), sanitize_text()
│
├── requirements.txt ← Python dependencies
│ flask>=3.0.0, gunicorn>=21.2.0
│
├── Dockerfile ← Multi-stage production image
│ Stage 1: pip install → /install
│ Stage 2: copy sources + binaries, run as appuser (uid 1001)
│
├── docker-compose.yml ← Compose service definition
│ Port: 5100:5100, restart: unless-stopped
│ Limits: 1 CPU, 256 MB RAM
│
├── esp32-tft-container.sh ← Docker management script
│ Commands: build start stop restart logs status shell clean dev help
│
├── README.md ← This file (English)
├── README.fr.md ← French version
│
├── templates/
│ └── index.html ← Single-page HTML/CSS/JS editor
│ 3-panel layout: tools | canvas | layers+code
│ Inline JS is loaded from static/js/app.js
│
└── static/
├── css/
│ └── app.css ← Dark theme (CSS custom properties, grid layout)
└── js/
└── app.js ← Full editor implementation (~1300 lines)
Sections: state, canvas events, draw tools,
resize handles, grid, snap, undo/redo,
clipboard, code generation, panel resizers
| MSP4020 Pin | Signal | ESP32-S3 GPIO | Notes |
|---|---|---|---|
| VCC | Power | 3V3 | 3.3V only — never 5V |
| GND | Ground | GND | Common ground |
| CS | SPI Chip Select | GPIO 10 | Active LOW |
| RESET | Hardware reset | GPIO 9 | Active LOW pulse on init |
| DC / RS | Data / Command | GPIO 8 | HIGH = data, LOW = command |
| SDI / MOSI | SPI data out | GPIO 11 | SPI2_HOST MOSI |
| SCK / CLK | SPI clock | GPIO 12 | SPI2_HOST CLK |
| LED / BL | Backlight | GPIO 46 | PWM via LEDC timer 0 |
| SDO / MISO | SPI data in | GPIO 13 | Optional — read-back only |
| Parameter | Value |
|---|---|
| SPI host | SPI2_HOST |
| SPI mode | 0 (CPOL=0, CPHA=0) |
| Write frequency | 40 MHz |
| Read frequency | 10 MHz |
| DMA channel | Auto |
| DMA buffer | 16 rows × 480 px = 15 360 bytes |
The ST7796S controller RAM is 320 columns × 480 rows. The MADCTL register configures how the RAM maps to the physical panel:
| Rotation | MADCTL value | Logical size | Use case |
|---|---|---|---|
| 0° | MV | BGR |
480 × 320 | Landscape native (default) |
| 90° | MX | BGR |
320 × 480 | Portrait |
| 180° | MV | MX | MY | BGR |
480 × 320 | Landscape mirrored |
| 270° | MY | BGR |
320 × 480 | Portrait mirrored |
These MADCTL values have been validated on the physical MSP4020 panel. Other ST7796S breakout boards may require different bit combinations.
Symptom: python app.py shows an error immediately.
| Cause | Solution |
|---|---|
| Python version < 3.10 | Upgrade Python: python3 --version |
| Dependencies not installed | Run pip install -r requirements.txt |
| Port 5100 already in use | Kill the other process or change the port: app.run(port=5101) |
| Virtual environment not active | Run source venv/bin/activate |
./esp32-tft-container.sh logs # Check error messages
./esp32-tft-container.sh status # Check state| Cause | Solution |
|---|---|
| Port 5100 already in use | PORT=5101 ./esp32-tft-container.sh start |
| Image not built | ./esp32-tft-container.sh build |
| Docker daemon not running | sudo systemctl start docker |
| Out of disk space | docker system prune then rebuild |
- Check that you have selected a tool (pixel, line, rect…) in the left panel.
- The Select/Move tool does not draw — it moves existing elements.
- Make sure the click is inside the canvas border.
| Symptom | Cause | Solution |
|---|---|---|
tft_draw_screen undefined |
Missing header | Create tft_screen_st7796s.h (see §14) |
RGB565 macro not found |
Missing include | Add #include "st7796s.h" |
lgfx::color565 not found |
Missing LovyanGFX | Add #include <LovyanGFX.hpp> |
tft_draw_wifi not found |
Function at bottom of file | Move it above tft_draw_screen or add a forward declaration |
The MADCTL BGR flag may need to be inverted for your specific panel. In st7796s.c, change:
// In tft_set_rotation(), remove or add MADCTL_BGR:
madctl = MADCTL_MV | MADCTL_BGR; // Current (correct for MSP4020)
madctl = MADCTL_MV; // Try this if colors are swappedCharacters outside ASCII 32–126 are not supported by the built-in 8×16 bitmap font. Use only standard ASCII characters in the text tool.
TFT Designer — MIT License — ESP32-S3 / MSP4020 / ST7796S