From be0cd514aaf3ff767ecfaa4b5d081203d9ae3f86 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Fri, 8 May 2026 22:22:53 -0300 Subject: [PATCH] fix(esp32): worker subprocess fallback for esp32_flash_image import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's e2e test_hcsr04_simulation.mjs caught the regression introduced by a3f21a2 (the issue #101 fix). The worker crashes on boot with Firmware decode error: No module named 'app' esp32_worker.py runs as a subprocess via subprocess.Popen([sys.executable, WORKER_PATH, ...]). When Python launches a script directly, sys.path[0] is the SCRIPT's directory (backend/app/services/), not the backend root. So `from app.services.esp32_flash_image import pad_to_flash_size` fails because there is no `app/` under `backend/app/services/`. esp32_lib_bridge.py wasn't affected because it runs in-process inside uvicorn, where backend/ is implicitly on sys.path. Fix: same try/except + importlib fallback the worker already uses for esp32_i2c_slaves at the top of the file. First try the package import (works when imported by the bridge's tests or anything else with the backend root on sys.path), fall back to direct file loading otherwise. Verified the fallback works in isolation by simulating the subprocess context (sys.path containing only backend/app/services/) — the package import fails as expected and the file-load fallback returns a properly padded 4 MB buffer. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/services/esp32_worker.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/app/services/esp32_worker.py b/backend/app/services/esp32_worker.py index d938f9b2..ec6215fe 100644 --- a/backend/app/services/esp32_worker.py +++ b/backend/app/services/esp32_worker.py @@ -317,7 +317,20 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) # The compiler trims trailing 0xFF padding before serializing (issue # #101 — full 4 MB images blew nginx buffers). Re-pad here so QEMU's # MTD layer sees a valid power-of-2 flash size. - from app.services.esp32_flash_image import pad_to_flash_size + # Imported via fallback because this file runs as a subprocess and + # `app.*` is not on sys.path; mirrors the esp32_i2c_slaves pattern + # at the top of the file. + try: + from app.services.esp32_flash_image import pad_to_flash_size # type: ignore[import-not-found] + except ImportError: + import importlib.util as _ilu, pathlib as _pl + _spec = _ilu.spec_from_file_location( + 'esp32_flash_image', + _pl.Path(__file__).parent / 'esp32_flash_image.py', + ) + _mod = _ilu.module_from_spec(_spec) # type: ignore[arg-type] + _spec.loader.exec_module(_mod) # type: ignore[union-attr] + pad_to_flash_size = _mod.pad_to_flash_size fw_bytes = pad_to_flash_size(base64.b64decode(firmware_b64)) tmp = tempfile.NamedTemporaryFile(suffix='.bin', delete=False) tmp.write(fw_bytes)