Skip to content

Latest commit

 

History

History
216 lines (155 loc) · 6.79 KB

walkthrough.md

File metadata and controls

216 lines (155 loc) · 6.79 KB

Projeto Widget Display

Um Widget Display é um dispositivo embarcado construído com ESP32 que exibe três modos de informação em uma tela OLED.

Os três modos exibidos são: relógio sincronizado por NTP, dados de clima em tempo real via API e um visualizador de espectro de áudio animado.

O ambiente de simulação utilizado foi o Wokwi, já que não tinha o hardware em mãos durante o desenvolvimento.


Arquitetura

Primeiramente, defini como o software seria organizado. O display tem 128×64 pixels e dividi ele em três regiões fixas:

struct Regiao { int x, y, w, h; };

const Regiao CIMA  = {0,  0,  128, 12};  // cabeçalho
const Regiao CORPO = {0,  13, 128, 43};  // corpo
const Regiao BAIXO = {0,  57, 128,  7};  // rodapé

Utilizei essa separação em structs para evitar espalhar códigos de coordenadas pelo código. Toda função de renderização sabe exatamente onde pode desenhar..

O sistema de modos foi feito como um enum com uma sentinela no final, um padrão que descobri útil para fazer rotação cíclica sem contar manualmente quantos modos existem:

enum AppMode {
    MODO_RELOGIO = 0,
    MODO_CLIMA,
    MODO_MUSICA,
    MODO_TOTAL   // indica o tamanho do enum
};

Com isso, avançar e retroceder entre modos vira uma operação de módulo simples:

modoAtual = (AppMode)((modoAtual + 1) % MODO_TOTAL);

Modo relógio

O ESP32 conecta em pool.ntp.org via configTime() e depois lê o tempo com getLocalTime(). É importante notar que o NTP não funciona como uma API REST, e sim um protocolo UDP assíncrono, e o SDK do ESP32 faz a sincronização.

Caso não haja conexão wifi, existe um fallback que incrementa os segundos manualmente:

void atualizaRelogio() {
    struct tm timeinfo;
    if (getLocalTime(&timeinfo)) {
        relogio.hora    = timeinfo.tm_hour;
        relogio.minuto  = timeinfo.tm_min;
        relogio.segundo = timeinfo.tm_sec;
        // ...
        relogio.ntpSincronizado = true;
    } else {
        // fallback
        static unsigned long ultimo = 0;
        if (millis() - ultimo >= 1000UL) {
            ultimo = millis();
            if (++relogio.segundo >= 60) { relogio.segundo = 0;
            if (++relogio.minuto  >= 60) { relogio.minuto  = 0;
            if (++relogio.hora    >= 24) { relogio.hora    = 0;
                                           relogio.dia++;       }}}
        }
    }
}

Um asterisco no canto da tela indica se o NTP está sincronizado.


Modo clima

Esse modo foi o mais interessante de implementar. A cada 5 minutos, o ESP32 faz um GET para a API do OpenWeatherMap, usa o JSON com a biblioteca ArduinoJson e atualiza a struct de estado.

void buscaClima() {
    if (clima.dadosValidos &&
        millis() - clima.ultimaBusca < INTERVALO_CLIMA) return;

    HTTPClient http;
    char url[256];
    snprintf(url, sizeof(url),
        "http://api.openweathermap.org/data/2.5/weather"
        "?q=%s,%s&appid=%s&units=metric&lang=pt_br",
        OWM_CIDADE, OWM_PAIS, OWM_API_KEY);

    http.begin(url);
    if (http.GET() == 200) {
        StaticJsonDocument<1024> doc;
        deserializeJson(doc, http.getString());
        clima.temperatura = (int)roundf((float)doc["main"]["temp"]);
        clima.humidade    = doc["main"]["humidity"];
        clima.vento       = (float)doc["wind"]["speed"] * 3.6f;
        // ...
        clima.dadosValidos = true;
    }
    http.end();
}

Como o heap do ESP32 é limitado e fragmentação pode causar crashes difíceis de debugar, o StaticJsonDocument<1024> aloca o buffer na stack.

Os ícones de clima (sol, nuvem, chuva, neve, tempestade) são bitmaps 8×8 armazenados na flash.

const uint8_t ICO_SOL[] PROGMEM = {
    0b00010000, 0b01010100, 0b00111000, 0b11111110,
    0b00111000, 0b01010100, 0b00010000, 0b00000000
};

Limitação do simulador: Para a simulação funcionar foi necessário substituir a função por dados estáticos, pois o simulador não realiza chamadas HTTP reais. O http.GET() nunca retorna 200, então o modo clima ficava eternamente em "Buscando dados...".


Modo música

Este modo serámelhorado futuramente, pois para funcionar o sistema precisaria de um microfone ADC e uma FFT.

Logo, o que o modo atual faz é simular visualmente o comportamento de um espectro com três técnicas:

1. Envelope de banda: frequências médias naturalmente aparecem mais altas que extremos em música. Modelei isso com uma curva senoidal:

float norm   = (float)i / (NUM_BARRAS - 1);
envelope[i]  = 0.3f + 0.7f * sinf(norm * 3.14159f);

2. Suavização exponencial: sem suavização, as barras saltam sem parar e parecem ruído. Suevizando, o movimento fica mais fluido:

espectro[i] = (espectroAnt[i] * 6 + alvo * 4) / 10;

3. Marcadores de pico: um pico que sobe instantaneamente junto com a barra, mas desce mais devagar:

if (espectro[i] > pico[i]) {
    pico[i]      = espectro[i];
    tempoPico[i] = millis();
} else if (millis() - tempoPico[i] > PICO_HOLD_MS) {
    if (pico[i] > 0) pico[i]--;
}

Botões e debounce

Cada botão conecta um GPIO ao GND. Com INPUT_PULLUP, o pino lê HIGH em repouso e LOW quando pressionado.

O debounce é feito por timestamp, não por delay. Motivo da escolha: um delay(200) trava o programa inteiro por 200ms. Com timestamp, o loop continua rodando normalmente e simplesmente ignora novas detecções dentro da janela de tempo:

void verificaBotoes() {
    if (millis() - ultimoBotao < DEBOUNCE_MS) return;

    if (proximoApertado()) {
        modoAtual   = (AppMode)((modoAtual + 1) % MODO_TOTAL);
        salvaConfiguracoes();
        ultimoBotao = millis();
    }
}

Memória

O ESP32 tem uma área de flash reservada para o NVS (Non-Volatile Storage), acessível pela biblioteca Preferences. Usei para salvar o modo atual, para que ele seja restaurado após um reset:

void salvaConfiguracoes() {
    prefs.begin("widget", false);
    prefs.putInt("modo", (int)modoAtual);
    prefs.end();
}

Loop

O loop segue uma ordem fixa: input > update > render. A renderização é limitada a 10 fps com um timestamp, para não desperdiçar ciclos com frames iguais no display:

void loop() {
    verificaBotoes();

    atualizaRelogio();
    if (modoAtual == MODO_CLIMA)  buscaClima();
    if (modoAtual == MODO_MUSICA) atualizaEspectro();

    if (millis() - ultimaRenderizacao >= INTERVALO_RENDER) {
        ultimaRenderizacao = millis();
        renderFrame();
    }
}

Atualizações futuras

  • Testar o código wm um harware real para verificar o funcionamento fora das limitações do simulador.
  • Adicionar um microfone ADC e uma FFT ao sistema para funcionamento do modo música.

O código completo, o diagrama e o README com as instruções de montagem estão no repositório do GitHub.