Загрузка данных
#include "display.h"
#include "core/wifi/webInterface.h" // for server
#include "core/wifi/wg.h" //for isConnectedWireguard to print wireguard lock
#include "mykeyboard.h"
#include "settings.h" //for timeStr
#include "utils.h"
#include <JPEGDecoder.h>
#include <interface.h> //for charging ischarging to print charging indicator
#include <memory>
#define MAX_MENU_SIZE (int)(tftHeight / 25)
// Send the ST7789 into or out of sleep mode
void panelSleep(bool on) {
#if defined(ST7789_2_DRIVER) || defined(ST7789_DRIVER)
if (on) {
tft.writecommand(0x10); // SLPIN: panel off
delay(5);
} else {
tft.writecommand(0x11); // SLPOUT: panel on
delay(120);
}
#endif
// Disables tft writings on the display
tft.setSleepMode(on);
}
bool __attribute__((weak)) isCharging() { return false; }
/***************************************************************************************
** Function name: displayScrollingText
** Description: Scroll large texts into screen
***************************************************************************************/
void displayScrollingText(const String &text, Opt_Coord &coord, bool highlight) {
int len = text.length();
String displayText = text + " "; // Add spaces for smooth looping
int scrollLen = len + 8; // Full text plus space buffer
static int i = 0;
static long _lastmillis = 0;
if (highlight) tft.setTextColor(coord.bgcolor, coord.fgcolor);
else tft.setTextColor(coord.fgcolor, coord.bgcolor);
if (len < coord.size) {
// Text fits within limit, no scrolling needed
return;
} else if (millis() > _lastmillis + 200) {
String scrollingPart =
displayText.substring(i, i + (coord.size - 1)); // Display charLimit characters at a time
tft.fillRect(
coord.x,
coord.y,
(coord.size - 1) * LW * tft.getTextSize(),
LH * tft.getTextSize(),
highlight ? coord.fgcolor : bruceConfig.bgColor
); // Clear display area
tft.setCursor(coord.x, coord.y);
tft.print(scrollingPart);
if (i >= scrollLen - coord.size) i = -1; // Loop back
_lastmillis = millis();
i++;
if (i == 1) _lastmillis = millis() + 1000;
}
}
/***************************************************************************************
** Function name: TouchFooter
** Description: Draw touch screen footer
***************************************************************************************/
void TouchFooter(uint16_t color) {
#if defined(HAS_TOUCH)
tft.drawRoundRect(5, tftHeight + 2, tftWidth - 10, 43, 5, color);
tft.setTextColor(color);
tft.setTextSize(FM);
tft.drawCentreString("PREV", tftWidth / 6, tftHeight + 4, 1);
tft.drawCentreString("SEL", tftWidth / 2, tftHeight + 4, 1);
tft.drawCentreString("NEXT", 5 * tftWidth / 6, tftHeight + 4, 1);
#endif
}
/***************************************************************************************
** Function name: TouchFooter
** Description: Draw touch screen footer
***************************************************************************************/
void MegaFooter(uint16_t color) {
tft.drawRoundRect(5, tftHeight + 2, tftWidth - 10, 43, 5, color);
tft.setTextColor(color);
tft.setTextSize(FM);
tft.drawCentreString("Exit", tftWidth / 6, tftHeight + 4, 1);
tft.drawCentreString("UP", tftWidth / 2, tftHeight + 4, 1);
tft.drawCentreString("DOWN", 5 * tftWidth / 6, tftHeight + 4, 1);
}
/***************************************************************************************
** Function name: resetTftDisplay
** Description: set cursor to 0,0, screen and text to default color
***************************************************************************************/
void resetTftDisplay(int x, int y, uint16_t fc, int size, uint16_t bg, uint16_t screen) {
tft.setCursor(x, y);
tft.fillScreen(screen);
tft.setTextSize(size);
tft.setTextColor(fc, bg);
tft.setTextDatum(0);
}
/***************************************************************************************
** Function name: setTftDisplay
** Description: set cursor, font color, size and bg font color
***************************************************************************************/
void setTftDisplay(int x, int y, uint16_t fc, int size, uint16_t bg) {
if (x >= 0 && y < 0) tft.setCursor(x, tft.getCursorY()); // if -1 on x, sets only y
else if (x < 0 && y >= 0) tft.setCursor(tft.getCursorX(), y); // if -1 on y, sets only x
else if (x >= 0 && y >= 0) tft.setCursor(x, y); // if x and y > 0, sets both
tft.setTextSize(size);
tft.setTextColor(fc, bg);
}
void turnOffDisplay() { setBrightness(0, false); }
bool wakeUpScreen() {
previousMillis = millis();
if (isScreenOff) {
isScreenOff = false;
dimmer = false;
getBrightness();
vTaskDelay(pdMS_TO_TICKS(200));
return true;
} else if (dimmer) {
dimmer = false;
getBrightness();
vTaskDelay(pdMS_TO_TICKS(200));
return true;
}
return false;
}
/***************************************************************************************
** Function name: wrapText
** Description: Wrap text to fit within a maximum width, returning vector of lines
***************************************************************************************/
std::vector<String> wrapText(const String &text, int maxCharsPerLine) {
std::vector<String> lines;
if (maxCharsPerLine <= 0) return lines;
String remaining = text;
while (remaining.length() > 0) {
if (remaining.length() <= maxCharsPerLine) {
lines.push_back(remaining);
break;
}
// Find last space within maxCharsPerLine
int splitPos = -1;
for (int i = maxCharsPerLine - 1; i >= 0; i--) {
if (remaining[i] == ' ' || remaining[i] == '-' || remaining[i] == '_') {
splitPos = i;
break;
}
}
if (splitPos <= 0) {
// No word boundary found, force split at max
splitPos = maxCharsPerLine;
}
lines.push_back(remaining.substring(0, splitPos));
remaining = remaining.substring(splitPos + 1);
}
return lines;
}
/***************************************************************************************
** Function name: displayRedStripe
** Description: Display Red Stripe with information (supports multi-line text wrapping)
***************************************************************************************/
void displayRedStripe(const String &text, uint16_t fgcolor, uint16_t bgcolor) {
// detect if not running in interactive mode -> show nothing onscreen and return immediately
// if (server || isSleeping || isScreenOff) return; // webui is running
int size;
if (fgcolor == bgcolor && fgcolor == TFT_WHITE) fgcolor = TFT_BLACK;
// Calculate max chars per line based on font size
int maxCharsFM = (tftWidth - 20) / (LW * FM);
int maxCharsFP = (tftWidth - 20) / (LW * FP);
// Determine if we need to wrap the text
std::vector<String> wrappedLines;
int boxHeight = 26; // Default height for single line
if (text.length() * LW * FM < (tftWidth - 2 * FM * LW)) {
// Text fits with FM font
size = FM;
wrappedLines = wrapText(text, maxCharsFM);
} else {
// Text needs FP font or larger
size = FP;
wrappedLines = wrapText(text, maxCharsFP);
}
// Adjust box height based on number of lines
if (wrappedLines.size() > 1) { boxHeight = 13 + (wrappedLines.size() * (size == FM ? 8 : 10)); }
tft.drawPixel(0, 0, 0);
tft.fillRoundRect(10, tftHeight / 2 - boxHeight / 2, tftWidth - 20, boxHeight, 7, bgcolor);
tft.setTextColor(fgcolor, bgcolor);
tft.setTextSize(size);
// Draw each line centered
int lineHeight = size == FM ? 8 : 10;
int startY = tftHeight / 2 - (wrappedLines.size() * lineHeight) / 2;
for (size_t i = 0; i < wrappedLines.size(); i++) {
tft.drawCentreString(wrappedLines[i], tftWidth / 2, startY + i * lineHeight);
}
}
void drawButton(
int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color, const char *text, bool inverted = false
) {
if (inverted) {
tft.fillRoundRect(x, y, w, h, 5, color);
} else {
tft.fillRoundRect(x, y, w, h, 5, TFT_BLACK);
tft.drawRoundRect(x, y, w, h, 5, color);
}
tft.setTextColor(inverted ? TFT_BLACK : color);
tft.drawString(text, x + w / 2, y + h);
}
int8_t displayMessage(
const char *message, const char *leftButton, const char *centerButton, const char *rightButton,
uint16_t color
) {
#ifdef HAS_SCREEN
uint8_t oldTextDatum = tft.getTextDatum();
#endif
tft.setTextColor(color);
tft.setTextSize(FM);
tft.setTextDatum(TC_DATUM);
// Handle newline characters in message
String msg = String(message);
int y = tftHeight / 2 - 20;
int start = 0;
int end = msg.indexOf('\n');
while (end != -1) {
tft.drawString(msg.substring(start, end), tftWidth / 2, y);
y += FM * 8;
start = end + 1;
end = msg.indexOf('\n', start);
}
tft.drawString(msg.substring(start), tftWidth / 2, y);
tft.setTextDatum(BC_DATUM);
int16_t buttonHeight = 20;
int16_t buttonY = tftHeight - buttonHeight - 5;
int16_t buttonWidth = tftWidth / 3 - 10;
int8_t totalButtons = (leftButton ? 1 : 0) + (centerButton ? 1 : 0) + (rightButton ? 1 : 0);
int8_t selected = 0; // Start at first available button
bool redraw = true;
while (true) {
if (check(PrevPress) || check(EscPress)) {
selected = (selected - 1 + totalButtons) % totalButtons; // Cycle backward
redraw = true;
}
if (check(NextPress)) {
selected = (selected + 1) % totalButtons; // Cycle forward
redraw = true;
}
if (check(SelPress)) { break; }
// Draw buttons with selection highlighting
int8_t index = 0;
if (redraw) {
if (leftButton) {
drawButton(5, buttonY, buttonWidth, buttonHeight, color, leftButton, selected == index);
index++;
}
if (centerButton) {
drawButton(
tftWidth / 3 + 5,
buttonY,
buttonWidth,
buttonHeight,
color,
centerButton,
selected == index
);
index++;
}
if (rightButton) {
drawButton(
tftWidth * 2 / 3 + 5,
buttonY,
buttonWidth,
buttonHeight,
color,
rightButton,
selected == index
);
}
redraw = false;
}
delay(10);
}
#ifdef HAS_SCREEN
tft.setTextDatum(oldTextDatum);
#endif
return selected;
}
void displayError(const String &txt, bool waitKeyPress) {
displayRedStripe(txt);
Serial.println("ERR: " + txt);
#ifndef HAS_SCREEN
return;
#endif
delay(200);
while (waitKeyPress && !check(AnyKeyPress)) vTaskDelay(10 / portTICK_PERIOD_MS);
}
void displayWarning(const String &txt, bool waitKeyPress) {
displayRedStripe(txt, TFT_BLACK, TFT_YELLOW);
Serial.println("WARN: " + txt);
#ifndef HAS_SCREEN
return;
#endif
delay(200);
while (waitKeyPress && !check(AnyKeyPress)) vTaskDelay(10 / portTICK_PERIOD_MS);
}
void displayInfo(const String &txt, bool waitKeyPress) {
displayRedStripe(txt, TFT_WHITE, TFT_BLUE);
Serial.println("INFO: " + txt);
#ifndef HAS_SCREEN
return;
#endif
delay(200);
while (waitKeyPress && !check(AnyKeyPress)) vTaskDelay(10 / portTICK_PERIOD_MS);
}
void displaySuccess(const String &txt, bool waitKeyPress) {
displayRedStripe(txt, TFT_WHITE, TFT_DARKGREEN);
Serial.println("SUCCESS: " + txt);
#ifndef HAS_SCREEN
return;
#endif
delay(200);
while (waitKeyPress && !check(AnyKeyPress)) vTaskDelay(10 / portTICK_PERIOD_MS);
}
void displayTextLine(const String &txt, bool waitKeyPress) {
displayRedStripe(txt, getComplementaryColor2(bruceConfig.priColor), bruceConfig.priColor);
Serial.println("MESSAGE: " + txt);
#ifndef HAS_SCREEN
return;
#endif
delay(200);
while (waitKeyPress && !check(AnyKeyPress)) vTaskDelay(10 / portTICK_PERIOD_MS);
}
void setPadCursor(int16_t padx, int16_t pady) {
for (int y = 0; y < pady; y++) tft.println();
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
}
void padprintf(int16_t padx, const char *format, ...) {
char buffer[64];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.printf("%s", buffer);
}
void padprintf(const char *format, ...) {
char buffer[64];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
tft.setCursor(BORDER_PAD_X, tft.getCursorY());
tft.printf("%s", buffer);
}
void padprint(const String &s, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(s);
}
void padprint(const char str[], int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(str);
}
void padprint(char c, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(c);
}
void padprint(unsigned char b, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(b, base);
}
void padprint(int n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(n, base);
}
void padprint(unsigned int n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(n, base);
}
void padprint(long n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(n, base);
}
void padprint(unsigned long n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(n, base);
}
void padprint(long long n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(n, base);
}
void padprint(unsigned long long n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(n, base);
}
void padprint(double n, int digits, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.print(n, digits);
}
void padprintln(const String &s, int16_t padx) {
if (s.isEmpty()) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(s);
return;
}
String buff;
size_t start = 0;
int _maxCharsInLine = (tftWidth - (padx + 1) * BORDER_PAD_X) / (FP * LW);
// automatically split into multiple lines
while (!(buff = s.substring(start, start + _maxCharsInLine)).isEmpty()) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(buff);
start += buff.length();
}
}
void padprintln(const char str[], int16_t padx) {
if (strcmp(str, "") == 0) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(str);
return;
}
String buff;
size_t start = 0;
int _maxCharsInLine = (tftWidth - (padx + 1) * BORDER_PAD_X) / (FP * LW);
// automatically split into multiple lines
while (!(buff = String(str).substring(start, start + _maxCharsInLine)).isEmpty()) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(buff);
start += buff.length();
}
}
void padprintln(char c, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(c);
}
void padprintln(unsigned char b, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(b, base);
}
void padprintln(int n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(n, base);
}
void padprintln(unsigned int n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(n, base);
}
void padprintln(long n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(n, base);
}
void padprintln(unsigned long n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(n, base);
}
void padprintln(long long n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(n, base);
}
void padprintln(unsigned long long n, int base, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(n, base);
}
void padprintln(double n, int digits, int16_t padx) {
tft.setCursor(padx * BORDER_PAD_X, tft.getCursorY());
tft.println(n, digits);
}
/*********************************************************************
** Function: loopOptions
** Where you choose among the options in menu
**********************************************************************/
int loopOptions(
std::vector<Option> &options, uint8_t menuType, const char *subText, int index, bool interpreter
) {
if (options.empty()) return -1;
auto findFirstEnabled = [&]() -> int {
for (size_t i = 0; i < options.size(); i++) {
if (options[i].enabled) return static_cast<int>(i);
}
return -1;
};
auto findNextEnabled = [&](int start, int step) -> int {
if (options.empty()) return -1;
int size = static_cast<int>(options.size());
int idx = start;
for (int i = 0; i < size; i++) {
idx = (idx + step + size) % size;
if (options[idx].enabled) return idx;
}
return -1;
};
if (index < 0 || index >= static_cast<int>(options.size())) index = 0;
if (!options[index].enabled) {
int firstEnabled = findFirstEnabled();
if (firstEnabled < 0) return -1;
index = firstEnabled;
}
Opt_Coord coord;
bool redraw = true;
bool exit = false;
int menuSize = options.size();
int devModeCounter = 0;
static unsigned long _clock_bat_timer = millis();
if (options.size() > MAX_MENU_SIZE) { menuSize = MAX_MENU_SIZE; }
if (index > 0)
tft.fillRoundRect(
tftWidth * 0.10,
tftHeight / 2 - menuSize * (FM * 8 + 4) / 2 - 5,
tftWidth * 0.8,
(FM * 8 + 4) * menuSize + 10,
5,
bruceConfig.bgColor
);
if (index >= options.size()) index = 0;
bool firstRender = true;
unsigned long menuOpenTs =
0; // timestamp when this menu was first rendered (per-invocation, not shared across nested menus)
drawMainBorder();
while (1) {
// Check for shutdown before drawing menu to avoid drawing a black bar on the screen
if (exit) break;
if (menuType == MENU_TYPE_MAIN) {
checkReboot();
if (devModeCounter >= 5 && !bruceConfig.devMode) {
bruceConfig.setDevMode(true);
displayInfo("Dev Mode Enabled", true);
}
if (millis() - _clock_bat_timer > 30000) {
_clock_bat_timer = millis();
drawStatusBar(); // update clock and battery status each 30s
}
}
if (redraw) {
menuOptionType = menuType; // updates menutype to the remote controller
menuOptionLabel = subText;
// update the hovered
for (auto &opt : options) opt.hovered = false;
options[index].hovered = true;
bool renderedByLambda = false;
if (options[index].hover)
renderedByLambda = options[index].hover(options[index].hoverPointer, true);
if (!renderedByLambda) {
if (menuType == MENU_TYPE_SUBMENU) drawSubmenu(index, options, subText);
else
coord = drawOptions(
index,
options,
bruceConfig.priColor,
bruceConfig.secColor,
bruceConfig.bgColor,
firstRender
);
}
if (firstRender) menuOpenTs = millis();
firstRender = false;
redraw = false;
}
// handleSerialCommands(); // always use serial task for it
#ifdef HAS_KEYBOARD
// Only process shortcuts on the main menu; the menuType check must short-circuit
// checkShortcutPress() so a held key can't re-fire inside the submenu it just opened.
// Break so the caller rebuilds the menu, since the shortcut refilled the shared global
// `options` (like selecting an option does) rather than repainting stale options.
if (menuType == MENU_TYPE_MAIN && checkShortcutPress()) break;
#endif
if (menuType == MENU_TYPE_REGULAR) {
String txt = options[index].label;
displayScrollingText(txt, coord, true);
}
// Checks ESC Press first, to not exit after PrevPress is processed
// PrevPress condition is a StickCPlus workaround, as it uses the same button for Prev and Esc
// Same happens to Core and some other boards
#ifdef HAS_3_BUTTONS
if (EscPress && PrevPress) EscPress = false;
#endif
if (menuType != MENU_TYPE_MAIN && check(EscPress)) {
index = -1;
break;
}
#ifdef HAS_ENCODER
int32_t rotarySteps = drainRotarySteps();
if (rotarySteps != 0) {
check(PrevPress);
check(NextPress);
check(UpPress);
check(DownPress);
devModeCounter = 0;
while (rotarySteps > 0) {
int prevEnabled = findNextEnabled(index, -1);
if (prevEnabled < 0) break;
index = prevEnabled;
rotarySteps--;
redraw = true;
}
while (rotarySteps < 0) {
int nextEnabled = findNextEnabled(index, +1);
if (nextEnabled < 0) break;
if (!bruceConfig.devMode && nextEnabled <= index) devModeCounter++;
index = nextEnabled;
rotarySteps++;
redraw = true;
}
vTaskDelay(4 / portTICK_PERIOD_MS);
PrevPress = false;
NextPress = false;
UpPress = false;
DownPress = false;
} else
#endif
{
if (PrevPress || check(UpPress)) {
devModeCounter = 0;
#ifdef HAS_KEYBOARD
check(PrevPress);
int prevEnabled = findNextEnabled(index, -1);
if (prevEnabled >= 0) index = prevEnabled;
redraw = true;
#else
long _tmp = millis();
#ifndef HAS_ENCODER // T-Embed doesn't need it
LongPress = true;
while (PrevPress && menuType != MENU_TYPE_MAIN) {
if (millis() - _tmp > 200)
tft.drawArc(
tftWidth / 2,
tftHeight / 2,
25,
15,
0,
360 * (millis() - (_tmp + 200)) / 500,
getColorVariation(bruceConfig.priColor),
bruceConfig.bgColor
);
vTaskDelay(10 / portTICK_RATE_MS);
}
tft.drawArc(
tftWidth / 2, tftHeight / 2, 25, 15, 0, 360, bruceConfig.bgColor, bruceConfig.bgColor
);
LongPress = false;
#endif
if (millis() - _tmp > 700) { // longpress detected to exit
index = -1;
break;
} else {
check(PrevPress);
int prevEnabled = findNextEnabled(index, -1);
if (prevEnabled >= 0) index = prevEnabled;
redraw = true;
}
#endif
}
/* DW Btn to next item */
if (check(NextPress) || check(DownPress)) {
int nextEnabled = findNextEnabled(index, +1);
if (nextEnabled >= 0) {
if (!bruceConfig.devMode && nextEnabled <= index) devModeCounter++;
index = nextEnabled;
}
redraw = true;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
/* Select and run function
forceMenuOption is set by a SerialCommand to force a selection within the menu
*/
// Prevent immediate selection if the SEL button was already being held when the menu opened.
// Allow a short grace period for the user to release the button first.
static const unsigned long MENU_SELECT_IGNORE_MS = 600; // ms to ignore SEL after menu opens
if (forceMenuOption >= 0 || (millis() - menuOpenTs > MENU_SELECT_IGNORE_MS && check(SelPress))) {
uint16_t chosen = index;
if (forceMenuOption >= 0) {
chosen = forceMenuOption;
forceMenuOption = -1; // reset SerialCommand navigation option
Serial.print("Forcely ");
}
if (chosen >= options.size() || !options[chosen].enabled) continue;
Serial.println("Selected: " + String(options[chosen].label));
options[chosen].operation();
break;
}
// interpreter_start -> running the interpreter
// interpreter -> loopOptions helper inside the Javascript
if (interpreter_state > 0 && !interpreter) { break; }
}
RotaryNetSteps = 0; // reset rotary steps to avoid unexpected jumps in the next menu
return index;
}
/***************************************************************************************
** Function name: progressHandler
** Description: Função para manipular o progresso da atualização
** Dependencia: prog_handler =>> 0 - Flash, 1 - LittleFS
***************************************************************************************/
void progressHandler(int progress, size_t total, const String &message) {
int barWidth = map(progress, 0, total, 0, tftWidth - 40);
if (barWidth < 3) {
tft.fillRect(6, 27, tftWidth - 12, tftHeight - 33, bruceConfig.bgColor);
tft.drawRect(18, tftHeight - 47, tftWidth - 36, 17, bruceConfig.priColor);
displayRedStripe(message, TFT_WHITE, bruceConfig.priColor);
}
tft.fillRect(20, tftHeight - 45, barWidth, 13, bruceConfig.priColor);
}
/***************************************************************************************
** Function name: drawOptions
** Description: Função para desenhar e mostrar as opçoes de contexto
***************************************************************************************/
Opt_Coord drawOptions(
int index, std::vector<Option> &options, uint16_t fgcolor, uint16_t selcolor, uint16_t bgcolor,
bool firstRender
) {
static int last_index = 0;
Opt_Coord coord;
int menuSize = options.size();
if (options.size() > MAX_MENU_SIZE) { menuSize = MAX_MENU_SIZE; }
// Uncomment to update the statusBar (causes flickering)
// drawStatusBar();
int32_t optionsTopY = tftHeight / 2 - menuSize * (FM * 8 + 4) / 2 - 5;
tft.drawPixel(0, 0, bruceConfig.bgColor);
if (firstRender) {
tft.fillRoundRect(
tftWidth * 0.10, optionsTopY, tftWidth * 0.8, (FM * 8 + 4) * menuSize + 10, 5, bgcolor
);
tft.drawRoundRect(
tftWidth * 0.10,
tftHeight / 2 - menuSize * (FM * 8 + 4) / 2 - 5,
tftWidth * 0.8,
(FM * 8 + 4) * menuSize + 10,
5,
fgcolor
);
}
tft.setTextColor(fgcolor, bgcolor);
tft.setTextSize(FM);
tft.setCursor(tftWidth * 0.10 + 5, tftHeight / 2 - menuSize * (FM * 8 + 4) / 2);
int i = 0;
int init = 0;
int cont = 1;
if (index >= MAX_MENU_SIZE) init = index - MAX_MENU_SIZE + 1;
// check if cycling from last item to first
if (abs(index - last_index) >= menuSize) {
if (index > last_index) last_index = init; // from first to last
else last_index = menuSize - 1; // from last to first
}
cont = 1;
for (i = 0; i < options.size(); i++) {
if (i >= init) {
int16_t cursorY = tft.getCursorY();
// Erase previously highlited element,
if (i == last_index) {
tft.fillRoundRect(
tftWidth * 0.10 + 2, cursorY + 2, tftWidth * 0.8 - 4, FM * LH + 2, 3, bruceConfig.bgColor
);
}
// Draw selection highlight bar
if (i == index) {
tft.fillRoundRect(
tftWidth * 0.10 + 2, cursorY + 2, tftWidth * 0.8 - 4, FM * LH + 2, 3, bruceConfig.priColor
);
}
if (options[i].selected) tft.setTextColor(selcolor, bgcolor); // if selected, change Text color
else tft.setTextColor(fgcolor, bgcolor);
if (!options[i].enabled) tft.setTextColor(TFT_DARKGREY, bgcolor);
String text = "";
if (i == index) {
text += ">";
coord.x = tftWidth * 0.10 + 5 + FM * LW;
coord.y = tft.getCursorY() + 4;
coord.size = (tftWidth * 0.8 - 10) / (LW * FM) - 1;
coord.fgcolor = fgcolor;
coord.bgcolor = bgcolor;
} else text += " ";
text += String(options[i].label) + " ";
tft.setCursor(tftWidth * 0.10 + 5, tft.getCursorY() + 4);
// Draw text with appropriate colors for selection
if (i == index) { tft.setTextColor(bgcolor, bruceConfig.priColor); }
tft.println(text.substring(0, (tftWidth * 0.8 - 10) / (LW * FM) - 1));
// Reset text color for next item
tft.setTextColor(fgcolor, bgcolor);
cont++;
}
if (cont > MAX_MENU_SIZE) break;
}
// update history
last_index = index;
#if defined(HAS_TOUCH)
TouchFooter();
#endif
return coord;
}
/***************************************************************************************
** Function name: drawSubmenu
** Description: Função para desenhar e mostrar as opçoes de contexto
***************************************************************************************/
void drawSubmenu(int index, std::vector<Option> &options, const char *title) {
drawStatusBar();
int menuSize = options.size();
tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor);
tft.setTextSize(FP);
tft.drawPixel(0, 0, 0);
tft.fillRect(6, 30, tftWidth - 12, 8 * FP, bruceConfig.bgColor);
tft.drawString(title, 12, 30);
// middle of the drawing area
int middle = 25 /*status*/ + (tftHeight - 30 /*status + bottom margin*/) / 2;
// drawCentreString uses TC_DATUM, so we need to adjust the Y position
// 42 ensures that title isnt touched( 30 + 8 (LH) + 4(Margin))
int middle_up = middle - (tftHeight - 42) / 3 - FM * LH / 2 + 4;
int middle_down = middle + (tftHeight - 42) / 3 - FM * LH / 2;
tft.setTextSize(FM);
#if defined(HAS_TOUCH)
tft.drawCentreString("/\\", tftWidth / 2, middle_up - (FM * LH + 6), 1);
#endif
// Previous item
int firstIndex = index - 1 >= 0 ? index - 1 : menuSize - 1;
const char *firstOption = options[firstIndex].label.c_str();
tft.setTextColor(options[firstIndex].enabled ? bruceConfig.secColor : TFT_DARKGREY);
tft.fillRect(6, middle_up, tftWidth - 12, 8 * FM, bruceConfig.bgColor);
tft.drawCentreString(firstOption, tftWidth / 2, middle_up, SMOOTH_FONT);
// Selected item
int selectedTextSize = options[index].label.length() <= tftWidth / (LW * FG) - 1 ? FG : FM;
tft.setTextSize(selectedTextSize);
tft.setTextColor(options[index].enabled ? bruceConfig.priColor : TFT_DARKGREY);
tft.fillRect(6, middle - FG * LH / 2 - 1, tftWidth - 12, FG * LH + 5, bruceConfig.bgColor);
tft.drawCentreString(options[index].label, tftWidth / 2, middle - selectedTextSize * LH / 2, SMOOTH_FONT);
tft.drawFastHLine(
tftWidth / 2 - strlen(options[index].label.c_str()) * selectedTextSize * LW / 2,
middle + selectedTextSize * LH / 2 + 1,
strlen(options[index].label.c_str()) * selectedTextSize * LW,
bruceConfig.priColor
);
// Next Item
int thirdIndex = index + 1 < menuSize ? index + 1 : 0;
const char *thirdOption = options[thirdIndex].label.c_str();
tft.setTextSize(FM);
tft.setTextColor(options[thirdIndex].enabled ? bruceConfig.secColor : TFT_DARKGREY);
tft.fillRect(6, middle_down, tftWidth - 12, 8 * FM, bruceConfig.bgColor);
tft.drawCentreString(thirdOption, tftWidth / 2, middle_down, SMOOTH_FONT);
tft.fillRect(tftWidth - 5, 0, 5, tftHeight, bruceConfig.bgColor);
tft.fillRect(tftWidth - 5, index * tftHeight / menuSize, 5, tftHeight / menuSize, bruceConfig.priColor);
#if defined(HAS_TOUCH)
tft.drawCentreString("\\/", tftWidth / 2, middle_down + (FM * LH + 6), 1);
tft.setTextColor(getColorVariation(bruceConfig.priColor), bruceConfig.bgColor);
tft.drawString("[ x ]", 7, 7, 1);
TouchFooter();
#endif
}
void drawStatusBar() {
uint8_t bat = getBattery();
if (bat > 0) drawBatteryStatus(bat);
if (bruceConfig.theme.border) {
tft.drawRoundRect(5, 5, tftWidth - 10, tftHeight - 10, 5, bruceConfig.priColor);
tft.drawLine(5, 25, tftWidth - 6, 25, bruceConfig.priColor);
}
if (clock_set) {
setTftDisplay(12, 12, bruceConfig.priColor, 1, bruceConfig.bgColor);
tft.fillRect(12, 12, 60, LH, bruceConfig.bgColor);
#if defined(HAS_RTC)
updateTimeStr(_rtc.getTimeStruct());
#else
updateTimeStr(rtc.getTimeStruct());
#endif
tft.print(timeStr);
} else {
setTftDisplay(12, 12, bruceConfig.priColor, 1, bruceConfig.bgColor);
tft.print("BRUCE " + String(BRUCE_VERSION));
}
int iconCount = 0;
bool showSD = sdcardMounted;
bool showGPS = gpsConnected;
bool showWifi = (WiFi.getMode() != 0);
bool showWeb = isWebUIActive;
bool showBLE = BLEConnected;
bool showWG = isConnectedWireguard;
if (showSD) iconCount++;
if (showGPS) iconCount++;
if (showWifi) iconCount++;
if (showWeb) iconCount++;
if (showBLE) iconCount++;
if (showWG) iconCount++;
if (iconCount > 0) {
const int IW = 16;
const int IH = 16;
const int GAP = 6;
int totalW = iconCount * IW + (iconCount - 1) * GAP;
int sx = (tftWidth - totalW) / 2;
int iy = 7;
int idx = 0;
if (showSD) {
int x = sx + idx * (IW + GAP);
tft.fillRect(x, iy, IW, IH, bruceConfig.bgColor);
drawSdSmall(x, iy);
idx++;
}
if (showGPS) {
int x = sx + idx * (IW + GAP);
tft.fillRect(x, iy, IW, IH, bruceConfig.bgColor);
drawGpsSmall(x, iy);
idx++;
}
if (showWifi) {
int x = sx + idx * (IW + GAP);
tft.fillRect(x, iy, IW, IH, bruceConfig.bgColor);
drawWifiSmall(x, iy);
idx++;
}
if (showWeb) {
int x = sx + idx * (IW + GAP);
tft.fillRect(x, iy, IW, IH, bruceConfig.bgColor);
drawWebUISmall(x, iy);
idx++;
}
if (showBLE) {
int x = sx + idx * (IW + GAP);
tft.fillRect(x, iy, IW, IH, bruceConfig.bgColor);
drawBLESmall(x, iy);
idx++;
}
if (showWG) {
int x = sx + idx * (IW + GAP);
tft.fillRect(x, iy, IW, IH, bruceConfig.bgColor);
drawWireguardStatus(x, iy);
idx++;
}
}
}
void drawMainBorder(bool clear) {
if (clear) {
tft.drawPixel(0, 0, 0);
tft.fillScreen(bruceConfig.bgColor);
}
setTftDisplay(12, 12, bruceConfig.priColor, 1, bruceConfig.bgColor);
tft.setTextDatum(0);
// if(wifiConnected) {tft.print(timeStr);} else {tft.print("BRUCE 1.0b");}
drawStatusBar();
#if defined(HAS_TOUCH)
TouchFooter();
#endif
}
void drawMainBorderWithTitle(const String &title, bool clear) {
drawMainBorder(clear);
printTitle(title);
}
void printTitle(const String &title) {
String t = title;
t.toUpperCase();
tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor);
// Scale down title font if it doesn't fit the screen width
int titleSize = FM;
while (titleSize > FP && (int)(t.length() * titleSize * LW) > tftWidth - 2 * BORDER_PAD_X) {
titleSize--;
}
tft.setTextSize(titleSize);
tft.setCursor((tftWidth - (t.length() * titleSize * LW)) / 2, BORDER_PAD_Y);
tft.println(t);
tft.setTextSize(FP);
}
void printSubtitle(const String &subtitle, bool withLine) {
int16_t cursorX = (tftWidth - (subtitle.length() * FP * LW)) / 2;
tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor);
tft.setTextSize(FP);
tft.setCursor(cursorX, BORDER_PAD_Y + FM * LH);
tft.println(subtitle);
if (withLine) {
String line = "";
for (byte i = 0; i < subtitle.length(); i++) line += "-";
tft.setCursor(cursorX, tft.getCursorY());
tft.println(line);
}
}
void printFootnote(const String &text) {
tft.setTextSize(FP);
tft.drawRightString(text, tftWidth - BORDER_PAD_X, tftHeight - BORDER_PAD_X - FP * LH, SMOOTH_FONT);
}
void printCenterFootnote(const String &text) {
tft.fillRect(10, tftHeight - BORDER_PAD_X - FP * LH, tftWidth - 20, FP * LH, bruceConfig.bgColor);
tft.setTextSize(FP);
tft.drawCentreString(text, tftWidth / 2, tftHeight - BORDER_PAD_X - FP * LH, SMOOTH_FONT);
}
void drawBatteryStatus(uint8_t bat) {
if (bat == 0) return;
bool charging = isCharging();
uint16_t color = bruceConfig.priColor;
uint16_t barcolor = bruceConfig.priColor;
if (bat < 16) barcolor = color = TFT_RED;
tft.drawRoundRect(tftWidth - 42, 7, 34, 17, 2, color);
tft.setTextSize(FP);
tft.fillRect(tftWidth - 85, 7, 42, 18, bruceConfig.bgColor);
if (charging) {
tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor);
tft.drawRightString("CHG", tftWidth - 44, 12, 1);
} else {
tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor);
tft.drawRightString((bat == 100 ? "" : " ") + String(bat) + "%", tftWidth - 44, 12, 1);
}
tft.fillRoundRect(tftWidth - 40, 9, 30 * bat / 100, 13, 2, barcolor);
tft.drawLine(tftWidth - 30, 9, tftWidth - 30, 9 + 13, bruceConfig.bgColor);
tft.drawLine(tftWidth - 20, 9, tftWidth - 20, 9 + 13, bruceConfig.bgColor);
}
void drawWireguardStatus(int x, int y) {
tft.fillRect(x, y, 16, 16, bruceConfig.bgColor);
if (isConnectedWireguard) {
tft.drawRoundRect(x + 4, y + 2, 8, 8, 3, bruceConfig.priColor);
tft.fillRoundRect(x + 3, y + 7, 10, 7, 1, bruceConfig.priColor);
} else {
tft.drawRoundRect(x + 4, y + 2, 8, 8, 3, bruceConfig.priColor);
tft.drawRoundRect(x + 3, y + 7, 10, 7, 1, bruceConfig.priColor);
}
}
/***************************************************************************************
** Function name: listFiles
** Description: Função para desenhar e mostrar o menu principal
***************************************************************************************/
#define MAX_ITEMS (int)(tftHeight - 20) / (LH * FM)
Opt_Coord listFiles(int index, std::vector<FileList> fileList) {
Opt_Coord coord;
tft.drawPixel(0, 0, bruceConfig.bgColor);
if (index == 0) {
tft.fillScreen(bruceConfig.bgColor);
tft.drawRoundRect(5, 5, tftWidth - 10, tftHeight - 10, 5, bruceConfig.priColor);
}
tft.setCursor(10, 10);
tft.setTextSize(FM);
int i = 0;
int arraySize = fileList.size();
int start = 0;
if (index >= MAX_ITEMS) {
start = index - MAX_ITEMS + 1;
if (start < 0) start = 0;
}
int nchars = (tftWidth - 20) / (6 * tft.getTextSize());
String txt = ">";
while (i < arraySize) {
if (i >= start) {
tft.setCursor(10, tft.getCursorY());
if (fileList[i].folder == true)
tft.setTextColor(getColorVariation(bruceConfig.priColor), bruceConfig.bgColor);
else if (fileList[i].operation == true) tft.setTextColor(ALCOLOR, bruceConfig.bgColor);
else { tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); }
if (index == i) {
txt = ">";
coord.x = 10 + FM * LW;
coord.y = tft.getCursorY();
coord.size = nchars;
coord.fgcolor =
fileList[i].folder ? getColorVariation(bruceConfig.priColor) : bruceConfig.priColor;
coord.bgcolor = bruceConfig.bgColor;
} else txt = " ";
txt += fileList[i].filename + " ";
tft.println(txt.substring(0, nchars));
}
i++;
if (i == (start + MAX_ITEMS) || i == arraySize) break;
}
return coord;
}
// desenhos do menu principal, sprite "draw" com 80x80 pixels
void drawSdSmall(int x, int y) {
tft.fillRect(x, y, 16, 16, bruceConfig.bgColor);
tft.drawLine(x + 3, y + 2, x + 3, y + 14, bruceConfig.priColor);
tft.drawLine(x + 3, y + 14, x + 13, y + 14, bruceConfig.priColor);
tft.drawLine(x + 13, y + 14, x + 13, y + 5, bruceConfig.priColor);
tft.drawLine(x + 13, y + 5, x + 10, y + 2, bruceConfig.priColor);
tft.drawLine(x + 10, y + 2, x + 3, y + 2, bruceConfig.priColor);
tft.drawLine(x + 5, y + 4, x + 5, y + 6, bruceConfig.priColor);
tft.drawLine(x + 7, y + 4, x + 7, y + 6, bruceConfig.priColor);
tft.drawLine(x + 9, y + 4, x + 9, y + 6, bruceConfig.priColor);
tft.drawLine(x + 11, y + 5, x + 11, y + 6, bruceConfig.priColor);
}
void drawWifiSmall(int x, int y) {
tft.fillRect(x, y, 16, 16, bruceConfig.bgColor);
tft.fillCircle(x + 8, y + 13, 1, bruceConfig.priColor);
tft.drawArc(x + 8, y + 13, 4, 6, 135, 225, bruceConfig.priColor, bruceConfig.bgColor);
tft.drawArc(x + 8, y + 13, 9, 11, 135, 225, bruceConfig.priColor, bruceConfig.bgColor);
}
void drawWebUISmall(int x, int y) {
tft.fillRect(x, y, 16, 16, bruceConfig.bgColor);
tft.drawCircle(x + 8, y + 8, 6, bruceConfig.priColor);
tft.drawLine(x + 3, y + 4, x + 13, y + 4, bruceConfig.priColor);
tft.drawLine(x + 2, y + 8, x + 14, y + 8, bruceConfig.priColor);
tft.drawLine(x + 3, y + 12, x + 13, y + 12, bruceConfig.priColor);
}
void drawBLESmall(int x, int y) {
tft.fillRect(x, y, 16, 16, bruceConfig.bgColor);
tft.drawWideLine(x + 8, y + 8, x + 4, y + 4, 2, bruceConfig.priColor, bruceConfig.bgColor);
tft.drawWideLine(x + 8, y + 8, x + 4, y + 12, 2, bruceConfig.priColor, bruceConfig.bgColor);
tft.drawTriangle(x + 8, y + 8, x + 8, y + 2, x + 12, y + 5, bruceConfig.priColor);
tft.drawTriangle(x + 8, y + 8, x + 8, y + 14, x + 12, y + 11, bruceConfig.priColor);
}
void drawBLE_beacon(int x, int y, uint16_t color) {
tft.fillRect(x, y, 40, 80, bruceConfig.bgColor);
tft.drawWideLine(40 + x, 53 + y, 2 + x, 26 + y, 5, color, bruceConfig.bgColor);
tft.drawWideLine(40 + x, 26 + y, 2 + x, 53 + y, 5, color, bruceConfig.bgColor);
tft.drawWideLine(40 + x, 53 + y, 20 + x, 68 + y, 5, color, bruceConfig.bgColor);
tft.drawWideLine(40 + x, 26 + y, 20 + x, 12 + y, 5, color, bruceConfig.bgColor);
tft.drawWideLine(20 + x, 12 + y, 20 + x, 68 + y, 5, color, bruceConfig.bgColor);
tft.fillTriangle(40 + x, 26 + y, 20 + x, 40 + y, 20 + x, 12 + y, color);
tft.fillTriangle(40 + x, 53 + y, 20 + x, 40 + y, 20 + x, 68 + y, color);
}
void drawGPS(int x, int y) {
tft.fillRect(x, y, 80, 80, bruceConfig.bgColor);
tft.drawEllipse(40 + x, 70 + y, 15, 8, bruceConfig.priColor);
tft.drawArc(40 + x, 25 + y, 23, 7, 0, 340, bruceConfig.priColor, bruceConfig.bgColor);
tft.fillTriangle(40 + x, 70 + y, 20 + x, 64 + y, 60 + x, 64 + y, bruceConfig.priColor);
}
void drawGpsSmall(int x, int y) {
tft.fillRect(x, y, 16, 16, bruceConfig.bgColor);
tft.drawEllipse(x + 8, y + 13, 4, 3, bruceConfig.priColor);
tft.drawArc(x + 8, y + 5, 5, 2, 0, 360, bruceConfig.priColor, bruceConfig.bgColor);
tft.fillTriangle(x + 8, y + 14, x + 4, y + 8, x + 12, y + 8, bruceConfig.priColor);
}
void drawCreditCard(int x, int y) {
tft.fillRect(x, y, 70, 50, bruceConfig.bgColor);
tft.fillRoundRect(x + 5, y + 5, 60, 40, 5, bruceConfig.priColor);
tft.fillRect(x + 5, y + 15, 60, 10, getColorVariation(bruceConfig.priColor, 3, -1));
tft.fillRect(x + 10, y + 30, 12, 10, getColorVariation(bruceConfig.priColor, 3, 1));
tft.drawRect(x + 10, y + 30, 12, 10, getColorVariation(bruceConfig.priColor, 5, -1));
tft.drawRect(x + 10 + 4, y + 30, 4, 10, getColorVariation(bruceConfig.priColor, 5, -1));
tft.drawRect(x + 10, y + 33, 5, 4, getColorVariation(bruceConfig.priColor, 5, -1));
tft.drawRect(x + 17, y + 33, 5, 4, getColorVariation(bruceConfig.priColor, 5, -1));
tft.fillRect(x + 30, y + 35, 30, 5, getColorVariation(bruceConfig.priColor, 5, 1));
}
void drawMfkey32Icon(int x, int y) {
tft.drawRect(x + 2, y + 15, 24, 40, bruceConfig.priColor);
tft.drawRect(x + 5, y + 18, 18, 12, bruceConfig.priColor);
tft.drawRect(x + 5, y + 34, 18, 18, bruceConfig.priColor);
tft.drawLine(x + 5, y + 40, x + 22, y + 40, bruceConfig.priColor);
tft.drawLine(x + 5, y + 46, x + 22, y + 46, bruceConfig.priColor);
tft.drawLine(x + 11, y + 34, x + 11, y + 51, bruceConfig.priColor);
tft.drawLine(x + 17, y + 34, x + 17, y + 51, bruceConfig.priColor);
tft.drawRect(x + 30, y + 10, 25, 35, bruceConfig.priColor);
int startX = x + 32;
int startY = y + 12;
int endX = x + 52;
int endY = y + 32;
int step = 2;
int turns = 0;
while (startX <= endX && startY <= endY && turns < 3) {
for (int i = startX; i <= endX; i++) { tft.drawPixel(i, startY, bruceConfig.priColor); }
startY += step;
for (int i = startY; i <= endY; i++) { tft.drawPixel(endX, i, bruceConfig.priColor); }
endX -= step;
for (int i = endX; i >= startX; i--) { tft.drawPixel(i, endY, bruceConfig.priColor); }
endY -= step;
for (int i = endY; i >= startY; i--) { tft.drawPixel(startX, i, bruceConfig.priColor); }
startX += step;
turns++;
}
tft.fillRect(x + 40, y + 36, 6, 6, getColorVariation(bruceConfig.priColor, 3, 1));
}
void drawMfkey64Icon(int x, int y) {
drawMfkey32Icon(x, y);
tft.fillRoundRect(x + 40, y + 6, 24, 14, 4, bruceConfig.bgColor);
tft.drawRoundRect(x + 40, y + 6, 24, 14, 4, getColorVariation(bruceConfig.priColor, 3, -1));
tft.drawCircle(x + 48, y + 12, 4, getColorVariation(bruceConfig.priColor, 3, -1));
}
// ####################################################################################################
// Draw a JPEG on the TFT, images will be cropped on the right/bottom sides if they do not fit
// ####################################################################################################
// from:
// https://github.com/Bodmer/TFT_eSPI/blob/master/examples/Generic/ESP32_SDcard_jpeg/ESP32_SDcard_jpeg.ino
// This function assumes xpos,ypos is a valid screen coordinate. For convenience images that do not
// fit totally on the screen are cropped to the nearest MCU size and may leave right/bottom borders.
void jpegRender(int xpos, int ypos) {
// jpegInfo(); // Print information from the JPEG file (could comment this line out)
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;
bool swapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);
// Jpeg images are draw as a set of image block (tiles) called Minimum Coding Units (MCUs)
// Typically these MCUs are 16x16 pixel blocks
// Determine the width and height of the right and bottom edge image blocks
uint32_t min_w = jpg_min(mcu_w, max_x % mcu_w);
uint32_t min_h = jpg_min(mcu_h, max_y % mcu_h);
// save the current image block size
uint32_t win_w = mcu_w;
uint32_t win_h = mcu_h;
// save the coordinate of the right and bottom edges to assist image cropping
// to the screen size
max_x += xpos;
max_y += ypos;
// Fetch data from the file, decode and display
tft.fillRect(xpos, ypos, JpegDec.width, JpegDec.height, TFT_BLACK);
while (JpegDec.read()) { // While there is more data in the file
pImg = JpegDec.pImage; // Decode a MCU (Minimum Coding Unit, typically a 8x8 or 16x16 pixel block)
// Calculate coordinates of top left corner of current MCU
int mcu_x = JpegDec.MCUx * mcu_w + xpos;
int mcu_y = JpegDec.MCUy * mcu_h + ypos;
// check if the image block size needs to be changed for the right edge
if (mcu_x + mcu_w <= max_x) win_w = mcu_w;
else win_w = min_w;
// check if the image block size needs to be changed for the bottom edge
if (mcu_y + mcu_h <= max_y) win_h = mcu_h;
else win_h = min_h;
// copy pixels into a contiguous block
if (win_w != mcu_w) {
uint16_t *cImg;
int p = 0;
cImg = pImg + win_w;
for (int h = 1; h < win_h; h++) {
p += mcu_w;
for (int w = 0; w < win_w; w++) {
*cImg = *(pImg + w + p);
cImg++;
}
}
}
// calculate how many pixels must be drawn
uint32_t mcu_pixels = win_w * win_h;
// draw image MCU block only if it will fit on the screen
if ((mcu_x + win_w) <= tft.width() && (mcu_y + win_h) <= tft.height())
tft.pushImage(mcu_x, mcu_y, win_w, win_h, pImg);
else if ((mcu_y + win_h) > tft.height())
JpegDec.abort(); // Image has run off bottom of screen so abort decoding
}
tft.setSwapBytes(swapBytes);
}
bool showJpeg(FS &fs, const String &filename, int x, int y, bool center) {
// record the current time so we can measure how long it takes to draw an image
uint32_t drawTime = millis();
File picture;
if (fs.exists(filename)) picture = fs.open(filename, FILE_READ);
else return false;
const size_t data_size = picture.size();
// Alloc memory into heap
uint8_t *data_array = new uint8_t[data_size];
if (data_array == nullptr) {
// Fail allocating memory
picture.close();
delete[] data_array;
return false;
}
uint8_t data;
int i = 0;
byte line_len = 0;
while (picture.available()) {
data = picture.read();
data_array[i] = data;
i++;
// print array on Serial
/*
Serial.print("0x");
if (abs(data) < 16) {
Serial.print("0");
}
Serial.print(data, HEX);
Serial.print(","); // Add value and comma
line_len++;
if (line_len >= 32) {
line_len = 0;
Serial.println();
}
*/
}
picture.close();
bool decoded = false;
if (data_array) {
decoded = JpegDec.decodeArray(data_array, data_size);
} else {
displayError(filename + " Fail");
delay(2500);
delete[] data_array; // free heap before leaving
return false;
}
if (decoded) {
if (center) {
x = x + (tftWidth - JpegDec.width) / 2;
y = y + (tftHeight - JpegDec.height) / 2;
}
jpegRender(x, y);
}
// calculate how long it took to draw the image
drawTime = millis() - drawTime; // Calculate the time it took
// print the results to the serial port
Serial.print("Total render time was : ");
Serial.print(drawTime);
Serial.println(" ms");
Serial.println("=====================================");
delete[] data_array; // free heap before leaving
return true;
}
bool showJpeg(const uint8_t *data_array, size_t data_size, int x, int y, bool center) {
bool decoded = false;
if (data_array) {
decoded = JpegDec.decodeArray(data_array, data_size);
} else {
return false;
}
if (decoded) {
if (center) {
x = x + (tftWidth - JpegDec.width) / 2;
y = y + (tftHeight - JpegDec.height) / 2;
}
jpegRender(x, y);
}
return true;
}
#if !defined(LITE_VERSION)
// ####################################################################################################
// Draw a GIF on the TFT
// derived from
// https://github.com/bitbank2/AnimatedGIF/blob/master/examples/TFT_eSPI_memory/TFT_eSPI_memory.ino and
// https://github.com/bitbank2/AnimatedGIF/blob/master/examples/best_practices_example/best_practices_example.ino
// ####################################################################################################
Gif::Gif() : gifPosition(0, 0) {}
Gif::~Gif() {
if (gif != nullptr) {
gif->close();
delete gif;
gif = nullptr;
}
}
FS *Gif::GifFs = NULL;
void *Gif::openFile(const char *fname, int32_t *pSize) {
File GifFile;
if (GifFs != NULL) {
GifFile = GifFs->open(fname);
} else {
if (SD.exists(fname)) GifFile = SD.open(fname);
else if (LittleFS.exists(fname)) GifFile = LittleFS.open(fname);
}
File *FSGifFile = new File(GifFile);
if (FSGifFile) {
*pSize = FSGifFile->size();
return (void *)FSGifFile;
}
return NULL;
}
void Gif::closeFile(void *pHandle) {
File *f = static_cast<File *>(pHandle);
if (f != NULL) { f->close(); }
delete f;
}
int32_t Gif::readFile(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen) {
int32_t iBytesRead;
iBytesRead = iLen;
File *f = static_cast<File *>(pFile->fHandle);
// Note: If you read a file all the way to the last byte, seek() stops working
if ((pFile->iSize - pFile->iPos) < iLen)
iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around
if (iBytesRead <= 0) return 0;
iBytesRead = (int32_t)f->read(pBuf, iBytesRead);
pFile->iPos = f->position();
return iBytesRead;
}
int32_t Gif::seekFile(GIFFILE *pFile, int32_t iPosition) {
int i = micros();
File *f = static_cast<File *>(pFile->fHandle);
f->seek(iPosition);
pFile->iPos = (int32_t)f->position();
i = micros() - i;
return pFile->iPos;
}
void Gif::GIFDraw(GIFDRAW *pDraw) {
uint8_t *s;
uint16_t *d, *usPalette, usTemp[tftWidth];
int x, y, iWidth;
GifPosition *position = (GifPosition *)(pDraw->pUser);
iWidth = pDraw->iWidth;
if (iWidth > tftWidth) iWidth = tftWidth;
usPalette = pDraw->pPalette;
y = pDraw->iY + pDraw->y; // current line
s = pDraw->pPixels;
if (pDraw->ucDisposalMethod == 2) { // restore to background color
for (x = 0; x < iWidth; x++) {
if (s[x] == pDraw->ucTransparent) s[x] = pDraw->ucBackground;
}
pDraw->ucHasTransparency = 0;
}
// Apply the new pixels to the main image
if (pDraw->ucHasTransparency) { // if transparency used
uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent;
int x, iCount;
pEnd = s + iWidth;
x = 0;
iCount = 0; // count non-transparent pixels
while (x < iWidth) {
c = ucTransparent - 1;
d = usTemp;
while (c != ucTransparent && s < pEnd) {
c = *s++;
if (c == ucTransparent) { // done, stop
s--; // back up to treat it like transparent
} else { // opaque
*d++ = usPalette[c];
iCount++;
}
} // while looking for opaque pixels
if (iCount) { // any opaque pixels?
tft.drawPixel(0, 0, 0);
tft.pushImage(pDraw->iX + x + position->x, y + position->y, iCount, 1, (uint16_t *)usTemp);
x += iCount;
iCount = 0;
}
// no, look for a run of transparent pixels
c = ucTransparent;
while (c == ucTransparent && s < pEnd) {
c = *s++;
if (c == ucTransparent) iCount++;
else s--;
}
if (iCount) {
x += iCount; // skip these
iCount = 0;
}
}
} else {
s = pDraw->pPixels;
// Translate the 8-bit pixels through the RGB565 palette (already byte reversed)
for (x = 0; x < iWidth; x++) usTemp[x] = usPalette[*s++];
tft.drawPixel(0, 0, 0);
tft.pushImage(pDraw->iX + position->x, y + position->y, iWidth, 1, (uint16_t *)usTemp);
}
} /* GIFDraw() */
bool Gif::openGIF(FS *fs, const char *filename) {
if (fs != NULL) {
GifFs = fs;
if (!fs->exists(filename)) return false;
} else {
GifFs = NULL;
}
gif = new AnimatedGIF();
gif->begin(BIG_ENDIAN_PIXELS);
if (gif->open(filename, openFile, closeFile, readFile, seekFile, GIFDraw)) { return true; }
log_e("GIF opening error: %d\n", gif->getLastError());
return false;
}
// Play a single frame
// returns:
// 2 = skipped waiting for another frame
// 1 = good result and more frames exist
// 0 = no more frames exist, a frame may or may not have been played: use getLastError() and look for
// GIF_SUCCESS to know if a frame was played -1 = error
int Gif::playFrame(int x, int y, bool bSync) {
if (gif == nullptr) return -1;
gifPosition.x = x;
gifPosition.y = y;
return gif->playFrame(bSync, nullptr, &gifPosition);
}
int Gif::getLastError() { return gif->getLastError(); }
/*
* playDurationMs:
* -1 : Play the GIF in an infinite loop
* 0 : Play the GIF once
* >0 : Play the GIF for the specified duration in milliseconds
* (e.g., 1000 = play for 1 second)
*/
bool showGif(
FS *fs, const char *filename, int x, int y, bool center, int playDurationMs, bool resetButtonStatus
) {
if (!fs->exists(filename)) return false;
Gif gif;
bool success = gif.openGIF(fs, filename);
if (!success) { return false; }
if (center) {
x = x + (tftWidth - gif.getCanvasWidth()) / 2;
y = y + (tftHeight - gif.getCanvasHeight()) / 2;
}
int result = 0;
long timeStart = millis();
do {
result = gif.playFrame(x, y);
if (result == -1) log_e("GIF playFrame error: %d\n", gif.getLastError());
if (check(AnyKeyPress, resetButtonStatus)) break;
if (playDurationMs > 0 && (millis() - timeStart) > playDurationMs) break;
if (playDurationMs == 0 && result == 0) break;
} while (result >= 0);
return true;
}
#endif
/***************************************************************************************
** Function name: getComplementaryColor2
** Description: Get simple complementary color in RGB565 format
***************************************************************************************/
uint16_t getComplementaryColor2(uint16_t color) {
int r = 31 - ((color >> 11) & 0x1F);
int g = 63 - ((color >> 5) & 0x3F);
int b = 31 - (color & 0x1F);
return (r << 11) | (g << 5) | b;
}
/***************************************************************************************
** Function name: getComplementaryColor
** Description: Get complementary color in RGB565 format
***************************************************************************************/
uint16_t getComplementaryColor(uint16_t color) {
double r = ((color >> 11) & 0x1F) / 31.0;
double g = ((color >> 5) & 0x3F) / 63.0;
double b = (color & 0x1F) / 31.0;
double cmax = fmax(r, fmax(g, b));
double cmin = fmin(r, fmin(g, b));
double delta = cmax - cmin;
double hue = 0.0;
if (delta == 0) hue = 0.0;
else if (cmax == r) hue = 60 * fmod((g - b) / delta, 6);
else if (cmax == g) hue = 60 * ((b - r) / delta + 2);
else hue = 60 * ((r - g) / delta + 4);
if (hue < 0) hue += 360;
double lightness = (cmax + cmin) / 2;
double saturation = (delta == 0) ? 0 : delta / (1 - std::abs(2 * lightness - 1));
double compHue = fmod(hue + 180, 360);
double c = (1 - std::abs(2 * lightness - 1)) * saturation;
double x = c * (1 - std::abs(fmod(compHue / 60, 2) - 1));
double m = lightness - c / 2;
double compR = 0, compG = 0, compB = 0;
if (compHue >= 0 && compHue < 60) {
compR = c;
compG = x;
} else if (compHue >= 60 && compHue < 120) {
compR = x;
compG = c;
} else if (compHue >= 120 && compHue < 180) {
compG = c;
compB = x;
} else if (compHue >= 180 && compHue < 240) {
compG = x;
compB = c;
} else if (compHue >= 240 && compHue < 300) {
compB = c;
compR = x;
} else {
compB = x;
compR = c;
}
uint16_t compl_color = uint8_t(compR * 31) << 11 | uint8_t(compG * 63) << 5 | uint8_t(compB * 31);
// change black color
if (compl_color == 0) compl_color = color - 0x1111;
return compl_color;
}
/***************************************************************************************
** Function name: getColorVariation
** Description: Get a variation of color in RGB565 format
***************************************************************************************/
uint16_t getColorVariation(uint16_t color, int delta, int direction) {
uint8_t r = ((color >> 11) & 0x1F);
uint8_t g = ((color >> 5) & 0x3F);
uint8_t b = (color & 0x1F);
float brightness = 0.299 * r / 31 + 0.587 * g / 63 + 0.114 * b / 31;
if (direction < 0 || (direction == 0 && brightness >= 0.5)) {
r = max(0, r - delta);
g = max(0, g - 2 * delta);
b = max(0, b - delta);
} else {
r = min(31, r + delta);
g = min(63, g + 2 * delta);
b = min(31, b + delta);
}
uint16_t compl_color = r << 11 | g << 5 | b;
return compl_color;
}
// Draw BITMAP files
// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.
uint16_t read16(fs::File &f) {
uint16_t result = 0; // Initialize to prevent undefined behavior
((uint8_t *)&result)[0] = f.read(); // LSB
((uint8_t *)&result)[1] = f.read(); // MSB
return result;
}
uint32_t read32(fs::File &f) {
uint32_t result = 0; // Initialize to prevent undefined behavior
((uint8_t *)&result)[0] = f.read(); // LSB
((uint8_t *)&result)[1] = f.read();
((uint8_t *)&result)[2] = f.read();
((uint8_t *)&result)[3] = f.read(); // MSB
return result;
}
bool drawBmp(FS &fs, const String &filename, int x, int y, bool center) {
if ((x >= tft.width()) || (y >= tft.height())) return false;
uint32_t startTime = millis();
File bmpFS;
// Open requested file on SD card
bmpFS = fs.open(filename, "r");
if (!bmpFS) {
Serial.print("File not found");
goto ERROR;
}
uint32_t seekOffset;
uint16_t w, h, row, col;
uint8_t r, g, b;
if (read16(bmpFS) == 0x4D42) {
read32(bmpFS);
read32(bmpFS);
seekOffset = read32(bmpFS);
read32(bmpFS);
w = read32(bmpFS);
h = read32(bmpFS);
if (center) {
x = x + (tftWidth - w) / 2;
y = y + (tftHeight - h) / 2;
}
if ((read16(bmpFS) == 1) && (read16(bmpFS) == 24) && (read32(bmpFS) == 0)) {
y += h - 1;
bool oldSwapBytes = tft.getSwapBytes();
tft.setSwapBytes(true);
bmpFS.seek(seekOffset);
uint16_t padding = (4 - ((w * 3) & 3)) & 3;
uint8_t lineBuffer[w * 3 + padding];
for (row = 0; row < h; row++) {
bmpFS.read(lineBuffer, sizeof(lineBuffer));
uint8_t *bptr = lineBuffer;
uint16_t *tptr = (uint16_t *)lineBuffer;
// Convert 24 to 16-bit colours
for (uint16_t col = 0; col < w; col++) {
b = *bptr++;
g = *bptr++;
r = *bptr++;
*tptr++ = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
// Push the pixel row to screen, pushImage will crop the line if needed
// y is decremented as the BMP image is drawn bottom up
tft.drawPixel(
0, 0, 0
); // shared TFT_Spi devices struggle to work, need call a line first sometimes
tft.pushImage(x, y--, w, 1, (uint16_t *)lineBuffer);
}
tft.setSwapBytes(oldSwapBytes);
Serial.print("BMP Loaded in ");
Serial.print(millis() - startTime);
Serial.println(" ms");
} else {
goto ERROR;
}
} else {
ERROR:
Serial.println("BMP format not recognized.");
bmpFS.close();
return false;
}
bmpFS.close();
return true;
}
bool drawImg(
FS &fs, const String &filename, int x, int y, bool center, int playDurationMs, bool resetButtonStatus
) {
String ext = filename.substring(filename.lastIndexOf('.'));
ext.toLowerCase();
uint8_t fls = 2; // 2 for Little FS
if (&fs == &SD) fls = 0; // 0 for SD
tft.imageToBin(fls, filename, x, y, center, playDurationMs);
if (ext.endsWith("jpg")) return showJpeg(fs, filename, x, y, center);
else if (ext.endsWith("bmp")) return drawBmp(fs, filename, x, y, center);
else if (ext.endsWith("png")) return drawPNG(fs, filename, x, y, center);
#if !defined(LITE_VERSION)
else if (ext.endsWith("gif"))
return showGif(&fs, filename.c_str(), x, y, center, playDurationMs, resetButtonStatus);
#endif
else log_e("Image not supported");
return false;
}
#if !defined(LITE_VERSION)
/// Draw PNG files
#include <PNGdec.h>
#if TFT_WIDTH > TFT_HEIGHT
#define MAX_IMAGE_WIDTH TFT_WIDTH
#else
#define MAX_IMAGE_WIDTH TFT_HEIGHT
#endif
PNG *png = nullptr;
// Optional pointer to write decoded lines into a cached BIN file
static File *pngBinOut = nullptr;
static bool pngCacheOnly = false;
// Optionally use heap capabilities on ESP32 to pick the best memory region for the decoder
#if defined(ESP32)
#include <esp_heap_caps.h>
#endif
// Functions to access a file on the SD card
File myfile;
FS *_fs;
void *myOpen(const char *filename, int32_t *size) {
// Serial.printf("Attempting to open %s\n", filename);
myfile = _fs->open(filename);
*size = myfile.size();
return &myfile;
}
void myClose(void *handle) {
if (myfile) myfile.close();
}
int32_t myRead(PNGFILE *handle, uint8_t *buffer, int32_t length) {
if (!myfile) return 0;
return myfile.read(buffer, length);
}
int32_t mySeek(PNGFILE *handle, int32_t position) {
if (!myfile) return 0;
return myfile.seek(position);
}
// Function to draw pixels to the display
int16_t xpos = 0;
int16_t ypos = 0;
int PNGDraw(PNGDRAW *pDraw) {
uint16_t usPixels[MAX_IMAGE_WIDTH];
// static uint16_t dmaBuffer[MAX_IMAGE_WIDTH]; // static so buffer persists after fn exit
uint8_t r = ((uint16_t)bruceConfig.bgColor & 0xF800) >> 8;
uint8_t g = ((uint16_t)bruceConfig.bgColor & 0x07E0) >> 3;
uint8_t b = ((uint16_t)bruceConfig.bgColor & 0x001F) << 3;
png->getLineAsRGB565(pDraw, usPixels, PNG_RGB565_BIG_ENDIAN, b << 16 | g << 8 | r);
if (!pngCacheOnly) {
tft.drawPixel(0, 0, 0);
tft.drawPixel(0, 0, 0);
tft.pushImage(xpos, ypos + pDraw->y, pDraw->iWidth, 1, usPixels);
}
if (pngBinOut) { pngBinOut->write((uint8_t *)usPixels, pDraw->iWidth * sizeof(uint16_t)); }
return 1;
}
// Build a cache path alongside the PNG: <dir>/tmp/<basename>.bin
static String buildPngBinPath(const String &pngPath) {
int slash = pngPath.lastIndexOf('/');
String dir = (slash >= 0) ? pngPath.substring(0, slash) : "";
String name = pngPath.substring(slash + 1);
int dot = name.lastIndexOf('.');
if (dot > 0) name = name.substring(0, dot);
String tmpDir = dir.length() ? dir + "/tmp" : "/tmp";
if (!tmpDir.startsWith("/")) tmpDir = "/" + tmpDir;
return tmpDir + "/" + name + ".bin";
}
static bool ensureTmpDir(FS &fs, const String &binPath) {
int slash = binPath.lastIndexOf('/');
if (slash < 0) return false;
String dir = binPath.substring(0, slash);
if (fs.exists(dir)) return true;
return fs.mkdir(dir);
}
// Render a previously cached BIN (RGB565 LE with 2-byte width/height header)
static bool drawPngBin(FS &fs, const String &binPath, int x, int y, bool center) {
File f = fs.open(binPath, FILE_READ);
if (!f) return false;
uint16_t w = 0, h = 0;
if (f.read((uint8_t *)&w, sizeof(uint16_t)) != sizeof(uint16_t) ||
f.read((uint8_t *)&h, sizeof(uint16_t)) != sizeof(uint16_t)) {
f.close();
return false;
}
if (center) {
x = x + (tftWidth - w) / 2;
y = y + (tftHeight - h) / 2;
}
if (x >= tft.width() || y >= tft.height()) {
f.close();
return false;
}
std::unique_ptr<uint16_t[]> line(new (std::nothrow) uint16_t[w]);
if (!line) {
f.close();
return false;
}
size_t rowBytes = w * sizeof(uint16_t);
for (uint16_t row = 0; row < h; ++row) {
if (f.read((uint8_t *)line.get(), rowBytes) != rowBytes) {
f.close();
return false;
}
tft.pushImage(x, y + row, w, 1, line.get());
}
f.close();
return true;
}
bool drawPNG(FS &fs, const String &filename, int x, int y, bool center) {
if ((x >= tft.width()) || (y >= tft.height())) return false;
_fs = &fs;
uint32_t dt = millis();
String binPath = buildPngBinPath(filename);
if (fs.exists(binPath)) {
if (pngCacheOnly) return true; // cache already ready
if (drawPngBin(fs, binPath, x, y, center)) return true;
fs.remove(binPath); // stale cache, fall back to decode
}
// Allocate decoder only while drawing, then release to keep RAM available for Wi-Fi/AP usage
#if defined(ESP32)
bool usedHeapCaps = true;
void *mem = psramFound() ? heap_caps_malloc(sizeof(PNG), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)
: heap_caps_malloc(sizeof(PNG), MALLOC_CAP_8BIT);
if (!mem) {
mem = malloc(sizeof(PNG));
usedHeapCaps = false;
}
#else
void *mem = malloc(sizeof(PNG));
#endif
#if !defined(ESP32)
bool usedHeapCaps = false;
#endif
if (!mem) {
Serial.println("Fail alloc PNG!");
bruceConfig.theme.label = true;
return false;
}
png = new (mem) PNG();
int16_t rc = png->open(filename.c_str(), myOpen, myClose, myRead, mySeek, PNGDraw);
if (rc == PNG_SUCCESS) {
// Serial.printf("image specs: (%d x %d), %d bpp, pixel type: %d\n", png->getWidth(),
// png->getHeight(), png->getBpp(), png->getPixelType());
File binFile;
if (ensureTmpDir(fs, binPath)) {
binFile = fs.open(binPath, FILE_WRITE);
if (binFile) {
uint16_t w = png->getWidth();
uint16_t h = png->getHeight();
binFile.write((uint8_t *)&w, sizeof(uint16_t));
binFile.write((uint8_t *)&h, sizeof(uint16_t));
pngBinOut = &binFile;
}
}
if (center) {
xpos = x + (tftWidth - png->getWidth()) / 2;
ypos = y + (tftHeight - png->getHeight()) / 2;
}
if (png->getWidth() > MAX_IMAGE_WIDTH) {
Serial.println("Image too wide for allocated line buffer size!");
} else {
rc = png->decode(NULL, 0);
png->close();
}
if (pngBinOut) {
pngBinOut->close();
pngBinOut = nullptr;
} else {
if (fs.exists(binPath) && rc != PNG_SUCCESS) fs.remove(binPath);
}
if (rc != PNG_SUCCESS && fs.exists(binPath)) { fs.remove(binPath); }
// How long did rendering take...
Serial.print("PNG Loaded in ");
Serial.print(millis() - dt);
Serial.println("ms");
} else {
// Decode/open failed, ensure no stale cache
if (fs.exists(binPath)) fs.remove(binPath);
}
// Destroy placement-new object and free memory so RAM is available after rendering
png->~PNG();
#if defined(ESP32)
if (usedHeapCaps) heap_caps_free(mem);
else free(mem);
#else
free(mem);
#endif
png = nullptr;
return rc == PNG_SUCCESS;
}
// Prepare (or verify) the cached BIN for a PNG without rendering it on screen
bool preparePngBin(FS &fs, const String &filename) {
bool previous = pngCacheOnly;
pngCacheOnly = true;
bool ok = drawPNG(fs, filename, 0, 0, false);
pngCacheOnly = previous;
return ok;
}
#else
bool preparePngBin(FS &fs, const String &filename) {
log_w("PNG: Not supported in this version");
return true;
}
bool drawPNG(FS &fs, const String &filename, int x, int y, bool center) {
log_w("PNG: Not supported in this version");
return false;
}
#endif