Hello,
i am currently writing the code for a project. The device (ESP32-S3) should connect to my phone via BLE and respond to commands, change states, etc. I am currently testing the device on a breadboard which also has a MicroSD-Module on it. The connection is correct.
The Problem:
The SD-Card seems to be connected/disconnected at random times.
Main.cpp
#include <Arduino.h>
#include <BleHandler.h>
#include <SDHandler.h>
#include <StateManager.h>
#include <VMD.h>
// Global objects
BluetoothManager bleManager;
SDHandler sdHandler;
StateManager stateManager;
// Global variable for the BLE command
BleCommand currentCommand = BleCommand::INVALID_COMMAND;
// Command callback: Store the received command
void handleCommand(BleCommand cmd)
{
currentCommand = cmd;
}
// State handlers
void handleInit()
{
Serial.println("Initializing...");
// Initialize BLE
if (sdHandler.init())
{
Serial.println("SD card initialized!");
}
else
{
Serial.println("Error initializing SD card!");
stateManager.setState(State::ERROR);
return;
}
bleManager.init("VMD_Device");
bleManager.setCommandCallback(handleCommand);
// Move to advertising state
stateManager.setState(State::ADVERTISING);
}
void handleAdvertising()
{
if (bleManager.isConnected())
{
Serial.println("Device connected!");
stateManager.setState(State::WAITING);
}
}
// Function which gets called after "GET:" and sends the data set from the SD-card
void handleDatasetRequest(const char *timestamp)
{
static char datasetList[256]; // Smaller buffer
if (sdHandler.getDatasetList(datasetList, sizeof(datasetList)))
{
bleManager.sendDatasetList(datasetList);
}
else
{
bleManager.sendResponse("Error Sending Dataset!");
Serial.println("Error Sending Dataset!");
stateManager.setState(State::ERROR);
}
}
void handleWaiting()
{
// Nothing to do, just wait for commands
}
void handleMeasuring()
{
// Nothing to do for now
}
void handleError()
{
Serial.println("In ERROR state");
delay(5000);
// Add error recovery logic if needed
}
void setup()
{
Serial.begin(115200);
// Register state handlers
stateManager.setStateHandler(State::INIT, handleInit);
stateManager.setStateHandler(State::ADVERTISING, handleAdvertising);
stateManager.setStateHandler(State::WAITING, handleWaiting);
stateManager.setStateHandler(State::MEASURING, handleMeasuring);
stateManager.setStateHandler(State::ERROR, handleError);
bleManager.setDatasetRequestCallback(handleDatasetRequest);
// Start with INIT state
stateManager.setState(State::INIT);
}
void loop()
{
stateManager.update();
// Check if the BLE device is still connected
if (!bleManager.isConnected() && stateManager.getCurrentState() != State::ADVERTISING && stateManager.getCurrentState() != State::ERROR)
{
if (stateManager.getCurrentState() == State::WAITING)
{
stateManager.setState(State::ADVERTISING);
return;
}
Serial.println("Device disconnected, transitioning to ERROR state");
stateManager.setState(State::ERROR);
return;
}
// Handle the command in the main loop
if (bleManager.isCommandReceived())
{
Serial.println(int(currentCommand));
switch (currentCommand)
{
case BleCommand::START_MEASURE:
Serial.println("Received START_MEASURE command");
stateManager.setState(State::MEASURING);
break;
case BleCommand::STOP_MEASURE:
Serial.println("Received STOP_MEASURE command");
if (stateManager.getCurrentState() == State::MEASURING)
{
stateManager.setState(State::WAITING);
}
break;
case BleCommand::GET_DATASET_LIST:
Serial.println("Received GET_DATASET_LIST command");
char datasetList[1024];
if (sdHandler.getDatasetList(datasetList, sizeof(datasetList)))
{
if (strlen(datasetList) == 0)
{
Serial.println("Dataset list is empty");
bleManager.sendDatasetList("empty");
}
else
{
Serial.print("The dataset list is: ");
Serial.println(datasetList);
bleManager.sendDatasetList(datasetList);
}
}
break;
case BleCommand::GET_BATTERY:
{
Serial.println("Received GET_BATTERY command");
// Read battery level
int battery_level = static_cast<int>(2 * analogRead(BATTERY_ADC));
if (bleManager.isConnected())
{
bleManager.sendResponse(battery_level);
Serial.printf("Sent battery level: %u\n", battery_level);
}
else
{
Serial.println("BLE not connected, can't send battery level");
}
stateManager.setState(State::WAITING);
break;
}
case BleCommand::GET_DATASET:
Serial.println("Received GET_DATASET command");
break;
case BleCommand::INVALID_COMMAND:
Serial.println("Received INVALID_COMMAND, going back");
stateManager.setState(State::WAITING);
break;
}
bleManager.resetCommandReceived();
}
delay(10);
}
SDHandler.cpp
#include <SDHandler.h>
#include <VMD.h>
const char *SDHandler::CONFIG_FILE = "/config.csv";
const char *SDHandler::DATA_DIRECTORY = "/data";
const char *SDHandler::FILE_EXTENSION = ".csv";
SDHandler::SDHandler() : cardPresent(false) {}
bool SDHandler::init()
{
// Setup SPI pins only once
SPI.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_CS);
// Add a power-up delay
delay(1000);
// Initialize SD card with CS pin
if (!SD.begin(SD_CS))
{
Serial.println("SD card initialization failed");
return false;
}
// Check card type
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE)
{
Serial.println("No SD card attached");
return false;
}
// Create data directory if it doesn't exist
if (!checkCreatePath(DATA_DIRECTORY))
{
Serial.println("Failed to create data directory");
return false;
}
cardPresent = true;
return true;
}
bool SDHandler::startNewLog()
{
if (!SD.exists(DATA_DIRECTORY))
{
SD.mkdir(DATA_DIRECTORY);
}
// Count the number of existing log files
File root = SD.open(DATA_DIRECTORY);
if (!root || !root.isDirectory())
return false;
int fileCount = 0;
File file;
while (file = root.openNextFile())
{
if (!file.isDirectory() && String(file.name()).endsWith(FILE_EXTENSION))
{
fileCount++;
}
file.close();
}
root.close();
// If the limit is reached, delete the file named "MeasurementLog1.csv"
if (fileCount >= 100)
{
String oldestFileName = String(DATA_DIRECTORY) + "/MeasurementLog1.csv";
SD.remove(oldestFileName);
// Rename remaining files to maintain the sequence
for (int i = 2; i <= fileCount; i++)
{
String oldName = String(DATA_DIRECTORY) + "/MeasurementLog" + String(i) + ".csv";
String newName = String(DATA_DIRECTORY) + "/MeasurementLog" + String(i - 1) + ".csv";
SD.rename(oldName, newName);
}
}
// Create a new log file
String filename = createFileName(generateTimestamp());
currentLogFile = SD.open(filename.c_str(), FILE_WRITE);
if (!currentLogFile)
return false;
return writeHeader(currentLogFile);
}
bool SDHandler::logData(float *data, int dataSize)
{
if (!currentLogFile)
return false;
// Write timestamp
currentLogFile.print(generateTimestamp());
// Write data values
for (int i = 0; i < dataSize; i++)
{
currentLogFile.print(",");
currentLogFile.print(data[i]);
}
currentLogFile.println();
// Ensure data is written
currentLogFile.flush();
return true;
}
bool SDHandler::getDatasetList(char *buffer, size_t bufferSize)
{
File root = SD.open(DATA_DIRECTORY);
if (!root || !root.isDirectory())
return false;
String list = "";
File file;
while (file = root.openNextFile())
{
if (!file.isDirectory() && String(file.name()).endsWith(FILE_EXTENSION))
{
// Extract timestamp from filename
String filename = file.name();
String timestamp = filename.substring(0, filename.lastIndexOf('.'));
list += timestamp + ";";
}
file.close();
}
root.close();
if (list.length() >= bufferSize)
return false;
strcpy(buffer, list.c_str());
return true;
}
bool SDHandler::readDataset(const char *timestamp, char *buffer, size_t *dataSize)
{
String filename = createFileName(timestamp);
File file = SD.open(filename.c_str(), FILE_READ);
if (!file)
return false;
*dataSize = file.size();
if (*dataSize > 0)
{
file.read((uint8_t *)buffer, *dataSize);
}
file.close();
return true;
}
bool SDHandler::getDatasetInfo(const char *timestamp, uint32_t *size, uint32_t *records)
{
String filename = createFileName(timestamp);
File file = SD.open(filename.c_str(), FILE_READ);
if (!file)
return false;
*size = file.size();
// Count number of lines (records)
*records = 0;
while (file.available())
{
if (file.read() == '\n')
(*records)++;
}
file.close();
return true;
}
String SDHandler::generateTimestamp()
{
// Scan data directory for existing logs
File root = SD.open(DATA_DIRECTORY);
if (!root || !root.isDirectory())
{
return "MeasurementLog1";
}
int maxIndex = 0;
File file;
// Look through all files
while (file = root.openNextFile())
{
if (!file.isDirectory())
{
String filename = file.name();
// Check if filename matches our pattern
if (filename.startsWith("MeasurementLog"))
{
// Extract the number after "MeasurementLog"
String numberPart = filename.substring(14); // "MeasurementLog" is 14 characters
// Remove the .csv extension if present
numberPart = numberPart.substring(0, numberPart.indexOf('.'));
// Convert to integer and update maxIndex if larger
int currentIndex = numberPart.toInt();
if (currentIndex > maxIndex)
{
maxIndex = currentIndex;
}
}
}
file.close();
}
root.close();
// Return next number in sequence
return "MeasurementLog" + String(maxIndex + 1);
}
String SDHandler::createFileName(const String ×tamp)
{
return String(DATA_DIRECTORY) + "/" + timestamp + FILE_EXTENSION;
}
bool SDHandler::writeHeader(File &file)
{
return file.println("timestamp,value1,value2,value3"); // Adjust headers as needed
}
bool SDHandler::checkCreatePath(const char *path)
{
if (!SD.exists(path))
{
if (!SD.mkdir(path))
{
Serial.printf("Failed to create directory: %s\n", path);
return false;
}
}
return true;
}
bool SDHandler::closeCurrentLog()
{
if (currentLogFile)
{
currentLogFile.close();
return true;
}
return false;
}
bool SDHandler::isCardPresent() const
{
return cardPresent;
}
BleHandler.cpp
#include "BleHandler.h"
// Define UUIDs
const char *BluetoothManager::SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
const char *BluetoothManager::COMMAND_CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
const char *BluetoothManager::RESPONSE_CHAR_UUID = "beb5483f-36e1-4688-b7f5-ea07361b26a8";
const char *BluetoothManager::DATA_CHAR_UUID = "beb54840-36e1-4688-b7f5-ea07361b26a8";
BluetoothManager::BluetoothManager() : deviceConnected(false),
hasErrorFlag(false),
commandReceived(false),
onCommandReceived(nullptr),
onDatasetRequested(nullptr)
{
memset(lastError, 0, sizeof(lastError));
}
void BluetoothManager::init(const char *deviceName)
{
// Initialize BLE device
BLEDevice::init(deviceName);
// Create server
pServer = BLEDevice::createServer();
pServer->setCallbacks(this);
// Create service
pService = pServer->createService(SERVICE_UUID);
// Create characteristics
pCommandCharacteristic = pService->createCharacteristic(
COMMAND_CHAR_UUID,
BLECharacteristic::PROPERTY_WRITE);
pCommandCharacteristic->setCallbacks(this);
pResponseCharacteristic = pService->createCharacteristic(
RESPONSE_CHAR_UUID,
BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ);
pResponseCharacteristic->addDescriptor(new BLE2902());
pDataCharacteristic = pService->createCharacteristic(
DATA_CHAR_UUID,
BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ);
pDataCharacteristic->addDescriptor(new BLE2902());
// Start service and advertising
pService->start();
startAdvertising();
}
void BluetoothManager::startAdvertising()
{
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
}
void BluetoothManager::stopAdvertising()
{
BLEDevice::stopAdvertising();
}
void BluetoothManager::onConnect(BLEServer *pServer)
{
deviceConnected = true;
clearError();
}
void BluetoothManager::onDisconnect(BLEServer *pServer)
{
deviceConnected = false;
// Start advertising again
startAdvertising();
}
void BluetoothManager::onWrite(BLECharacteristic *pCharacteristic)
{
if (pCharacteristic == pCommandCharacteristic)
{
std::string value = pCharacteristic->getValue();
if (value.length() > 0)
{
processCommand(value.c_str());
}
}
}
void BluetoothManager::processCommand(const char *command)
{
if (!command)
return;
BleCommand cmd = parseCommand(command);
commandReceived = true;
Serial.print("In processCommand: ");
Serial.println(int(cmd));
// Use static buffer instead of stack
static char errorMsg[32];
if (onCommandReceived)
onCommandReceived(cmd);
switch (cmd)
{
case BleCommand::INVALID_COMMAND:
snprintf(errorMsg, sizeof(errorMsg), "ERROR: Invalid command");
sendResponse(errorMsg);
break;
case BleCommand::GET_DATASET:
if (onDatasetRequested)
{
const char *timestamp = strchr(command, ':');
if (timestamp)
onDatasetRequested(timestamp + 1);
}
break;
default:
break;
}
}
BleCommand BluetoothManager::parseCommand(const char *cmd)
{
Serial.print("In parseCommand: ");
Serial.println(cmd);
if (!cmd || strlen(cmd) == 0)
return BleCommand::INVALID_COMMAND;
else if (strcmp(cmd, "START") == 0)
return BleCommand::START_MEASURE;
else if (strcmp(cmd, "STOP") == 0)
return BleCommand::STOP_MEASURE;
else if (strcmp(cmd, "BATTERY") == 0)
return BleCommand::GET_BATTERY;
else if (strcmp(cmd, "LIST") == 0)
return BleCommand::GET_DATASET_LIST;
else if (strncmp(cmd, "GET:", 4) == 0 && strlen(cmd) > 4)
return BleCommand::GET_DATASET;
else
{
Serial.print("In parseCommand/else: ");
Serial.println("Invalid");
return BleCommand::INVALID_COMMAND;
}
}
void BluetoothManager::sendResponse(const char *data)
{
if (!deviceConnected || strlen(data) == 0)
{
return;
}
std::string convertedString(data);
pResponseCharacteristic->setValue(convertedString);
pResponseCharacteristic->notify();
delay(RESPONSE_DELAY);
}
void BluetoothManager::sendResponse(u16_t value)
{
if (!deviceConnected)
{
return;
}
pResponseCharacteristic->setValue(value); // Send the integer
pResponseCharacteristic->notify();
delay(RESPONSE_DELAY);
}
void BluetoothManager::sendDatasetList(const char *list)
{
if (deviceConnected)
{
pResponseCharacteristic->setValue((uint8_t *)list, strlen(list));
pResponseCharacteristic->notify();
delay(RESPONSE_DELAY);
}
}
void BluetoothManager::sendMeasurementData(const char *data, size_t length)
{
if (deviceConnected)
{
// Send data in chunks if necessary (MTU size consideration)
const size_t maxChunkSize = 512;
size_t sent = 0;
while (sent < length)
{
size_t chunkSize = min(maxChunkSize, length - sent);
pDataCharacteristic->setValue((uint8_t *)(data + sent), chunkSize);
pDataCharacteristic->notify();
sent += chunkSize;
delay(RESPONSE_DELAY);
}
}
}
void BluetoothManager::setCommandCallback(CommandCallback callback)
{
onCommandReceived = callback;
}
void BluetoothManager::setDatasetRequestCallback(DatasetRequestCallback callback)
{
onDatasetRequested = callback;
}
bool BluetoothManager::isConnected() const
{
return deviceConnected;
}
bool BluetoothManager::hasError() const
{
return hasErrorFlag;
}
const char *BluetoothManager::getLastError() const
{
return lastError;
}
void BluetoothManager::setError(const char *error)
{
hasErrorFlag = true;
strncpy(lastError, error, sizeof(lastError) - 1);
lastError[sizeof(lastError) - 1] = '\0';
}
void BluetoothManager::clearError()
{
hasErrorFlag = false;
lastError[0] = '\0';
}
bool BluetoothManager::isCommandReceived()
{
return commandReceived;
}
void BluetoothManager::resetCommandReceived()
{
if (commandReceived)
{
commandReceived = false;
}
}
My problem arises when:
Connect to device -> Send "LIST" (works) -> Send "BATTERY" (works) -> Send "LIST"
I get this on the serial port:
Initializing...
SD card initialized!
State change: INIT -> ADVERTISING
Device connected!
State change: ADVERTISING -> WAITING
Seems good, device has connected
Then:
In parseCommand: LIST
In processCommand: 3
3
Received GET_DATASET_LIST command
Dataset list is empty
This seems to work, the first 4 outputs are for debugging. I also get "empty" on my phone.
Then:
In parseCommand: BATTERY
In processCommand: 2
2
Received GET_BATTERY command
Sent battery level: 7846
Again, this Command works
In parseCommand: LIST
In processCommand: 3
3
Received GET_DATASET_LIST command
[ 20765][E][sd_diskio.cpp:126] sdSelectCard(): Select Failed
[ 20770][E][sd_diskio.cpp:624] ff_sd_status(): Check status failed
[ 21276][E][sd_diskio.cpp:126] sdSelectCard(): Select Failed
[ 21781][E][sd_diskio.cpp:126] sdSelectCard(): Select Failed
[ 21786][E][vfs_api.cpp:105] open(): /sd/data does not exist, no permits for creation
This is the problem.
Somehow, the SD Card wont work correctly. More Context:
When I reset/start the ESP, the SD Card wont mount sometimes. I need to press Reset a few times so that it works. Could this be the problem?
Since i am working on the breadboard, the problem could also lie at the SPI bus, which could have problems because of the cables (jumpers) and the module.
I tried the SD test example and it works, but only sometimes, like i explained. I need to press reset sometimes until it works.