Assistentes de IA de Bancada — Parte 1: integração das APIs comerciais (OpenAI / Gemini / Claude) direto no microcontrolador via HTTPS

Ouvir

Carregando voz…

Assistentes de IA de Bancada — Parte 1: integração das APIs comerciais (OpenAI / Gemini / Claude) direto no microcontrol

(a) Conceito e Arquitetura

Um assistente de IA de bancada feito com ESP32 não roda o modelo: ele é o corpo do assistente, enquanto o cérebro fica atrás de uma API HTTPS. O microcontrolador captura áudio ou texto, monta uma requisição JSON, envia por TLS e renderiza a resposta em um display, fala por um amplificador I2S ou aciona uma GPIO. Esse desenho é o oposto do “LLM local” em hardware próprio — aqui a inferência é remota e o ESP32-S3 atua como orquestrador de rede.

O fluxo típico de uma rodada é: microfone I2S → (opcional) transcrição por API → LLM (OpenAI, Gemini ou Claude) → (opcional) síntese de voz → amplificador I2S. Texto puro pelo display/botão também é perfeitamente válido e elimina duas chamadas de rede, o que reduz custo e latência.

Existem três topologias. (1) Direto do MCU para a API comercial: menos peças, menor latência, mas expõe a chave de API no dispositivo — só é aceitável em bancada, nunca em produto em campo. (2) Proxy/gateway local: um serviço em Raspberry Pi ou PC (por exemplo, o endpoint compatível com OpenAI exposto pelo ollama/ollama) guarda a chave, aplica rate limit, cache e logging. (3) Híbrida: comandos de bancada vão para o proxy local e consultas abertas vão para a nuvem. Este artigo cobre a topologia 1 do ponto de vista de TLS e JSON, deixando a topologia 2 como recomendação de produção.

Requisitos mínimos: TLS 1.2+, heap livre confortável (o handshake TLS sozinho consome dezenas de KB), PSRAM recomendada para buffers de resposta e um relógio confiável — sem hora correta o certificado é rejeitado.

(b) BOM (Bill of Materials)

Componente Modelo sugerido Função Observação
Placa MCU ESP32-S3-DevKitC-1 (N16R8) CPU, Wi-Fi, TLS, I2S 16 MB flash + 8 MB PSRAM para buffers
Display ST7789 240×240 SPI Status e resposta textual Barramento SPI compartilhado, CS dedicado
Microfone INMP441 (I2S digital) Entrada de voz I2S0, sem ADC externo
Amplificador MAX98357A (I2S) Saída de áudio Alto-falante 4 Ω / 3 W
Regulador Buck/Linear 3,3 V ≥ 1 A Alimentação estável Picos de TX Wi-Fi ~500 mA
Capacitores 470 µF eletrolítico + 100 nF cerâmico Filtro/desacoplamento Colocar perto do pino 3V3
Botão Push momentary Push-to-talk / reset Pull-up interno + RC antirruído
Resistores 10 kΩ, 100 Ω (série I2S) Pull-up e casamento Opcional para integridade de sinal

(c) Wiring e Esquemático (alimentação e estabilidade)

O ponto que mais derruba projetos de assistente é a alimentação. O rádio Wi-Fi do ESP32 puxa correntes de pico entre 350 e 500 mA durante as transmissões, e um regulador fraco ou fiação fina provocam o clássico brownout reset — sintoma típico de “cai justamente na hora de chamar a API”. Diretrizes práticas:

  • Use regulador de 3,3 V com capacidade ≥ 1 A e boa resposta a transientes.
  • Coloque um capacitor bulk de 470 µF em paralelo com um cerâmico de 100 nF o mais próximo possível do pino 3V3 do módulo.
  • Evite alimentar o ESP32 pelo USB e por fonte externa ao mesmo tempo; se necessário, use um diodo de isolamento.
  • Adote ground em estrela e mantenha as trilhas de clock I2S curtas para não injetar ruído no áudio.
  • Pino EN com circuito RC (10 kΩ + 1 µF) garante reset limpo na energização.
Sinal GPIO (sugerido) Periférico
I2S0 BCLK / WS / DIN GPIO 4 / 5 / 6 Microfone INMP441
I2S1 BCLK / WS / DOUT GPIO 15 / 16 / 7 Amplificador MAX98357A
SPI MOSI / SCLK / CS / DC / RST GPIO 11 / 12 / 10 / 9 / 8 Display ST7789

(d) Código — TLS, certificados, POST JSON, streaming e parse

O primeiro passo é sincronizar o relógio. Sem isso a validação de certificado falha porque a CA parece “do futuro”.

#include "esp_sntp.h"
#include <time.h>

// Obrigatorio antes de qualquer handshake TLS valido:
// certificados sao checados contra o relogio do sistema.
static void sync_time(void) {
    setenv("TZ", "<-03>3", 1);        // America/Sao_Paulo
    tzset();
    esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
    esp_sntp_setservername(0, "pool.ntp.org");
    esp_sntp_init();

    time_t now = 0;
    while (time(&now) < 1700000000) {  // espera o relogio passar de ~2023
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

Com o relógio certo, o cliente HTTPS usa o bundle de CAs do ESP-IDF (esp_crt_bundle_attach), evitando embutir certificados PEM manualmente. O mesmo esqueleto vale para OpenAI, Gemini e Claude — muda só a URL, o cabeçalho de autenticação e o nome dos campos JSON.

#include "esp_http_client.h"
#include "esp_crt_bundle.h"   // bundle de CAs do ESP-IDF

static char s_buf[4096];

static esp_err_t on_data(esp_http_client_event_t *e) {
    if (e->event_id == HTTP_EVENT_ON_DATA) {
        // streaming: cada chunk da resposta (SSE) chega aqui
        int n = e->data_len < (int)sizeof(s_buf) - 1 ? e->data_len : (int)sizeof(s_buf) - 1;
        memcpy(s_buf, e->data, n);
        s_buf[n] = 0;
        // aqui voce pode fazer parse incremental do campo "delta"
    }
    return ESP_OK;
}

esp_err_t chat_post(const char *bearer, const char *body_json) {
    esp_http_client_config_t cfg = {};
    cfg.url = "https://api.openai.com/v1/chat/completions";
    cfg.method = HTTP_METHOD_POST;
    cfg.crt_bundle_attach = esp_crt_bundle_attach; // valida a cadeia de CAs
    cfg.event_handler = on_data;
    cfg.timeout_ms = 20000;

    esp_http_client_handle_t c = esp_http_client_init(&cfg);
    esp_http_client_set_header(c, "Content-Type", "application/json");
    esp_http_client_set_header(c, "Authorization", bearer); // "Bearer sk-..."

    esp_http_client_open(c, strlen(body_json));
    esp_http_client_write(c, body_json, strlen(body_json));
    esp_http_client_fetch_headers(c);
    int status = esp_http_client_get_status_code(c);
    while (esp_http_client_read(c, s_buf, sizeof(s_buf)) > 0) { /* drena SSE */ }
    esp_http_client_close(c);
    esp_http_client_cleanup(c);
    return status == 200 ? ESP_OK : ESP_FAIL;
}

O streaming é importante para a sensação de tempo real: em vez de esperar a resposta inteira, cada chunk (formato Server-Sent Events, data: {...}) é tratado dentro do event handler e o texto pode ser exibido progressivamente no display. Por fim, o parse com ArduinoJson, usando um filtro para carregar só os campos úteis e diminuir o consumo de RAM.

#include <ArduinoJson.h>

// Filtro: mantem apenas os campos usados -> economiza RAM
const char *filter =
  "{\"choices\":[{\"message\":{\"content\":true}}],"
  " \"usage\":{\"total_tokens\":true}}";

StaticJsonDocument<768> doc;              // doc pequeno, sem heap frag
auto err = deserializeJson(doc, s_buf,
                           DeserializationOption::Filter(filter));
if (!err) {
    const char *txt = doc["choices"][0]["message"]["content"] | "";
    Serial.printf("resposta: %s\n", txt);
    int tokens = doc["usage"]["total_tokens"] | 0;   // custo da interacao
    Serial.printf("tokens: %d\n", tokens);
}

(e) Troubleshooting

  • Handshake TLS falha: na maioria dos casos o relógio está errado. Sincronize por SNTP antes da primeira chamada. Verifique também o bundle de CAs atualizado e a URL exata do endpoint.
  • Memória: o handshake TLS pode precisar de 40–60 KB de heap; respostas JSON longas fragmentam a heap. Habilite PSRAM, aumente o buffer e use filtros no ArduinoJson.
  • Timeouts: ajuste timeout_ms (20 s é um bom começo) e implemente retry com backoff. Requisições que passam do limite devem ser canceladas e repetidas.
  • Custo por interação: tokens de entrada e saída contam; o modo streaming não reduz preço. Leia usage.total_tokens para telemetria e considere modelos “mini” para tarefas de bancada. Cacheie respostas repetidas.
  • Limites de cota: erros 429 exigem respeitar o cabeçalho Retry-After. Um proxy local (topologia 2) centraliza esse controle.

(f) Aplicações reais

Com a base de TLS e JSON funcionando, as aplicações aparecem rápido: um terminal de voz sobre a bancada que mostra a resposta no ST7789; um painel doméstico que resume leituras de sensores via LLM; um tutor de código que responde por alto-falante; e um monitor que narra alertas de telemetria. O projeto 78/xiaozhi-esp32 é a melhor referência pública de firmware completo (voz + display + MCP) para ESP32-S3 e serve de ponto de partida para quem não quer começar do zero. No próximo artigo da série, o foco sai da rede e vai para a memória: como dar contexto e continuidade ao assistente usando NVS, littlefs e uma camada leve de RAG local.

Não some depois da matéria

Radar do hub: Núcleo Hits, Além do Oculto, Bobinho. Sem spray, sem lista comprada.

Radar do hub

Um e-mail quando sair matéria que importa — sem spray de newsletter genérica.