Files
bambuddy/backend/app/services/hms_errors.py
T
maziggy 033eff1254 Show Bambu's own HMS descriptions and the real alert level (issue #2728)
Document HMS levels, Bambu's descriptions and uncounted faults (issue #2728)
2026-09-28 13:57:02 +02:00

93 lines
3.9 KiB
Python

"""HMS fault descriptions.
The texts come from Bambu Studio's own HMS files, generated into
``backend/app/data/hms_catalog.json`` by ``scripts/generate_hms_catalog.py``
(issue #2728). Do not edit the JSON by hand; rerun the script.
Two key spaces, never mixed:
``hms`` faults from the report's ``hms[]`` array, keyed by the 16-hex code
the printer screen shows: ``attr`` then ``code``, i.e. module,
module no., part, part no., alert level, error.
``error`` ``print_error`` faults, keyed by the 8-hex value.
Each has a merged table plus, per 3-character serial prefix, the entries whose
text differs on that model (0300_8001 is "paused by the user" on some models and
"paused by a pause command in the file" on others). A code Bambu lists with
empty text is stored as "" and reads as no description.
Only English ships; Bambu Studio has more languages if that is ever wanted.
"""
import json
from pathlib import Path
_DATA_FILE = Path(__file__).resolve().parent.parent / "data" / "hms_catalog.json"
# Loaded once at import, like hms_actions.json. Absolute path so the load does
# not depend on the working directory (systemd unit, Docker, tests).
with _DATA_FILE.open(encoding="utf-8") as _f:
_CATALOG: dict = json.load(_f)
_TABLES: dict[int, tuple[dict[str, str], dict[str, dict[str, str]]]] = {
16: (_CATALOG["hms"], _CATALOG["hms_by_model"]),
8: (_CATALOG["error"], _CATALOG["error_by_model"]),
}
def lookup_fault(full_code: str | None, model: str | None = None) -> str | None:
"""Return the catalogue entry for a fault: its text, "" when Bambu lists the
code without text, or None when the code is not listed at all.
``full_code`` is the identifier the firmware matches on: 8 hex chars for a
``print_error``, 16 for an ``hms[]`` entry. ``model`` is the printer's
3-character serial prefix; a model-specific text wins over the merged one,
and an unknown or missing model uses the merged table.
There is no fallback from one key space to the other. A 16-char code used to
be collapsed to its first and last groups and looked up as a ``print_error``,
but no real ``hms[]`` code has an error group at or above 0x4000, where every
``print_error`` key sits, so the collapse could only ever attach a
neighbouring fault's sentence (#2728).
"""
if not full_code:
return None
code = full_code.strip().upper()
tables = _TABLES.get(len(code))
if tables is None:
return None
merged, by_model = tables
if model:
specific = by_model.get(model.upper(), {}).get(code)
if specific is not None:
return specific
return merged.get(code)
def describe_fault(full_code: str | None, model: str | None = None) -> str | None:
"""The fault's description, or None when there is no text for it.
Resolved once at parse time so every surface that reports a fault -- the
status response, the WebSocket broadcast, the completion payload,
notifications -- says the same thing (#2926). None covers both "not listed"
and "listed without text"; ``lookup_fault`` tells the two apart.
"""
return lookup_fault(full_code, model) or None
def get_error_description(error_code: str, model: str | None = None) -> str | None:
"""Description for a ``print_error`` short code such as "0300_400C"."""
return describe_fault(error_code.replace("_", ""), model)
def alert_level_from_print_error(error: int) -> int:
"""Alert level of a ``print_error``, from the first hex digit of its error.
A ``print_error`` is a bare 32-bit module/error word with no level field.
Its error number carries the level instead: 0x4xxx stops the task, 0x8xxx
pauses it, 0xCxxx is a prompt. Mapped onto the ``hms[]`` alert levels
(1 error, 2 warning, 3 notification) so both kinds sort and filter the same
way. 0 for anything else, which Bambu defines as an invalid level.
"""
return {0x4: 1, 0x8: 2, 0xC: 3}.get((error >> 12) & 0xF, 0)