CRC32 比較を使って HTTP 経由で Web インターフェースディレクトリを ESP32 LittleFS に同期する方法

ESP32 が Web インターフェースを LittleFS から配信している場合、開発中に更新した HTML/JS ファイルをプッシュする方法が必要。4 つの小さな HTTP エンドポイント——list、upload、CRC32、delete——を使えば、数行の Python で rsync 風の同期を構築できる:CRC32 が異なるファイルだけアップロードし、ローカルに存在しなくなったリモートファイルを削除する。

ファームウェアのエンドポイント

同期スクリプト

sync_webinterface.py
import urllib.parse
import zlib
from pathlib import Path

import requests

BASE_URL = "http://10.56.11.162"

def compute_crc32(path: Path) -> int:
    crc = 0
    with path.open("rb") as f:
        while chunk := f.read(65536):
            crc = zlib.crc32(chunk, crc)
    return crc & 0xFFFFFFFF

def get_device_file_crc(path: str):
    url = f"{BASE_URL}/api/file-crc?path={urllib.parse.quote(path, safe='')}"
    r = requests.get(url, timeout=10)
    if r.status_code == 200:
        j = r.json()
        return j.get("crc32_u32"), j.get("size")
    return None, None

def upload_file(path: str, local_path: Path) -> bool:
    url = f"{BASE_URL}/api/files/put?path={urllib.parse.quote(path, safe='')}"
    # 完全にメモリに読み込む:requests は Content-Length を送る必要があり、
    # chunked Transfer-Encoding ではだめ(esp_http_server が拒否する)。
    r = requests.put(url, data=local_path.read_bytes(), timeout=30)
    return r.status_code in (200, 201)

def list_device_files() -> list[str]:
    r = requests.get(f"{BASE_URL}/api/files", timeout=10)
    return [f["path"] for f in r.json().get("files", [])]

def delete_device_file(path: str) -> bool:
    url = f"{BASE_URL}/api/files/delete?path={urllib.parse.quote(path, safe='')}"
    return requests.delete(url, timeout=10).status_code == 200

def sync(local_dir: Path):
    local_files = {
        p.relative_to(local_dir).as_posix(): p
        for p in local_dir.rglob("*") if p.is_file()
    }

    for rel, full in local_files.items():
        local_crc = compute_crc32(full)
        dev_crc, _ = get_device_file_crc(rel)
        if dev_crc is None:
            print(f"{rel}: missing on device, uploading")
            upload_file(rel, full)
        elif dev_crc != local_crc:
            print(f"{rel}: CRC mismatch (local=0x{local_crc:08X} "
                  f"device=0x{dev_crc:08X}), uploading")
            upload_file(rel, full)
        else:
            print(f"{rel}: up-to-date")

    for dev in list_device_files():
        if dev not in local_files:
            if dev == "nodes.json":
                continue  # 保護されたシステムファイル
            print(f"{dev}: deleted on device")
            delete_device_file(dev)

全部再アップロードではなく CRC32 を使う理由

LittleFS は書き込み回数に限りのあるフラッシュ上にあり、ESP32 への Wi-Fi/イーサネット経由のアップロードは遅い(約 100 kbit/s 級)。先に CRC を比較すれば、変更のないファイルは完全なアップロードではなく小さな GET 1 回で済む。esp_rom_crc32_le(最後の ~crc ^ 0xFFFFFFFF 修正込み)は zlib.crc32 と一致するので、比較は純粋な整数の等価チェックになる。

落とし穴

  • chunked ボディを絶対送らない — bytes(またはファイルオブジェクト)を渡して requests が Content-Length を送るようにする;req->content_len を読む esp_http_server ハンドラは chunked 転送で失敗する。
  • path パラメータを URL エンコード(urllib.parse.quote)して、サブディレクトリ内のファイルが動くようにする。
  • システムファイルをサーバ側で保護(例:nodes.json)し、同期でもスキップする。

関連記事


同じカテゴリの類似記事をチェック: ESP32 Python