CNPQ Sensores

Guia de configuração — ESP32 Master com autenticação HMAC

Documentação central para integração da API e configuração do firmware no ESP32 (Master). Inclui visão geral para desenvolvedores e detalhes de campo para operação IoT com HMAC.

Conteúdo alinhado ao ficheiro docs/guia_esp32_hmac.md no repositório, com secção adicional de integração geral da API.

Integração geral da API

Base URL: /api/v1/. A ingestão de sensores aceita HMAC, JWT de aplicação, JWT de utilizador e HTTP Basic (ordem de tentativa definida no backend).

Para dispositivos, o fluxo recomendado é HMAC com X-Api-Key, X-Api-Timestamp e X-Api-Signature. Para integrações server-to-server, JWT de aplicação tende a simplificar gestão de acesso por escopo.

Autenticação (JWT, Basic e HMAC)

  • JWT de utilizador: POST /api/v1/auth/login/ e POST /api/v1/auth/refresh/.
  • JWT de aplicação: criar Client no admin e gerar token em POST /api/v1/applications/auth/token/ com {"api_key":"...","api_secret":"..."}.
  • Clientes API (staff): GET /api/v1/application-clients/ e GET /api/v1/application-clients/{id}/.
  • HMAC (IoT): exige escopo sensors:write; tenant é resolvido pelo Client autenticado (não vai no JSON da leitura).
  • Basic: compatível para cenários legados com utilizador Django quando permitido pelo endpoint.

Endpoints principais

  • POST /api/v1/sensors/readings/ — ingestão de leitura (HMAC/JWT app/JWT user/Basic).
  • GET /api/v1/sensors/readings/list/ — listagem paginada no tenant ativo.
  • POST /api/v1/applications/auth/token/ — troca API Key + Secret por JWT de aplicação.

OpenAPI e contrato de respostas

Schema OpenAPI em /api/schema/. Documentação interativa em /api/docs/ e /api/redoc/ quando habilitado no ambiente.

Opcionalmente, API_SUCCESS_ENVELOPE=true envolve respostas de sucesso em {"data": ..., "meta": {...}}. Paginação mantém metadados próprios.

Quando configurado em REST_FRAMEWORK, o TenantScopedUserRateThrottle combina utilizador + tenant na chave de limitação.

1. Visão geral

O ESP32 Master lê sensores (DHT22, MQ4) e recebe dados de escravos na rede local. Com WiFi externo, NTP e credenciais HMAC configuradas, cada leitura nova é enviada diretamente à API por HTTPS assim que é registada (registrarLeituraenviarDiretoParaAPI). Se esse envio falhar ou a rede não estiver disponível, a leitura entra na fila no cartão SD. A cada ~5 minutos o firmware tenta drenar essa fila (linhas pendentes em /dados.jsonl).

As leituras em fila no SD usam NDJSON (/dados.jsonl, uma linha JSON por registo, mesmo formato do corpo da API). O ficheiro legado /dados.csv é migrado automaticamente para JSONL no arranque. Quando todas as linhas pendentes são enviadas com sucesso, dados.jsonl é removido e o offset de envio é zerado — evita o cartão encher indefinidamente.

Se o cartão SD falhar mas o WiFi externo, NTP e credenciais HMAC estiverem OK, as leituras novas continuam a ir direto para a API sem passar pela fila no SD. Sem SD e sem condições para falar com a API, os registos podem ser perdidos (mensagem no Serial, prefixo [REG]).

A autenticação usa HMAC-SHA256 (sem JWT, sem expiração de credencial). O dispositivo assina cada pedido com API Key + API Secret; o servidor valida e associa a leitura ao tenant (fazenda) correto.

O corpo JSON da leitura não indica fazenda nem slug de tenant: o Django resolve o tenant pelo Client encontrado pela X-Api-Key (campo client.tenant). Por isso o par Key+Secret no ESP deve ser o do Client dessa fazenda.

ESP32                              Servidor Django
  │                                      │
  │  POST /api/v1/sensors/readings/      │
  │  Headers:                            │
  │    X-Api-Key: <api_key>              │
  │    X-Api-Timestamp: <unix_utc>       │
  │    X-Api-Signature: <hmac_hex>       │
  │  Body: JSON da leitura               │
  │ ──────────────────────────────────►  │
  │                                      │ Valida HMAC, timestamp, escopo
  │                                      │ Grava no tenant do Client
  │  ◄──────────────────────────────── 201 {"ok": true}

2. Credenciais necessárias

Criadas no Django Admin (menu «Clientes API»). Cada fazenda tem um Client com tenant e escopos.

2.1 Dashboard do ESP (http://192.168.4.1)

Campo no dashboard Valor (exemplo) De onde vem
URLhttps://adilson.cc/api/v1/sensors/readings/Fixo (o teu domínio + path)
API Keyc3e78ab1059fd1573d05eadd7bca424a300abfecf269e25a5c3061ebe0e0d116Coluna API KEY no admin (sempre visível)
API Secret64 caracteres hex (0–9, a–f)Mensagem de aviso no topo após criar o Client (Key + Secret numa caixa destacada); não reaparece em claro

Os exemplos são de desenvolvimento; em produção cada fazenda tem o seu par Key + Secret.

2.2 Detalhe por campo

CampoExemploOnde obter
URLhttps://…/api/v1/sensors/readings/Domínio + path da API
API Keyhex longoAdmin, coluna API KEY
API Secret64 hexAviso único após criar (admin redireciona para edição e mostra credenciais)

Ao criar o Client, em Escopos permitidos use JSON com "sensors:write" (ex.: ["sensors:write"]), necessário para a ingestão HMAC.

3. Configurar o ESP32 (provisionamento)

Pré-requisitos: firmware COD_MASTER_V6.ino (Arduino IDE ou PlatformIO, board esp32); telemóvel ou PC com WiFi.

  1. Ligar o ESP32 — AP: SSID FAZENDA_METANO, senha 12345678.
  2. Ligar o telemóvel a esse WiFi.
  3. Abrir http://192.168.4.1.
  4. No card «Servidor remoto (HMAC)»: URL, API Key e API Secret (como na secção 2).
  5. «Salvar configuração».
  6. No «WiFi externo», rede da fazenda e «Conectar».
  7. NTP sincroniza; leituras enviam-se à API quando há rede; a fila no SD é drenada automaticamente a cada ~5 minutos (se existir ficheiro pendente).
  8. No painel de estado aparece Modo API (SD + API, Direto sem SD ou Sem SD / API indisponível) conforme o cartão e a rede.

4. Assinatura HMAC (referência técnica)

4.1 Chave HMAC

chave = SHA-256(api_secret em UTF-8).digest()  →  32 bytes

No servidor: bytes.fromhex(client.api_secret_hash).

4.2 String canónica

Quatro linhas separadas por \n (sem \r):

POST\n/api/v1/sensors/readings/\n1712955600\n<sha256_hex_do_body>
ParteDescrição
POSTMétodo em maiúsculas
/api/v1/sensors/readings/Path sem domínio nem query
timestampIgual ao header X-Api-Timestamp
sha256 do bodyHex minúsculo, 64 caracteres

4.3 Assinatura e cabeçalhos

assinatura = HMAC-SHA256(chave, string_canónica).hexdigest()
X-Api-Key: <api_key>
X-Api-Timestamp: <unix_utc>
X-Api-Signature: <hex 64 chars>
Content-Type: application/json

4.4 Janela de tempo

O servidor rejeita se |agora − timestamp| > 300 s (predefinição). O firmware adia o envio se o relógio não estiver sincronizado (NTP); verifica o tempo antes do POST (ex.: valor Unix < 1000000000 → adia).

5. Corpo JSON esperado

{
  "id":     "master",
  "mac":    "192.168.4.1",
  "data":   "2026-04-12",
  "hora":   "14:30:00",
  "temp":   "25.0",
  "hum":    "60",
  "ppm":    "400",
  "origem": "master"
}

Todos os campos são strings (pode ser "ERRO" em falhas). origem: master ou slave. Os valores de id e mac vêm do firmware (ex.: master + IP do AP para o master; escravos enviam o seu id e MAC).

O campo data é uma string livre (o master usa tipicamente AAAA-MM-DD; outros clientes podem usar outro formato, dentro dos limites do serializer na API).

6. Alterações no firmware

Ficheiro: arduino/COD_MASTER_V6/COD_MASTER_V6.ino

  • Includes: mbedtls/sha256.h, mbedtls/md.h.
  • Autenticação na API: apiKey / apiSecret (Preferences, namespace servidor), sem Basic Auth.
  • Funções sha256Hex, computeHMAC — o path passado à assinatura coincide com o fixo /api/v1/sensors/readings/ (ver nota na secção 4.2).
  • Fila no SD em NDJSON (/dados.jsonl); migração automática de /dados.csv legado; remoção do ficheiro quando a fila é totalmente enviada.
  • enviarDiretoParaAPI(payload): POST HTTPS + HMAC para uma linha JSON já serializada; aceita HTTP 200, 201 ou 204 como sucesso.
  • registrarLeitura(...): tenta envio direto; se falhar ou não houver rede, grava na fila SD; log [REG] se não houver SD nem API.
  • enviarParaServidor(): drena dados.jsonl usando enviarDiretoParaAPI; guardas para credenciais vazias e NTP.
  • Disparo periódico (~5 min) com WiFi externo, sem exigir SD montado (sem cartão, a função sai logo se não houver ficheiro).
  • Dashboard: API Key / Secret; campo Modo API; GET mascara o secret.
  • Rotas /api (objeto master) e /status incluem modo_envio (normal, direto_sem_sd, sem_sd_sem_api).

7. Múltiplas fazendas

Um Client por fazenda (tenant + escopo sensors:write). Novo ESP: novo Client, provisionar no dashboard com as credenciais desse Client. Isolamento por tenant.

8. Troubleshooting

SintomaCausa provávelSolução
HTTP 401Key/Secret ou Client inativoDashboard + admin
401 timestampRelógio ESPWiFi, NTP, Serial [NTP]
HTTP 403Sem sensors:writeEscopos no admin
Painel mostra outra fazendaAPI Key de outro Clientpython manage.py iot_verify_client --api-key … (confirma tenant e escopo)
Sem envioCredenciais vaziasConfigurar em 192.168.4.1
Sem envio + Serial [REMOTO] NTP nao sincronizadoSem WiFi externo ou NTP bloqueadoWiFi, firewall e NTP; prefixo [NTP] no Serial
SD com falha mas dados na APIEnvio direto ativoEsperado se WiFi + NTP + HMAC OK; painel mostra «Direto sem SD»
[REG] leitura perdidaSem SD e sem POST à API (rede/NTP/credenciais)Restaurar SD ou WiFi + NTP + credenciais
Log NTPRede / firewallWiFi externo, NTP permitido

Serial Monitor 115200: prefixos [SETUP], [NTP], [REMOTO], [SERVIDOR], [REG].

9. Segurança

  • api_secret na NVS; flash encryption opcional.
  • O secret não vai na rede em claro — só a assinatura HMAC.
  • HTTPS em produção; firmware pode usar setInsecure() — ideal fazer pin de certificado.
  • Credencial não expira; revogar no admin ou iot_rotate_client_secret + atualizar o dashboard do ESP.