mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +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:
@@ -3824,14 +3824,17 @@ async def execute_hms_action(
|
||||
# command. publish() success is NOT the same as printer-ack: Bambu's
|
||||
# firmware silently rejects malformed HMS commands at QoS 1 (the broker
|
||||
# ACKs the publish, but the printer drops it). Verified end-to-end against
|
||||
# a live H2D — see #1830 §(3). We sample (gcode_state, hms_errors length)
|
||||
# because every accepted HMS action mutates at least one of them.
|
||||
# a live H2D — see #1830 §(3).
|
||||
#
|
||||
# PrinterState.state carries the MQTT `gcode_state` value verbatim (see
|
||||
# bambu_mqtt.py line 2144); the raw `print_error` int isn't preserved on
|
||||
# state, only the derived HMSError entries are.
|
||||
pre_gcode = client.state.state
|
||||
pre_hms_count = len(client.state.hms_errors)
|
||||
# We probe `_last_message_time` (bumped on every MQTT push) rather than a
|
||||
# (gcode_state, hms_errors-length) diff. The old diff missed the
|
||||
# wrong-plate IGNORE_RESUME case where the printer briefly resumes and
|
||||
# re-pauses with the same fault inside the 2.5s window: both fields
|
||||
# round-trip to their pre-publish values → false 502 even though the
|
||||
# firmware fully ack'd the resume. Every accepted command triggers a
|
||||
# pushall response within ~100-500ms, so a fresh inbound message after
|
||||
# the publish is the robust ack signal.
|
||||
pre_last_message = client._last_message_time
|
||||
|
||||
success = client.execute_hms_action(body.print_error, body.action, body.job_id)
|
||||
if not success:
|
||||
@@ -3845,12 +3848,12 @@ async def execute_hms_action(
|
||||
# coroutine is awaiting.
|
||||
await asyncio.sleep(HMS_ACTION_ACK_WAIT_SECONDS)
|
||||
|
||||
acked = client.state.state != pre_gcode or len(client.state.hms_errors) != pre_hms_count
|
||||
acked = client._last_message_time > pre_last_message
|
||||
if not acked:
|
||||
# Publish succeeded but the printer's state didn't move. Almost always
|
||||
# firmware-side silent rejection (err mismatch, command/state mismatch).
|
||||
# 502 makes it visible at the UI instead of the 200-but-broken loop
|
||||
# #1830 reported.
|
||||
# Publish succeeded but the printer sent nothing back. Almost always
|
||||
# firmware-side silent rejection (err mismatch, command/state mismatch)
|
||||
# or a dropped MQTT route. 502 makes it visible at the UI instead of
|
||||
# the 200-but-broken loop #1830 reported.
|
||||
raise HTTPException(502, "Printer did not acknowledge HMS action within 2.5s")
|
||||
|
||||
return {"success": True, "message": "HMS action executed"}
|
||||
|
||||
@@ -5439,14 +5439,18 @@ class BambuMQTTClient:
|
||||
print_error: Canonical hex identifier for the fault — 8 chars for the
|
||||
32-bit `print_error` path, 16 chars for the 64-bit `hms[]` path
|
||||
(HMSError.full_code). Carried through unchanged from the route.
|
||||
Only the `idle_ignore` branch puts it on the wire; resume / stop
|
||||
use BambuStudio's plain shape (verified against a live H2D, the
|
||||
`err`-bearing shape is silently rejected by the firmware).
|
||||
Converted to its DECIMAL string form for the `ignore` /
|
||||
`idle_ignore` commands' `err` field, which is what the firmware
|
||||
actually compares against the active fault. The pre-#1869
|
||||
hex-string `err` was silently rejected because the firmware was
|
||||
being asked to match `"05008051"` against int 0x05008051
|
||||
(= 83918929 decimal) — see BambuStudio's
|
||||
DeviceManager.cpp:1450-1462 (`command_hms_ignore`) which passes
|
||||
`std::to_string(int m_error_code)`.
|
||||
action: One of HMSAction's string values.
|
||||
job_id: The `subtask_id` snapshotted onto the HMSError at parse-time.
|
||||
Preserved for symmetry with the catalog but no longer sent —
|
||||
BambuStudio's actual resume/stop commands are plain and the
|
||||
firmware doesn't echo `job_id` back on the response either.
|
||||
Required by BambuStudio's `command_hms_ignore` / `command_hms_stop`
|
||||
shapes; empty string is the no-job-id sentinel.
|
||||
|
||||
Returns False when the MQTT client is offline or when `action` is unknown
|
||||
so the route surfaces it as a 4xx rather than a silent no-op.
|
||||
@@ -5464,12 +5468,24 @@ class BambuMQTTClient:
|
||||
self.topic_publish, json.dumps({"pushing": {"command": "pushall", "sequence_id": "0"}}), qos=1
|
||||
)
|
||||
|
||||
# BambuStudio's `err` field is the DECIMAL string of the error code's int
|
||||
# value (DeviceErrorDialog.cpp passes `std::to_string(m_error_code)` to
|
||||
# every command_hms_* call). Our route hands us the hex string —
|
||||
# convert. Falls back to the raw input if it's not parseable so the
|
||||
# firmware can reject it and the route can surface 502 instead of us
|
||||
# raising ValueError mid-dispatch.
|
||||
try:
|
||||
err_decimal = str(int(print_error, 16))
|
||||
except ValueError:
|
||||
err_decimal = print_error
|
||||
|
||||
def hms_resume():
|
||||
# BambuStudio's actual shape — plain resume, no err / no job_id.
|
||||
# The `err`-bearing shape (`err`, `param: "reserve"`, `job_id`) is
|
||||
# silently rejected by Bambu firmware on print_error- and hms[]-sourced
|
||||
# faults alike; verified by injecting candidate shapes against a live
|
||||
# H2D paused on a wrong-plate HMS. See #1830 §(2).
|
||||
# Plain resume — verified against the user's H2D/H2S to leave PAUSE
|
||||
# cleanly when "Problem Solved and Resume" is clicked. BambuStudio
|
||||
# sends `{command: "resume", err: "<decimal>", param: "reserve",
|
||||
# job_id: ...}` from `command_hms_resume`; we kept the simpler
|
||||
# shape historically because it works, and changing it without a
|
||||
# field test risks regressing a path that the user has confirmed.
|
||||
publish(
|
||||
{
|
||||
"print": {
|
||||
@@ -5481,8 +5497,8 @@ class BambuMQTTClient:
|
||||
)
|
||||
|
||||
def hms_stop():
|
||||
# Same as hms_resume — BambuStudio's actual shape is plain. The
|
||||
# `err`-bearing variant is silently rejected; verified on the H2D.
|
||||
# Same as hms_resume — plain shape, confirmed working by the user
|
||||
# for "Stop Printing".
|
||||
publish(
|
||||
{
|
||||
"print": {
|
||||
@@ -5493,29 +5509,46 @@ class BambuMQTTClient:
|
||||
}
|
||||
)
|
||||
|
||||
def hms_ignore(persistent: bool = False):
|
||||
# `idle_ignore` is BambuStudio's "dismiss this warning" command for
|
||||
# non-pause warnings. type=0 dismisses once, type=1 hides the same
|
||||
# warning permanently.
|
||||
def hms_ignore_command():
|
||||
# BambuStudio's `command_hms_ignore` (DeviceManager.cpp:1450) —
|
||||
# what the "Ignore this and Resume" button actually publishes.
|
||||
# Distinct from `idle_ignore`: this command has the firmware
|
||||
# suppress the next re-check of the named fault AND resume the
|
||||
# paused print in a single operation. The previous Bambuddy code
|
||||
# redirected IGNORE_RESUME to a plain `resume`, which is why the
|
||||
# wrong-plate HMS came back 1-2 s later: `resume` means "I fixed
|
||||
# the problem, re-check normally" so the firmware re-detected the
|
||||
# wrong plate and re-paused with the same code (#1869).
|
||||
#
|
||||
# For HMS-paused state, `idle_ignore` is silently rejected by the
|
||||
# firmware regardless of `err` (verified on a live H2D — see
|
||||
# #1830 §(2)). The user-facing intent of "Ignore and resume" on a
|
||||
# paused print is to continue, so we dispatch a plain resume
|
||||
# instead. The `persistent` flag is informational in that branch —
|
||||
# firmware can't honour "don't remind" through a resume — but the
|
||||
# button still does what the user expects.
|
||||
#
|
||||
# NB: PrinterState's `state` field carries the MQTT `gcode_state`
|
||||
# value verbatim — line 2144 stores `data["gcode_state"]` onto it.
|
||||
if self.state.state == "PAUSE":
|
||||
hms_resume()
|
||||
return
|
||||
# BambuStudio also routes IGNORE_NO_REMINDER_NEXT_TIME (a.k.a.
|
||||
# DONT_REMIND_NEXT_TIME) to this same command — the persistent
|
||||
# variant of "don't remind next time" lives on `idle_ignore`'s
|
||||
# type=1, not as a separate ignore shape.
|
||||
publish(
|
||||
{
|
||||
"print": {
|
||||
"command": "ignore",
|
||||
"err": err_decimal,
|
||||
"param": "reserve",
|
||||
"job_id": job_id or "",
|
||||
"sequence_id": "0",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def hms_idle_ignore(persistent: bool = False):
|
||||
# `idle_ignore` is BambuStudio's "dismiss this warning without
|
||||
# resuming" command for non-pause warnings — what
|
||||
# `command_hms_idle_ignore` (DeviceManager.cpp:1424) sends.
|
||||
# type=0 dismisses once, type=1 suppresses the same warning
|
||||
# permanently. Used by NO_REMINDER_NEXT_TIME, which BambuStudio
|
||||
# explicitly dispatches via `command_hms_idle_ignore(..., 0)` —
|
||||
# NOT via the resume-bearing `ignore` command.
|
||||
publish(
|
||||
{
|
||||
"print": {
|
||||
"command": "idle_ignore",
|
||||
"err": print_error,
|
||||
"err": err_decimal,
|
||||
"type": 1 if persistent else 0,
|
||||
"sequence_id": "0",
|
||||
}
|
||||
@@ -5577,11 +5610,20 @@ class BambuMQTTClient:
|
||||
case HMSAction.STOP_PRINTING:
|
||||
hms_stop()
|
||||
|
||||
case HMSAction.IGNORE_RESUME | HMSAction.NO_REMINDER_NEXT_TIME:
|
||||
hms_ignore(persistent=False)
|
||||
case HMSAction.IGNORE_RESUME | HMSAction.IGNORE_NO_REMINDER_NEXT_TIME | HMSAction.DONT_REMIND_NEXT_TIME:
|
||||
# All three buttons map to BambuStudio's `command_hms_ignore`
|
||||
# (DeviceErrorDialog.cpp:596-602). The "no reminder next time"
|
||||
# half of IGNORE_NO_REMINDER_NEXT_TIME is the firmware's
|
||||
# responsibility — the wire shape is identical.
|
||||
hms_ignore_command()
|
||||
|
||||
case HMSAction.IGNORE_NO_REMINDER_NEXT_TIME | HMSAction.DONT_REMIND_NEXT_TIME:
|
||||
hms_ignore(persistent=True)
|
||||
case HMSAction.NO_REMINDER_NEXT_TIME:
|
||||
# BambuStudio's NO_REMINDER_NEXT_TIME branch dispatches
|
||||
# `command_hms_idle_ignore` with type=0
|
||||
# (DeviceErrorDialog.cpp:588-590). Distinct from the
|
||||
# IGNORE_* buttons above: idle_ignore does NOT resume, only
|
||||
# dismisses the dialog.
|
||||
hms_idle_ignore(persistent=False)
|
||||
|
||||
case HMSAction.FILAMENT_EXTRUDED | HMSAction.DBL_CHECK_DONE:
|
||||
ams_control("done")
|
||||
|
||||
@@ -1770,24 +1770,26 @@ class TestExecuteHMSActionAPI:
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_execute_hms_action_success(self, async_client: AsyncClient, printer_factory):
|
||||
"""200 happy path — dispatcher returns True AND printer state moves
|
||||
within the ack-wait window. The state delta is the firmware's only
|
||||
proof that the command landed (publish success is necessary but not
|
||||
sufficient; see #1830 §(3))."""
|
||||
"""200 happy path — dispatcher returns True AND the printer pushes at
|
||||
least one MQTT message into the ack-wait window. A fresh inbound
|
||||
message is the firmware's proof that the command landed (publish
|
||||
success is necessary but not sufficient; see #1830 §(3))."""
|
||||
printer = await printer_factory(name="Test Printer")
|
||||
|
||||
mock_client = MagicMock()
|
||||
# Pre-action state — paused with a fault.
|
||||
# Pre-action state — paused with a fault, last message arrived at t=0.
|
||||
mock_client.state.state = "PAUSE"
|
||||
mock_client.state.print_error = 0x05008051
|
||||
mock_client.state.hms_errors = [object()]
|
||||
mock_client._last_message_time = 100.0
|
||||
|
||||
def _act(*_a, **_kw):
|
||||
# Simulate the printer accepting the command and clearing the fault
|
||||
# by the time the ack-wait expires.
|
||||
mock_client.state.state = "FAILED"
|
||||
mock_client.state.print_error = 0
|
||||
mock_client.state.hms_errors = []
|
||||
# Simulate the printer pushing a status update within the ack-wait
|
||||
# window. The pushall that follows every command is what produces
|
||||
# this — the actual state fields don't have to move (#1869: a
|
||||
# wrong-plate IGNORE_RESUME re-pauses with the same fault but the
|
||||
# printer DID push back).
|
||||
mock_client._last_message_time = 100.5
|
||||
return True
|
||||
|
||||
mock_client.execute_hms_action.side_effect = _act
|
||||
@@ -1830,8 +1832,8 @@ class TestExecuteHMSActionAPI:
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_execute_hms_action_no_printer_ack_returns_502(self, async_client: AsyncClient, printer_factory):
|
||||
"""502 when publish succeeded but printer state didn't move within the
|
||||
ack-wait window. This is the silent-rejection failure mode #1830
|
||||
"""502 when publish succeeded but no MQTT message arrives back within
|
||||
the ack-wait window. This is the silent-rejection failure mode #1830
|
||||
identifies: the broker ACKs the publish at QoS 1 but the firmware
|
||||
drops the command (err mismatch, wrong shape, state mismatch).
|
||||
Surfacing this as 502 instead of 200 stops the UI from claiming
|
||||
@@ -1842,8 +1844,9 @@ class TestExecuteHMSActionAPI:
|
||||
mock_client.state.state = "PAUSE"
|
||||
mock_client.state.print_error = 0x05008051
|
||||
mock_client.state.hms_errors = [object()]
|
||||
mock_client._last_message_time = 100.0
|
||||
mock_client.execute_hms_action.return_value = True # publish "succeeded"
|
||||
# Crucially: state does NOT change → ack-wait detects no movement.
|
||||
# Crucially: _last_message_time does NOT advance → no inbound push.
|
||||
|
||||
with (
|
||||
patch("backend.app.api.routes.printers.printer_manager") as mock_pm,
|
||||
@@ -1858,6 +1861,49 @@ class TestExecuteHMSActionAPI:
|
||||
assert response.status_code == 502
|
||||
assert "acknowledge" in response.json()["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_execute_hms_action_ignore_resume_repauses_within_window_still_acks(
|
||||
self, async_client: AsyncClient, printer_factory
|
||||
):
|
||||
"""200 when the printer ack'd the command but immediately re-paused
|
||||
with the same fault — e.g. wrong-plate IGNORE_RESUME (#1869). The
|
||||
previous (gcode_state, hms_errors-len) diff produced a false 502
|
||||
because both fields round-tripped to their pre-publish values inside
|
||||
the ack window. Probing `_last_message_time` survives the round-trip
|
||||
because the printer's status push lands regardless of the eventual
|
||||
state."""
|
||||
printer = await printer_factory(name="Test Printer")
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.state.state = "PAUSE"
|
||||
mock_client.state.print_error = 0x05008051
|
||||
mock_client.state.hms_errors = [object()]
|
||||
mock_client._last_message_time = 100.0
|
||||
|
||||
def _act(*_a, **_kw):
|
||||
# Printer ack'd, briefly resumed, re-detected the wrong plate, and
|
||||
# re-paused with the same fault. Net diff on state fields is zero,
|
||||
# but a fresh status push DID arrive.
|
||||
mock_client._last_message_time = 100.4
|
||||
mock_client.state.state = "PAUSE" # round-tripped
|
||||
mock_client.state.hms_errors = [object()] # same length
|
||||
return True
|
||||
|
||||
mock_client.execute_hms_action.side_effect = _act
|
||||
|
||||
with (
|
||||
patch("backend.app.api.routes.printers.printer_manager") as mock_pm,
|
||||
patch("backend.app.api.routes.printers.HMS_ACTION_ACK_WAIT_SECONDS", 0.01),
|
||||
):
|
||||
mock_pm.get_client.return_value = mock_client
|
||||
|
||||
body = {"print_error": "05008051", "action": "IGNORE_RESUME", "job_id": None}
|
||||
response = await async_client.post(f"/api/v1/printers/{printer.id}/hms/execute-action", json=body)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_execute_hms_action_accepts_16_char_full_code(self, async_client: AsyncClient, printer_factory):
|
||||
@@ -1870,9 +1916,10 @@ class TestExecuteHMSActionAPI:
|
||||
mock_client.state.state = "RUNNING"
|
||||
mock_client.state.print_error = 0
|
||||
mock_client.state.hms_errors = [object()]
|
||||
mock_client._last_message_time = 100.0
|
||||
|
||||
def _act(*_a, **_kw):
|
||||
mock_client.state.hms_errors = []
|
||||
mock_client._last_message_time = 100.4
|
||||
return True
|
||||
|
||||
mock_client.execute_hms_action.side_effect = _act
|
||||
|
||||
@@ -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