Manual

do

Maker

.

com

Como escrever arquivos no SPIFFS com ESP32

Como escrever arquivos no SPIFFS com ESP32

Apesar do SPIFFS estar descontinuado, a sintaxe para o novo sistema de arquivos (LittleFS) é a mesma.

Para manipular o SPIFFS com ESP32, vamos começar pela maneira mais simples de todas, utilizando o exemplo contido em File > Examples > SPIFFS.

Vou aproveitar o momento para dar uma dica que considero importante na hora de desenvolver.  É comum olhar pra um código, estranhar uma função/método por conter uma declaração com conceitos que não estão no nosso dia-a-dia e sentir preguiça. Nesse caso, a primeira coisa que vem à mente é: "Depois eu vejo". A mesma coisa acontece quando olhamos pra um código buscando a última linha pra ver o quanto tem de trabalho. Mas isso é mais psicológico que outra coisa. Explico.

(adsbygoogle = window.adsbygoogle || []).push({});

Nós vamos ver aqui diversas funções para manipulação de arquivos, mas quase não tem trabalho nenhum. Tudo bem, 159 linhas é pouco, mas olhando o que está implementado, é menos ainda. E veja porque:

  • Precisamos listar arquivos contidos no SPIFFS.
  • Precisamos ler arquivos criados, senão eles não tem finalidade.
  • Precisamos escrever arquivos no sistema de arquivos.
  • Podemos ter a habilidade de adicionar conteúdo a um arquivo existente.
  • Podemos querer renomear um arquivo.
  • Precisamos ter a capacidade de excluir um arquivo.
  • Podemos querer ver o tamanho de um arquivo.

Já é bastante satisfatória essa quantidade de recursos de manipulação de arquivos, não? E veja, são apenas 7 ítens. Mas aí a mente prega outra peça: Quanto vou ter que escrever de código para ter esses 7 ítens funcionando? - Bem, não devemos nos preocupar com isso. Se dividirmos em 7 etapas, podemos fazer uma função hoje, outra mais tarde, outra amanhã e daí por diante. Cada etapa é um objetivo alcançado, então, invés de implementar tudo de uma vez, defina objetivos.

Olhando a lista de tarefas acima, para poder satisfazer-se com cada etapa criada, qual seria o caminho ideal a seguir? Se estamos falando de manipulação de arquivos, a primeira coisa que precisamos é que os arquivos possam existir. Daí implementamos a função de criação de arquivos, mas como constatar o sucesso da operação? Então, suponhamos que só fossemos testar a criação de arquivos hoje, a implementação de código deveria ser:

1 - Função para a criação de arquivos

2 - Função para listar arquivos, assim comprovamos que ambas as funções funcionaram.

Vamos ao código.

Criação de arquivos com ESP32

Às vezes devemos tomar um pouco do nosso tempo estudando conceitos e entendendo as características de cada individuo de um recurso. Isso deixa as coisas mais claras, por isso que às vezes escrevo artigos mostrando uma pequena porção de código não funcional, apenas para poder discorrer a respeito. Eu poderia simplesmente colar código aqui e cumprir a tarefa de provar o conceito, mas isso não seria nada prazeroso pra você e nem para mim. O melhor é olhar pra um recurso e compreendê-lo, por essa razão elevo o nível de detalhamento sobre o que escrevo. Agora vamos compreender como funciona a criação de arquivos com o ESP32 na IDE do Arduino.

Para criar um arquivo, nós devemos escrevê-lo no sistema de arquivos SPIFFS. A manipulação do sistema de arquivos não é em tempo real porque o tempo de escrita de cada arquivo pode variar, mas isso está abstraido, portanto não precisamos nos preocupar com isso. A função contida no exemplo para escrita é essa:

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

    /* cria uma variável "file" do tipo "File", então chama a função 
    open do parâmetro fs recebido. Para abrir, a função open recebe
    os parâmetros "path" e o modo em que o arquivo deve ser aberto 
    (nesse caso, em modo de escrita com FILE_WRITE) 
    */
    File file = fs.open(path, FILE_WRITE);
    //verifica se foi possivel criar o arquivo
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }
    /*grava o parâmetro "message" no arquivo. Como a função print
    tem um retorno, ela foi executada dentro de uma condicional para
    saber se houve erro durante o processo.*/
    if(file.print(message)){
        Serial.println("File written");
    } else {
        Serial.println("Write failed");
    }
}

Tem muitíssimas pessoas que só aprenderam a programar por causa do Arduino e muitas dessas programam apenas para Arduino. Olhando  a declaração da função pode lhes causar incômodo:

void writeFile(fs::FS &fs, const char * path, const char * message)

Em C++ utilizamos "::" para chamadas estáticas de uma classe e FS é uma classe de C++.  Em seguida, esse primeiro parâmetro da função writeFile tem um &fs, significando que esse parâmetro recebe um endereço (&) denominado fs.

O segundo parâmetro da função recebe um array constante de char. O ponteiro (*) indica o começo do array que será varrido. Esse segundo parâmetro é o nome de arquivo com o caminho completo. Isto é, você poderá criar diretórios. Vale lembrar que na documentação da versão anterior do ESP-IDF (não li a mais recente) está explicitado que não há suporte a criação de diretórios, mas no ESP8266 eu já fiz muito disso, então alguém aplicou um patch e não atualizaram a documentação. Além disso, esse é um exemplo da IDE do Arduino, portanto de algum modo, sim, é possível criar diretórios na atual versão.

O terceiro e último parâmetro é o conteúdo do arquivo. Não importa se você está manipulando fórmulas matemáticas para guardar seus valores, isso deverá ser convertido para um array de char, senão vai ficar muito esquisito.

Uma tabela ASCII para consultas, quando precisar:

ascii_table.webp

Listar diretório

Se não houver erro, a função writeFile imprime a mensagem de sucesso da operação. Mas não temos constatação visual e como humanos, queremos provar o conceito, hum?

Então vamos agora implementar a função de listar arquivos (e diretórios):

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();
    }
}

Listar diretório é mais simples ainda. Repare apenas que é feito um loop para listar os arquivos. Esse loop avalia file, que é subtraido pelo retorno da função root.openNextFile(). Se reparar, a maior parte dessa função é composta por condicionais para avaliar excessões; condições que podem gerar erro. Se você não tratar as exceções, algumas anomalias podem acontecer, como por exemplo, reset do ESP32.

Apagar um arquivo

A função é trivial, nem tem o que falar dela:

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");
    }
}

Da implementação nativa do ESP-IDF, o arquivo é removido com unlink(arquivo), que faz diversos tratamentos de excessões. Depois na classe FSremove é chamado, tratando mais uma exceção.

Por isso que quanto melhor for a implementação, menos código precisamos escrever. Em contrapartida, quando mais camadas, menos sabemos como as coisas funcionam.

Primeiro é verificado se o sistema de arquivos está "montado", depois o path, depois se o arquivo ou diretório existe, depois tem uma alocação de memória que também tem tratamento de excessão, daí só então é feito o unlink. Bem mais simples fazer utilizando a API para a IDE do Arduino, hum?

Com essas 3 funções já consegui explicar tudo o que gostaria e seria o suficiente para um teste inicial. Agora vou colocar o código completo abaixo para você ver as demais funções e suas chamadas.

#include "FS.h"
#include "SPIFFS.h"

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 readFile(fs::FS &fs, const char * path){
    Serial.printf("Reading file: %s\n", path);

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

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

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");
    }
}

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");
    }
}

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 && !file.isDirectory()){
        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();
}

void setup(){
    Serial.begin(115200);
    if(!SPIFFS.begin(true)){
        Serial.println("SPIFFS Mount Failed");
        return;
    }
    
    listDir(SPIFFS, "/", 0);
    writeFile(SPIFFS, "/hello.txt", "Hello ");
    appendFile(SPIFFS, "/hello.txt", "World!\n");
    readFile(SPIFFS, "/hello.txt");
    deleteFile(SPIFFS, "/foo.txt");
    renameFile(SPIFFS, "/hello.txt", "/foo.txt");
    readFile(SPIFFS, "/foo.txt");
    testFileIO(SPIFFS, "/test.txt");
}

void loop(){

}

Comprar o ESP32

E continuo seguindo com minha recomendação de comprar na CurtoCircuito. Tenho certeza de que se tornará um hábito comprar com eles a partir da próxima leva de produtos que está por vir.

Agora pretendo escrever alguns artigos práticos antes de irmos para os próximos recursos do ESP32 e do FreeRTOS. Espero que esteja gostando da série!

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.