Introduction
In this tutorial we are going to learn how to use the C++ ifstream and ofstream classes to write and read files on the ESP32. We will be using the Arduino core.
For our example to work, we need to have a file system mounted on the ESP32. In this example we will be using the SPIFFS file system. For an introductory tutorial on how to mount the SPIFFS file system, you can check here.
Note that the Arduino core offers APIs to work with files, which are flexible enough for most basic operations. The reason for us to explore these C++ classes is related with the possibility of using C++ libraries on the ESP32, such as we have covered before with Nlohmann/json and Inja.
As such, some of the features of these libraries are built to work with standard C++ classes. For example, the Nlohmann/json allows to write and read JSON files to / from the file system using the ifstream and ofstream classes, as can be seen here.
The tests shown below were performed on a ESP32-E FireBeetle board from DFRobot. The Arduino core version used was 2.0.0 and the Arduino IDE version was 1.8.15, operating on Windows 8.1.
The code
We will start by the library includes. We will need the SPIFFS.h library, which will allow us to initialize the SPIFFS file system, and the fstream library, which we need to be able to use the classes that allow us to write and read from a file.
#include <SPIFFS.h>
#include <fstream>
We will then move to the Arduino setup, where we will write the rest of our code. As usual, we will start by opening a serial connection, so we can later output the results of our program.
Serial.begin(115200);
After this we will take care of mounting the SPIFFS file system. To do so, we simply need to call the begin method on the SPIFFS extern variable. This variable becomes available from the SPIFFS.h library include.
Note that the begin method receives, as optional input, a Boolean flag that specifies if the SPIFFS file system should be formatted in case the mounting process fails. We will pass the value true.
Since the begin method returns a Boolean value indicating if the mounting procedure was successful (true) or not (false), we will enclose this method call in a IF block for error checking. In case of error, we will simply print a message to the serial port and end the setup execution, as we won’t be able to do the rest of the process.
if (!SPIFFS.begin(true)) {
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
Assuming the file system is successfully mounted, we will now take care of writing a file using the ofstream class. We will start by creating an object of this class.
std::ofstream fileToWrite;
Then we will call the open method on our object, passing as input the name of the file we want to create and write to. Note that the file name should include the whole path which, in our case, should start with “/spiffs“, which is the default root path when we mount the file system without passing additional parameters (you can check here the default arguments of the begin method).
We will call our file “test.txt“. As such, the full name, including the path, should be “/spiffs/test.txt“.
Note that this method returns void but, if opening the file fails, the failbit state flag is set for the stream [1]. To keep this tutorial simple we are assuming that opening the file works correctly, but in a real application scenario you should do error validations.
fileToWrite.open("/spiffs/test.txt");
Then, to write content to the file using a very short syntax, we can leverage the insertion operator. The syntax is as simple as:
stream << "string to write to the file";
For our example, we are going to write a very simple “Test” string.
fileToWrite << "Test";
Finally, we need to call the close method to close the file currently associated with the object, disassociating it from the stream [2]. Any pending output sequence is written to the file [2].
fileToWrite.close();
Now that we have our file written to the SPIFFS file system, we will check how to open and read it. For that, we will start by creating an ifstream object.
This time, we will actually use one of the overloads of the constructor where we can pass as input the name of the file. Note that we are doing this just for illustration purposes, since we could have taken the same approach as before: creating the object first and calling the open method after.
Naturally, we will use the same file name from before.
std::ifstream fileToRead("/spiffs/test.txt");
Finally we will use the get method to obtain all the characters of the file and print them to the serial port. The overloaded version of the method we are using receives as input a reference to a char, where the extracted value is stored [3].
This method returns a “*this” pointer, which can be casted to a Boolean [3]. Consequently, we can enclose the get method call in a while loop, ensuring that the loop will finish when there are no more characters to read.
char c;
while(fileToRead.get(c)){
Serial.print(c);
}
The complete code is shown below.
#include <SPIFFS.h>
#include <fstream>
void setup() {
Serial.begin(115200);
if (!SPIFFS.begin(true)) {
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
std::ofstream fileToWrite;
fileToWrite.open("/spiffs/test.txt");
fileToWrite << "Test";
fileToWrite.close();
std::ifstream fileToRead("/spiffs/test.txt");
char c;
while(fileToRead.get(c)){
Serial.print(c);
}
}
void loop() {}
Testing the code
To test the code, simply compile it and upload it to your device, using the Arduino IDE. Once the procedure is finished, open the IDE serial monitor and you should get a result similar to figure 1.
As can be seen, we have obtained the content we originally wrote to the file, as expected.
Additional resources
References
[1] https://www.cplusplus.com/reference/fstream/ofstream/open/
[2] https://www.cplusplus.com/reference/fstream/ofstream/close/
[3] https://www.cplusplus.com/reference/istream/istream/get/