Skip to content
Draft
Show file tree
Hide file tree
Changes from 15 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
56 changes: 9 additions & 47 deletions usermods/TTGO-T-Display/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ I have designed a 3D printed case around this board and an ["ElectroCookie"](htt

Based on a rework of the ssd1306_i2c_oled_u8g2 usermod from the WLED repo.

In December 2024, I made some changes to fix compilation errors, and modified the platformio_override.ini file to include ALL changes needed (no more need to edit the origiginal "stock" platformio.ini file). I am also forcing an older version of the TFT_eSPI library.

## Hardware
![Hardware](assets/ttgo_hardware1.png)
![Hardware](assets/ttgo-tdisplay-enclosure1a.png)
Expand All @@ -37,55 +39,15 @@ Functionality checked with:
* As with all usermods, copy the usermod.cpp file from the TTGO-T-Display usermod folder to the wled00 folder (replacing the default usermod.cpp file).

## Platformio Requirements
### Platformio.ini changes
Under the root folder of the project, in the `platformio.ini` file, uncomment the `TFT_eSPI` line within the [common] section, under `lib_deps`:
```ini
# platformio.ini
...
[common]
...
lib_deps =
...
#For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line
#TFT_eSPI
...
```

In the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows:

Comment out the line described below:
```ini
# Release binaries
; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32s2_saola, esp32c3
```
and uncomment the following line in the 'Single binaries' section:
```ini
default_envs = esp32dev
```
Save the `platformio.ini` file. Once saved, the required library files should be automatically downloaded for modifications in a later step.
### Platformio_override.ini (added)
Copy the `platformio_override.ini` file which is contained in the `usermods/TTGO-T-Display/` folder into the root of your project folder. This file contains an override that remaps the button pin of WLED to use the on-board button to the right of the USB-C connector (when viewed with the port oriented downward - see hardware photo).

### Platformio_overrides.ini (added)
Copy the `platformio_overrides.ini` file which is contained in the `usermods/TTGO-T-Display/` folder into the root of your project folder. This file contains an override that remaps the button pin of WLED to use the on-board button to the right of the USB-C connector (when viewed with the port oriented downward - see hardware photo).

### TFT_eSPI Library Adjustments (board selection)
You need to modify a file in the `TFT_eSPI` library to select the correct board. If you followed the directions to modify and save the `platformio.ini` file above, the `User_Setup_Select.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI_ID1559` folder.
### Platformio.ini changes
Once the platformio_override.ini file has been copied as described above, the platformio.ini file isn't actually changed, but it is helpful to save the platformio.ini file in the VS Code application. This should trigger the download of the library dependencies.

Modify the `User_Setup_Select.h` file as follows:
* Comment out the following line (which is the 'default' setup file):
```ini
//#include <User_Setup.h> // Default setup is root library folder
```
* Uncomment the following line (which points to the setup file for the TTGO T-Display):
```ini
#include <User_Setups/Setup25_TTGO_T_Display.h> // Setup file for ESP32 and TTGO T-Display ST7789V SPI bus TFT
```
### Change to the WLED_T-Display environment
This should appear as an option in the bottom toolbar.

Build the file. If you see a failure like this:
```ini
xtensa-esp32-elf-g++: error: wled00\wled00.ino.cpp: No such file or directory
xtensa-esp32-elf-g++: fatal error: no input files
```
try building again. Sometimes this happens on the first build attempt and subsequent attempts build correctly.
Once the compilation is done and loaded onto the TTGO T-Display module, the display should show "Loading...", and then it will show the IP of the WLED access point.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## Arduino IDE
- UNTESTED
10 changes: 10 additions & 0 deletions usermods/TTGO-T-Display/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name:": "TTGO-T-Display",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"dependencies": {
"TFT_eSPI" : "https://github.com/Bodmer/TFT_eSPI.git#5793878"
},
"build": {
"extraScript": "set_build_flags.py",
"libArchive": false
}
}
41 changes: 36 additions & 5 deletions usermods/TTGO-T-Display/platformio_override.ini
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
[env:esp32dev]
build_flags = ${common.build_flags_esp32}
; PIN defines - uncomment and change, if needed:
; -D LEDPIN=2
-D BTNPIN=35
# Example PlatformIO Project Configuration Override
# ------------------------------------------------------------------------------
# Copy to platformio_override.ini to activate overrides
# ------------------------------------------------------------------------------
# Please visit documentation: https://docs.platformio.org/page/projectconf.html

[platformio]
#default_envs = WLED_tasmota_1M # define as many as you need
default_envs = WLED_T-Display # define as many as you need
#----------
# SAMPLE
#----------
[env:WLED_T-Display]

board = esp32dev
upload_speed = 921600
monitor_speed = 115200
platform = ${esp32.platform}
platform_packages = ${esp32.platform_packages}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"WLED_T-Display\"
; -D IRPIN=4
; -D RLYPIN=12
; -D RLYMDE=1
; -D WLED_DISABLE_BROWNOUT_DET
;PIN defines - uncomment and change, if needed:
-D DATA_PINS=2
;or use this for multiple outputs
; -D DATA_PINS=1,3
-D BTNPIN=35
; -D IRPIN=4
; -D RLYPIN=12
; -D RLYMDE=1
; -D RLYODRAIN=0
; -D LED_BUILTIN=2 # GPIO of built-in LED
lib_deps = ${esp32.lib_deps}
custom_usermods = audioreactive TTGO-T-Display
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.default_partitions}
50 changes: 50 additions & 0 deletions usermods/TTGO-T-Display/set_build_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Select target board
# Ref: https://github.com/Bodmer/TFT_eSPI/wiki/Installing-on-PlatformIO#4-configure-library
# These flags are passed to this library and the TFT_eSPI library only

Import("env")
import os


# Add pin definitions to whole project environment if they haven't otherwise been specified
# WM: This is a feature of the original usermod; I'm not sure if it's globally applicable..
global_env = DefaultEnvironment()
global_defines = global_env.get("CPPDEFINES", [])
if "BTNPIN" not in [v[0] for v in global_defines if isinstance(v, tuple)]:
global_env.Append(CPPDEFINES=[("BTNPIN", "35")])

# Select display setup
display_setup = global_env.GetProjectOption("custom_display_setup","User_Setups/Setup25_TTGO_T_Display.h")

# Resolve this file's path
def find_in_paths(filename, paths):
for path in paths:
fullpath = os.path.join(str(path), filename)
if os.path.isfile(fullpath):
return fullpath
return None

lib_builders = env.GetLibBuilders()
if os.path.isfile(display_setup):
display_setup_path = display_setup
else:
search_paths = [global_env["PROJECT_SRC_DIR"],
global_env["PROJECT_INCLUDE_DIR"],
*[incdir for lb in lib_builders for incdir in lb.get_include_dirs()]]

display_setup_path = find_in_paths(display_setup, search_paths)

if display_setup_path is None:
print(f"Unable to find {display_setup} in any include path - {[str(path) for path in search_paths]}!")
raise RuntimeError("Missing display setup; use 'User_Setups/something.h' for setups out of TFT_eSPI, or put your setup in wled00")

def add_display_setup(tgt_env):
tgt_env.Append(CPPDEFINES=[("USER_SETUP_LOADED", "1")])
tgt_env.Append(CCFLAGS=f"-include {display_setup_path}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "set_build_flags.py" -path "*/usermods/*" 2>/dev/null

Repository: wled/WLED

Length of output: 97


🏁 Script executed:

# Check if the file exists and read the relevant section
if [ -f "./usermods/TTGO-T-Display/set_build_flags.py" ]; then
  echo "=== File found, reading lines 35-50 ===";
  sed -n '35,50p' "./usermods/TTGO-T-Display/set_build_flags.py" | cat -n;
else
  echo "File not found at expected path. Searching for any set_build_flags.py...";
  find . -name "set_build_flags.py" 2>/dev/null;
fi

Repository: wled/WLED

Length of output: 884


🌐 Web query:

PlatformIO SCons CCFLAGS string vs array argument handling spaces

💡 Result:

In PlatformIO’s SCons environment, env["CCFLAGS"] is a SCons CLVar (command-line variable). That affects how strings vs lists are handled:

  • If you pass a string (e.g. env.Append(CCFLAGS='-DNAME="A B" -O2')), SCons will run its Split() logic and split on whitespace into multiple arguments (with some quote-aware behavior). As a side effect, “extra” spaces in string inputs get stripped. (scons.org)
  • If you pass a list/array (e.g. env.Append(CCFLAGS=['-DNAME="A B"', '-O2'])), each list element is treated as one argument, so an element that contains spaces is preserved as a single flag. SCons explicitly recommends this when you need spaces preserved. (scons.org)

Rule of thumb:

  • Use a string for normal flags where spaces just separate flags.
  • Use a list when any single flag contains spaces (or you want to guarantee “one item = one argument”). (scons.org)

Citations:


Pass the forced include as separate argv entries.

CCFLAGS=f"-include {display_setup_path}" breaks as soon as custom_display_setup resolves to a path containing spaces. SCons splits string arguments on whitespace, so paths with spaces (common on Windows) will be split into multiple arguments, breaking the -include flag and undermining the new full-path workflow.

Suggested fix
 def add_display_setup(tgt_env):
     tgt_env.Append(CPPDEFINES=[("USER_SETUP_LOADED", "1")])
-    tgt_env.Append(CCFLAGS=f"-include {display_setup_path}")
+    tgt_env.Append(CCFLAGS=["-include", display_setup_path])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/TTGO-T-Display/set_build_flags.py` around lines 41 - 43, The
forced-include flag is being passed as a single whitespace-containing string in
add_display_setup (tgt_env.Append(CCFLAGS=f"-include {display_setup_path}")),
which SCons will split and thus break when display_setup_path contains spaces;
change the CCFLAGS Append to pass the flag and the path as separate argv entries
(e.g., provide a list/sequence containing the "-include" token and
display_setup_path) so SCons treats them as two distinct arguments and handles
paths with spaces correctly.


# Add it for this library and the TFT_eSPI dependency
add_display_setup(env)
for lb in lib_builders:
if lb.name == "TFT_eSPI":
add_display_setup(lb.env)
break
115 changes: 66 additions & 49 deletions usermods/TTGO-T-Display/usermod.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,89 +17,93 @@
* when a change in the dipslayed info is detected (within a 5 second interval).
*/


//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t)

#include "wled.h"
#include <TFT_eSPI.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include "WiFi.h"
#include <Wire.h>

#ifndef TFT_DISPOFF
#define TFT_DISPOFF 0x28
#endif
// #ifndef TFT_DISPOFF
// #define TFT_DISPOFF 0x28
// #endif

#ifndef TFT_SLPIN
#define TFT_SLPIN 0x10
#endif
// #ifndef TFT_SLPIN
// #define TFT_SLPIN 0x10
// #endif

#define TFT_MOSI 19
#define TFT_SCLK 18
#define TFT_CS 5
#define TFT_DC 16
#define TFT_RST 23
// #define TFT_MOSI 19
// #define TFT_SCLK 18
// #define TFT_CS 5
// #define TFT_DC 16
// #define TFT_RST 23

//#define TFT_BL 4 // Display backlight control pin
//#define ADC_EN 14 // Used for enabling battery voltage measurements - not used in this program
//#define WLED_WATCHDOG_TIMEOUT 3

// How often we are redrawing screen
#define USER_LOOP_REFRESH_RATE_MS 5000

#define TFT_BL 4 // Display backlight control pin
#define ADC_EN 14 // Used for enabling battery voltage measurements - not used in this program
class TTGO_T_DisplayMod : public Usermod {

TFT_eSPI tft = TFT_eSPI(135, 240); // Invoke custom library
// Member variables
TFT_eSPI tft; // Invoke custom library
// needRedraw marks if redraw is required to prevent often redrawing.
bool needRedraw = true;
// Next variables hold the previous known values to determine if redraw is
// required.
String knownSsid = "";
IPAddress knownIp;
uint8_t knownBrightness = 0;
uint8_t knownMode = 0;
uint8_t knownPalette = 0;
uint8_t tftcharwidth = 19;

long lastUpdate_mod = 0;
long lastRedraw = 0;
bool displayTurnedOff = false;

public:

TTGO_T_DisplayMod() : Usermod()
{}

//gets called once at boot. Do all initialization that doesn't depend on network here
void userSetup() {
Serial.begin(115200);
Serial.println("Start");
void setup() override {
DEBUG_PRINTLN("Start");
tft.init();
tft.setRotation(3); //Rotation here is set up for the text to be readable with the port on the left. Use 1 to flip.
tft.fillScreen(TFT_BLACK);
tft.setTextSize(2);
//tft.setTextSize(1);
tft.setTextColor(TFT_WHITE);
tft.setCursor(1, 10);
tft.setTextDatum(MC_DATUM);
tft.setTextSize(3);
//tft.setTextSize(1);
tft.print("Loading...");
DEBUG_PRINTLN("Loading...");

if (TFT_BL > 0) { // TFT_BL has been set in the TFT_eSPI library in the User Setup file TTGO_T_Display.h
pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode
digitalWrite(TFT_BL, HIGH); // Turn backlight on.
digitalWrite(TFT_BL, TFT_BACKLIGHT_ON); // Turn backlight on.
}

// tft.setRotation(3);
}

// gets called every time WiFi is (re-)connected. Initialize own network
// interfaces here
void userConnected() {}

// needRedraw marks if redraw is required to prevent often redrawing.
bool needRedraw = true;

// Next variables hold the previous known values to determine if redraw is
// required.
String knownSsid = "";
IPAddress knownIp;
uint8_t knownBrightness = 0;
uint8_t knownMode = 0;
uint8_t knownPalette = 0;
uint8_t tftcharwidth = 19; // Number of chars that fit on screen with text size set to 2

long lastUpdate = 0;
long lastRedraw = 0;
bool displayTurnedOff = false;
// How often we are redrawing screen
#define USER_LOOP_REFRESH_RATE_MS 5000

void userLoop() {
void loop() override {

// Check if we time interval for redrawing passes.
if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) {
if (millis() - lastUpdate_mod < USER_LOOP_REFRESH_RATE_MS) {
return;
}
lastUpdate = millis();
lastUpdate_mod = millis();

// Turn off display after 5 minutes with no change.
if(!displayTurnedOff && millis() - lastRedraw > 5*60*1000) {
digitalWrite(TFT_BL, LOW); // Turn backlight off.
digitalWrite(TFT_BL, !TFT_BACKLIGHT_ON); // Turn backlight off.
displayTurnedOff = true;
}

Expand Down Expand Up @@ -140,6 +144,7 @@ void userLoop() {
knownPalette = strip.getMainSegment().palette;

tft.fillScreen(TFT_BLACK);
//tft.setTextColor(TFT_WHITE);
tft.setTextSize(2);
// First row with Wifi name
tft.setCursor(1, 1);
Expand All @@ -156,7 +161,7 @@ void userLoop() {
// tft.print(apPass);
// else
// tft.print(knownIp);

DEBUG_PRINTLN("Print known AP");
if (apActive) {
tft.print("AP IP: ");
tft.print(knownIp);
Expand All @@ -165,6 +170,7 @@ void userLoop() {
tft.print(apPass);
}
else {
DEBUG_PRINTLN("Print IP");
tft.print("IP: ");
tft.print(knownIp);
tft.setCursor(1,46);
Expand All @@ -180,16 +186,27 @@ void userLoop() {
char lineBuffer[tftcharwidth+1];
extractModeName(knownMode, JSON_mode_names, lineBuffer, tftcharwidth);
tft.print(lineBuffer);
DEBUG_PRINTLN("Print mode name");

// Fourth row with palette name
tft.setCursor(1, 90);
extractModeName(knownPalette, JSON_palette_names, lineBuffer, tftcharwidth);
tft.print(lineBuffer);
DEBUG_PRINTLN("Print palette");

// Fifth row with estimated mA usage
tft.setCursor(1, 112);
// Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate).
tft.print(strip.currentMilliamps);
//tft.print(strip.currentMilliamps);
tft.print(BusManager::currentMilliamps());
DEBUG_PRINTLN(BusManager::currentMilliamps());
DEBUG_PRINTLN(" mA (estimated)");
//tft.print("test ");
tft.print("mA (estimated)");
DEBUG_PRINTLN("Print estimated current");

}
}; // TTGO_T_DisplayMod

static TTGO_T_DisplayMod usermod_ttgo_t_display;
REGISTER_USERMOD(usermod_ttgo_t_display);
6 changes: 3 additions & 3 deletions wled00/usermod.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@
//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t)

//gets called once at boot. Do all initialization that doesn't depend on network here
void userSetup()
__attribute__((weak)) void userSetup()
{

}

//gets called every time WiFi is (re-)connected. Initialize own network interfaces here
void userConnected()
__attribute__((weak)) void userConnected()
{

}

//loop. You can use "if (WLED_CONNECTED)" to check for successful connection
void userLoop()
__attribute__((weak)) void userLoop()
{

}
Loading