Contexts in which ESP32 can write to SD card

I am writing a wireless controller GUI on the M5Stack Basic Core that incorporates an ESP32, a small display, three buttons for which I have setup to trigger hardware interrupts, and an SD card interface (CS is pin 4 on this device). It's rather large, and I'm a new user so I can't upload the files, so please excuse the long post. A lot of the below code is specific to the M5Unified.h library and the application for which I'm writing the GUI - most of which is probably not relevant (but who knows). However, the SD write functions are from SD.h library.

Base file "ALLremote.ino"

#include <esp_now.h>
#include <WiFi.h>
#include <SD.h>
#include "FS.h"

#include <M5Unified.h>

#define CHANNEL 1
#define MENU_TIMEOUT_S 10

volatile int GMToffset;
volatile int surveyIdx = 1;
bool connected = false;
int lastPacket_s = 101;

bool sdWrite = false;
File sdroot;
String currentLogFileName = "No Current Log Files";
String currentLogFilePath;

const uint8_t rxMAC[6] = {0xEC, 0x64, 0xC9, 0x06, 0x12, 0x44};
esp_now_peer_info_t ALLReceiver;
struct remote_packet incoming_p;
struct rx_packet command_p;
esp_err_t result;
enum buttonPress {
  ABUTN, BBUTN, CBUTN, NONE
};

enum menus {
  MN_DISPLAY, MN_MENU, FS_MENU
};

buttonPress butn = NONE;

void IRAM_ATTR Apress() {
    butn = ABUTN;
}

void IRAM_ATTR Bpress() {
    butn = BBUTN;
}

void IRAM_ATTR Cpress() {
    butn = CBUTN;
}

// Init ESP Now with fallback
void InitESPNow() {
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    //M5.Lcd.println("ESPNow Init Success");
  }
  else {
    M5.Lcd.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

void configDeviceAP() {
  const char *SSID = "ALLRemote";
  bool result = WiFi.softAP(SSID, "imegallremote", CHANNEL, 0);
  if (!result) {
    M5.Lcd.println("AP Config failed.");
  } else {
    //M5.Lcd.println("AP Config Success. Broadcasting with AP: " + String(SSID));
  }
}

void setup() {

  M5.begin();
  M5.Power.begin();
  M5.Rtc.begin();
  Serial.begin(115200);

  attachInterrupt(39, Apress, FALLING);
  attachInterrupt(38, Bpress, FALLING);
  attachInterrupt(37, Cpress, FALLING);
  
  //Set device in AP mode to begin with
  WiFi.mode(WIFI_STA);
  // configure device AP mode
  configDeviceAP();
  // Init ESPNow with a fallback logic
  InitESPNow();

  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info.
  esp_now_register_recv_cb(OnDataRecv);

  memcpy(ALLReceiver.peer_addr, rxMAC, 6);
  ALLReceiver.channel = CHANNEL;
  ALLReceiver.ifidx = WIFI_IF_STA;
  result = esp_now_add_peer(&ALLReceiver);
  if (result != ESP_OK)
  {
    Serial.println("Failed to add peer");
  }

  if (!SD.begin(4, SPI, 4000000)) {  
    M5.Lcd.println("Card failed, or not present");
    while (1)
        ;
  }

  if (!SD.exists("/surveys"))
  {
    SD.mkdir("/surveys");
    Serial.println("surveys directory created");
  }  
  
  //newSurvey(SD); //it works here
  mainDisplay();

}

// callback when data is recv from ALL Meter
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  
  memcpy(&incoming_p, data, data_len);
  
  lastPacket_s = 0;
  
}

void loop() {


  M5.Lcd.setBrightness(0);
  while (butn == NONE)
  {
    M5.Power.lightSleep(300000);
    delay(10);//feeds the RTC watch dog
  }

  //restart radio stuff after waking up
  WiFi.mode(WIFI_STA);

  configDeviceAP();

  InitESPNow();

  mainDisplay();
  
}

struct remote_packet {
  uint16_t lux = 45;
  uint8_t satsInView = 23;
  float horizAcc = 0.02;
  
  //use 8 decminals
  double latit  = 45.12345678;
  double longit = 23.87654321;
  
  //from Rx RTC
  int hour = 12;
  int minute = 34;
  int month = 12;
  int day = 29;
  int year = 2024;
  
  int rxBatt = 9;
  
  bool read_done = true;
    
};
enum rx_command {
	LUX_READ, READ_DONE, PWR_OFF, VEML_PARAM
};

struct rx_packet {
	
	rx_command cmd = READ_DONE;
	
	uint8_t data[32] = {0};
};

mainDisplay.ino < line 150 is where I'd like to call the logPoint function



void init_label(int x_pos, int y_pos, int fg_color, int bg_color, float text_size, String show_text)
{
  M5.Lcd.setCursor(x_pos, y_pos);
  M5.Lcd.setTextSize(text_size);
  M5.Lcd.setTextColor(fg_color, bg_color);
  M5.Lcd.print(show_text);

}

static void updateMainDisplay()
{
  //receiver Battery
  init_label(10, 0, (connected ? BLACK : WHITE), (connected ? GREEN : BLACK), 3, "  ");
  init_label(12, 2, (connected ? BLACK : WHITE), (connected ? GREEN : BLACK), 2.5, "Rx");
  String rx_batt = "";
  rx_batt.concat(incoming_p.rxBatt);
  rx_batt.concat("% ");

  init_label(60, 2, GREEN, BLACK, 2.5, rx_batt);

  //Time
  String display_time = "";
  int d_hour = incoming_p.hour + GMToffset;
  if (d_hour < 0) d_hour += 24;
  else if (d_hour < 10) display_time.concat(" ");
  display_time += d_hour;
  
  display_time += ":";
  if (incoming_p.minute < 10) display_time.concat("0");
  display_time += incoming_p.minute; 
  init_label(225, 0, RED, BLACK, 3, display_time);

  //SIV
  String display_siv = "SIV:";
  display_siv += incoming_p.satsInView;
  display_siv += " ";//covers up the last digit if we go <10
  init_label(10, 50, (incoming_p.satsInView > 20) ? GREEN : (incoming_p.satsInView > 0) ? WHITE : RED, BLACK, 3, display_siv);

  //Horizontal Accuracy
  String display_hza = "Acc:";
  if (incoming_p.horizAcc > 99) 
    init_label(10, 80, WHITE, BLACK, 3, "--             ");
  else if (incoming_p.horizAcc > 0.3)
  {
    display_hza += incoming_p.horizAcc;
    display_hza += "m    ";
    init_label(10, 80, WHITE, BLACK, 3, display_hza);
  }
  else
  {
    display_hza += incoming_p.horizAcc;
    display_hza += "m";
    init_label(10, 80, GREEN, BLACK, 3, display_hza);
  }
  
  //Lat, Long
  /*
  String display_lat = "LAT:";
  init_label(10, 80, WHITE, BLACK, 2, display_lat);
  M5.Lcd.print(incoming_p.latit, 8);//just use the print settings from the above init Fn

  String display_long = "LONG:";
  init_label(10, 100, WHITE, BLACK, 2, display_long);
  M5.Lcd.print(incoming_p.longit, 8);//just use the print settings from the above init Fn
  */

  //Last Lux
  String display_lux = "Last: ";
  if (incoming_p.lux < 65535)
  {
    display_lux += incoming_p.lux;
    display_lux += " Lux";
  }
  else display_lux += "Unstable";
  init_label(10, 120, MAGENTA, BLACK, 3, display_lux);

  String batteryLevel = "Remote:";
  batteryLevel += M5.Power.getBatteryLevel();
  batteryLevel += "%    ";
  init_label( 10, 155, CYAN, BLACK, 2, batteryLevel);

  //Current File Name
  String currentSurvey = "Log:";
  currentSurvey.concat(currentLogFileName);
  init_label(10, 180, WHITE, BLACK, 2, currentSurvey);

  //Button Graphics
  init_label( 30, 216, BLACK, BLUE, 3, "    ");
  init_label(125, 216, BLACK, BLUE, 3, "    ");
  init_label(220, 216, BLACK, BLUE, 3, "    ");

  //Button Labels
  init_label(35, 220, BLACK, BLUE, 2.5, "READ");
  init_label(140, 220, BLACK, BLUE, 2.5, "LOG");
  init_label(225, 220, BLACK, BLUE, 2.5, "MENU");

   
}

void mainDisplay()
{
  M5.Lcd.setBrightness(80);
  updateMainDisplay();
  butn = NONE;

  int display_time = millis();
  while((millis() - display_time) < (MAIN_DISPLAY_TIMEOUT_S*1000))
  {

    String prg_bar = "";
    switch(butn){
      case ABUTN:
        //MEASURE
        butn = NONE;
        if (connected)
        {
          command_p.cmd = LUX_READ;
          result = esp_now_send(rxMAC, (uint8_t*) &command_p, sizeof(command_p));
          incoming_p.read_done = false;
          init_label(10, 120, YELLOW, BLACK, 3, "                ");
          while (!incoming_p.read_done)
          {
            init_label(10, 120, BLACK, YELLOW, 3, prg_bar);
            prg_bar += " ";
            delay(1000);
          }
        }
        display_time = millis();
        break;

      case BBUTN:
        //MEASURE and LOG
        butn = NONE;
        if (connected)
        {
          command_p.cmd = LUX_READ;
          result = esp_now_send(rxMAC, (uint8_t*) &command_p, sizeof(command_p));
          incoming_p.read_done = false;
          init_label(10, 120, YELLOW, BLACK, 3, "                ");
          while (!incoming_p.read_done)
          {
            init_label(10, 120, BLACK, YELLOW, 3, prg_bar);
            prg_bar += " ";
            delay(1000);
          }
        }
        
        if (logPoint(SD, currentLogFilePath, incoming_p.lux, incoming_p.latit, incoming_p.longit))
        {
          //success
          Serial.println("File written to");
        }
        else
        {
          //failed
          Serial.println("Failed to write");
        }
        
        
        display_time = millis();
        break;

      case CBUTN:
        butn = NONE;
        M5.Lcd.clear();
        mainMenu();
        break;
      case NONE:
        //fall through
      default: updateMainDisplay();
    }

    delay(500);
    lastPacket_s++;
    if (lastPacket_s > 100) connected = false; //connected timeout = delay
    else                  connected = true;
  }
  M5.Lcd.clear();
}

mainMenu.ino

static void updateMenu(int index)
{
  
  M5.Lcd.setCursor(0,0);
  M5.Lcd.setTextSize(3);

  if (index == 1) M5.Lcd.setTextColor(BLACK, WHITE);//Highlight the current selection
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("TIME ZONE SET");

  if (index == 2) M5.Lcd.setTextColor(BLACK, WHITE);//Highlight the current selection
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("FS MENU");

  if (index == 3) M5.Lcd.setTextColor(BLACK, WHITE);//Highlight the current selection
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("VEML7700 MENU");

  if (index == 4) M5.Lcd.setTextColor(BLACK, WHITE);//Highlight the current selection
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("RX MENU");

  if (index == 5) M5.Lcd.setTextColor(BLACK, WHITE);//Highlight the current selection
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("<BACK");

}

void updateTimeZoneSelect()
{
  M5.Lcd.setCursor(0,0);
  M5.Lcd.setTextSize(3);
  M5.Lcd.setTextColor(WHITE, BLACK);

  M5.Lcd.print("GMT     ");if(GMToffset >= 0) M5.Lcd.print("+"); M5.Lcd.print(GMToffset); M5.Lcd.println(" ");
}

void timeZoneSelect()
{
  M5.Lcd.setCursor(0,0);
  M5.Lcd.setTextSize(3);
  M5.Lcd.setTextColor(WHITE, BLACK);

  M5.Lcd.print("GMT     ");if(GMToffset >= 0) M5.Lcd.print("+"); M5.Lcd.println(GMToffset); M5.Lcd.println(" ");

  int display_time = millis();
  while((millis() - display_time) < (MENU_TIMEOUT_S*1000))
  {
    switch(butn){
      case ABUTN:
        butn = NONE;
        GMToffset--;
        updateTimeZoneSelect();
        display_time = millis();
        break;

      case BBUTN:
        butn = NONE;
        GMToffset++;
        updateTimeZoneSelect();
        display_time = millis();
        break;

      case CBUTN:
        butn = NONE;
        mainMenu();
        break;
      case NONE:
        //fall through
      default: break;
    }
    delay(300);
  }
  M5.Lcd.clear();
}

void mainMenu()
{
  int menu_idx = 1;

  updateMenu(menu_idx);

  int display_time = millis();
  while((millis() - display_time) < (MENU_TIMEOUT_S*1000))
  {
    switch(butn){
      case ABUTN:
        butn = NONE;
        menu_idx++;
        if (menu_idx>5) menu_idx = 1;
        updateMenu(menu_idx);
        display_time = millis();
        break;

      case BBUTN:
        butn = NONE;
        menu_idx--;
        if (menu_idx<1) menu_idx = 5;
        updateMenu(menu_idx);
        display_time = millis();
        break;

      case CBUTN:
        butn = NONE;
        if (menu_idx == 5){//BACK
          M5.Lcd.clear();
          mainDisplay();
        }
        else if (menu_idx == 4){//RX MENU
          //M5.Lcd.clear();
          //TODO
        }
        else if (menu_idx == 3){//VEML7700 
          //M5.Lcd.clear();
          //TODO
        }
        else if (menu_idx == 2){//FS MENU
          M5.Lcd.clear();
          FSmenu();
          
          
        }
        else if (menu_idx == 1){//TIME ZONE MENU
          M5.Lcd.clear();
          timeZoneSelect();
        }

        break;
      case NONE:
        //fall through
      default: break;
    }
    delay(300);
  }
  M5.Lcd.clear();
  
}

menuFS.ino < line 166 is where I would like to execute the newSurvey function

#include "FS.h"
#include "SD.h"
#include "SPI.h"

#define DELETE_OP 1
#define RESUME_OP 2

void resumeSurvey(int sel)
{
  File root = SD.open("/surveys");
  File file = root.openNextFile();

  for (int i = 1; i < sel; i++)
  {
    file = root.openNextFile();
  }

  currentLogFilePath = file.path();
  currentLogFileName = file.name();
}

int updateSurveySelect(fs::FS &fs, int index){
    M5.Lcd.setCursor(0,0);
    M5.Lcd.setTextSize(3);

    File root = fs.open("/");
    
    File file = root.openNextFile();
    int count = 0;
    while(file){
      if(file.isDirectory())
      {
          // do nothing
      } 
      else 
      {
        if (index == (count+1)) M5.Lcd.setTextColor(BLACK, WHITE);//Highlight the current selection
        else                    M5.Lcd.setTextColor(WHITE, BLACK);
        M5.Lcd.println(file.name());
      }
      file = root.openNextFile();
      count++;
    }

    return count;
}

void surveySelect(int fileOp)
{
  int menu_idx = 1;
  int numFiles = updateSurveySelect(SD, menu_idx);
  if (numFiles == 0) 
    return;
  int display_time = millis();
  while((millis() - display_time) < (MENU_TIMEOUT_S*1000))
  {
    switch(butn){
        case ABUTN:
          butn = NONE;
          menu_idx++;
          if (menu_idx >= numFiles) menu_idx = 1;
          updateSurveySelect(SD, menu_idx);
          display_time = millis();
          break;

        case BBUTN:
          butn = NONE;
          menu_idx--;
          if (menu_idx < 1) menu_idx = (numFiles-1);
          updateSurveySelect(SD, menu_idx);
          display_time = millis();
          break;

        case CBUTN:
          butn = NONE;
          if (fileOp == RESUME_OP)
          {
            resumeSurvey(menu_idx);
            M5.Lcd.clear();
            init_label(10, 10, WHITE, BLACK, 2, "Current Survey is now:\n");
            M5.Lcd.println(currentLogFileName);
            delay(3000);
            M5.Lcd.clear();
            mainDisplay();
          } 
          else if (fileOp == DELETE_OP)
          {
            deleteSurvey(menu_idx);
            delay(3000);
            M5.Lcd.clear();
            FSmenu();
          }
          break;
        case NONE:
          //fall through
        default: break;
      }
  }
}

void updateFSmenu(int index)
{
  M5.Lcd.setCursor(0,0);
  M5.Lcd.setTextSize(3);

  if (index == 1) M5.Lcd.setTextColor(BLACK, WHITE);//Highlight the current selection
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("NEW SURVEY");

  if (index == 2) M5.Lcd.setTextColor(BLACK, WHITE);
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("RESUME SURVEY");

  if (index == 3) M5.Lcd.setTextColor(BLACK, WHITE);
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("DELETE SURVEY");

  if (index == 4) M5.Lcd.setTextColor(BLACK, WHITE);
  else            M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println("<BACK");
}

void FSmenu()
{
   int menu_idx = 1;

  updateFSmenu(menu_idx);

  int display_time = millis();
  while((millis() - display_time) < (MENU_TIMEOUT_S*1000))
  {
    switch(butn){
      case ABUTN:
        butn = NONE;
        menu_idx++;
        if (menu_idx>4) menu_idx = 1;
        updateFSmenu(menu_idx);
        display_time = millis();
        break;

      case BBUTN:
        butn = NONE;
        menu_idx--;
        if (menu_idx<1) menu_idx = 4;
        updateFSmenu(menu_idx);
        display_time = millis();
        break;

      case CBUTN:
        butn = NONE;
        if (menu_idx == 4){//BACK
          M5.Lcd.clear();
          mainMenu();
        }
        else if (menu_idx == 3){//DELETE
          M5.Lcd.clear();
          surveySelect(DELETE_OP);          
        }
        else if (menu_idx == 2){//RESUME
          M5.Lcd.clear();
          surveySelect(RESUME_OP);
          
        }
        else if (menu_idx == 1){//NEW
          M5.Lcd.clear();
          newSurvey(SD);//doesn't work here
        }

        break;
      case NONE:
        //fall through
      default: break;
    }
    delay(300);
  }
  M5.Lcd.clear();
}

SDwrites.ino < contains the functions that write the new files.

void newSurvey(fs::FS &fs)
{
  String fileName = "/surveys/";
  
  fileName.concat(incoming_p.month);
  fileName.concat(incoming_p.day);
  fileName.concat(incoming_p.year);
  fileName.concat("_");
  fileName.concat(incoming_p.hour);
  fileName.concat(incoming_p.minute);
  fileName.concat(".csv");
  File newSurveyFile = fs.open(fileName, FILE_WRITE, true);
  if (newSurveyFile)
  {
    currentLogFilePath = newSurveyFile.path();
    currentLogFileName = newSurveyFile.name();

    Serial.print("Current Log Path: ");
    Serial.println(newSurveyFile.path());
  }
  else 
    Serial.println("Error creating file");
  newSurveyFile.close();
}

bool logPoint(fs::FS &fs, String path, uint16_t luxVal, double latittude, double longitude){
    
    bool retVal;
    
    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 false;
    }
    if(file.print(luxVal) && file.print(", ") && 
        file.print(latittude, 8) && file.print(", ") &&
        file.print(longitude, 8) && file.println()){
        Serial.println("Data Point Logged");
        retVal = true;
    } else {
        Serial.println("Append failed");
        retVal = false;
    }
    file.close();

    return retVal;
}

void deleteSurvey(int sel){

  File file;
  
  for (int i = 1; i < sel; i++)
  {
    file = sdroot.openNextFile();
  }
  
  SD.remove(file.path());
  M5.Lcd.clear();
  M5.Lcd.setTextSize(2);
  M5.Lcd.println(file.name());
  M5.Lcd.println("Deleted");
}

I am having trouble successfully writing new files and appending to existing files on the SD card with the GUI code I have written so far. Code compiles but I can never get my functions to actually write the file. The new file object always evaluates to false, so the program knows it doesn't exist, and I get the printed error message.

I know for certain that I have instantiated the SD interface correctly because I can successfully perform these writes in other contexts on the same device. I have tried replicating the issue many times in many ways but to no avail. I use the exact same function code in other contexts and it successfully writes as expected, as I can observe on Serial monitor and by plugging the SD card into a PC.

But when I try to use the newSurvey and logPoint functions in my GUI code, it never writes. I really don't think it has anything to do with the parameters I'm giving it because I can even place the problem functions in the setup() function and it works as expected. So my initial thought was that I was calling the those functions in a bad thread context, but again, I cannot replicate issue with the exact same context in a test sketch. So honestly I have no idea what I'm doing wrong or how to get these functions to work in the desired context.

Apologies for the lengthy post but I think posting all my code is the only way to find the issue here.

Thanks in advance.

Most Arduino libraries, including the SD library, contain example programs that demonstrate many, if not all of the features available.

The most productive way to start with a new library is to run those examples, and verify that they work as expected. Have you done that? Start with the SD library "Files" example, which shows how to create and delete a file.

Yes I have done all of the basic to in-depth trouble shooting. The SD library is not new to me, however some of the specifics of how why some of its functions fail in certain contexts is less familiar.

The SD_test.ino test sketch performs as expected on the same device. As I mentioned, even the functions I am calling will perform as expected depending on where in the program I call them. I'm confident that I've isolated the problem I'm experiencing to the context in which I am calling functions that create and write to files on the SD card. Not the functions nor the SD interface itself.

For help on this forum, most members will want to see either the complete code, or better, the minimum code that will compile, run and demonstrate at least one of the problems.

Code compiles but I can never get my functions to actually write the file.

Very vague description. What happens instead? Post any error message or symptoms. Do you always check return values of SD library functions, to see if those return values are as expected?

Are you able to create a file on the card even if you can't write data to it?

I don't know about ESP32 SD libraries, but in Arduinos the filenames must be in 8.3 format. I don't know what the rules are on subdirectories.

After something like

File newFile = fs.open("/newFile.txt", FILE_WRITE, true);
if (newFile) //works
else //doesn't work

It only works in certain contexts.

It's a similar story for both creating a new file and writing to an existing one I'm afraid. I can confirm it's not the filename because it works when I call it in the setup function.