How to solve this memory usage problem?

Hello all!

I am using a TTGO board for the first time, and for my project I need to use multiple libraries. a few of them are rather big.. here are the libraries included in my project:

#include <NTPClient.h>
#include <WiFi.h>
#include <time.h>
#include <WiFiUdp.h>
#include <AsyncTelegram2.h> //***
#include <WiFiClientSecure.h>
#include <Timezone.h>    
#include <TFT_eSPI.h>  //***
#include <SPI.h>
#include <sqlite3.h> //***
#include "FS.h" 
#include "SD.h" //***

I marked the heavier ones with '//***'. the problem I am facing, is that telegram is not able to connect to the server. but If I avoid using TFT_eSPI or sqlite3 then telegram connects. since removing any one of those makes telegram work, I am assuming that the problem is that I am running out of memory. My problem is that I cannot remove any of those libraries from the project as I need them all. what options do I have? Here is my Setup part of the code, as the problem is presenting itself in here..


void setup() {

  pinMode(HSPI_CS, OUTPUT);

  pinMode(ROTA, INPUT_PULLUP );
  pinMode(ROTB, INPUT_PULLUP );
  pinMode(ENTERA, INPUT_PULLUP);
  pinMode(ENTERB, INPUT_PULLUP);
  // put your setup code here, to run once:
  Serial.begin(115200);

  sqlite3_initialize();

  spiSD.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
  if (!SD.begin(HSPI_CS, spiSD)) {
    Serial.println("Card Mount Failed");
    return;
  }
  else
  {
    Serial.println("Card ok!");
  }

  uint8_t cardType = SD.cardType();

  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }

  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);

  if (openDb("/sd/db.db3", &db1))
    return;

  initialize(); //get the WIFI credentials from the DB
  delay(500);

  wificonnect();
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);


  timeClient.begin();
  delay ( 1000 );
  if (timeClient.update()) {
    Serial.println ( "Adjust local clock" );
    unsigned long epoch = timeClient.getEpochTime();
    setTime(epoch);
  } else {
    Serial.println ( "NTP Update not WORK!!" );
  }


  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setSwapBytes(true);
  img.setSwapBytes(true);
  img.createSprite(IWIDTH, IHEIGHT);
  img.fillSprite(TFT_BLACK);


  //Set certficate, session and some other base client properies
  client.setCACert(telegram_cert);

  // Set the Telegram bot properies
  myBot.setUpdateTime(1000);
  myBot.setTelegramToken(token);


  Serial.print("\nTest Telegram connection... ");
  myBot.begin() ? Serial.println("OK") : Serial.println("NOK");
  myReplyKbd.addButton("Abrechnung");
  myReplyKbd.enableResize();

}

moving this:

  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setSwapBytes(true);
  img.setSwapBytes(true);
  img.createSprite(IWIDTH, IHEIGHT);
  img.fillSprite(TFT_BLACK);

after the telegram connect, makes it connect properly, but but then once it is connected it is not able to send message. removing it entirely makes telegram connect and send messages properly.

same for the SQLite part. I get the wifi credentials from the sqllite database on the sd card. If i comment that part out, and hardcode the wifi credentials, and never initialize the sqlite library then it works too..

the board is the Lilygo TTGO ESP32 based T-Display board.

any help is greatly appreciated..

thanks

What is the IDE says after compilation?

The bucket only holds so much stuff before a bigger bucket is needed.

Which MCU is being used?

When you post code, post the whole code, please.

How much ram is left after the complie?

There are severals.

What controller is on YOUR board? ATmega328? ESP8266? ESP32?
Please give a link.

Sketch uses 1387470 bytes (44%) of program storage space. Maximum is 3145728 bytes.
Global variables use 49416 bytes (15%) of dynamic memory, leaving 278264 bytes for local variables. Maximum is 327680 bytes.

This..

thank you

I am using the lilygo TTGO ESP32 based board. The entire code is reallllly long, but the problem starts before it even gets to the loop. But if needed I can add more..

you don't need external NTP Libraries
Use the NTP functions which come with the core.

If the IDE example is to complex, try what I have extracted on my page:

https://werner.rothschopf.net/microcontroller/202103_arduino_esp32_ntp_en.htm

What does the memory readout indicate after the program has been complied?

When you print the remaining memory with esp_get_free_heap_size() what does it indicate ?

Serial.println( esp_get_free_heap_size() );

You might need to do this at various points in the program load and operations to see if there is a memory leak or there is not enough memory to do the thing.

oh, dint know this.. thank you very much!

@sharkyenergy
it looks like you want too much from your board. Even if the problem is not a lack of memory - in such a huge code with so many parts, it is very difficult to achieve correct interaction. The esp32 has only two cores, and one of them is occupied by the WiFi stack most of the time.
I think you need to abandon some parts of the program or split it into two or three controllers

Or use a BeagleBone Black.

Where is img declared, and what is it?

OK I see it post#5 Thanks.

Does not look like a compile time memory issue.

Post the whole code.

#include "sdkconfig.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void setup() {

  pinMode(HSPI_CS, OUTPUT);

  pinMode(ROTA, INPUT_PULLUP );
  pinMode(ROTB, INPUT_PULLUP );
  pinMode(ENTERA, INPUT_PULLUP);
  pinMode(ENTERB, INPUT_PULLUP);
  // put your setup code here, to run once:
  Serial.begin(115200);

  sqlite3_initialize();

  spiSD.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
  if (!SD.begin(HSPI_CS, spiSD)) {
    Serial.println("Card Mount Failed");
    return;
  }
  else
  {
    Serial.println("Card ok!");
  }

  uint8_t cardType = SD.cardType();

  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }

  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);

  if (openDb("/sd/db.db3", &db1))
    return;

  initialize(); //get the WIFI credentials from the DB
  delay(500);

  wificonnect();
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);


  timeClient.begin();
  delay ( 1000 );
  if (timeClient.update()) {
    Serial.println ( "Adjust local clock" );
    unsigned long epoch = timeClient.getEpochTime();
    setTime(epoch);
  } else {
    Serial.println ( "NTP Update not WORK!!" );
  }

xTaskCreatePinnedToCore( fTickerTask, "fTickerTask", 2000, NULL, 2, NULL, 1 );

  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setSwapBytes(true);
  img.setSwapBytes(true);
  img.createSprite(IWIDTH, IHEIGHT);
  img.fillSprite(TFT_BLACK);


  //Set certficate, session and some other base client properies
  client.setCACert(telegram_cert);

  // Set the Telegram bot properies
  myBot.setUpdateTime(1000);
  myBot.setTelegramToken(token);


  Serial.print("\nTest Telegram connection... ");
  myBot.begin() ? Serial.println("OK") : Serial.println("NOK");
  myReplyKbd.addButton("Abrechnung");
  myReplyKbd.enableResize();



}

void fTickerTask( void * pvParameters )
{
  for (;;)
  {
    Serial.println( "tick");
    vTaskDelay(1000);

  }
  vTaskDelete( NULL );
}

Added a freeRTOS task that should print tick one a second, does the task start? Does the task run? Does the task keep on running?

Post a schematic.

Post an Image of your project.

On an ESP32 use unit_64t instead of unsigned long for unsigned long epoch = timeClient.getEpochTime();

The #include <ESP32Time.h> is much better at manipulating time for a ESP32, then most other libraries.

I added multiple..

before initialize sqlite3:
249536
before open DB3:
221924
after open DB3:
184376
before TFT init:
124056
after TFT Init:
59112
after telegram begin (end of setup()):
57160

Using the HSPI bus on a ESP32 can cause issues depending upon wiring and other factors. Post a schematic. Post an image of your project.

I cannot see this code where you added multiple.

looks like the tft loaded from your test.

Most likely the HSPI bus is being used incorrectly.

Yes it starts and keeps tickting every second...

and thanks for all the sugestions,will try to do those things

If it ticks every second then the board is not locked up and there is not a memory issue.

What's the problem?

Post the entire code.

Post a schematic.

Post an image of your project.

It's post#19 and no progress had been made.

What's the problem?

Post the entire code.

Post a schematic.

Post an image of your project.

Here is the whole code.
lato and roller are fonts and a image used in the tft library.

as for the schematics, i dont have one. I have 4 buttons connected at the pins 21,22,36,37.

A sd card module is connected like this:

CS Pin 33
SCK Pin 25
MOSI Pin 26
MISO Pin 27

There is nothing else beside an external pul up resistor on pins 36 and 37

Thank you


#include <NTPClient.h>
#include <WiFi.h>
#include <time.h>
#include <WiFiUdp.h>
#include <AsyncTelegram2.h>
#include <WiFiClientSecure.h>
#include <Timezone.h>    // https://github.com/JChristensen/Timezone
#include <TFT_eSPI.h> // Graphics and font library
#include <SPI.h>
#include <sqlite3.h>
#include "FS.h"
#include "SD.h"
#include "roller.h"
#include "lato14.h"
#include "lato22.h"
#include "lato30.h"
#include "lato38.h"
TFT_eSPI tft = TFT_eSPI();  // Invoke library, pins defined in User_Setup.h
TFT_eSprite img = TFT_eSprite(&tft);
#include "sdkconfig.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define HSPI_MISO   27
#define HSPI_MOSI   26
#define HSPI_SCLK   25
#define HSPI_CS     33
#define ROTA        36
#define ROTB        37
#define ENTERA      21
#define ENTERB      22
#define led 2

int rc;
sqlite3_stmt *res;
int rec_count = 0;
const char *tail;
sqlite3 *db1;
char *zErrMsg = 0;
SPIClass spiSD(HSPI);

File myFile;

bool initial = 1;
bool getdata = false;
String sql;
unsigned long tStamp1;

String user_name[30];
int user_id[30];
//int user_balanceCurr[30];
//int user_balanceOverall[30];


int movements_id[50];
String movements_productname[50];
String movements_date[50];
int movements_move[50];

String product_name[30];
int product_id[30];
int product_balance[30];
int productbalanceoverall ;

//int balance_idproduct[30];
String balance_nameproduct[30];
int balance_curr[30];
int balance_overall[30];
String setting_name[4];

int numusers = 0;
int numproducts = 0;
int nummovements = 0;
int numbalance = 0;
int curruser = 0;
int currproduct = 0;
int currmovement = 0;

int olduser = 0;
int currsetting = 0;

int productcounter = 0;

int state1;
int state2;

int currpage = 0;
/*  00 = main
    10 = select drink
    15 = quantity
    18 = confirm
    20 = menu
    30 = history names
    40 = history
    50 = status
    60 = reset confirm
*/

#define TFT_GREY 0x5AEB
#define IWIDTH  240
#define IHEIGHT 135

WiFiClientSecure client;


/***************TELEGRAM*******************************/

#define MYTZ "CET-1CEST-2,M3.5.0/2,M10.5.0/3"

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 0;
const int   daylightOffset_sec = 0;
struct tm timeinfo;

AsyncTelegram2 myBot(client);
ReplyKeyboard myReplyKbd;   // reply keyboard object helper

const char* token = "TOKEN:TOKEN";   // REPLACE myToken WITH YOUR TELEGRAM BOT TOKEN

/***************END TELEGRAM***************************/

//WIFI VARS
WiFiUDP ntpUDP;
unsigned long time_now = 0;
int period = 1800000;
String ssid;
String password;


//TIME VARS
int Hour;
int Min;
int Sec;
int Day;
int DayInMonth;
int Month;
int Year;
unsigned long currTime;
unsigned long lastTime;
String currDateTime;
String currDate;

static tm getDateTimeByParams(long time) {
  struct tm *newtime;
  const time_t tim = time;
  newtime = localtime(&tim);
  return *newtime;
}

static String getDateTimeStringByParams(tm *newtime, char* pattern = (char *)"%d/%m/%Y %H:%M:%S") {
  char buffer[30];
  strftime(buffer, 30, pattern, newtime);
  return buffer;
}


static tm getEpochTmByParams(long time, char* pattern = (char *)"%d/%m/%Y %H:%M:%S") {
  //    struct tm *newtime;
  tm newtime;
  newtime = getDateTimeByParams(time);
  return (newtime);
}

static String getEpochStringByParams(long time, char* pattern = (char *)"%d/%m/%Y %H:%M:%S") {
  //    struct tm *newtime;
  tm newtime;
  newtime = getDateTimeByParams(time);
  return getDateTimeStringByParams(&newtime, pattern);
}

int GTMOffset = 0; // SET TO UTC TIME
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", GTMOffset * 60 * 60, 60 * 60 * 1000);
// Central European Time (Frankfurt, Paris)
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};     // Central European Summer Time
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};       // Central European Standard Time
Timezone CE(CEST, CET);



void setup() {

Serial.println( esp_get_free_heap_size() );
  //Init settings list
  setting_name[0] = "Historie";
  setting_name[1] = "Status";
  setting_name[2] = "Bestand";
  setting_name[3] = "Reset";

  pinMode(HSPI_CS, OUTPUT);

  pinMode(ROTA, INPUT_PULLUP );
  pinMode(ROTB, INPUT_PULLUP );
  pinMode(ENTERA, INPUT_PULLUP);
  pinMode(ENTERB, INPUT_PULLUP);
  // put your setup code here, to run once:
  Serial.begin(115200);
  Serial.println( esp_get_free_heap_size() );
  sqlite3_initialize();

  spiSD.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS);
  if (!SD.begin(HSPI_CS, spiSD)) {
    Serial.println("Card Mount Failed");
    return;
  }
  else
  {
    Serial.println("Card ok!");
  }

  uint8_t cardType = SD.cardType();

  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }

  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);
Serial.println( esp_get_free_heap_size() );
  if (openDb("/sd/db.db3", &db1))
    return;
Serial.println( esp_get_free_heap_size() );
  initialize();
  delay(500);

  wificonnect();
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);


  timeClient.begin();
  delay ( 1000 );
  if (timeClient.update()) {
    Serial.println ( "Adjust local clock" );
    unsigned long epoch = timeClient.getEpochTime();
    setTime(epoch);
  } else {
    Serial.println ( "NTP Update not WORK!!" );
  }

xTaskCreatePinnedToCore( fTickerTask, "fTickerTask", 2000, NULL, 2, NULL, 1 );

Serial.println( esp_get_free_heap_size() );
  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setSwapBytes(true);
  img.setSwapBytes(true);
  img.createSprite(IWIDTH, IHEIGHT);
  img.fillSprite(TFT_BLACK);
  
Serial.println( esp_get_free_heap_size() );
  //Set certficate, session and some other base client properies
  client.setCACert(telegram_cert);

  // Set the Telegram bot properies
  myBot.setUpdateTime(1000);
  myBot.setTelegramToken(token);


  Serial.print("\nTest Telegram connection... ");
  myBot.begin() ? Serial.println("OK") : Serial.println("NOK");
  myReplyKbd.addButton("Abrechnung");
  myReplyKbd.enableResize();
Serial.println( esp_get_free_heap_size() );


}
void fTickerTask( void * pvParameters )
{
  for (;;)
  {
    Serial.println( "tick");
    vTaskDelay(1000);

  }
  vTaskDelete( NULL );
}
void wificonnect() {
  // Connect to WiFi access point.
  Serial.println(); Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);


  WiFi.begin(ssid.c_str(), password.c_str());

  //  WiFi.config(ip, gateway, subnet);
  int startTime = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - startTime) <= 10000) {
    delay(500);
    Serial.print(".");
  }

  Serial.println();

  digitalWrite(led, LOW);

  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

}

void loop() {
  /*
     Startpage
  */

  if ((WiFi.status() != WL_CONNECTED) and (millis() > time_now + period)) {
    time_now = millis();
    wificonnect();
  }

  if (WiFi.status() == WL_CONNECTED) 
  {
    // a variable to store telegram message data
    TBMessage msg;

    // if there is an incoming message...
    if (myBot.getNewMessage(msg))
    {
      Serial.println("ok2");
      String tgReply;
      static String document;
      switch (msg.messageType)
      {
        case MessageDocument :
          break;
        case MessageQuery:
          break;
        default:


          if ( msg.text == "Abrechnung")
          {
            Serial.println("Abrechnung....");
            myBot.sendMessage(msg, "Abrechnung ... ... ...", myReplyKbd);
          }

          break;
      }
    }
  }


  if ((currpage == 0 and not initial) or currpage == 30 or currpage == 60)
  {

    curruser = counter(curruser, 0, numusers - 1, true);
    int action = exitconfirm();

    if (action == 1)
    {
      //change page. ENTER
      if (currpage == 0)
      {
        if (numproducts = 1)
        {
          currpage = 15;
          currproduct = 0;
        }
        else
        {
          currpage = 10;
        }
      }
      else if (currpage == 30)
      {
        getdata = true;
        currpage = 40;
      }
      else
      {
        currpage = 65;
      }
      productcounter = 0;
    }
    if (action == 2)
    {
      if (currpage == 0)
      {
        //change page. RETURN
        currpage = 20;
      }
      else if (currpage == 30)
      {
        currpage = 20;
      }
      else
      {
        currpage = 20;
      }
    }

    img.fillSprite(TFT_BLACK);
    img.pushImage(5, 6, 170, 123, roller);

    img.setTextColor(TFT_WHITE);
    img.setTextDatum(0);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h;
    img.setFreeFont(&lato38);
    w = img.textWidth(user_name[curruser]);
    h = img.fontHeight();
    img.drawString(user_name[curruser], 88 - w / 2, IHEIGHT / 2 - h / 2);

    img.setTextColor(TFT_GREY);
    if (curruser > 0)
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(user_name[curruser - 1]);
      h = img.fontHeight();
      img.drawString(user_name[curruser - 1], 88 - w / 2, 23 - h / 2);
    }
    else
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(user_name[numusers - 1]);
      h = img.fontHeight();
      img.drawString(user_name[numusers - 1], 88 - w / 2, 23 - h / 2);
    }

    if (curruser < numusers - 1)
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(user_name[curruser + 1]);
      h = img.fontHeight();
      img.drawString(user_name[curruser + 1], 88 - w / 2, 112 - h / 2);
    }
    else
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(user_name[0]);
      h = img.fontHeight();
      img.drawString(user_name[0], 88 - w / 2, 112 - h / 2);
    }

    if (currpage == 0)
    {
      img.setTextColor(TFT_WHITE);
      img.setTextDatum(1);

      int i;

      for (i = 0; i < numproducts; i++)
      {
        img.drawString(product_name[i], 210, 10 + i * 40, 2);
        img.drawNumber(product_balance[i], 210, 25 + i * 40, 2);
        if (i < numproducts - 1)
        {
          img.drawLine(190, 45 + i * 40, 230, 45 + i * 40, TFT_WHITE);
        }
      }
    }
    img.setTextDatum(0);

    img.pushSprite(0, 0);
  }

  /*
     Page Select Drink
  */

  if (currpage == 10)
  {

    currproduct = counter(currproduct, 0, numproducts - 1, true);
    int action = exitconfirm();

    if (action == 1)
    {
      //change page. ENTER
      currpage = 15;
      productcounter = 0;
    }
    if (action == 2)
    {
      //change page. RETURN
      currpage = 0;
    }

    img.fillSprite(TFT_BLACK);
    img.pushImage(5, 6, 170, 123, roller);

    img.setTextColor(TFT_WHITE);
    img.setTextDatum(0);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h;
    img.setFreeFont(&lato38);
    w = img.textWidth(product_name[currproduct]);
    h = img.fontHeight();
    img.drawString(product_name[currproduct], 88 - w / 2, IHEIGHT / 2 - h / 2);

    img.setTextColor(TFT_GREY);
    if (currproduct > 0)
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(product_name[currproduct - 1]);
      h = img.fontHeight();
      img.drawString(product_name[currproduct - 1], 88 - w / 2, 23 - h / 2);
    }
    else
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(product_name[numproducts - 1]);
      h = img.fontHeight();
      img.drawString(product_name[numproducts - 1], 88 - w / 2, 23 - h / 2);
    }

    if (currproduct < numproducts - 1)
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(product_name[currproduct + 1]);
      h = img.fontHeight();
      img.drawString(product_name[currproduct + 1], 88 - w / 2, 112 - h / 2);
    }
    else
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(product_name[0]);
      h = img.fontHeight();
      img.drawString(product_name[0], 88 - w / 2, 112 - h / 2);
    }

    img.setTextColor(TFT_WHITE);
    img.setTextDatum(1);

    int i;

    for (i = 0; i < numproducts; i++)
    {
      img.drawString(product_name[i], 210, 10 + i * 40, 2);
      img.drawNumber(product_balance[i], 210, 25 + i * 40, 2);
      if (i < numproducts - 1)
      {
        img.drawLine(190, 45 + i * 40, 230, 45 + i * 40, TFT_WHITE);
      }
    }

    img.setTextDatum(0);

    img.pushSprite(0, 0);
  }

  /*
     Page Quantity Drink
  */

  if (currpage == 15)
  {

    productcounter = counter(productcounter, -20, 0, false);
    int action = exitconfirm();

    if (action == 1)
    {
      //change page. ENTER
      currpage = 18;
    }
    if (action == 2)
    {
      if (numproducts > 1)
      {
        //change page. RETURN
        currpage = 10;
      }
      else
      {
        currpage = 0;
      }
    }

    img.fillSprite(TFT_BLACK);

    img.setTextColor(TFT_WHITE);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h;
    img.setFreeFont(&lato38);
    h = img.fontHeight();
    w = img.textWidth(user_name[curruser]);
    img.drawString(user_name[curruser], 5, 10);

    img.setFreeFont(&lato22);
    h = img.fontHeight();
    w = img.textWidth(product_name[currproduct]);
    img.drawString(product_name[currproduct], 85, 65);

    img.drawNumber(abs(productcounter), 10, 70, 7);

    img.setFreeFont(&lato22);
    if (productcounter > 0)
    {
      img.drawString("hinzugefugt", 101, 100);
      img.fillRect(188, 102, 2, 3, TFT_WHITE);
      img.fillRect(193, 102, 2, 3, TFT_WHITE);
    }
    if (productcounter < 0)
    {
      img.drawString("genommen", 100, 100);
    }

    img.pushSprite(0, 0);
  }

  /*
      Page Confirm Quantity Drink
  */

  if (currpage == 18)
  {

    int action = exitconfirm();

    if (action == 1)
    {
      //change page. ENTER

      //save the data to the database...
      sql = "insert into movements ('iduser','idproduct','date','move') values ('";
      sql += user_id[curruser];
      sql += "','";
      sql += product_id[currproduct];
      sql += "','";
      sql += currDate;
      sql += "','";
      sql += productcounter;
      sql += "');";
      rc = db_exec(db1, sql.c_str());

      sql = "update products set balance = balance + ";
      sql += productcounter;
      sql += " where id = ";
      sql += product_id[currproduct];
      sql += ";";
      rc = db_exec(db1, sql.c_str());


      /*    sql = "insert into balance (iduser, idproduct, balancecurr, balanceoverall) SELECT ";
          sql += user_id[curruser];
          sql += ", ";
          sql += product_id[currproduct];
          sql += ", 0, 0 ";
          sql += "WHERE NOT EXISTS(SELECT 1 FROM balance WHERE iduser='";
          sql += user_id[curruser];
          sql += "' AND idproduct='";
          sql += product_id[currproduct];
          sql += "');";*/

      sql = "INSERT OR IGNORE INTO balance (iduser, idproduct, balancecurr, balanceoverall) VALUES( ";
      sql += user_id[curruser];
      sql += ", ";
      sql += product_id[currproduct];
      sql += ", 0, 0);";

      rc = db_exec(db1, sql.c_str());
      if (rc != SQLITE_OK) {
        String resp = "Failed to fetch data: ";
        resp += sqlite3_errmsg(db1);
        Serial.println(resp.c_str());
        return;
      }


      sql = "update balance set balanceCurr = balanceCurr + ";
      sql += productcounter;
      sql += ", balanceOverall = balanceOverall + ";
      sql += productcounter;
      sql += " where iduser = ";
      sql += user_id[curruser];
      sql += ";";
      rc = db_exec(db1, sql.c_str());

      product_balance[currproduct] += productcounter;
      //  user_balanceCurr[curruser] +=  productcounter;
      //  user_balanceOverall[curruser] +=  productcounter;

      currpage = 0;
    }
    if (action == 2)
    {
      //change page. RETURN
      currpage = 15;
    }

    img.fillSprite(TFT_BLACK);

    img.setTextColor(TFT_WHITE);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h;
    img.setFreeFont(&lato30);
    h = img.fontHeight();
    w = img.textWidth(user_name[curruser]);
    img.drawString(user_name[curruser], 5, 10);

    img.setFreeFont(&lato22);
    h = img.fontHeight();
    w = img.textWidth(product_name[currproduct]);
    img.drawString(product_name[currproduct], 40, 60);

    img.drawNumber(abs(productcounter), 10, 60);

    img.setFreeFont(&lato22);
    if (productcounter > 0)
    {
      img.drawString("hinzugefugt", 111, 60);
      // img.fillRect(157,102,2,3,TFT_WHITE);
      // img.fillRect(162,102,2,3,TFT_WHITE);
    }
    if (productcounter < 0)
    {
      img.drawString("genommen", 110, 60);
    }


    img.setFreeFont(&lato14);
    img.drawString("<-Zuruck", 10, 115);
    img.drawString("Bestatigen->", 140, 115);

    img.pushSprite(0, 0);
  }


  /*
     Page Settings
  */

  if (currpage == 20)
  {

    currsetting = counter(currsetting, 0, 4 - 1, true);
    int action = exitconfirm();

    if (action == 1)
    {
      //change page. ENTER
      if (currsetting == 0)
      {
        currmovement = 0;
        currpage = 30;      //Historie
      }
      if (currsetting == 1)
      {
        getdata = true;
        currpage = 50;      //Stand
      }
      if (currsetting == 2)
      {
        currpage = 70;      //Bestand
        productbalanceoverall = product_balance[0];
      }
      if (currsetting == 3)
      {
        currpage = 60;      //Zurücksetzen Bestätigen
      }
      currsetting = 0;
    }
    if (action == 2)
    {
      //change page. RETURN
      currpage = 0;
    }

    img.fillSprite(TFT_BLACK);
    img.pushImage(5, 6, 170, 123, roller);

    img.setTextColor(TFT_WHITE);
    img.setTextDatum(0);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h;
    img.setFreeFont(&lato38);
    w = img.textWidth(setting_name[currsetting]);
    h = img.fontHeight();
    img.drawString(setting_name[currsetting], 88 - w / 2, IHEIGHT / 2 - h / 2);

    img.setTextColor(TFT_GREY);
    if (currsetting > 0)
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(setting_name[currsetting - 1]);
      h = img.fontHeight();
      img.drawString(setting_name[currsetting - 1], 88 - w / 2, 23 - h / 2);
    }
    else
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(setting_name[4 - 1]);
      h = img.fontHeight();
      img.drawString(setting_name[4 - 1], 88 - w / 2, 23 - h / 2);
    }

    if (currsetting < 4 - 1)
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(setting_name[currsetting + 1]);
      h = img.fontHeight();
      img.drawString(setting_name[currsetting + 1], 88 - w / 2, 112 - h / 2);
    }
    else
    {
      img.setFreeFont(&lato22);
      w = img.textWidth(setting_name[0]);
      h = img.fontHeight();
      img.drawString(setting_name[0], 88 - w / 2, 112 - h / 2);
    }


    img.pushSprite(0, 0);
  }

  /*
       Page Historie
  */

  if (currpage == 40)
  {

    if (getdata)
    {
      getdata = false;
      //get the movements of the user form the DB

      sql = "select m.id, p.name, m.date, m.move from movements as m left join products as p on p.id = m.idproduct where m.iduser = '";
      sql += user_id[curruser];
      sql += "' order by m.id desc limit 50";

      //reset the arrays
      int i;
      for (i = 0; i < 50; i++)
      {
        movements_id[i] = 0;
        movements_productname[i] = "";
        movements_date[i] = "";
        movements_move[i] = 0;
      }

      rc = sqlite3_prepare_v2(db1, sql.c_str(), sql.length() + 1, &res, &tail);
      if (rc != SQLITE_OK) {
        String resp = "Failed to fetch data: ";
        resp += sqlite3_errmsg(db1);
        Serial.println(resp.c_str());
        return;
      }
      rec_count = 0;
      nummovements = 0;
      while (sqlite3_step(res) == SQLITE_ROW) {
        movements_id[rec_count] = sqlite3_column_int(res, 0);
        movements_productname[rec_count] =  (const char *) sqlite3_column_text(res, 1);
        movements_date[rec_count] =  (const char *) sqlite3_column_text(res, 2);
        movements_move[rec_count] =  sqlite3_column_int(res, 3);
        nummovements++;
        rec_count++;
      }
      sqlite3_finalize(res);
    }


    currmovement = counter(currmovement, 0, nummovements - 5, false);
    // curruser = counter(curruser, 0, numusers - 5, false);
    int action = exitconfirm();


    if (action == 1)
    {
      //change page. ENTER
      currpage = 0;      //Main page
    }
    if (action == 2)
    {
      //change page. RETURN
      currsetting = 0;
      currpage = 30;
    }

    img.fillSprite(TFT_BLACK);

    img.setTextColor(TFT_WHITE);
    img.setTextDatum(0);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h1;
    img.setFreeFont(&lato30);
    h1 = img.fontHeight();
    img.drawString(user_name[curruser], 10, 5);
    //img.drawNumber(nummovements, 150, 5);
    int i;
    int cnt = 0;
    for (i = currmovement; i < nummovements; i++)
    {
      cnt ++;
      img.setFreeFont(&lato14);
      int h2;
      h2 = img.fontHeight();
      int ypos = h1 + 2 + h2 * (cnt - 1) + 10;
      img.drawNumber(movements_id[i], 0, ypos);
      img.drawString(movements_date[i], 25, ypos);
      img.drawString(movements_productname[i], 140, ypos);
      img.drawNumber(movements_move[i], 210, ypos);
    }


    img.pushSprite(0, 0);
  }

  /*
        Page Stand
  */

  if (currpage == 50)
  {

    if (getdata)
    {
      getdata = false;
      //get the movements of the user form the DB

      sql = "select p.name, b.balancecurr, b.balanceoverall from balance as b left join products as p on p.id = b.idproduct where b.iduser = '";
      sql += user_id[curruser];
      sql += "' order by b.idproduct asc";

      //reset the arrays
      int i;
      for (i = 0; i < 30; i++)
      {
        balance_nameproduct[i] = "";
        balance_curr[i] = 0;
        balance_overall[i] = 0;
      }

      rc = sqlite3_prepare_v2(db1, sql.c_str(), sql.length() + 1, &res, &tail);
      if (rc != SQLITE_OK) {
        String resp = "Failed to fetch data: ";
        resp += sqlite3_errmsg(db1);
        Serial.println(resp.c_str());
        return;
      }
      rec_count = 0;
      numbalance = 0;
      while (sqlite3_step(res) == SQLITE_ROW) {
        balance_nameproduct[rec_count] = (const char *) sqlite3_column_text(res, 0);
        balance_curr[rec_count] = sqlite3_column_int(res, 1);
        balance_overall[rec_count] =  sqlite3_column_int(res, 2);
        numbalance++;
        rec_count++;
      }
      sqlite3_finalize(res);
    }

    curruser = counter(curruser, 0, numusers - 1, true);
    int action = exitconfirm();

    if (curruser != olduser)
    {
      olduser = curruser;
      getdata = true;
    }

    if (action == 1)
    {
      //change page. ENTER
      currpage = 0;      //Main page
    }
    if (action == 2)
    {
      //change page. RETURN
      currsetting = 0;
      currpage = 20;
    }

    img.fillSprite(TFT_BLACK);

    img.setTextColor(TFT_WHITE);
    img.setTextDatum(0);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h1;
    img.setFreeFont(&lato30);
    h1 = img.fontHeight();
    img.drawString(user_name[curruser], 10, 5);
    //img.drawNumber(nummovements, 150, 5);


    img.setFreeFont(&lato14);
    int h2;
    h2 = img.fontHeight();

    img.drawString("Akt.", 112, h1 + 2);
    img.drawString("Ges.", 182, h1 + 2);

    int i;

    for (i = 0; i < numproducts; i++)
    {
      int ypos = h1 + 8 + h2 * i + 10;
      img.drawString(balance_nameproduct[i], 10, ypos);
      // img.drawNumber(balance_idproduct[i], 90, ypos);
      img.drawNumber(balance_curr[i], 120, ypos);
      img.drawNumber(balance_overall[i], 190, ypos);

    }


    img.pushSprite(0, 0);
  }

  /*
    Page 60 is on Page 10
  */
  /*
        Page Confirm Quantity Drink Total
  */

  if (currpage == 65)
  {

    int action = exitconfirm();

    if (action == 1)
    {
      //change page. ENTER

      sql = "update balance set balancecurr = 0 where iduser = ";
      sql += user_id[curruser];
      sql += " and idproduct = ";
      sql += product_id[0];
      sql += ";";
      Serial.println(sql);
      rc = db_exec(db1, sql.c_str());


      // product_balance[0] = productbalanceoverall;

      currpage = 0;
    }
    if (action == 2)
    {
      //change page. RETURN
      currpage = 60;
    }

    img.fillSprite(TFT_BLACK);

    img.setTextColor(TFT_WHITE);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h;
    img.setFreeFont(&lato22);
    h = img.fontHeight();
    img.drawString("Bestand fur ", 5, 10);
    img.drawString(user_name[curruser], 5, 40);
    img.drawString("zuruck setzen?", 5, 70);





    img.setFreeFont(&lato14);
    img.drawString("<-Zuruck", 10, 115);
    img.drawString("Bestatigen->", 140, 115);

    img.pushSprite(0, 0);
  }



  /*
      Page Quantity Drink Total
  */

  if (currpage == 70)
  {

    productbalanceoverall = counter(productbalanceoverall, 0, 150, false);
    int action = exitconfirm();

    if (action == 1)
    {
      //change page. ENTER
      currpage = 75;
    }
    if (action == 2)
    {

      currpage = 20;

    }

    img.fillSprite(TFT_BLACK);

    img.setTextColor(TFT_WHITE);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h;
    img.setFreeFont(&lato22);
    h = img.fontHeight();
    //    w = img.textWidth(balance_overall[0]);
    img.drawString("Akt. Bestand:", 5, 10);
    img.drawNumber(product_balance[0], 185, 10);
    img.drawString("Neuer Bestand:", 5, 50);
    img.drawNumber(productbalanceoverall, 185, 50);



    img.setFreeFont(&lato14);
    img.drawString("<-Zuruck", 10, 115);
    img.drawString("Bestatigen->", 140, 115);

    img.pushSprite(0, 0);
  }



  /*
        Page Confirm Quantity Drink Total
  */

  if (currpage == 75)
  {

    int action = exitconfirm();

    if (action == 1)
    {
      //change page. ENTER

      sql = "update products set balance = ";
      sql += productbalanceoverall;
      sql += " where id = ";
      sql += product_id[0];
      sql += ";";
      Serial.println(sql);
      rc = db_exec(db1, sql.c_str());


      product_balance[0] = productbalanceoverall;

      currpage = 0;
    }
    if (action == 2)
    {
      //change page. RETURN
      currpage = 70;
    }

    img.fillSprite(TFT_BLACK);

    img.setTextColor(TFT_WHITE);
    img.setTextSize(1);           // Font size scaling is x1
    int w;
    int h;
    img.setFreeFont(&lato22);
    h = img.fontHeight();
    img.drawString("Bestand von ", 5, 10);
    img.drawNumber(product_balance[0], 155, 10);
    img.drawString("auf ", 5, 40);
    img.drawNumber(productbalanceoverall, 48, 40);
    img.drawString("andern? ", 100, 40);





    img.setFreeFont(&lato14);
    img.drawString("<-Zuruck", 10, 115);
    img.drawString("Bestatigen->", 140, 115);

    img.pushSprite(0, 0);
  }

  /*
      INIT
  */

  updatetime();
}

/*

   ---------------------------------------------------------------------------------------
    FUNCTIONS
   ---------------------------------------------------------------------------------------

*/

void initialize()
{
  if (initial) {
    initial = 0;

    sql = "Select * from user order by id asc";


    rc = sqlite3_prepare_v2(db1, sql.c_str(), sql.length() + 1, &res, &tail);


    if (rc != SQLITE_OK) {
      String resp = "Failed to fetch data: ";
      resp += sqlite3_errmsg(db1);
      Serial.println(resp.c_str());
      return;
    }
    rec_count = 0;
    numusers = 0;
    while (sqlite3_step(res) == SQLITE_ROW) {
      user_id[rec_count] = sqlite3_column_int(res, 0);
      user_name[rec_count] = (const char *) sqlite3_column_text(res, 1);
      //  user_balanceCurr[rec_count] =  sqlite3_column_int(res, 2);
      //  user_balanceOverall[rec_count] =  sqlite3_column_int(res, 3);
      numusers++;
      rec_count++;
    }
    sqlite3_finalize(res);

    sql = "Select * from products order by id asc";

    rc = sqlite3_prepare_v2(db1, sql.c_str(), sql.length() + 1, &res, &tail);
    if (rc != SQLITE_OK) {
      String resp = "Failed to fetch data: ";
      resp += sqlite3_errmsg(db1);
      Serial.println(resp.c_str());
      return;
    }
    rec_count = 0;
    numproducts = 0;
    while (sqlite3_step(res) == SQLITE_ROW) {
      product_id[rec_count] = sqlite3_column_int(res, 0);
      product_name[rec_count] = (const char *) sqlite3_column_text(res, 1);
      product_balance[rec_count] = sqlite3_column_int(res, 2);
      numproducts++;
      rec_count++;
    }
    sqlite3_finalize(res);


    sql = "Select * from settings where setting = 'ssid'";

    rc = sqlite3_prepare_v2(db1, sql.c_str(), sql.length() + 1, &res, &tail);
    if (rc != SQLITE_OK) {
      String resp = "Failed to fetch data: ";
      resp += sqlite3_errmsg(db1);
      Serial.println(resp.c_str());
      return;
    }
    while (sqlite3_step(res) == SQLITE_ROW) {
      ssid = (const char *) sqlite3_column_text(res, 2);
    }
    sqlite3_finalize(res);

    sql = "Select * from settings where setting = 'password'";

    rc = sqlite3_prepare_v2(db1, sql.c_str(), sql.length() + 1, &res, &tail);
    if (rc != SQLITE_OK) {
      String resp = "Failed to fetch data: ";
      resp += sqlite3_errmsg(db1);
      Serial.println(resp.c_str());
      return;
    }
    while (sqlite3_step(res) == SQLITE_ROW) {
      password = (const char *) sqlite3_column_text(res, 2);
    }
    sqlite3_finalize(res);

  }
}


void updatetime()
{

  currTime = millis();
  if (currTime - lastTime > 1000)
  {
    lastTime = currTime;
    // "%d/%m/%Y %H:%M:%S"

    //static String getEpochStringByParams(long time, char* pattern = (char *)"%d/%m/%Y %H:%M:%S"){

    Hour = getEpochTmByParams(CE.toLocal(now())).tm_hour;
    Min = getEpochTmByParams(CE.toLocal(now())).tm_min;
    Sec = getEpochTmByParams(CE.toLocal(now())).tm_sec;
    Day = getEpochTmByParams(CE.toLocal(now())).tm_wday;
    Year = getEpochTmByParams(CE.toLocal(now())).tm_year;

    if (Year < 120 )
    {
      currDate = "---";
    }
    else
    {
      currDateTime = getEpochStringByParams(CE.toLocal(now()));
      currDate = currDateTime.substring(0, 10);
    }
    //Serial.println(currDate);
    /* timeClient.update();


      Serial.println(CE.locIsDST(timeClient.)));

      Hour = timeClient.getHours();
      Min = timeClient.getMinutes();
      Sec = timeClient.getSeconds();
      Day = timeClient.getDay();*/

    /*  Serial.print(" H1: ");
      Serial.print(Hl);
      Serial.print(" S1: ");
      Serial.println(Sl);

      Serial.print(SecondsLeft );
      Serial.print(", ");
      Serial.print(daysOfTheWeek[timeClient.getDay()]);
      Serial.print(", ");
      Serial.print(timeClient.getHours());
      Serial.print(":");
      Serial.print(timeClient.getMinutes());
      Serial.print(":");
      Serial.println(timeClient.getSeconds());
      Serial.print("DAY: ");
      Serial.println(timeClient.getDay());
      //Serial.println(timeClient.getFormattedTime());*/

  }
}

int counter(int currval, int minval, int maxval, bool wrap)
{
  if (minval > maxval)
  {
    maxval = minval;
  }
  bool in1;
  bool in2;
  in1 = digitalRead(ROTA);   // read the input pin
  in2 = digitalRead(ROTB);   // read the input pin


  if (not in1 and in2)
  {
    if (state1 == 0)
    {
      currval++;
      state1 = 1; //busy
      if (currval > maxval) {
        if (wrap)
        {
          currval = minval;
        }
        else
        {
          currval = maxval;
        }
      }
    }
  }
  if (in1 and not in2)
  {
    if (state1 == 0)
    {
      currval--;
      state1 = 1; //busy
      if (currval < minval) {
        if (wrap)
        {
          currval = maxval;
        }
        else
        {
          currval = minval;
        }
      }
    }
  }
  if (in1 and in2)
  {
    state1 = 0;
  }
  return currval;
}


int exitconfirm()
{
  bool in1;
  bool in2;
  in1 = digitalRead(ENTERA);   // read the input pin
  in2 = digitalRead(ENTERB);   // read the input pin

  int ret = 0;

  unsigned long tNow = millis();

  if (not in1 and in2 and state2 == 0 and tNow - tStamp1 > 300)
  {
    state2 = 1;
    ret = 1;
    //Serial.println("ENTER! >>");
  }
  if (in1 and in2 and state2 == 0)
  {
    state2 = 1;
    ret = 2;
    //Serial.println("Return <<!");
  }

  if (not in1 and not in2 and state2 > 0 and tNow - tStamp1 > 300)
  {
    tStamp1 = millis();
    state2 = 0;
  }

  return ret;
}

const char* data = "Callback function called";
static int callback(void *data, int argc, char **argv, char **azColName) {
  int i;
  Serial.printf("%s: ", (const char*)data);
  for (i = 0; i < argc; i++) {
    Serial.printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
  }
  Serial.printf("\n");
  return 0;
}

int openDb(const char *filename, sqlite3 **db) {
  int rc = sqlite3_open(filename, db);
  if (rc) {
    Serial.printf("Can't open database: %s\n", sqlite3_errmsg(*db));
    return rc;
  } else {
    Serial.printf("Opened database successfully\n");
  }
  return rc;
}

int db_exec(sqlite3 * db, const char *sql) {
  Serial.println(sql);
  long start = micros();
  int rc = sqlite3_exec(db, sql, callback, (void*)data, &zErrMsg);
  if (rc != SQLITE_OK) {
    Serial.printf("SQL error: %s\n", zErrMsg);
    sqlite3_free(zErrMsg);
  } else {
    Serial.printf("Operation done successfully\n");
  }
  Serial.print(F("Time taken:"));
  Serial.println(micros() - start);
  return rc;
}


void listDir(fs::FS & fs, const char * dirname, uint8_t levels) {
  Serial.printf("Listing directory: %s\n", dirname);

  File root = fs.open(dirname);
  if (!root) {
    Serial.println("Failed to open directory");
    return;
  }
  if (!root.isDirectory()) {
    Serial.println("Not a directory");
    return;
  }

  File file = root.openNextFile();
  while (file) {
    if (file.isDirectory()) {
      Serial.print("  DIR : ");
      Serial.println(file.name());
      if (levels) {
        listDir(fs, file.name(), levels - 1);
      }
    } else {
      Serial.print("  FILE: ");
      Serial.print(file.name());
      Serial.print("  SIZE: ");
      Serial.println(file.size());
    }
    file = root.openNextFile();
  }
}

void createDir(fs::FS & fs, const char * path) {
  Serial.printf("Creating Dir: %s\n", path);
  if (fs.mkdir(path)) {
    Serial.println("Dir created");
  } else {
    Serial.println("mkdir failed");
  }
}

void removeDir(fs::FS & fs, const char * path) {
  Serial.printf("Removing Dir: %s\n", path);
  if (fs.rmdir(path)) {
    Serial.println("Dir removed");
  } else {
    Serial.println("rmdir failed");
  }
}

void readFile(fs::FS & fs, const char * path) {
  Serial.printf("Reading file: %s\n", path);

  File file = fs.open(path);
  if (!file) {
    Serial.println("Failed to open file for reading");
    return;
  }

  Serial.print("Read from file: ");
  while (file.available()) {
    Serial.write(file.read());
  }
  file.close();
}

void writeFile(fs::FS & fs, const char * path, const char * message) {
  Serial.printf("Writing file: %s\n", path);

  File file = fs.open(path, FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing");
    return;
  }
  if (file.print(message)) {
    Serial.println("File written");
  } else {
    Serial.println("Write failed");
  }
  file.close();
}

void appendFile(fs::FS & fs, const char * path, const char * message) {
  Serial.printf("Appending to file: %s\n", path);

  File file = fs.open(path, FILE_APPEND);
  if (!file) {
    Serial.println("Failed to open file for appending");
    return;
  }
  if (file.print(message)) {
    Serial.println("Message appended");
  } else {
    Serial.println("Append failed");
  }
  file.close();
}

void renameFile(fs::FS & fs, const char * path1, const char * path2) {
  Serial.printf("Renaming file %s to %s\n", path1, path2);
  if (fs.rename(path1, path2)) {
    Serial.println("File renamed");
  } else {
    Serial.println("Rename failed");
  }
}

void deleteFile(fs::FS & fs, const char * path) {
  Serial.printf("Deleting file: %s\n", path);
  if (fs.remove(path)) {
    Serial.println("File deleted");
  } else {
    Serial.println("Delete failed");
  }
}

void testFileIO(fs::FS & fs, const char * path) {
  File file = fs.open(path);
  static uint8_t buf[512];
  size_t len = 0;
  uint32_t start = millis();
  uint32_t end = start;
  if (file) {
    len = file.size();
    size_t flen = len;
    start = millis();
    while (len) {
      size_t toRead = len;
      if (toRead > 512) {
        toRead = 512;
      }
      file.read(buf, toRead);
      len -= toRead;
    }
    end = millis() - start;
    Serial.printf("%u bytes read for %u ms\n", flen, end);
    file.close();
  } else {
    Serial.println("Failed to open file for reading");
  }


  file = fs.open(path, FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing");
    return;
  }

  size_t i;
  start = millis();
  for (i = 0; i < 2048; i++) {
    file.write(buf, 512);
  }
  end = millis() - start;
  Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
  file.close();
}