Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d20713d
Updated to fix compilation errors, adapt to some of the new WLED
spiff72 Dec 24, 2024
e5abc46
fixed forced version number of TFT_eSPI in platformio_override.ini
spiff72 Dec 24, 2024
ef7c807
Update platformio_override.ini
spiff72 Dec 24, 2024
76a08c7
adjustments based on feedback
spiff72 Jan 5, 2025
64d5d07
Updated readme file and tweaked indents on the platformio_override.in…
spiff72 Jan 5, 2025
5502d37
Update usermod.cpp
spiff72 Mar 12, 2025
987bf1f
Make usermod v1 calls weak
willmmiles Mar 17, 2025
c7a35a7
usermods/TTGO-T-Display: Make locals static
willmmiles Mar 17, 2025
5e6f243
usermods/TTGO-T-Display: Make into library
willmmiles Mar 17, 2025
3338efc
TTGO-T-Display: Add libArchive
willmmiles Jul 18, 2025
d6dcb96
TTGO-T-Display: Use TFT_BACKLIGHT_ON
willmmiles Jul 18, 2025
4bf6409
TTGO-T-Display: Support custom_display_setup
willmmiles Jul 18, 2025
e37db43
TTGO-T-Display: Use newer TFT_eSPI
willmmiles Jul 18, 2025
2f6723a
TTGO-T-Display: Convert to v2 usermod
willmmiles Aug 15, 2025
c0dbe63
TTGO-T-Display: Allow full path for display_setup
willmmiles Aug 15, 2025
18d4a41
Merge branch 'main' into ttgo-t-display-usermod-fixup
softhack007 May 31, 2026
2f888ec
Merge remote-tracking branch 'upstream/main' into ttgo-t-display-user…
willmmiles Jul 1, 2026
b084bb3
usermods/TTGO-T-Display: Update lib info
willmmiles May 18, 2026
f2cff53
usermod/TTGO-T-Display: Register pins
willmmiles May 18, 2026
a0a472d
TTGO-T-Display: Add info and disable feature
willmmiles May 27, 2026
64252ff
usermod/TTGO-T-Display: Fix pin mapping
willmmiles Jul 1, 2026
a9f5d25
usermod/TTGO-T-Display: Backlight handling fixes
willmmiles Jul 2, 2026
68808f4
Add TFT_eSPI_compat
willmmiles Jun 21, 2026
454a409
usermod/TTGO-T-Display: Fix up readme
willmmiles Jul 1, 2026
fd2c84f
usermod/TTGO-T-Display: debug cleanup
willmmiles Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions lib/TFT_eSPI_compat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# TFT_eSPI_compat

A compatibility wrapper for the [TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) library in WLED usermods. Handles two things that every TFT_eSPI usermod would otherwise need to sort out independently:

1. **SPIFFS stub** — TFT_eSPI pulls in `SPIFFS.h` when `SMOOTH_FONT` is enabled, but SPIFFS is not available on modern ESP32 Arduino platforms. A minimal stub header is injected automatically, but placed last on the include search path. This ensures that, if present, the real header is found first and used instead.

2. **Display setup** — TFT_eSPI requires a *User Setup* header to be processed before any of its headers. This wrapper generates a `tft_setup.h` at build time (using TFT_eSPI's own `__has_include` hook) and injects it into the compiler's search path, so usermods don't need their own build scripts for this.

---

## Using TFT_eSPI_compat in a usermod

Depend on `TFT_eSPI_compat` instead of `TFT_eSPI` directly. In your usermod's `library.json`:

```json
{
"name": "my_display_usermod",
"build": { "libArchive": false },
"dependencies": {
"TFT_eSPI_compat": "*"
}
}
```

TFT_eSPI itself is pulled in transitively. In your source, include it normally:

```cpp
#include <TFT_eSPI.h>
```

---

## Configuring the display setup

TFT_eSPI needs to know which display driver and pin mapping to use. Set `custom_display_setup` in your environment's `platformio_override.ini`:

```ini
custom_display_setup = User_Setups/Setup23_ST7789.h
```

The value can be:
- A path relative to any include directory (e.g. a filename inside TFT_eSPI's `User_Setups/` folder), or
- An absolute path to a custom setup header anywhere on disk.

If `custom_display_setup` is not set, no display setup is applied — your usermod must either set a default (see below) or configure the display another way (e.g. via global `build_flags`).

---

## Providing a default setup (for usermod authors)

If your usermod targets a specific display, you can declare a preferred default that users can still override via `custom_display_setup`. Add a short `extraScript` to your `library.json`:

```json
"build": {
"extraScript": "set_build_flags.py",
"libArchive": false
}
```

```python
# set_build_flags.py
Import("env")

global_env = DefaultEnvironment()
global_env.SetDefault(TFT_DISPLAY_SETUP_DEFAULT="User_Setups/Setup25_TTGO_T_Display.h")

# Add any other usermod-specific build flags here (pin defaults, etc.)
```

`SetDefault` only takes effect when neither `custom_display_setup` nor `TFT_DISPLAY_SETUP_DEFAULT` has already been set, so it never overrides a user's explicit configuration.

`TFT_DISPLAY_SETUP_DEFAULT` is read at build time (when `tft_setup.h` is generated), after all library extraScripts have run.

---

## How it works

`configure.py` appends two directories to the compiler search path for all libraries (including TFT_eSPI) — appended rather than prepended, so a real `SPIFFS.h` found anywhere else on the path always takes priority over the stub:

- `shims/` — contains `SPIFFS.h` (stub) and `User_Setup.h` (dependency-tracking shim)
- `$BUILD_DIR/TFT_eSPI_compat/` — contains the generated `tft_setup.h`

TFT_eSPI.h checks `__has_include(<tft_setup.h>)` at compile time; because `tft_setup.h` is built by a SCons `Command` node before any TFT_eSPI source is compiled, it is always present. `User_Setup.h` carries a plain `#include <tft_setup.h>` that gives SCons's header scanner the dependency edge it needs to guarantee ordering.

---

## TFT_eSPI version

The pinned TFT_eSPI version is declared in `library.json`. All usermods depending on `TFT_eSPI_compat` share this version; update it in one place when needed.
136 changes: 136 additions & 0 deletions lib/TFT_eSPI_compat/configure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
Import("env")
import json
import os
from os.path import realpath

import SCons.Errors
from SCons.Script import Action

global_env = DefaultEnvironment()

shims_dir = realpath("shims")
# BUILD_DIR is itself defined as "$PROJECT_BUILD_DIR/$PIOENV" (see PlatformIO's
# builder/main.py) — env["BUILD_DIR"] returns that raw, unexpanded template
# string, not a real path. Without subst(), os.path.join() below would create
# a literal "./shims/$PROJECT_BUILD_DIR/$PIOENV/TFT_eSPI_compat" directory.
build_shims_dir = os.path.join(global_env.subst("$BUILD_DIR"), "TFT_eSPI_compat")

# Create the generated-file directory now so get_include_dirs() reports it
# and so SCons's FindFile can locate Command nodes inside it.
os.makedirs(build_shims_dir, exist_ok=True)

# Inject shims/ and build_shims_dir into DefaultEnvironment(), so they'll be
# visible anywhere in the build. They're appended, not prepended, so any
# real SPIFFS.h or tft_setup.h will take precedence over these.
global_env.AppendUnique(CPPPATH=[shims_dir, build_shims_dir])

# Also add to our own lib env so load_usermods.py propagates them to usermods
# via cached_add_includes → dep.get_include_dirs().
env.AppendUnique(CPPPATH=[shims_dir, build_shims_dir])

# ---------------------------------------------------------------------------
# Generate tft_setup.h — proper SCons target
# ---------------------------------------------------------------------------
# TFT_eSPI.h checks __has_include(<tft_setup.h>); providing the file lets us
# inject the display config via TFT_eSPI's own hook without patching it.
#
# The file is generated from the resolved display-setup configuration so
# SCons knows to rebuild it (and therefore recompile TFT_eSPI) exactly when
# that configuration changes — not on every build.
#
# SCons's CScanner does not parse __has_include(), so shims/User_Setup.h
# carries a plain #include <tft_setup.h> that gives the scanner a normal
# include directive to follow, ensuring the generated file is built before
# any TFT_eSPI source is compiled.

_lib_env = env # capture before the function definition shadows the name

# Enumerating every file that could influence custom_display_setup /
# TFT_DISPLAY_SETUP_DEFAULT is unbounded, so we depend on the *resolved
# value* instead, via a real intermediate file (SCons Value() nodes don't
# track post-construction writes for signature purposes, so they can't be
# used as a build-time-computed dependency). Resolution needs
# GetLibBuilders(), which isn't populated until build time, so it happens
# in a build action rather than here at parse time. AlwaysBuild reruns the
# resolution every build, but we only rewrite the file when the resolved
# value actually changes.
def _resolve_display_setup(target, source, env): # argument names are required by SCons
genv = DefaultEnvironment()
all_lbs = _lib_env.GetLibBuilders()

display_setup = (
genv.GetProjectOption("custom_display_setup", "")
or genv.get("TFT_DISPLAY_SETUP_DEFAULT", "")
)

setup_path = None
if display_setup:
if os.path.isfile(display_setup):
setup_path = display_setup
else:
search_paths = [
genv["PROJECT_SRC_DIR"],
genv["PROJECT_INCLUDE_DIR"],
*[d for lb in all_lbs for d in lb.get_include_dirs()],
]
setup_path = next(
(
os.path.join(str(p), display_setup)
for p in search_paths
if os.path.isfile(os.path.join(str(p), display_setup))
),
None,
)

target_path = str(target[0])
new_contents = json.dumps([display_setup, setup_path])
if os.path.isfile(target_path):
with open(target_path) as f:
if f.read() == new_contents:
return
with open(target_path, "w") as f:
f.write(new_contents)


_resolved_display_setup = global_env.Command(
os.path.join(build_shims_dir, "display_setup.resolved.json"),
[],
Action(_resolve_display_setup, cmdstr=None),
)
global_env.AlwaysBuild(_resolved_display_setup)
# NoCache: this target has no real sources, so its CacheDir signature never
# changes — without this, CacheDir would keep serving the first build's
# file instead of re-running _resolve_display_setup.
global_env.NoCache(_resolved_display_setup)
# Precious: SCons deletes a target before rebuilding it by default, which
# would defeat the write-if-changed check above (it needs to read the
# previous build's file to compare against).
global_env.Precious(_resolved_display_setup)


def _generate_tft_setup(target, source, env): # argument names are required by SCons
with open(str(source[0])) as f:
display_setup, setup_path = json.load(f)

lines = ["// Generated by TFT_eSPI_compat — do not edit\n", "#pragma once\n"]

if display_setup:
if setup_path:
escaped = setup_path.replace("\\", "\\\\")
lines.append(f'#include "{escaped}"\n')
else:
raise SCons.Errors.StopError(
f"TFT_eSPI_compat: display setup '{display_setup}' not found "
"in any include path"
)

os.makedirs(os.path.dirname(str(target[0])), exist_ok=True)
with open(str(target[0]), "w") as f:
f.writelines(lines)


global_env.Command(
os.path.join(build_shims_dir, "tft_setup.h"),
_resolved_display_setup,
Action(_generate_tft_setup, cmdstr="Generating tft_setup.h"),
)
11 changes: 11 additions & 0 deletions lib/TFT_eSPI_compat/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "TFT_eSPI_compat",
"build": {
"extraScript": "configure.py",
"libArchive": false
},
"platforms": ["espressif32"],
"dependencies": {
"TFT_eSPI": "https://github.com/Bodmer/TFT_eSPI.git#16e37595040eac69cd628e4bffb56fc30cad6299"
}
}
18 changes: 18 additions & 0 deletions lib/TFT_eSPI_compat/shims/SPIFFS.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once
// Stub: TFT_eSPI includes SPIFFS.h when SMOOTH_FONT is defined, but the
// usermods that use it don't load fonts from a filesystem. This shim
// satisfies the include and the `fs::FS &fontFS = SPIFFS` declaration in
// Smooth_font.h without pulling in the SPIFFS Arduino library.
// SPIFFS-backed font loading will silently do nothing at runtime.
//
// shims/ is appended (not prepended) to CPPPATH in configure.py, so this is
// only reached by the compiler's #include search when no real SPIFFS.h
// exists earlier on the include path.
#include <FS.h>

class SPIFFSStub : public fs::FS {
public:
SPIFFSStub() : fs::FS(nullptr) {}
};

inline SPIFFSStub SPIFFS;
12 changes: 12 additions & 0 deletions lib/TFT_eSPI_compat/shims/User_Setup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once
// TFT_eSPI_compat: dependency shim.
//
// At compile time this file is NOT used — TFT_eSPI.h's __has_include(<tft_setup.h>)
// fires first, so USER_SETUP_LOADED is defined before User_Setup_Select.h ever
// reaches the fallback #include <User_Setup.h>.
//
// The only purpose of this file is to give SCons's C/C++ header scanner a plain
// #include directive that it can follow to discover tft_setup.h as a build
// dependency, ensuring the Command that generates tft_setup.h runs before any
// TFT_eSPI source file is compiled.
#include <tft_setup.h>
Loading
Loading