Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ Versioning follows [SemVer](https://semver.org/): **MAJOR.MINOR.PATCH**

---

## [1.37.0] — 2026-06-10

### Added
- **Gravação do firmware da balança pela web** (Admin → **Estação de pesagem**,
`/admin/scale`, só admin). O usuário conecta o ESP32‑C3 por USB e grava o firmware
direto do navegador via **Web Serial + esptool‑js** — irmão do gravador Niimbot (Web
Bluetooth): Chrome/Edge no computador, HTTPS (ou `localhost`), sem instalar nada.
- O firmware é compilado e gravado em **4 pedaços separados nos seus offsets**
(`bootloader 0x0 · partitions 0x8000 · boot_app0 0xe000 · app 0x10000`) — exatamente
como o `pio upload` — via `deploy/build-firmware-bin.sh`, em `static/firmware/` +
manifesto (`balanca-c3.json`), **versionados no git** e servidos como estáticos (o
deploy por clone público/`git archive` leva tudo, sem build no servidor). Pedaços
separados (não uma imagem merged única em 0x0) + `data` como **`Uint8Array`** são o
que funciona com o esptool-js — validado em hardware (ESP32‑C3 SuperMini).
- O `esptool-js` é **vendorado** (`static/esptool.js`, via `deploy/vendor-esptool.sh`,
sem CDN/runtime); o adaptador `static/esp-flash.js` baixa o `.bin` e grava em `0x0`.
**Nenhuma mudança de CSP** (Web Serial é API JS, scripts são `'self'`). Mensagens
traduzidas no servidor (EN/ES). Ver `docs/balanca-web-flash.md`.
- Escopo desta versão: **só gravação** (binário genérico, sem segredos). Provisionamento
de Wi‑Fi/URL/chave de API (firmware com Wi‑Fi + `POST /api/weigh` + handshake
serial‑JSON + fallback SoftAP) virá numa próxima versão.

## [1.36.0] — 2026-06-10

Endurecimento de segurança a partir de um teste externo (caixa-preta). Vários
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.36.0
1.37.0
107 changes: 107 additions & 0 deletions deploy/build-firmware-bin.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# ── build-firmware-bin.sh — gera os binários da balança p/ flash pela web ─────
#
# O gravador web (Web Serial + esptool-js, em /admin/scale) grava os MESMOS 4
# pedaços que o `pio run -t upload` (CLI), cada um no seu offset:
# bootloader 0x0 · partitions 0x8000 · boot_app0 0xe000 · app 0x10000
#
# IMPORTANTE: gravar os pedaços SEPARADOS (e não uma imagem "merged" única em 0x0)
# é o que funciona com o esptool-js — a imagem merged em 0x0 falha no meio
# ("Failed to write compressed data to flash after seq N"), pois o esptool-js
# erra o cálculo de endereço de bloco numa imagem única grande. O CLI grava
# separado, por isso funciona; aqui replicamos isso.
#
# Os binários + o manifesto (offsets) são gravados em static/firmware/ e VÃO
# committados no git: o deploy é por clone público + `git archive`, então o que
# está no git é o que o site serve (sem build no servidor).
#
# Uso: bash deploy/build-firmware-bin.sh
# Requisitos: PlatformIO (pio) no PATH.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
FW_DIR="$ROOT/firmware"
ENV="balanca-c3"
CHIP="esp32c3"
BUILD_DIR="$FW_DIR/.pio/build/$ENV"
OUT_DIR="$ROOT/static/firmware"
OUT_MANIFEST="$OUT_DIR/$ENV.json"

command -v pio >/dev/null 2>&1 || { echo "PlatformIO (pio) não encontrado no PATH." >&2; exit 1; }

# Python que de fato roda (no Windows o python/python3 do Store é stub).
PY=""
for _c in python3 py python; do
if command -v "$_c" >/dev/null 2>&1 && "$_c" --version >/dev/null 2>&1; then PY="$_c"; break; fi
done
[ -n "$PY" ] || { echo "Python 3 necessário (python3/py/python)." >&2; exit 1; }

echo "==> Compilando firmware ($ENV)…"
pio run -d "$FW_DIR" -e "$ENV"

# Offsets padrão do ESP32-C3 Arduino. O boot_app0 (seletor OTA) vem do framework.
BOOTLOADER="$BUILD_DIR/bootloader.bin"
PARTITIONS="$BUILD_DIR/partitions.bin"
APP="$BUILD_DIR/firmware.bin"
for f in "$BOOTLOADER" "$PARTITIONS" "$APP"; do
[ -f "$f" ] || { echo "Faltando $f (rode o build antes)." >&2; exit 1; }
done

# boot_app0.bin: prefere o diretório do framework SEM sufixo de versão (o ativo).
BOOT_APP0="$(ls -1 \
"$HOME"/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
"$HOME"/.platformio/packages/framework-arduinoespressif32*/tools/partitions/boot_app0.bin \
2>/dev/null | head -1)"
[ -n "$BOOT_APP0" ] && [ -f "$BOOT_APP0" ] || { echo "boot_app0.bin não encontrado no framework." >&2; exit 1; }

mkdir -p "$OUT_DIR"

echo "==> Copiando os 4 pedaços para $OUT_DIR…"
cp -f "$BOOTLOADER" "$OUT_DIR/bootloader.bin"
cp -f "$PARTITIONS" "$OUT_DIR/partitions.bin"
cp -f "$BOOT_APP0" "$OUT_DIR/boot_app0.bin"
cp -f "$APP" "$OUT_DIR/app.bin"

# Remove a imagem merged legada (não é mais usada — o flasher grava os pedaços).
rm -f "$OUT_DIR/$ENV.bin"

echo "==> Gerando manifesto (offsets + sha)…"
"$PY" - "$OUT_DIR" "$OUT_MANIFEST" "$CHIP" "$ROOT" <<'PYEOF'
import hashlib, json, os, subprocess, sys, datetime
out_dir, manifest, chip, root = sys.argv[1:5]
# offset → arquivo, na MESMA ordem/endereços do `pio run -t upload`.
layout = [
("0x0", "bootloader.bin"),
("0x8000", "partitions.bin"),
("0xe000", "boot_app0.bin"),
("0x10000", "app.bin"),
]
def sha(p):
return hashlib.sha256(open(p, "rb").read()).hexdigest()
parts = []
for off, fn in layout:
p = os.path.join(out_dir, fn)
parts.append({"offset": off, "file": fn,
"size": os.path.getsize(p), "sha256": sha(p)})
try:
commit = subprocess.check_output(
["git", "-C", root, "log", "-1", "--format=%h", "--", "firmware"],
text=True).strip()
except Exception:
commit = ""
m = {
"chip": chip,
"parts": parts,
# "versão" exibida = sha do app (firmware.bin); muda quando o firmware muda.
"app_sha256": next(p["sha256"] for p in parts if p["file"] == "app.bin"),
"source_commit": commit,
"built_at": datetime.datetime.now(datetime.timezone.utc)
.strftime("%Y-%m-%dT%H:%M:%SZ"),
}
json.dump(m, open(manifest, "w"), indent=2)
print(json.dumps(m, indent=2))
PYEOF

echo "==> OK. Pedaços + manifesto em $OUT_DIR."
echo "Revise o git diff e committe os artefatos."
45 changes: 45 additions & 0 deletions deploy/vendor-esptool.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# ── vendor-esptool.sh — re-vendora o esptool-js (gravador Web Serial) ──────────
#
# spool-control é PÚBLICO e o servidor clona anonimamente, então o esptool-js NÃO
# pode ser baixado em deploy/runtime — é VENDORADO no repo (igual ao driver Niimbot).
# O upstream é o pacote npm `esptool-js` (Espressif), que publica um `bundle.js`
# ESM único (mesmo bundle usado pelo ESP Web Tools).
#
# Esta é a única forma deliberada e reprodutível de atualizar a cópia vendorada:
# baixa o bundle de uma VERSÃO FIXA, carimba versão/origem no topo e grava em
# static/esptool.js. Sem download em runtime/deploy e sem CDN — o que está no git é
# o que roda (preserva CSP, deploy à prova de falhas e operação offline).
#
# Uso:
# deploy/vendor-esptool.sh # versão pinada abaixo
# deploy/vendor-esptool.sh 0.6.0 # uma versão específica
#
# Depois: revise `git diff`, bump VERSION + CHANGELOG, committe, deploy.
set -euo pipefail

ESPTOOL_VERSION="${1:-0.6.0}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DEST="$ROOT/static/esptool.js"
URL="https://unpkg.com/esptool-js@${ESPTOOL_VERSION}/bundle.js"
TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT

echo "==> Baixando esptool-js@${ESPTOOL_VERSION} de unpkg…"
curl -fsSL --max-time 60 "$URL" -o "$TMP"

# Sanidade: precisa ser o bundle ESM com os exports que o esp-flash.js usa.
grep -q "as ESPLoader" "$TMP" || { echo "Bundle inesperado: não exporta ESPLoader." >&2; exit 1; }
grep -q "as Transport" "$TMP" || { echo "Bundle inesperado: não exporta Transport." >&2; exit 1; }

{
echo "// VENDORADO — esptool-js@${ESPTOOL_VERSION} (Apache-2.0), Espressif Systems."
echo "// Fonte: ${URL}"
echo "// NÃO EDITAR À MÃO — atualize via deploy/vendor-esptool.sh. Bundle ESM único"
echo "// (importado por static/esp-flash.js). Carimbado em $(date -u +%Y-%m-%dT%H:%M:%SZ)."
cat "$TMP"
} > "$DEST"

echo "==> OK: $DEST ($(wc -c < "$DEST") bytes)"
echo "Revise o git diff e committe."
88 changes: 88 additions & 0 deletions docs/balanca-web-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Gravar o firmware da balança pela web

A página **Admin → Estação de pesagem** (`/admin/scale`, só admin) grava o firmware da
balança (ESP32‑C3) direto do navegador, via **Web Serial + esptool‑js** — o irmão do
gravador Niimbot (Web Bluetooth). Sem instalar PlatformIO, sem app.

> **Escopo atual:** só **gravação** de um binário genérico. A configuração de Wi‑Fi, URL
> e chave de API (provisionamento) virá numa próxima versão (firmware com Wi‑Fi STA +
> `POST /api/weigh` + handshake serial‑JSON; ver roadmap no fim).

## Requisitos do usuário

- **Chrome ou Edge no computador** (Web Serial não existe em Safari/Firefox nem em
celular).
- **Secure context:** HTTPS (produção atrás do Traefik) **ou** `http://localhost` no dev.
- A balança ligada por USB. No diálogo do navegador, escolher a porta serial da placa.

## Como funciona

- O firmware é compilado e gravado em **4 pedaços separados, cada um no seu offset** —
exatamente como o `pio upload`: `bootloader 0x0 · partitions 0x8000 · boot_app0 0xe000
· app 0x10000`. Os pedaços (`bootloader.bin`, `partitions.bin`, `boot_app0.bin`,
`app.bin`) + o manifesto `static/firmware/balanca-c3.json` (offsets, sha256) ficam em
`static/firmware/`.
- **Gravar pedaços separados (e não uma imagem "merged" única em 0x0) é o que funciona
com o esptool-js.** Uma imagem única grande falha no meio (o esptool-js erra o cálculo
de endereço de bloco). Além disso, o `data` de cada pedaço é passado como **`Uint8Array`**
— passar uma "binary string" faz o pako expandir os bytes ≥ 0x80 como UTF‑8 e o stub
rejeita o bloco final com `ESP_TOO_MUCH_DATA` (0xC9). É assim que o ESP Web Tools / ESPHome
fazem. Validado em hardware (ESP32‑C3 SuperMini).
- Os artefatos são **versionados no git** e servidos como estáticos. O deploy é por clone
público + `git archive`, então **o que está no git é o que o site serve** — não há build
no servidor (preserva o deploy à prova de falhas).
- O driver `esptool-js` é **vendorado** em `static/esptool.js` (sem CDN, sem download em
runtime); o adaptador `static/esp-flash.js` importa o driver como módulo, lê o manifesto,
baixa os pedaços e grava cada um no seu offset. As mensagens visíveis são traduzidas no
servidor.

## Atualizar o binário quando o firmware mudar

```bash
# 1) Compila o firmware (env balanca-c3) e gera os pedaços + manifesto:
bash deploy/build-firmware-bin.sh
# → static/firmware/{bootloader,partitions,boot_app0,app}.bin + balanca-c3.json
# 2) Revise o git diff, committe os artefatos, bump VERSION + CHANGELOG.
```

Requer PlatformIO (`pio`) no PATH; o `esptool.py` vem com a plataforma espressif32
(chamado via `pio pkg exec`). Offsets ESP32‑C3 Arduino: bootloader `0x0`, partições
`0x8000`, boot_app0 `0xe000`, app `0x10000`.

## Atualizar o esptool‑js vendorado

```bash
deploy/vendor-esptool.sh # versão pinada (0.6.0)
deploy/vendor-esptool.sh 0.6.1 # uma versão específica
```

Baixa o `bundle.js` (ESM único) da versão fixa para `static/esptool.js`, carimbando a
origem. Sem download em runtime/deploy.

## Testar

- **Local (hardware real):** rode o app em `http://localhost:5000` (secure context para
Web Serial), plugue o ESP32‑C3, abra `/admin/scale` no Chrome/Edge → **Conectar e
gravar** → confirme o boot no monitor serial (`pio device monitor -e balanca-c3`, 115200).
- **Rede / staging (sem afetar produção):** `git push` da branch e, numa **LXC separada**,
`bash /opt/spool-control/deploy/update-lxc.sh --ref <branch>` (o script clona o repo
público no ref dado e aplica a árvore). Acesse via HTTPS e repita o flash. **Não** mexa
na LXC 117 (produção).
- **Testes automatizados:** `tests/test_scale_flash.py` cobre o gate admin, a entrega do
binário/manifesto e a presença do adaptador no HTML.

## Notas

- O ESP32‑C3 SuperMini usa **USB‑Serial/JTAG** nativo; o esptool‑js grava os pedaços nos
seus offsets sem precisar do botão BOOT.
- `platformio.ini` fixa `upload_port=COM23` — irrelevante para o flash web; só afeta o
`pio upload` manual. A porta real aparece no diálogo do navegador.

## Roadmap (próxima fase — provisionamento)

1. Firmware: Wi‑Fi STA + cliente HTTP `POST /api/weigh` + leitura de config no NVS
(`Preferences`).
2. Handshake **serial‑JSON pequeno** logo após o flash: o site envia SSID/senha + URL +
chave de API (lida de Admin → Integrações, integração `scale`) pela mesma sessão Web
Serial; o firmware grava no NVS.
3. Fallback **SoftAP captive portal** para configurar Wi‑Fi sem Web Serial.
20 changes: 20 additions & 0 deletions routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,26 @@ def admin_settings():
)


# ── Estação de pesagem (gravar firmware da balança pela web) ─────────────────

def _firmware_manifest():
"""Lê static/firmware/balanca-c3.json (gerado por deploy/build-firmware-bin.sh).
Fail-safe: ausente/corrompido → {} (a página mostra o estado sem versão)."""
path = Path(app.static_folder) / "firmware" / "balanca-c3.json"
try:
return json.loads(path.read_text())
except Exception:
return {}


@app.route("/admin/scale")
@admin_required
def admin_scale():
manifest = _firmware_manifest()
manifest_url = url_for("static", filename="firmware/balanca-c3.json")
return render_template("admin/scale.html", manifest=manifest, manifest_url=manifest_url)


# ── Atualização do sistema ───────────────────────────────────────────────────

@app.route("/admin/update")
Expand Down
Loading