15 de maio de 2021

Do bit Ao Byte

Embarcados, Linux e programação

ESPFileManager – Gerenciador de arquivos para ESP8266 e ESP32

Gerenciador de arquivos para ESP8266

ESPFileManager: guarde esse nome, porque ele é quem vai te ajudar a economizar tempo e facilitar sua vida usando o sistema de arquivos no ESP8266 e ESP32! No artigo “Sistema de arquivos no ESP8266” vimos como manipular arquivos no sistema de arquivos LittleFS. No artigo tem um código eficiente para criação, leitura, concatenação, renomeação, exclusão e escrita; tudo isso usando um recurso nativo do ESP8266 ou ESP32, dispensando o uso de micro SD ou gravação com bibliotecas para EEPROM. No vídeo relacionado (ao final do artigo do início do parágrafo) sugeri a utilização do recurso para criar arquivos .ini (ou .txt, ou sem extensão, tanto faz) para fazer parametrização do programa e evitar subir novos firmwares apenas para mudar coisas como SSID, senhas ou qualquer outra coisa que poderia ser resolvida através de leitura de arquivos. Mostrarei uma implementação bem legal em outro artigo, mas nesse vamos ver um gerenciador de arquivos para o ESP8266 e ESP32, para simplificar a interação pela serial.

Agradeço a todos que de boa vontade atenderam meu pedido para se inscrever no canal DobitaobyteBrasil. Nunca havia dado atenção pro Youtube, mas agora desejo tê-lo tão grande quanto o blog. E cumprindo a primeira parte da minha oferta, eis o artigo com o código prometido. E adicionalmente, a versão gráfica em C++ usando o framwork Qt.

Gerenciador de arquivos para ESP8266 em Python

Para interagir com o ESP (para desenvolvimento utilizei o ESP-01), basta chamar o script com os parâmetros por linha de comando. O programa Python deve rodar em Windows, mas em Linux e Mac é certeza.

Recursos

Podemos começar pelo help do programa:

Gerenciador de arquivos para ESP8266

O programa aceita flags curtas ou longas, tanto faz. As velocidades da serial estão implementadas apenas como 9600115200, que são as mais comumente usadas.

Exemplos de uso

Para não precisar nem pensar, nas flags, criei um parâmetro –examples (ou se preferir a flag curta, -e). É só copiar e colar a ação desejada. Os exemplos são:

================ ESP File Manager =========================
*** Os parâmetros podem ser passados em qualquer ordem ***
===========================================================

Listar arquivos:
./fileManager.py --list --port /dev/ttyUSB0 --speed 115200

Criar arquivos:
./fileManager.py --filename arquivo.txt --write 'foo bar' --port /dev/ttyUSB0 --speed 9600

Ler conteudo de arquivo:
./fileManager.py --filename nome_do_arquivo.txt --read --port /dev/ttyUSB0 --speed 115200

Excluir arquivo:
./fileManager.py --filename arquivo_alvo.txt --delete --port /dev/ttyUSB0 --speed 9600

Concatenar:
./fileManager.py --filename arquivo.txt --append 'alguma coisa' --speed 9600 --port /dev/ttyUSB0

Renomear arquivo:
./fileManager.py --newname nome_sem_espaco.txt --filename original.txt --speed 115200 --port /dev/ttyUSB0

No Linux, podemos transformar o arquivo em executável e depois executá-lo com ./, ou então podemos chamar o interpretador antes do nome do programa. Por exemplo, para listar arquivos poderia ser:

python fileManager.py --list --port /dev/ttyUSB0 --speed 9600

O script foi desenvolvido para Python 3.x.

Código do gerenciador parar Raspberry e Linux x86

Esse programa funciona em qualquer Linux, basta instalar as bibliotecas argparserpython-serial.

#!/usr/bin/env python
import argparse
import serial
import os
import sys
from time import sleep

if len(sys.argv) < 2:
    print("use: ",end=' ')
    print(sys.argv[0],end=' ')
    print(" --help")
    sys.exit(0)

#-*-*- coding: utf-8 -*-*-

ap = argparse.ArgumentParser()

ap.add_argument("-a","--append",help="Texto para adicionar ao arquivo",default=False)
ap.add_argument("-d","--delete",help="Apaga um arquivo",action="store_true",default=False)
ap.add_argument("-e","--examples",help="Exemplos de uso",action="store_true",default=False)
ap.add_argument("-f","--filename",help="Nome do arquivo",default=False)
ap.add_argument("-l","--list",help="Lista os arquivos existentes",action="store_true",default=False)
ap.add_argument("-n","--newname",help="Renomeia o arquivo",default=False)
ap.add_argument("-p","--port",help="Porta serial a se conectar",default=False)
ap.add_argument("-r","--read",help="ler arquivo",action="store_true",default=False)
ap.add_argument("-s","--speed",help="Velocidade de conexão",type=int,choices=[9600, 115200],default=False)
ap.add_argument("-w","--write",help="Texto para adicionar ao arquivo",default=False)


args = ap.parse_args()

if args.examples:
    print("================ ESP File Manager =========================")
    print("*** Os parâmetros podem ser passados em qualquer ordem ***")
    print("===========================================================") 
    print("")

    print("Listar arquivos:")
    print("./fileManager.py --list --port /dev/ttyUSB0 --speed 115200")
    print("")
   
    print("Criar arquivos:")
    print("./fileManager.py --filename arquivo.txt --write 'foo bar' --port /dev/ttyUSB0 --speed 9600")
    print("")
   
    print("Ler conteudo de arquivo:")
    print("./fileManager.py --filename nome_do_arquivo.txt --read --port /dev/ttyUSB0 --speed 115200")
    print("")
   
    print("Excluir arquivo:")
    print("./fileManager.py --filename arquivo_alvo.txt --delete --port /dev/ttyUSB0 --speed 9600")
    print("")

    print("Concatenar:")
    print("./fileManager.py --filename arquivo.txt --append 'alguma coisa' --speed 9600 --port /dev/ttyUSB0")
    print("")

    print("Renomear arquivo:")
    print("./fileManager.py --newname nome_sem_espaco.txt --filename original.txt --speed 115200 --port /dev/ttyUSB0")
    print("")

    sys.exit(0)

if args.filename and not args.write and not args.read and not args.append and not args.delete and not args.newname:
    print("A acao para o arquivo precisa ser escolhida. Use --help para saber mais")
    sys.exit(0)

if args.list and args.filename:
    print("--list nao recebe outra flag")
    sys.exit(0)

if not args.port:
    print(u"O parametro --port é mandatório")
    sys.exit(0)

elif not  os.path.exists(args.port):
    print("A porta ",end=' ')
    print(args.port,end=' ')
    print(" não existe")
    sys.exit(0)

remote_fs = serial.Serial(args.port,args.speed,timeout=2)

if args.read:
    file_mode = "r"

elif args.write:
    file_mode = "w"
    file_content = args.write

elif args.append:
    file_mode = "a"
    file_content = args.append

elif args.list:
    file_mode = "l"

elif args.delete:
    file_mode = "d"

elif args.newname:
    file_mode = "n"
    file_content = args.newname

if not args.filename:
    filename = "all"
else:
    filename = args.filename

if args.write and not args.filename:
    print("use --filename para passar o nome do arquivo a escrever")
    sys.exit(0)

elif args.read and not args.filename:
    print("use --filename para passar o nome do arquivo a ler")
    sys.exit(0)

elif args.delete and not args.filename:
    print("use --filename para passar o nome do arquivo a excluir")
    sys.exit(0)


#TODO: pegar -w, -a, -r, -d e passar o modo e conteudo 
if args.write or args.append:
    msg = "^" + filename + "-" + file_mode + "-" + file_content + "$"
elif args.read:
    msg = "^" + filename + "-" + "r" + "-" + "none" + "$"
elif file_mode == "l":
    msg = "^" + "none" + "-" + "l" + "-" + "none" + "$"
elif file_mode == "d":
    msg = "^" + filename + "-" + file_mode + "-" + "none" + "$"
elif file_mode == "n":
    msg = "^" + filename + "-" + file_mode + "-" + file_content + "$" 

remote_fs.write(bytes(msg.encode('utf-8')))
sleep(1)
value = remote_fs.read_until('\n')
print(value.decode('utf-8'))
remote_fs.close()

 

Sketch do gerenciador de arquivos para ESP8266

No sketch, criei apenas uma função bastante curta, chamada fileManager(). O sketch completo é o mesmo do artigo do primeiro parágrafo, apenas complementado:

#include <Arduino.h>
#include "FS.h"
#include "LittleFS.h"
#include <ESP8266WiFi.h>
#include <string.h>

#define SSID   "SuhankoFamily"
#define PASSWD "fsjmr112"

char msg[150]           = {0};

uint8_t is_overflow    = 0;
uint8_t overflow_limit = 150;


void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    Serial.printf("Listing directory: %s\n", dirname);

    File root = fs.open(dirname,"r");
    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, "r");
    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, "w");
    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, "a");
    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,"r");
    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, "w");
    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 fileManager(){
    memset(msg,0,sizeof(msg)); //zera o array a cada ciclo
    is_overflow = 0;           //zera o interador de limite de leitura
    while (Serial.available() > 0){
        msg[is_overflow] = Serial.read();
        //se exceder o limite do overflow_limit, interrompe a leitura
        if (is_overflow == overflow_limit){
            break;
        }
        is_overflow++;
    }

    if (msg[0] == '^'){
        //verificado inicio, procura pelo fim
        if (String(msg).lastIndexOf("$") == -1){
            return;
        }

        //só chega aqui se início (^) e fim ($) de mensagens forem encontrados
        uint8_t first_delimiter = String(msg).indexOf("-");
        uint8_t end_of_line     = String(msg).indexOf("$");

        String filename = "/" + String(msg).substring(1,first_delimiter);
    
        //1 - ler do arquivo
        if (msg[first_delimiter+1] == 'r'){
            readFile(LittleFS, filename.c_str());
            //Serial.println(msg);
        }
        //2 - ler todos os arquivos
        else if (msg[first_delimiter+1] == 'l'){
            listDir(LittleFS,"/",0);
        }
        //3 - criar arquivo
        else if (msg[first_delimiter+1] == 'w'){
            String msg_to_write = String(msg).substring(first_delimiter+3,end_of_line);
            writeFile(LittleFS,filename.c_str(),msg_to_write.c_str());
        }
        //4 - concatenar a um arquivo existente
        else if (msg[first_delimiter+1] == 'a'){
            String msg_to_write = String(msg).substring(first_delimiter+3,end_of_line);
            appendFile(LittleFS,filename.c_str(),msg_to_write.c_str());
        }
        //5 - excluir arquivo
        else if (msg[first_delimiter+1] == 'd'){
            deleteFile(LittleFS, filename.c_str());
        }
        //6 - renomear arquivo
        else if (msg[first_delimiter+1] == 'n'){
            String new_name = String(msg).substring(first_delimiter+3,end_of_line);
            renameFile(LittleFS,filename.c_str(),new_name.c_str());
        }
    }
}

void setup(){
    LittleFSConfig cfg;
    cfg.setAutoFormat(true);
    LittleFS.setConfig(cfg);

    WiFi.begin(SSID,PASSWD);
    while (WiFi.status() != WL_CONNECTED){delay(100);}

    Serial.begin(9600);
    delay(3000);
    if(!LittleFS.begin()){
        Serial.println("LittleFS Mount Failed");
        return;
    }
    
    //exemplos do artigo anterior
    listDir(LittleFS, "/", 0);
    writeFile(LittleFS, "/hello.txt", "Hello ");
    appendFile(LittleFS, "/hello.txt", "World!\n");
    readFile(LittleFS, "/hello.txt");
    deleteFile(LittleFS, "/foo.txt");
    renameFile(LittleFS, "/hello.txt", "/foo.txt");
    readFile(LittleFS, "/foo.txt");
    testFileIO(LittleFS, "/test.txt");
}

void loop(){
    fileManager();
    delay(1000);
}

Simples, hum? Mas ficará mais.

Gerenciador de arquivos para ESP8266 e ESP32: ESPFileManager

Tive alguns dias de trabalho intenso para oferecer a vocês (gratuitamente) um gerenciador de arquivos gráfico e fácil de usar. Ainda estou trabalhando na biblioteca a instalar para ficar mais fácil ainda e em breve estará ambos estarão disponíveis para download. Nada mal, hum?

Por enquanto você já pode ir usando ele com esse sketch no ESP:

#include <Arduino.h>
#include "FS.h"
#include "LittleFS.h"
#include <ESP8266WiFi.h>
#include <string.h>

#define SSID   "SuhankoFamily"
#define PASSWD "fsjmr112"

uint8_t is_overflow        = 0;
const int overflow_limit   = 250;
char msg[overflow_limit]   = {0};

void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
    File root = fs.open(dirname,"r");
    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.println(file.name());
            if(levels){
                listDir(fs, file.name(), levels -1);
            }
        } else {
            Serial.println(file.name());
        }
        file = root.openNextFile();
    }
}

void readFile(fs::FS &fs, const char * path){
    File file = fs.open(path, "r");
    if(!file || file.isDirectory()){
        Serial.println("#Failed to open file for reading");
        return;
    }

    Serial.write('#');
    while(file.available()){
        Serial.write(file.read());
    }
    Serial.write('$');
}

void writeFile(fs::FS &fs, const char * path, const char * message){
    File file = fs.open(path, "w");
    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){
    File file = fs.open(path, "a");
    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){
    if (fs.rename(path1, path2)) {
        Serial.println("#File renamed");
    } else {
        Serial.println("#Rename failed");
    }
}

void deleteFile(fs::FS &fs, const char * path){
    if(fs.remove(path)){
        Serial.println("#File deleted");
    } else {
        Serial.println("#Delete failed");
    }
}

void fileManager(){
    memset(msg,0,sizeof(msg)); //zera o array a cada ciclo
    is_overflow = 0;           //zera o interador de limite de leitura
    while (Serial.available() > 0){
        msg[is_overflow] = Serial.read();
        //se exceder o limite do overflow_limit, interrompe a leitura
        if (is_overflow == overflow_limit){
            break;
        }
        is_overflow++;
    }

    if (msg[0] == '^'){
        //verificado inicio, procura pelo fim
        if (String(msg).lastIndexOf("$") == -1){
            return;
        }

        //só chega aqui se início (^) e fim ($) de mensagens forem encontrados
        uint8_t first_delimiter = String(msg).indexOf("-");
        uint8_t end_of_line     = String(msg).indexOf("$");

        String filename = "/" + String(msg).substring(1,first_delimiter);
    
        //1 - ler do arquivo
        if (msg[first_delimiter+1] == 'r'){
            readFile(LittleFS, filename.c_str());
            //Serial.println(msg);
        }
        //2 - ler todos os arquivos
        else if (msg[first_delimiter+1] == 'l'){
            listDir(LittleFS,"/",0);
        }
        //3 - criar arquivo
        else if (msg[first_delimiter+1] == 'w'){
            String msg_to_write = String(msg).substring(first_delimiter+3,end_of_line);
            writeFile(LittleFS,filename.c_str(),msg_to_write.c_str());
        }
        //4 - concatenar a um arquivo existente
        else if (msg[first_delimiter+1] == 'a'){
            String msg_to_write = String(msg).substring(first_delimiter+3,end_of_line);
            appendFile(LittleFS,filename.c_str(),msg_to_write.c_str());
        }
        //5 - excluir arquivo
        else if (msg[first_delimiter+1] == 'd'){
            deleteFile(LittleFS, filename.c_str());
        }
        //6 - renomear arquivo
        else if (msg[first_delimiter+1] == 'm'){
            String new_name = String(msg).substring(first_delimiter+3,end_of_line);
            renameFile(LittleFS,filename.c_str(),new_name.c_str());
        }
    }
}

void setup(){
    LittleFSConfig cfg;
    cfg.setAutoFormat(true);
    LittleFS.setConfig(cfg);

    WiFi.begin(SSID,PASSWD);
    while (WiFi.status() != WL_CONNECTED){delay(100);}

    Serial.begin(9600);
    delay(3000);
    if(!LittleFS.begin()){
        Serial.println("#LittleFS Mount Failed");
        return;
    }
    
    /*
    listDir(LittleFS, "/", 0);
    writeFile(LittleFS, "/hello.txt", "Hello ");
    appendFile(LittleFS, "/hello.txt", "World!\n");
    readFile(LittleFS, "/hello.txt");
    deleteFile(LittleFS, "/foo.txt");
    renameFile(LittleFS, "/hello.txt", "/foo.txt");
    readFile(LittleFS, "/foo.txt");
    */
}

void loop(){
    fileManager();
    delay(1000);
}

 

 

Esse da esquerda na imagem de destaque é ele e a janelinha entre o ESPFileManager e o Dolphin é a leitura de um de seus arquivos. Para fazer upload, basta arrastar o arquivo para a janela. Duplo clique no nome do arquivo edita  para renomeá-lo. Qualquer operação é seguida por uma reconexão e listagem automática dos arquivos.

Download

Código fonte

A biblioteca para ES8266 e ESP32 estará disponíveis em breve. O código fonte está disponível em nosso repositório ESPFileManager no github.

O programa foi feito em Qt, portanto a aparência se adapta à decoração do sistema operacional, não vai ter a mesma cara pra todo mundo. Para compilar, basta abrir o projeto no QtCreator e clicar em compilar. A versão utilizada do Qt é a 5.15.2. Pretendo disponibilizar instaladores para não precisar compilar, mas é fácil. O resultado estará no diretório build-alguma_coisa.

Binário parar Linux

Desculpem, ainda não compilei uma versão para Windows. Por enquanto, só o procedimento acima para qualquer outra plataforma.

Para Linux, o binário do ESPFileManager está disponível como release nesse link. O programa foi compilado em um Ubuntu Xenial  (16.04) para tentar manter o suporte com a  libc desde essa versão à versões superiores. Aqui rodou em um Ubuntu 20.04 e 20.10, mas não se decepcione muito se não rodar em uma versão intermediária ou outra distribuição GNU/Linux.

Vídeo do gerenciador de arquivos para ESP8266 e ESP32

O canal DobitaobyteBrasil no Youtube ainda é bem modesto, mas com o tempo chegaremos longe.

Se você veio primeiro a esse artigo, o vídeo relacionado já foi publicado há alguns dias, clique aqui. O vídeo do ESPFileManager é esse. Nele, mostro todos seus recursos.

Mesmo não sendo um código perfeito (porque não desenhei o projeto previamente e fui escrevendo o código conforme o que me veio na cabeça, além de ter incutido uma classe de um exemplo para não reescrever o Drag & Drop), está funcional, relativamente bonito e deu um trabalho considerável. Quer retribuir? Aqui os leitores são bastante parceiros: se inscreva em nosso canal e essa será a minha recompensa, motivando assim novos projetos de software para vocês!

Conto com vocês!

 

Revisão: Ricardo Amaral de Andrade