mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
The HMS catalogue has been in the backend all along and the status response never carried it, so every consumer that wanted to tell a user why a print halted resolved the same 853 codes from its own duplicate of the same sentences -- this repo's Python table, the frontend modal's, and at least one third-party client whose catalogue exists purely because the server would not say. Each ages separately, and a relay watching a printer could only manage "your printer needs attention" while the server already knew it was "Filament ran out. Please load new filament." hms_errors[] entries now carry a description, defaulting to null so a client that has never seen the field is unaffected. It is resolved where the fault is parsed rather than at the boundary that prompted the request, because there are three serializers of a fault, not one: the status response, the WebSocket broadcast, and the completion payload the queue's failure reason is built from. Adding it to only the first would have handed half the feature to a relay watching the stream, which is the likelier consumer of the three. The queue's failure reason now quotes the resolved sentence instead of looking the code up a fourth time, and the notification path reads it rather than re-deriving. That they cannot report different text for one fault is the point, and a test asserts they agree. describe_fault is the single mapping from either code shape onto the table. An 8-char print_error is the catalogue's MMMM_EEEE key with the separator removed -- the parser derives full_code and that key from the same 32-bit value -- so it resolves exactly. A 16-char hms[] identifier is tried whole and then collapsed to its first and last groups. That collapse is lossy, and keeping it was the decision worth making carefully. #2728 counts 65 documented faults falling onto 0300_0001 alone, so a hit can attribute a neighbour's sentence to this fault, and refusing it looks like the stricter reading. It is not: the notification path, the queue's failure-reason helper and the frontend modal have all resolved hms[] faults this way for as long as they have existed, and it resolves real ones -- a 0500_4038 nozzle mismatch arrives in that shape. Declining to collapse would have stopped describing faults that are described today, silently suppressed the notifications they raise, and left this field null while the UI showed text for the same fault. Narrowing it belongs with #2728, where both key spaces can move together. So the lookup is exactly what it was, verified rather than asserted: a test walks every catalogue code in both fault shapes across all three alert levels and checks the result against the derivation this replaces. A future change to the lookup cannot quietly stop notifications firing. The catalogue ships one language, so the field is English and unlocalized, which the schema and the API reference both say next to it. The camwall feed is deliberately left alone -- it is code-only because its token travels in a URL on a screen, and a readable sentence discloses more than the camera picture already does. The frontend keeps resolving its own text: switching it would change what filterKnownHMSErrors counts across eight call sites, which is #1840 and #2728's argument to have. HMSError.message goes with this -- a text field that was never set or read anywhere, and an invitation to populate the wrong one now that a live description sits beside it. ----- Record a failure code the user can actually look up The queue's failure reason formats a fault's module and error into MMMM_EEEE, and that one derivation never masked the error to 16 bits. A fault arriving from the printer's hms[] array carries its alert level in the code's high half, so the label came out as 0500_24038 -- five digits in a group that has four. It is not a code anyone can find on Bambu's HMS index, and because it matches no catalogue key the sentence explaining the failure was dropped along with it, leaving the bare number alone. The nozzle-size mismatch behind #1111 is exactly such a fault. Reported one way it read "[0500_4038] The nozzle diameter in sliced file is not consistent with the current nozzle setting"; reported the other, the same physical fault read "[0500_24038]" and nothing else. There is already a helper that gets this right, used by the archive's own failure-reason lookup, so this calls it instead of keeping a fourth copy of the derivation. It also takes the raw integer code the MQTT payload carries, which the local version only handled as a string.
166 lines
7.7 KiB
Python
166 lines
7.7 KiB
Python
"""Tests for HMS error code translations."""
|
|
|
|
from backend.app.services.hms_errors import HMS_ERROR_DESCRIPTIONS, describe_fault, get_error_description
|
|
|
|
|
|
class TestHMSErrorDescriptions:
|
|
"""Tests for the HMS error descriptions dictionary."""
|
|
|
|
def test_dictionary_is_not_empty(self):
|
|
"""Verify the error descriptions dictionary has entries."""
|
|
assert len(HMS_ERROR_DESCRIPTIONS) > 0
|
|
|
|
def test_dictionary_has_expected_count(self):
|
|
"""Verify we have the expected number of error codes."""
|
|
# Should have 853 error codes from the frontend
|
|
assert len(HMS_ERROR_DESCRIPTIONS) == 853
|
|
|
|
def test_all_keys_are_valid_format(self):
|
|
"""Verify all keys follow the XXXX_YYYY format."""
|
|
import re
|
|
|
|
pattern = re.compile(r"^[0-9A-F]{4}_[0-9A-F]{4}$")
|
|
for code in HMS_ERROR_DESCRIPTIONS:
|
|
assert pattern.match(code), f"Invalid error code format: {code}"
|
|
|
|
def test_all_values_are_non_empty_strings(self):
|
|
"""Verify all descriptions are non-empty strings."""
|
|
for code, description in HMS_ERROR_DESCRIPTIONS.items():
|
|
assert isinstance(description, str), f"Description for {code} is not a string"
|
|
assert len(description) > 0, f"Description for {code} is empty"
|
|
|
|
|
|
class TestGetErrorDescription:
|
|
"""Tests for the get_error_description function."""
|
|
|
|
def test_returns_description_for_known_code(self):
|
|
"""Verify known error codes return their descriptions."""
|
|
# 0300_400C = "The task was canceled."
|
|
result = get_error_description("0300_400C")
|
|
assert result == "The task was canceled."
|
|
|
|
def test_returns_description_for_ams_error(self):
|
|
"""Verify AMS error codes return their descriptions."""
|
|
# 0700_8010 = AMS assist motor overloaded
|
|
result = get_error_description("0700_8010")
|
|
assert "AMS assist motor" in result
|
|
|
|
def test_returns_none_for_unknown_code(self):
|
|
"""Verify unknown error codes return None."""
|
|
result = get_error_description("XXXX_YYYY")
|
|
assert result is None
|
|
|
|
def test_handles_lowercase_input(self):
|
|
"""Verify function handles lowercase input."""
|
|
result = get_error_description("0300_400c")
|
|
assert result == "The task was canceled."
|
|
|
|
def test_handles_mixed_case_input(self):
|
|
"""Verify function handles mixed case input."""
|
|
result = get_error_description("0300_400C")
|
|
assert result == "The task was canceled."
|
|
|
|
def test_common_error_codes_have_descriptions(self):
|
|
"""Verify common error codes have descriptions."""
|
|
common_codes = [
|
|
"0300_4000", # Z axis homing failed
|
|
"0300_4006", # Nozzle clogged
|
|
"0300_8004", # Filament ran out
|
|
"0500_4001", # Failed to connect to Bambu Cloud
|
|
"0700_8010", # AMS assist motor overloaded
|
|
]
|
|
for code in common_codes:
|
|
result = get_error_description(code)
|
|
assert result is not None, f"Missing description for common code: {code}"
|
|
|
|
|
|
class TestDescribeFault:
|
|
"""`describe_fault` maps a fault's canonical `full_code` onto the catalogue,
|
|
so every surface that reports a fault resolves it the same way (#2926)."""
|
|
|
|
def test_resolves_an_eight_char_print_error_code(self):
|
|
"""The parser derives full_code and the catalogue key from the same
|
|
32-bit value, so the split is exact rather than a guess."""
|
|
assert describe_fault("03008004") == "Filament ran out. Please load new filament."
|
|
|
|
def test_resolves_regardless_of_case(self):
|
|
"""Firmware-facing code is uppercase, but a client echoing a value back
|
|
from its own store may not be."""
|
|
assert describe_fault("0300400c") == "The task was canceled."
|
|
|
|
def test_tolerates_surrounding_whitespace(self):
|
|
assert describe_fault(" 03008004 ") == "Filament ran out. Please load new filament."
|
|
|
|
def test_returns_none_for_an_hms_code_outside_the_catalogue(self):
|
|
"""A real P2S fault from #2728. Neither the whole 16-char key nor its
|
|
G1_G4 collapse ("0500_000A") is in the catalogue — no catalogue key has
|
|
an error group below 0x4000, and this family's is 0x000A."""
|
|
assert describe_fault("050002000003000A") is None
|
|
|
|
def test_collapses_a_sixteen_char_code_to_its_g1_g4_short_key(self):
|
|
"""Lossy, and kept deliberately: this is how the notification path, the
|
|
queue's failure-reason helper and the frontend modal have always
|
|
resolved `hms[]` faults, and it resolves real ones. Refusing would stop
|
|
describing faults that are described today (see the module docstring)."""
|
|
key = next(iter(HMS_ERROR_DESCRIPTIONS)) # e.g. "0300_4000"
|
|
module, error = key.split("_")
|
|
forced = f"{module}02000003{error}" # four 4-hex groups; G1 and G4 are the key
|
|
assert len(forced) == 16
|
|
assert describe_fault(forced) == HMS_ERROR_DESCRIPTIONS[key]
|
|
|
|
def test_prefers_the_whole_sixteen_char_key_over_the_collapse(self):
|
|
"""The full identifier is lossless, so it wins when the catalogue has
|
|
both. No 16-char keys ship today; this pins the order for when they do."""
|
|
key = next(iter(HMS_ERROR_DESCRIPTIONS))
|
|
module, error = key.split("_")
|
|
forced = f"{module}02000003{error}"
|
|
HMS_ERROR_DESCRIPTIONS[forced] = "specific variant"
|
|
try:
|
|
assert describe_fault(forced) == "specific variant"
|
|
finally:
|
|
del HMS_ERROR_DESCRIPTIONS[forced]
|
|
|
|
def test_matches_the_derivation_it_replaced(
|
|
self,
|
|
):
|
|
"""The regression guard for the consolidation: for every fault shape the
|
|
codebase can produce, `describe_fault` returns exactly what the
|
|
attr/code short-code lookup in the notification path returned before it.
|
|
Covers both families and all three alert levels a real `hms[]` code
|
|
carries — a divergence here means notifications silently stop firing for
|
|
faults that used to raise them."""
|
|
for key, expected in HMS_ERROR_DESCRIPTIONS.items():
|
|
module, error = int(key[:4], 16), int(key[5:], 16)
|
|
|
|
# print_error: attr is the whole 32-bit value, code its low half.
|
|
print_error = (module << 16) | error
|
|
assert describe_fault(f"{print_error:08X}") == expected
|
|
|
|
# hms[]: attr is groups 1-2, code is groups 3-4 (alert level + id).
|
|
for alert_level in (0x0000, 0x0002, 0x0003):
|
|
attr = (module << 16) | 0x0200
|
|
code = (alert_level << 16) | error
|
|
legacy = get_error_description(f"{(attr >> 16) & 0xFFFF:04X}_{code & 0xFFFF:04X}")
|
|
assert describe_fault(f"{attr:08X}{code:08X}") == legacy == expected
|
|
|
|
def test_returns_none_for_an_unknown_eight_char_code(self):
|
|
assert describe_fault("99999999") is None
|
|
|
|
def test_returns_none_for_empty_or_missing(self):
|
|
"""The dataclass default is "" and the field is optional on the wire."""
|
|
assert describe_fault("") is None
|
|
assert describe_fault(None) is None
|
|
|
|
def test_returns_none_for_a_malformed_length(self):
|
|
"""Neither 8 nor 16 chars — no shape to interpret, so no guess."""
|
|
assert describe_fault("0300") is None
|
|
assert describe_fault("030080040") is None
|
|
|
|
def test_agrees_with_the_short_code_lookup_for_print_error_codes(self):
|
|
"""Pins the equivalence the consolidation rests on: for every 8-char
|
|
code the catalogue covers, describe_fault returns exactly what the
|
|
pre-existing short-code lookup did."""
|
|
for key, expected in HMS_ERROR_DESCRIPTIONS.items():
|
|
assert describe_fault(key.replace("_", "")) == expected
|
|
assert get_error_description(key) == expected
|