28 de julho de 2021

Do bit Ao Byte

Embarcados, Linux e programação

Consultar o valor do bitcoin com ESP8266

bitcoin com ESP8266

Se você é leitor assíduo do blog, deve ter suspirado agora. mas calma, eu sei que já mostrei como consultar o valor do bitcoin com ESP8266, mas foi em outra situação, agora tem outro propósito e novos conceitos.

Se não leu os artigos relacionados a essa consulta escritos anteriormente, deixo os links:

Maquineta do Banco Imobiliário

Esse projeto que inicialmente me pareceu simples está consumindo uma boa quantidade de tempo na formatação do projeto. Essa é a última etapa antes de apresentar o brinquedo. Aqui vamos preparar o ambiente para fazer a consulta do valor do bitcoin com ESP8266 online no Mercado Bitcoin, mas nesse ponto mesmo já temos um comportamento fora do padrão.

Wiring do ESP-01 com o Arduino Mega 2560

O ESP-01 (ou qualquer outro ESP) é 3V3, portanto é necessário utilizar um conversor de nível lógico ou um divisor resistivo no TX do Arduino para o RX do ESP-01. Coloquei o divisor na protoboard, como poderá ser visto no artigo da maquineta eletrônica, onde disponho o wiring de todos os componentes do projeto de forma isolada, mas tem uma coisa que pode ser feita para eliminar essa necessidade, que explico no parágrafo seguinte. Coloque um jumper do pino CH_PD ao 3v3, passando por um resistor de 10K!

serial com Bus Pirate - esp-01
bitcoin com ESP8266

Comunicação entre o Arduino e o ESP-01 para pegar o valor do bitcoin com ESP8266

Tudo o que precisamos do ESP-01 é que ele entregue o valor atual do Bitcoin. Para fazer essa consulta podemos enviar qualquer Byte só pra despertar uma consulta, ou então, podemos ler periodicamente a serial. Se lermos a serial periodicamente, sequer precisaremos fazer a comunicação bi-direcional e com isso eliminamos alguns jumpers e resistores. Mas fiz como disposto na imagem acima.

Definição do projeto

Vou preparar o ESP-01 para a maquineta do Banco Imobiliário, mas não preciso ter todo o projeto integrado para fazer essa parte da implementação. Isso se dá graças ao adaptador para ESP-01, que me permite fazer a comunicação com o computador, onde simularei a consulta.

Outra coisa muito importante é que não utilizaremos a PROGMEM dessa vez. Pra quem não sabe, a PROGMEM é uma keyword modificadora de variáveis que orienta o compilador a armazenar a variável na memória flash em vez da SRAM. Mas como ela tem um tipo constante e como quereremos atualizar essa variável ao menos 1 vez por ano, é melhor criá-la em arquivo no sistema de arquivos do ESP8266 e quando preciso, manipular o arquivo com o gerenciador de arquivos que fiz pro ESP8266 e ESP32 – o ESPFileManager.

O valor guardado no arquivo será o fingerprint da conexão HTTPS, que apesar de já explicado em um dos artigos supracitados, repetirei o procedimento aqui.

O uso do PROGMEM é uma maneira de economizar memória. Além dela, outro recurso que faz uso da flash é a macro F() no print da serial, mas é outro dado estático. Vejamos a declaração de uma variável a ser armazenada na flash:

const char fingerPrint[] PROGMEM = "31 B5 D0 C3 74 CC 25 98 7F 67 32 9D DE FE 5149 E9 AD 8C D1";

A palavra PROGMEM só não pode ser colocada na posição posterior à declaração do tipo, porque essa posição pertence à definição do nome da variável. Coloque-a após const ou após a declaração do nome da variável.

A macro F() não tem nenhum segredo:

Serial.println(F("mensagem"));

Código para consulta HTTPS (ESP-01)

Vamos deixar bem sucinto. Instale as bibliotecas que estiverem no código disposto. Se não tiver alguma delas, o programa não compilará. O include Arduino.h só é necessário se estiver usando VS Code. Recomendo. No vídeo “Consultar valor do bitcoin com Arduino e ESP-01” eu explico como pegar o fingerprint. Esse código já contempla os recursos necessários para usar o ESPFileManger para fazer upload do arquivo com o fingerprint, mas essa parte deixaremos para o artigo da maquineta.

Se quiser ter uma ideia do formato que será lido, veja esse link do json.

#include <Arduino.h> //esse include deve ser removido se não estiver usando vscode
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFiMulti.h>
#include <WiFiClientSecureBearSSL.h>
#include <WiFiClient.h>
#include <string.h>
#include "ArduinoJson.h"
#include "FS.h"
#include "LittleFS.h"

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

char msg[150]          = {0}; 
char json[400]         = {0};

uint8_t is_overflow    = 0;
uint8_t overflow_limit = 150;

StaticJsonDocument<256> doc;


ESP8266WiFiMulti WiFiMulti;

String fingerprint = "32 59 93 CE 8E 10 B9 BF 34 1D 19 4F 71 1C 0A 53 60 7E 17 7D";

void getSample();

String resultOfGet(String msg){
    memset(json,0,sizeof(json));
    msg.toCharArray(json, 400);
    deserializeJson(doc, json);

    JsonObject ticker = doc["ticker"];
    const char* ticker_high = ticker["high"]; // "33395.00000000"
    const char* ticker_low = ticker["low"]; // "32911.01001000"
    const char* ticker_vol = ticker["vol"]; // "29.80139592"
    const char* ticker_last = ticker["last"]; // "33146.89715000"
    const char* ticker_buy = ticker["buy"]; // "33005.10011000"
    const char* ticker_sell = ticker["sell"]; // "33146.89715000"
    const char* ticker_open = ticker["open"]; // "33094.94851000"
    long ticker_date = ticker["date"]; // 1578889119

    Serial.println(ticker_last);
}

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());
        }
    }
    else if (msg[0] == '@'){
        //Serial.println("256000.28000000"); //TODO: imprimir o valor contido no resultado da consulta
        getSample();
    }
}

void getSample(){
    if ((WiFiMulti.run() == WL_CONNECTED)){
          std::unique_ptr<BearSSL::WiFiClientSecure>client(new BearSSL::WiFiClientSecure);
          char buf[150];
          memset(buf,0,sizeof(buf));

          fingerprint.toCharArray(buf,sizeof(buf));
          //Serial.println(buf);
          client->setFingerprint(buf);
          //Serial.println("connected...");
          //WiFiClient client;

          HTTPClient http;

        //3 - iniciamos a URL alvo, pega a resposta e finaliza a conexão
        if (http.begin(*client,"https://www.mercadobitcoin.net/api/BTC/ticker")){
          //Serial.println("http.begin ok");
          uint8_t x = 0; //somente para ignorar
        }
        int httpCode = http.GET();
        if (httpCode > 0) { //Maior que 0, tem resposta a ser lida
            String payload = http.getString();
            //Serial.println(httpCode);
            //Serial.println(payload);
            resultOfGet(payload);
        }
        else {
          Serial.println(httpCode);
            Serial.println("failed");
        }
        http.end();
    }
}

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

    /* E X E M P L O S
    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");
    */

    //acesse arduinojson.org/v6/assistant e passe uma amostra pra calcular a capacidade
    const size_t capacity = JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(8) + 146;
    DynamicJsonDocument doc(capacity);
}

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

Com isso, a serial é monitorada pela função fileManager() e caso seja enviado uma arroba (@), é devolvido o valor atual do Bitcoin, a ser lido pela maquineta do Banco Imobiliário. Tem outros valores no payload da mensagem que podem ser úteis, confira a função String resultOfGet(String msg).

Enfim, podemos ir para o artigo da maquineta do Banco Imobiliário agora. Acompanhe!