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.