mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 17:50:12 +02:00
fix(hms): wrong-plate Ignore actually ignores + buttons read as buttons + ack-detection survives transient re-pause (#1869)
The HMS error modal had three compounding bugs that surfaced when a user forced a wrong-plate HMS (0500_8051) and tried to dispatch the per-fault actions. (1) IGNORE_RESUME did not ignore. Bambuddy redirected the action on state=PAUSE to a plain `resume` command, citing a #1830 verdict that BambuStudio's "err-bearing shape" was firmware-silently-rejected. BambuStudio source disagrees: DeviceErrorDialog.cpp:600 dispatches IGNORE_RESUME via command_hms_ignore, whose wire shape is {command:"ignore", err:"<decimal>", param:"reserve", job_id:...}. That's a distinct command from `resume` — the firmware suppresses the next re-check AND auto-resumes in one operation. Plain resume means "re-check normally", which is exactly why the wrong-plate detection re-fired 1-2 s after the user clicked Ignore. The #1830 "err-bearing shape rejected" test almost certainly sent the err as a hex shortcode; BambuStudio passes std::to_string(int m_error_code) i.e. the DECIMAL form, which is what the firmware matches against. (2) Action buttons read as inert badges. The button className used `hover:${buttonHoverColor}` — a template-literal interpolation Tailwind's JIT scanner can't see as a literal string, so the per-severity hover utility never reached the compiled CSS. Same bg/text color as the severity badge above and no border made it read as another label. No disabled state and no spinner during the 2.5 s ack wait left clicks sitting silently inert. (3) Ack-detection 502'd on legitimate ack. The route compared (gcode_state, hms_errors-len) before vs after publish; wrong-plate re-pause round-tripped both fields to their pre-publish values inside the 2.5 s window → false 502 even though the firmware fully ack'd. PROBLEM_SOLVED_RESUME working but IGNORE_RESUME 502'ing on the same fault was the same race resolving differently. Fixes: bambu_mqtt.py — new hms_ignore_command() publishes the BambuStudio shape; existing hms_ignore(persistent) renamed to hms_idle_ignore (unchanged shape, used by NO_REMINDER_NEXT_TIME per DeviceErrorDialog.cpp:588). Dispatch routes IGNORE_RESUME, IGNORE_NO_REMINDER_NEXT_TIME, and DONT_REMIND_NEXT_TIME to hms_ignore_command (BambuStudio routes all three to the same command_hms_ignore — the "don't remind" half is the firmware's job). NO_REMINDER_NEXT_TIME stays on hms_idle_ignore type=0. Hex → decimal err conversion at the helper layer with a defensive fallback. job_id=None → empty string (matches BambuStudio's std::string default). HMSErrorModal.tsx — getSeverityInfo loses the dead buttonHoverColor field. Action button uses static `bg-white/10 hover:bg-white/20 active:bg-white/30 text-white border border-white/20`, wires `disabled={!hasPermission||mutation.isPending}`, and renders `<Loader2/>` only on the button whose (action,print_error) matches mutation.variables. printers.py — ack-detection probes `client._last_message_time` (bumped on every MQTT push regardless of payload) rather than diffing state fields. The pushall that follows every command guarantees a fresh push lands inside the 2.5 s window on any healthy printer; only firmware-silent-drop leaves the timestamp untouched, which is the 502 path #1830 wanted.
This commit is contained in:
@@ -131,61 +131,92 @@ class TestExecuteHmsActionDispatch:
|
||||
assert "err" not in cmds[0]["print"]
|
||||
assert "job_id" not in cmds[0]["print"]
|
||||
|
||||
def test_ignore_resume_dispatches_resume_when_print_paused(self, client):
|
||||
# Verified on H2D: idle_ignore is silently rejected while gcode_state
|
||||
# is PAUSE. The user's intent on a paused HMS modal is to continue,
|
||||
# so IGNORE_RESUME dispatches a plain resume instead. See #1830 §(2).
|
||||
def test_ignore_resume_sends_bambustudio_ignore_command_paused(self, client):
|
||||
# IGNORE_RESUME dispatches BambuStudio's `command_hms_ignore`
|
||||
# (DeviceManager.cpp:1450) — `command: "ignore"`, not `resume` and
|
||||
# not `idle_ignore`. The firmware handles both "skip this check on the
|
||||
# next attempt" and "resume the paused print" in one operation.
|
||||
# The previous Bambuddy code redirected to plain resume, which caused
|
||||
# wrong-plate to re-pause 1-2 s after the user clicked Ignore (#1869).
|
||||
# `err` is the DECIMAL int representation of the hex error code.
|
||||
client.state.state = "PAUSE"
|
||||
client.execute_hms_action("03008070", HMSAction.IGNORE_RESUME)
|
||||
client.execute_hms_action("05008051", HMSAction.IGNORE_RESUME, job_id="task-7")
|
||||
cmds = self._published_commands(client)
|
||||
assert cmds[0] == {
|
||||
"print": {
|
||||
"command": "resume",
|
||||
"param": "",
|
||||
"command": "ignore",
|
||||
"err": str(0x05008051), # "83918929"
|
||||
"param": "reserve",
|
||||
"job_id": "task-7",
|
||||
"sequence_id": "0",
|
||||
}
|
||||
}
|
||||
|
||||
def test_ignore_resume_uses_idle_ignore_when_not_paused(self, client):
|
||||
# For non-pause warnings (e.g. AMS-side prompts during printing),
|
||||
# idle_ignore IS the correct command and the firmware honours it.
|
||||
def test_ignore_resume_state_independent(self, client):
|
||||
# BambuStudio's DeviceErrorDialog dispatches IGNORE_RESUME via
|
||||
# `command_hms_ignore` unconditionally — there's no PAUSE-vs-RUNNING
|
||||
# branch. Bambuddy's previous code special-cased PAUSE to a plain
|
||||
# resume; the BambuStudio shape works in both states.
|
||||
client.state.state = "RUNNING"
|
||||
client.execute_hms_action("03008070", HMSAction.IGNORE_RESUME)
|
||||
client.execute_hms_action("05008051", HMSAction.IGNORE_RESUME)
|
||||
cmds = self._published_commands(client)
|
||||
assert cmds[0]["print"]["command"] == "ignore"
|
||||
assert cmds[0]["print"]["err"] == str(0x05008051)
|
||||
|
||||
def test_ignore_no_reminder_uses_ignore_command_not_idle_ignore(self, client):
|
||||
# BambuStudio routes IGNORE_NO_REMINDER_NEXT_TIME and
|
||||
# DONT_REMIND_NEXT_TIME to the same `command_hms_ignore` as
|
||||
# IGNORE_RESUME (DeviceErrorDialog.cpp:596-602) — the "don't remind"
|
||||
# half is the firmware's responsibility, the wire shape is identical.
|
||||
client.state.state = "PAUSE"
|
||||
client.execute_hms_action("03008070", HMSAction.DONT_REMIND_NEXT_TIME, job_id="task-1")
|
||||
cmds = self._published_commands(client)
|
||||
assert cmds[0] == {
|
||||
"print": {
|
||||
"command": "ignore",
|
||||
"err": str(0x03008070),
|
||||
"param": "reserve",
|
||||
"job_id": "task-1",
|
||||
"sequence_id": "0",
|
||||
}
|
||||
}
|
||||
|
||||
def test_no_reminder_next_time_uses_idle_ignore_type_zero(self, client):
|
||||
# NO_REMINDER_NEXT_TIME (distinct from IGNORE_NO_REMINDER_NEXT_TIME)
|
||||
# is BambuStudio's `command_hms_idle_ignore` with type=0
|
||||
# (DeviceErrorDialog.cpp:588-590). Dismisses the dialog without
|
||||
# resuming. The `err` is the same decimal-int format as the ignore
|
||||
# command — same `m_error_code` is passed in BambuStudio.
|
||||
client.state.state = "RUNNING"
|
||||
client.execute_hms_action("03008070", HMSAction.NO_REMINDER_NEXT_TIME)
|
||||
cmds = self._published_commands(client)
|
||||
assert cmds[0] == {
|
||||
"print": {
|
||||
"command": "idle_ignore",
|
||||
"err": "03008070",
|
||||
"err": str(0x03008070),
|
||||
"type": 0,
|
||||
"sequence_id": "0",
|
||||
}
|
||||
}
|
||||
|
||||
def test_dont_remind_dispatches_resume_when_paused(self, client):
|
||||
# The persistent variant still degrades to resume on a paused print —
|
||||
# the "don't remind" flag can't ride along on a resume, but the user
|
||||
# clicked an action whose top-level intent is to continue, so we
|
||||
# honour that. The behavioural contract is documented in hms_ignore.
|
||||
def test_ignore_accepts_16_char_full_code_as_decimal(self, client):
|
||||
# hms[]-array faults carry a 16-char full identifier. Bambu's firmware
|
||||
# matches `err` as a numeric string, so the 16-char hex parses to a
|
||||
# 64-bit int and serializes back as its decimal form.
|
||||
client.state.state = "PAUSE"
|
||||
client.execute_hms_action("03008070", HMSAction.DONT_REMIND_NEXT_TIME)
|
||||
cmds = self._published_commands(client)
|
||||
assert cmds[0]["print"]["command"] == "resume"
|
||||
|
||||
def test_dont_remind_uses_idle_ignore_type_one_when_not_paused(self, client):
|
||||
client.state.state = "RUNNING"
|
||||
client.execute_hms_action("03008070", HMSAction.DONT_REMIND_NEXT_TIME)
|
||||
cmds = self._published_commands(client)
|
||||
assert cmds[0]["print"]["command"] == "idle_ignore"
|
||||
assert cmds[0]["print"]["type"] == 1
|
||||
|
||||
def test_idle_ignore_accepts_16_char_full_code(self, client):
|
||||
# hms[]-array faults carry a 16-char full identifier. The firmware
|
||||
# matches against the full 64-bit code; the truncated 8-char form
|
||||
# (used pre-#1830) was silently rejected on H2C.
|
||||
client.state.state = "RUNNING"
|
||||
client.execute_hms_action("0C00030000020010", HMSAction.IGNORE_RESUME)
|
||||
cmds = self._published_commands(client)
|
||||
assert cmds[0]["print"]["err"] == "0C00030000020010"
|
||||
assert cmds[0]["print"]["command"] == "ignore"
|
||||
assert cmds[0]["print"]["err"] == str(0x0C00030000020010)
|
||||
|
||||
def test_ignore_with_no_job_id_sends_empty_string(self, client):
|
||||
# BambuStudio's `command_hms_ignore` always passes `m_obj->job_id_`
|
||||
# (a `std::string` — empty when there's no active subtask). Match the
|
||||
# shape: empty string, not None / missing key.
|
||||
client.state.state = "PAUSE"
|
||||
client.execute_hms_action("05008051", HMSAction.IGNORE_RESUME, job_id=None)
|
||||
cmds = self._published_commands(client)
|
||||
assert cmds[0]["print"]["job_id"] == ""
|
||||
|
||||
def test_filament_extruded_sends_ams_done(self, client):
|
||||
client.execute_hms_action("07008029", HMSAction.FILAMENT_EXTRUDED)
|
||||
|
||||
Reference in New Issue
Block a user