| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- """Regenerate backend/app/data/hms_catalog.json from a Bambu Studio checkout.
- Usage:
- python scripts/generate_hms_catalog.py /path/to/BambuStudio
- Bambu Studio bundles its HMS texts in resources/hms/hms_en_<model>.json, one
- file per 3-character serial prefix. Each file has two lists:
- device_hms faults from the report's `hms[]` array, keyed by the 16-hex code
- the printer shows (attr then code: module, module no., part,
- part no., alert level, error)
- device_error `print_error` faults, keyed by the 8-hex value
- Most texts are the same on every model, but not all: 0300_8001 means "paused by
- the user" on some models and "paused by a pause command in the file" on others.
- So the output holds one merged table plus, per model, only the entries that
- differ from it. A code listed with empty text stays in as "" -- Bambu keeping a
- code but publishing nothing for it is what Bambuddy uses to leave it out of the
- fault count (#2728), so it must not be dropped or filled in.
- backend/app/data/hms_catalog_extra.json holds `print_error` texts Bambu Studio does not
- ship (they came from the ha-bambulab table Bambuddy used before). They are
- merged in only where Studio has no entry, so Studio always wins.
- Run it by hand when Bambu Studio updates its HMS files, and review the diff:
- added codes, changed texts and codes that lost their text are all visible there.
- """
- import json
- import subprocess
- import sys
- from collections import Counter
- from pathlib import Path
- REPO_ROOT = Path(__file__).resolve().parent.parent
- OUTPUT = REPO_ROOT / "backend" / "app" / "data" / "hms_catalog.json"
- EXTRA = REPO_ROOT / "backend" / "app" / "data" / "hms_catalog_extra.json"
- SECTIONS = {"hms": ("device_hms", 16), "error": ("device_error", 8)}
- def _studio_commit(studio: Path) -> str:
- try:
- return subprocess.run(
- ["git", "-C", str(studio), "rev-parse", "--short=12", "HEAD"],
- capture_output=True,
- text=True,
- check=True,
- ).stdout.strip()
- except (OSError, subprocess.CalledProcessError):
- return "unknown"
- def _load_models(hms_dir: Path) -> tuple[dict[str, dict[str, dict[str, str]]], dict[str, int]]:
- """Return {section: {model: {code: text}}} and each model file's `ver`."""
- per_model: dict[str, dict[str, dict[str, str]]] = {section: {} for section in SECTIONS}
- versions: dict[str, int] = {}
- files = sorted(hms_dir.glob("hms_en_*.json"))
- if not files:
- sys.exit(f"no hms_en_*.json files in {hms_dir}")
- for path in files:
- model = path.stem.removeprefix("hms_en_")
- doc = json.loads(path.read_text(encoding="utf-8"))
- versions[model] = doc.get("ver", 0)
- for section, (source_key, length) in SECTIONS.items():
- entries = doc["data"][source_key]["en"]
- table: dict[str, str] = {}
- for entry in entries:
- code = entry["ecode"].strip().upper()
- if len(code) != length or any(c not in "0123456789ABCDEF" for c in code):
- sys.exit(f"{path.name}: unexpected {source_key} code {entry['ecode']!r}")
- table[code] = entry.get("intro", "").strip()
- per_model[section][model] = table
- return per_model, versions
- def _merge(tables: dict[str, dict[str, str]]) -> tuple[dict[str, str], dict[str, dict[str, str]], int]:
- """Pick one text per code, and keep per model only what differs from it.
- The merged text is the one most models use, ignoring models that list the
- code with no text; a tie goes to the model that sorts first, so the output
- is stable between runs. Returns (merged, overrides, codes with a conflict).
- """
- models = sorted(tables)
- codes = sorted({code for table in tables.values() for code in table})
- merged: dict[str, str] = {}
- overrides: dict[str, dict[str, str]] = {}
- conflicts = 0
- for code in codes:
- texts = [tables[m][code] for m in models if code in tables[m]]
- counts = Counter(t for t in texts if t)
- if counts:
- best = max(counts.values())
- merged[code] = next(t for t in texts if counts.get(t) == best)
- else:
- merged[code] = ""
- if len(counts) > 1:
- conflicts += 1
- for model in models:
- text = tables[model].get(code)
- if text is not None and text != merged[code]:
- overrides.setdefault(model, {})[code] = text
- return merged, overrides, conflicts
- def main() -> None:
- if len(sys.argv) != 2:
- sys.exit(__doc__)
- studio = Path(sys.argv[1]).resolve()
- hms_dir = studio / "resources" / "hms"
- per_model, versions = _load_models(hms_dir)
- catalog: dict = {
- "_source": {
- "generator": "scripts/generate_hms_catalog.py",
- "bambu_studio_commit": _studio_commit(studio),
- "model_file_versions": versions,
- "extra": str(EXTRA.relative_to(REPO_ROOT)),
- }
- }
- for section in SECTIONS:
- merged, overrides, conflicts = _merge(per_model[section])
- print(
- f"{section}: {len(merged)} codes, {sum(1 for t in merged.values() if not t)} without text, "
- f"{conflicts} with different texts per model, "
- f"{sum(len(o) for o in overrides.values())} model overrides"
- )
- catalog[section] = merged
- catalog[f"{section}_by_model"] = {model: overrides[model] for model in sorted(overrides)}
- extra = json.loads(EXTRA.read_text(encoding="utf-8"))
- added = 0
- for code, text in extra["error"].items():
- if code not in catalog["error"]:
- catalog["error"][code] = text
- added += 1
- catalog["error"] = dict(sorted(catalog["error"].items()))
- print(f"extra: {added} of {len(extra['error'])} codes added (the rest are now in Bambu Studio)")
- OUTPUT.write_text(json.dumps(catalog, indent=1, ensure_ascii=False) + "\n", encoding="utf-8")
- print(f"wrote {OUTPUT.relative_to(REPO_ROOT)} ({OUTPUT.stat().st_size // 1024} KB)")
- if __name__ == "__main__":
- main()
|