Files
bambuddy/backend/app/services/virtual_printer/_debug.py
T
maziggy 4ab7339fe4 debug(vp): env-flagged wire-payload dump for slicer-mirror triage (#1622)
Add a BAMBUDDY_VP_DUMP_WIRE=1 escape hatch that writes the bridge's
  cached push_status (in) and the 1Hz slicer-facing copy (out) to
  <log_dir>/vp_wire/<vp_name>_<direction>.json, overwritten each tick.

  #1622's symptom — empty filament dropdown in slicer's AMS slot details
  for P1S/A1 but not H2D in non-proxy VP modes — needs visibility into
  the actual wire bytes flowing through the bridge to bisect between
  "cache is missing fields" and "_send_status_report strips them on copy."
  The existing logs prove the bridge is bound and pushing at 1Hz, but
  not what's in the payload.

  Off by default, single env flag, single file per VP per direction
  (bounded disk footprint), failures swallowed at debug so a broken
  dump can never break the 1Hz loop. 21 tests pin the helper contract:
  disabled-by-default, atomic writes, sanitized vp_name (no path
  escape), per-call env check so toggling without restart works.
2026-06-11 10:02:22 +02:00

67 lines
2.4 KiB
Python

"""Env-flagged wire-payload dump for VP MQTT debug (gated; off by default).
Set ``BAMBUDDY_VP_DUMP_WIRE=1`` to write the most recent inbound (bridge
cache input) and outbound (slicer-facing 1Hz push) MQTT payloads to disk,
one file per VP per direction, overwritten each tick.
Used to triage shape-of-payload bugs (e.g. #1622) where the question is
"is the bridge missing fields in the cache, or is something else stripping
them on the way out to the slicer?" Compare ``*_in.json`` and ``*_out.json``
for the failing VP against a known-good VP (e.g. H2D vs P1S).
Layout: ``<log_dir>/vp_wire/<sanitized_vp_name>_<direction>.json``
Failure modes are swallowed at debug level — debug instrumentation must
never break the bridge or slicer-facing 1Hz loop. Disable by unsetting the
env var; the in-progress files stay on disk and can be deleted manually.
"""
from __future__ import annotations
import json
import logging
import os
import re
from backend.app.core.config import settings as app_settings
logger = logging.getLogger(__name__)
_ENV_FLAG = "BAMBUDDY_VP_DUMP_WIRE"
_NAME_SAFE = re.compile(r"[^A-Za-z0-9._-]+")
def _enabled() -> bool:
return os.environ.get(_ENV_FLAG, "").strip().lower() in ("1", "true", "yes", "on")
def _sanitize(name: str) -> str:
safe = _NAME_SAFE.sub("_", name or "vp").strip("_")
return safe or "vp"
def dump_wire(vp_name: str, direction: str, payload: dict | bytes | str) -> None:
"""Write ``payload`` to ``<log_dir>/vp_wire/<vp_name>_<direction>.json``.
No-op when the env flag is unset. Accepts dict (json-encoded with
``indent=2``), bytes (decoded as utf-8 with errors='replace'), or
str (written verbatim).
"""
if not _enabled():
return
try:
target_dir = app_settings.log_dir / "vp_wire"
target_dir.mkdir(parents=True, exist_ok=True)
path = target_dir / f"{_sanitize(vp_name)}_{_sanitize(direction)}.json"
if isinstance(payload, dict):
text = json.dumps(payload, indent=2, default=str)
elif isinstance(payload, bytes):
text = payload.decode("utf-8", errors="replace")
else:
text = str(payload)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(text, encoding="utf-8")
tmp.replace(path)
except OSError as e:
logger.debug("[%s] vp_wire dump (%s) failed: %s", vp_name, direction, e)