generate_hms_catalog.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. """Regenerate backend/app/data/hms_catalog.json from a Bambu Studio checkout.
  2. Usage:
  3. python scripts/generate_hms_catalog.py /path/to/BambuStudio
  4. Bambu Studio bundles its HMS texts in resources/hms/hms_en_<model>.json, one
  5. file per 3-character serial prefix. Each file has two lists:
  6. device_hms faults from the report's `hms[]` array, keyed by the 16-hex code
  7. the printer shows (attr then code: module, module no., part,
  8. part no., alert level, error)
  9. device_error `print_error` faults, keyed by the 8-hex value
  10. Most texts are the same on every model, but not all: 0300_8001 means "paused by
  11. the user" on some models and "paused by a pause command in the file" on others.
  12. So the output holds one merged table plus, per model, only the entries that
  13. differ from it. A code listed with empty text stays in as "" -- Bambu keeping a
  14. code but publishing nothing for it is what Bambuddy uses to leave it out of the
  15. fault count (#2728), so it must not be dropped or filled in.
  16. backend/app/data/hms_catalog_extra.json holds `print_error` texts Bambu Studio does not
  17. ship (they came from the ha-bambulab table Bambuddy used before). They are
  18. merged in only where Studio has no entry, so Studio always wins.
  19. Run it by hand when Bambu Studio updates its HMS files, and review the diff:
  20. added codes, changed texts and codes that lost their text are all visible there.
  21. """
  22. import json
  23. import subprocess
  24. import sys
  25. from collections import Counter
  26. from pathlib import Path
  27. REPO_ROOT = Path(__file__).resolve().parent.parent
  28. OUTPUT = REPO_ROOT / "backend" / "app" / "data" / "hms_catalog.json"
  29. EXTRA = REPO_ROOT / "backend" / "app" / "data" / "hms_catalog_extra.json"
  30. SECTIONS = {"hms": ("device_hms", 16), "error": ("device_error", 8)}
  31. def _studio_commit(studio: Path) -> str:
  32. try:
  33. return subprocess.run(
  34. ["git", "-C", str(studio), "rev-parse", "--short=12", "HEAD"],
  35. capture_output=True,
  36. text=True,
  37. check=True,
  38. ).stdout.strip()
  39. except (OSError, subprocess.CalledProcessError):
  40. return "unknown"
  41. def _load_models(hms_dir: Path) -> tuple[dict[str, dict[str, dict[str, str]]], dict[str, int]]:
  42. """Return {section: {model: {code: text}}} and each model file's `ver`."""
  43. per_model: dict[str, dict[str, dict[str, str]]] = {section: {} for section in SECTIONS}
  44. versions: dict[str, int] = {}
  45. files = sorted(hms_dir.glob("hms_en_*.json"))
  46. if not files:
  47. sys.exit(f"no hms_en_*.json files in {hms_dir}")
  48. for path in files:
  49. model = path.stem.removeprefix("hms_en_")
  50. doc = json.loads(path.read_text(encoding="utf-8"))
  51. versions[model] = doc.get("ver", 0)
  52. for section, (source_key, length) in SECTIONS.items():
  53. entries = doc["data"][source_key]["en"]
  54. table: dict[str, str] = {}
  55. for entry in entries:
  56. code = entry["ecode"].strip().upper()
  57. if len(code) != length or any(c not in "0123456789ABCDEF" for c in code):
  58. sys.exit(f"{path.name}: unexpected {source_key} code {entry['ecode']!r}")
  59. table[code] = entry.get("intro", "").strip()
  60. per_model[section][model] = table
  61. return per_model, versions
  62. def _merge(tables: dict[str, dict[str, str]]) -> tuple[dict[str, str], dict[str, dict[str, str]], int]:
  63. """Pick one text per code, and keep per model only what differs from it.
  64. The merged text is the one most models use, ignoring models that list the
  65. code with no text; a tie goes to the model that sorts first, so the output
  66. is stable between runs. Returns (merged, overrides, codes with a conflict).
  67. """
  68. models = sorted(tables)
  69. codes = sorted({code for table in tables.values() for code in table})
  70. merged: dict[str, str] = {}
  71. overrides: dict[str, dict[str, str]] = {}
  72. conflicts = 0
  73. for code in codes:
  74. texts = [tables[m][code] for m in models if code in tables[m]]
  75. counts = Counter(t for t in texts if t)
  76. if counts:
  77. best = max(counts.values())
  78. merged[code] = next(t for t in texts if counts.get(t) == best)
  79. else:
  80. merged[code] = ""
  81. if len(counts) > 1:
  82. conflicts += 1
  83. for model in models:
  84. text = tables[model].get(code)
  85. if text is not None and text != merged[code]:
  86. overrides.setdefault(model, {})[code] = text
  87. return merged, overrides, conflicts
  88. def main() -> None:
  89. if len(sys.argv) != 2:
  90. sys.exit(__doc__)
  91. studio = Path(sys.argv[1]).resolve()
  92. hms_dir = studio / "resources" / "hms"
  93. per_model, versions = _load_models(hms_dir)
  94. catalog: dict = {
  95. "_source": {
  96. "generator": "scripts/generate_hms_catalog.py",
  97. "bambu_studio_commit": _studio_commit(studio),
  98. "model_file_versions": versions,
  99. "extra": str(EXTRA.relative_to(REPO_ROOT)),
  100. }
  101. }
  102. for section in SECTIONS:
  103. merged, overrides, conflicts = _merge(per_model[section])
  104. print(
  105. f"{section}: {len(merged)} codes, {sum(1 for t in merged.values() if not t)} without text, "
  106. f"{conflicts} with different texts per model, "
  107. f"{sum(len(o) for o in overrides.values())} model overrides"
  108. )
  109. catalog[section] = merged
  110. catalog[f"{section}_by_model"] = {model: overrides[model] for model in sorted(overrides)}
  111. extra = json.loads(EXTRA.read_text(encoding="utf-8"))
  112. added = 0
  113. for code, text in extra["error"].items():
  114. if code not in catalog["error"]:
  115. catalog["error"][code] = text
  116. added += 1
  117. catalog["error"] = dict(sorted(catalog["error"].items()))
  118. print(f"extra: {added} of {len(extra['error'])} codes added (the rest are now in Bambu Studio)")
  119. OUTPUT.write_text(json.dumps(catalog, indent=1, ensure_ascii=False) + "\n", encoding="utf-8")
  120. print(f"wrote {OUTPUT.relative_to(REPO_ROOT)} ({OUTPUT.stat().st_size // 1024} KB)")
  121. if __name__ == "__main__":
  122. main()