[Feature]: HMS Actions (#1743)

This commit is contained in:
Zelda 2026-06-26 14:40:25 +02:00 committed by GitHub
parent d3fd8d6ad8
commit 3ddf8d847e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 10138 additions and 23 deletions

View file

@ -27,6 +27,7 @@ from backend.app.schemas.printer import (
AMSUnit,
DiagnosticRequest,
FilaSwitchResponse,
HmsActionBody,
HMSErrorResponse,
NozzleInfoResponse,
NozzleRackSlot,
@ -456,7 +457,9 @@ async def get_printer_status(
# Convert HMS errors to response format
hms_errors = [
HMSErrorResponse(code=e.code, attr=e.attr, module=e.module, severity=e.severity)
HMSErrorResponse(
code=e.code, attr=e.attr, module=e.module, severity=e.severity, actions=e.actions, job_id=e.job_id
)
for e in (state.hms_errors or [])
]
@ -3787,3 +3790,27 @@ async def get_runtime_debug(
else None,
"is_active": printer.is_active,
}
@router.post("/{printer_id}/hms/execute-action")
async def execute_hms_action(
printer_id: int,
body: HmsActionBody,
_=RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
db: AsyncSession = Depends(get_db),
):
"""Execute an HMS action on the printer."""
result = await db.execute(select(Printer).where(Printer.id == printer_id))
printer = result.scalar_one_or_none()
if not printer:
raise HTTPException(404, "Printer not found")
client = printer_manager.get_client(printer_id)
if not client:
raise HTTPException(400, "Printer not connected")
success = client.execute_hms_action(body.print_error, body.action, body.job_id)
if not success:
raise HTTPException(400, "Failed to execute HMS action")
return {"success": True, "message": "HMS action executed"}

File diff suppressed because it is too large Load diff

View file

@ -153,6 +153,8 @@ class HMSErrorResponse(BaseModel):
attr: int = 0 # Attribute value for constructing wiki URL
module: int
severity: int # 1=fatal, 2=serious, 3=common, 4=info
actions: list[str] = [] # List of user-facing action keys (e.g. "CHECK_FILAMENT")
job_id: str | None = None # Optional job ID for actions that require it (e.g. "CHECK_ASSISTANT")
class AMSTray(BaseModel):
@ -216,6 +218,18 @@ class AmsLabelBody(BaseModel):
ams_serial: str = Field(default="", max_length=50)
class HmsActionBody(BaseModel):
# 8-char hex short code without separator (e.g. "05000070") — frontend strips
# the underscore from the displayed `MMMM_EEEE` before sending.
print_error: str = Field(..., min_length=8, max_length=8, pattern=r"^[0-9A-Fa-f]{8}$")
# One of the HMSAction enum values. Length-capped to keep stray input from
# reaching the dispatcher's `match` statement.
action: str = Field(..., min_length=1, max_length=64)
# The `subtask_id` snapshot from the HMSError that surfaced this dialog.
# Bambu echoes it back in HMS-aware commands. Optional for idle errors.
job_id: str | None = Field(default=None, max_length=64)
class FilaSwitchResponse(BaseModel):
"""Filament Track Switch (FTS) state — accessory that mediates AMS-to-extruder routing.

View file

@ -21,6 +21,8 @@ from datetime import datetime, timezone
import paho.mqtt.client as mqtt
from backend.app.services.hms_actions import HMSAction, get_actions_for_error_code
logger = logging.getLogger(__name__)
# AMS module name prefixes used in get_version responses.
@ -169,6 +171,8 @@ class HMSError:
module: int
severity: int # 1=fatal, 2=serious, 3=common, 4=info
message: str = ""
actions: list[str] | None = None # List of user-facing action keys (e.g. "CHECK_FILAMENT")
job_id: str | None = None # Optional job ID for actions that require it (e.g. "CHECK_ASSISTANT")
# HMS short codes the firmware emits during normal user-cancel sequences.
@ -2729,12 +2733,15 @@ class BambuMQTTClient:
short_code = f"{(attr >> 16) & 0xFFFF:04X}_{code & 0xFFFF:04X}"
if short_code in _HMS_USER_ACTION_CODES:
continue
actions = get_actions_for_error_code(self.serial_number[:3], short_code.replace("_", ""))
self.state.hms_errors.append(
HMSError(
code=f"0x{code:x}" if code else "0x0",
attr=attr,
module=module,
severity=severity if severity > 0 else 2,
actions=actions,
job_id=self.state.subtask_id,
)
)
@ -2780,12 +2787,32 @@ class BambuMQTTClient:
existing_short_codes.add(f"{e_module:04X}_{e_error:04X}")
if short_code not in existing_short_codes:
# Bambu's HMS catalog keys by 3-letter device code (the SN
# prefix) and a 16-char short error code without the
# underscore separator we store internally.
actions = get_actions_for_error_code(self.serial_number[:3], short_code.replace("_", ""))
# Bambu pushes the current job as `subtask_id` on the
# state stream; the HMS-action commands echo it back as
# `job_id`. The error payload itself doesn't carry the
# id, so snapshot it from the live state at parse time
# and freeze it on the HMSError so subsequent
# job changes don't invalidate the action.
job_id = self.state.subtask_id
logger.debug(
"[%s, %s] HMS available actions: %s (job_id=%s)",
self.serial_number[:3],
short_code.replace("_", ""),
actions,
job_id,
)
self.state.hms_errors.append(
HMSError(
code=f"0x{error:x}",
attr=print_error, # Store full value for display
module=module >> 8, # High byte of module (e.g., 0x05)
severity=3, # Warning level for print_error
actions=actions,
job_id=job_id,
)
)
@ -5378,3 +5405,196 @@ class BambuMQTTClient:
self._client.publish(self.topic_publish, json.dumps(pushall), qos=1)
logger.info("[%s] Set liveview %s", self.serial_number, "enabled" if enable else "disabled")
return True
def execute_hms_action(self, print_error: str, action: str, job_id: str | None = None) -> bool:
"""Dispatch the user's choice from the HMS-error modal as a printer command.
Args:
print_error: 8-char hex short code with no separator (e.g. "05000070").
The frontend strips the underscore from the displayed `MMMM_EEEE`
before sending.
action: One of HMSAction's string values.
job_id: The `subtask_id` snapshotted onto the HMSError at parse-time.
Bambu's HMS-aware commands echo it back as `job_id`. May be None
for idle errors that never had a job.
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.
"""
if not self._client or not self.state.connected:
logger.warning("[%s] Cannot execute HMS action: not connected", self.serial_number)
return False
# Always re-push the full state after a command so the modal's underlying
# status query reflects the new error list (or absence) on the next tick.
def publish(payload: dict):
self._client.publish(self.topic_publish, json.dumps(payload), qos=1)
self._client.publish(
self.topic_publish, json.dumps({"pushing": {"command": "pushall", "sequence_id": "0"}}), qos=1
)
def hms_resume():
publish(
{
"print": {
"command": "resume",
"err": print_error,
"param": "reserve",
"job_id": job_id,
"sequence_id": "0",
}
}
)
def hms_stop():
publish(
{
"print": {
"command": "stop",
"err": print_error,
"param": "reserve",
"job_id": job_id,
"sequence_id": "0",
}
}
)
def hms_ignore(persistent: bool = False):
# `idle_ignore` is BambuStudio's "dismiss this warning" command.
# type=0 dismisses once, type=1 hides the same warning permanently.
publish(
{
"print": {
"command": "idle_ignore",
"err": print_error,
"type": 1 if persistent else 0,
"sequence_id": "0",
}
}
)
def ams_control(param: str):
publish(
{
"print": {
"command": "ams_control",
"param": param,
"sequence_id": "0",
}
}
)
def clean_print_error():
# Matches the existing `clear_hms_errors` shape — Bambu does not
# expect `print_error` in the body; the command clears whatever
# error dialog is currently active on the printer.
publish(
{
"print": {
"command": "clean_print_error",
"sequence_id": "0",
}
}
)
def uiop_close():
# `err` is the 8-char hex short code (already a string from the
# frontend), uppercased for consistency with how BambuStudio sends it.
publish(
{
"system": {
"command": "uiop",
"name": "print_error",
"action": "close",
"source": 1,
"type": "dialog",
"err": print_error.upper(),
"sequence_id": "0",
}
}
)
match action:
case (
HMSAction.RESUME_PRINTING
| HMSAction.RESUME_PRINTING_DEFECTS
| HMSAction.RESUME_PRINTING_PROBELM_SOLVED
| HMSAction.PROBLEM_SOLVED_RESUME
| HMSAction.FILAMENT_LOAD_RESUME
| HMSAction.PROCEED
):
hms_resume()
case HMSAction.STOP_PRINTING:
hms_stop()
case HMSAction.IGNORE_RESUME | HMSAction.NO_REMINDER_NEXT_TIME:
hms_ignore(persistent=False)
case HMSAction.IGNORE_NO_REMINDER_NEXT_TIME | HMSAction.DONT_REMIND_NEXT_TIME:
hms_ignore(persistent=True)
case HMSAction.FILAMENT_EXTRUDED | HMSAction.DBL_CHECK_DONE:
ams_control("done")
case (
HMSAction.RETRY_FILAMENT_EXTRUDED
| HMSAction.CONTINUE
| HMSAction.RETRY_PROBLEM_SOLVED
| HMSAction.DBL_CHECK_RETRY
):
ams_control("resume")
case HMSAction.ABORT:
ams_control("abort")
case HMSAction.OK_BUTTON:
clean_print_error()
case HMSAction.DBL_CHECK_OK:
clean_print_error()
uiop_close()
case HMSAction.DBL_CHECK_RESUME:
# Plain resume — not HMS-aware, no err/job_id.
publish(
{
"print": {
"command": "resume",
"param": "",
"sequence_id": "0",
}
}
)
case HMSAction.REFRESH_NOZZLE:
publish({"print": {"command": "refresh_nozzle", "sequence_id": "0"}})
case HMSAction.TURN_OFF_FIRE_ALARM:
publish({"print": {"command": "buzzer_ctrl", "mode": 0, "sequence_id": "0"}})
case HMSAction.STOP_DRYING:
publish({"print": {"command": "auto_stop_ams_dry", "sequence_id": "0"}})
case HMSAction.DISABLE_PURIFICATION:
publish({"print": {"command": "close_air_filt", "sequence_id": "0"}})
case (
HMSAction.CHECK_ASSISTANT
| HMSAction.JUMP_TO_LIVEVIEW
| HMSAction.OK_JUMP_RACK
| HMSAction.REMOVE_CLOSE_BTN
| HMSAction.LOAD_VIRTUAL_TRAY
| HMSAction.CANCLE
| HMSAction.DBL_CHECK_CANCEL
):
# UI-only actions — the printer's own screen handles these; the
# modal still surfaces them so the user has parity with Studio.
pass
case _:
logger.warning("[%s] Unknown HMS action '%s'", self.serial_number, action)
return False
return True

View file

@ -0,0 +1,75 @@
"""HMS action lookup.
Bambu printers report HMS errors with a fixed catalog of remediation actions
(resume / stop / check assistant / etc.). The catalog is bundled as JSON, keyed
by the 3-letter SN prefix (printer model code: 03W = A1, 31B = X1C, etc.) and
the short error code with no separator.
The action IDs and their string names are derived from BambuStudio's source via
`scripts/update_hms_actions.py`. The data file itself is fetched from Bambu's
public `e.bambulab.com/hms/GetActionImage.php` endpoint.
"""
import json
from enum import StrEnum
from pathlib import Path
_DATA_FILE = Path(__file__).resolve().parent.parent / "data" / "hms_actions.json"
# Loaded eagerly at import — the file is ~150KB and only read once. Using an
# absolute path keeps the load independent of CWD (systemd unit, Docker
# entrypoint, pytest run from `backend/`).
with _DATA_FILE.open("r", encoding="utf-8") as _f:
_actions: dict[str, dict[str, list[str]]] = json.load(_f)
class HMSAction(StrEnum):
"""Remediation actions a Bambu printer can offer for an HMS error.
Values intentionally match the constants used in BambuStudio's source so the
HMS-data fetcher can map Bambu's integer action IDs straight to these
strings. The CANCLE typo is preserved verbatim it's how BambuStudio spells
it, and changing it would break the action lookup against the catalog.
"""
RESUME_PRINTING = "RESUME_PRINTING"
RESUME_PRINTING_DEFECTS = "RESUME_PRINTING_DEFECTS"
RESUME_PRINTING_PROBELM_SOLVED = "RESUME_PRINTING_PROBELM_SOLVED"
STOP_PRINTING = "STOP_PRINTING"
CHECK_ASSISTANT = "CHECK_ASSISTANT"
FILAMENT_EXTRUDED = "FILAMENT_EXTRUDED"
RETRY_FILAMENT_EXTRUDED = "RETRY_FILAMENT_EXTRUDED"
CONTINUE = "CONTINUE"
LOAD_VIRTUAL_TRAY = "LOAD_VIRTUAL_TRAY"
OK_BUTTON = "OK_BUTTON"
FILAMENT_LOAD_RESUME = "FILAMENT_LOAD_RESUME"
JUMP_TO_LIVEVIEW = "JUMP_TO_LIVEVIEW"
NO_REMINDER_NEXT_TIME = "NO_REMINDER_NEXT_TIME"
REFRESH_NOZZLE = "REFRESH_NOZZLE"
IGNORE_NO_REMINDER_NEXT_TIME = "IGNORE_NO_REMINDER_NEXT_TIME"
IGNORE_RESUME = "IGNORE_RESUME"
PROBLEM_SOLVED_RESUME = "PROBLEM_SOLVED_RESUME"
TURN_OFF_FIRE_ALARM = "TURN_OFF_FIRE_ALARM"
RETRY_PROBLEM_SOLVED = "RETRY_PROBLEM_SOLVED"
STOP_DRYING = "STOP_DRYING"
CANCLE = "CANCLE" # sic — verbatim from BambuStudio
REMOVE_CLOSE_BTN = "REMOVE_CLOSE_BTN"
PROCEED = "PROCEED"
OK_JUMP_RACK = "OK_JUMP_RACK"
ABORT = "ABORT"
DISABLE_PURIFICATION = "DISABLE_PURIFICATION"
DONT_REMIND_NEXT_TIME = "DONT_REMIND_NEXT_TIME"
DBL_CHECK_CANCEL = "DBL_CHECK_CANCEL"
DBL_CHECK_DONE = "DBL_CHECK_DONE"
DBL_CHECK_RETRY = "DBL_CHECK_RETRY"
DBL_CHECK_RESUME = "DBL_CHECK_RESUME"
DBL_CHECK_OK = "DBL_CHECK_OK"
def get_actions_for_error_code(device: str, error_code: str) -> list[str]:
"""Look up the action list for a printer SN prefix + short error code.
Returns the empty list if the printer model or the error code is unknown
the modal renders no buttons in that case, which is the correct fallback.
"""
return _actions.get(device, {}).get(error_code, [])

View file

@ -1137,7 +1137,14 @@ def printer_state_to_dict(
"total_layers": state.total_layers,
"temperatures": temperatures,
"hms_errors": [
{"code": e.code, "attr": e.attr, "module": e.module, "severity": e.severity}
{
"code": e.code,
"attr": e.attr,
"module": e.module,
"severity": e.severity,
"actions": e.actions,
"job_id": e.job_id,
}
for e in (state.hms_errors or [])
],
# AMS data for filament colors

View file

@ -0,0 +1,229 @@
"""Tests for HMS-action lookup and the MQTT dispatcher in execute_hms_action.
The lookup tests confirm the bundled catalog round-trips correctly. The
dispatcher tests are payload-shape contracts wrong shape sends a bogus
command to the printer, which is the failure mode this PR is most exposed to,
so each HMSAction case publishes the expected JSON.
"""
import json
from unittest.mock import MagicMock
import pytest
from backend.app.services.bambu_mqtt import BambuMQTTClient
from backend.app.services.hms_actions import (
HMSAction,
get_actions_for_error_code,
)
class TestActionLookup:
def test_known_a1_error_returns_actions(self):
# 03W is the A1 model code; 03008070 is "Heat the nozzle…" and Bambu's
# catalog lists CHECK_ASSISTANT for it.
actions = get_actions_for_error_code("03W", "03008070")
assert isinstance(actions, list)
assert len(actions) > 0
for a in actions:
assert isinstance(a, str)
def test_unknown_device_returns_empty_list(self):
assert get_actions_for_error_code("ZZZ", "03008070") == []
def test_unknown_error_returns_empty_list(self):
# Real model code, made-up error.
assert get_actions_for_error_code("03W", "DEADBEEF") == []
def test_underscore_form_does_not_match(self):
# Caller is responsible for stripping the `_` before lookup. Guards
# against accidental rewires that pass the underscore form.
assert get_actions_for_error_code("03W", "0300_8070") == []
def test_action_enum_values_are_uppercase_strings(self):
# The catalog stores actions verbatim from BambuStudio. Drift here
# silently breaks the dispatcher's `match` because StrEnum compares
# by value.
assert HMSAction.RESUME_PRINTING == "RESUME_PRINTING"
assert HMSAction.CANCLE == "CANCLE" # sic — kept from BambuStudio
class TestExecuteHmsActionDispatch:
"""Each case in the `match` publishes a specific JSON shape. These tests
pin those shapes so silent regressions surface as test failures, not as
a printer receiving a malformed command on a live print.
"""
@pytest.fixture
def client(self):
c = BambuMQTTClient(
ip_address="192.168.1.100",
serial_number="03W-TEST",
access_code="12345678",
)
c._client = MagicMock()
c.state.connected = True
return c
def _published_commands(self, client):
"""Return the list of `print`/`system` command dicts from publish calls,
skipping the `pushing.pushall` echoes that follow every action."""
out = []
for call in client._client.publish.call_args_list:
_topic, payload = call.args[0], call.args[1]
data = json.loads(payload)
if "pushing" in data:
continue
out.append(data)
return out
def test_returns_false_when_disconnected(self, client):
client.state.connected = False
assert client.execute_hms_action("03008070", HMSAction.OK_BUTTON) is False
client._client.publish.assert_not_called()
def test_returns_false_on_unknown_action(self, client):
assert client.execute_hms_action("03008070", "DOES_NOT_EXIST") is False
# No printer command, but the publish-list check tolerates the pushall
# tail — just confirm no command went out by inspecting the helper.
assert self._published_commands(client) == []
def test_resume_carries_err_param_and_job_id(self, client):
ok = client.execute_hms_action("03008070", HMSAction.RESUME_PRINTING, job_id="task-42")
assert ok is True
cmds = self._published_commands(client)
assert cmds == [
{
"print": {
"command": "resume",
"err": "03008070",
"param": "reserve",
"job_id": "task-42",
"sequence_id": "0",
}
}
]
def test_proceed_falls_through_to_resume(self, client):
client.execute_hms_action("03008070", HMSAction.PROCEED, job_id="task-1")
cmds = self._published_commands(client)
assert cmds[0]["print"]["command"] == "resume"
assert cmds[0]["print"]["err"] == "03008070"
def test_stop_carries_err_and_job_id(self, client):
client.execute_hms_action("03008070", HMSAction.STOP_PRINTING, job_id="task-1")
cmds = self._published_commands(client)
assert cmds[0]["print"]["command"] == "stop"
assert cmds[0]["print"]["job_id"] == "task-1"
def test_ignore_resume_uses_idle_ignore_type_zero(self, client):
client.execute_hms_action("03008070", HMSAction.IGNORE_RESUME)
cmds = self._published_commands(client)
assert cmds[0] == {
"print": {
"command": "idle_ignore",
"err": "03008070",
"type": 0,
"sequence_id": "0",
}
}
def test_dont_remind_uses_idle_ignore_type_one(self, client):
# DONT_REMIND_NEXT_TIME and IGNORE_NO_REMINDER_NEXT_TIME are the
# persistent variants — Bambu hides the warning for future prints.
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_filament_extruded_sends_ams_done(self, client):
client.execute_hms_action("07008029", HMSAction.FILAMENT_EXTRUDED)
cmds = self._published_commands(client)
assert cmds[0] == {"print": {"command": "ams_control", "param": "done", "sequence_id": "0"}}
def test_retry_sends_ams_resume(self, client):
client.execute_hms_action("07008029", HMSAction.RETRY_FILAMENT_EXTRUDED)
cmds = self._published_commands(client)
assert cmds[0]["print"]["param"] == "resume"
assert cmds[0]["print"]["command"] == "ams_control"
def test_abort_sends_ams_abort(self, client):
client.execute_hms_action("07008029", HMSAction.ABORT)
cmds = self._published_commands(client)
assert cmds[0]["print"]["param"] == "abort"
def test_ok_button_sends_bare_clean_print_error(self, client):
# Matches the existing `clear_hms_errors` shape — no `print_error` body
# field, which the original PR mistakenly added.
client.execute_hms_action("03008070", HMSAction.OK_BUTTON)
cmds = self._published_commands(client)
assert cmds[0] == {"print": {"command": "clean_print_error", "sequence_id": "0"}}
def test_dbl_check_ok_sends_clean_then_uiop_close(self, client):
client.execute_hms_action("03008070", HMSAction.DBL_CHECK_OK)
cmds = self._published_commands(client)
assert len(cmds) == 2
assert cmds[0]["print"]["command"] == "clean_print_error"
assert cmds[1]["system"]["command"] == "uiop"
# `err` is the already-string short code, NOT `f"{x:08X}"` against a
# str (which would TypeError on the old code path).
assert cmds[1]["system"]["err"] == "03008070"
def test_uiop_close_uppercases_lowercase_input(self, client):
# Frontend may send the short code in either case; we normalise.
client.execute_hms_action("0300abcd", HMSAction.DBL_CHECK_OK)
cmds = self._published_commands(client)
assert cmds[1]["system"]["err"] == "0300ABCD"
def test_dbl_check_resume_is_plain_resume(self, client):
# No err/job_id — explicitly different from RESUME_PRINTING.
client.execute_hms_action("03008070", HMSAction.DBL_CHECK_RESUME)
cmds = self._published_commands(client)
assert cmds[0] == {"print": {"command": "resume", "param": "", "sequence_id": "0"}}
assert "err" not in cmds[0]["print"]
def test_refresh_nozzle(self, client):
client.execute_hms_action("03008070", HMSAction.REFRESH_NOZZLE)
cmds = self._published_commands(client)
assert cmds[0] == {"print": {"command": "refresh_nozzle", "sequence_id": "0"}}
def test_turn_off_fire_alarm_sends_buzzer_off(self, client):
client.execute_hms_action("03008044", HMSAction.TURN_OFF_FIRE_ALARM)
cmds = self._published_commands(client)
assert cmds[0]["print"]["command"] == "buzzer_ctrl"
assert cmds[0]["print"]["mode"] == 0
def test_stop_drying_sends_auto_stop_ams_dry(self, client):
client.execute_hms_action("07008017", HMSAction.STOP_DRYING)
cmds = self._published_commands(client)
assert cmds[0]["print"]["command"] == "auto_stop_ams_dry"
def test_disable_purification_sends_close_air_filt(self, client):
client.execute_hms_action("03008063", HMSAction.DISABLE_PURIFICATION)
cmds = self._published_commands(client)
assert cmds[0]["print"]["command"] == "close_air_filt"
@pytest.mark.parametrize(
"action",
[
HMSAction.CHECK_ASSISTANT,
HMSAction.JUMP_TO_LIVEVIEW,
HMSAction.OK_JUMP_RACK,
HMSAction.REMOVE_CLOSE_BTN,
HMSAction.LOAD_VIRTUAL_TRAY,
HMSAction.CANCLE,
HMSAction.DBL_CHECK_CANCEL,
],
)
def test_ui_only_actions_publish_nothing(self, client, action):
# These actions exist for parity with BambuStudio's modal but have no
# MQTT counterpart — the printer's own screen drives them.
assert client.execute_hms_action("03008070", action) is True
assert self._published_commands(client) == []
def test_every_publish_is_followed_by_pushall(self, client):
# The dispatcher pairs every command with a `pushing.pushall` echo so
# the state stream refreshes on the next tick. Regression guard.
client.execute_hms_action("03008070", HMSAction.RESUME_PRINTING)
payloads = [json.loads(c.args[1]) for c in client._client.publish.call_args_list]
assert any("pushing" in p for p in payloads)

View file

@ -333,6 +333,14 @@ export interface HMSError {
attr: number; // Attribute value for constructing wiki URL
module: number;
severity: number; // 1=fatal, 2=serious, 3=common, 4=info
actions?: string[]; // List of user-facing action keys (e.g. "CHECK_FILAMENT")
job_id?: string; // Optional job ID for actions that require it (e.g. "CHECK_ASSISTANT")
}
export interface HMSActionBody {
print_error: string; // HMS error code (e.g. "05000070")
action: string; // "HMS action to execute (e.g. 'resume_after_error')"
job_id: string | null; // Optional job ID for context (if applicable)
}
export interface AMSTray {
@ -3703,6 +3711,11 @@ export const api = {
// HMS Errors
clearHMSErrors: (printerId: number) =>
request<{ success: boolean; message: string }>(`/printers/${printerId}/hms/clear`, { method: 'POST' }),
executeHMSAction: (printerId: number, data: HMSActionBody) =>
request<{ success: boolean; message: string }>(`/printers/${printerId}/hms/execute-action`, {
method: 'POST',
body: JSON.stringify(data),
}),
// AMS Control
refreshAmsSlot: (printerId: number, amsId: number, slotId: number) =>

View file

@ -2,7 +2,7 @@
// Source: https://github.com/greghesp/ha-bambulab
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { X, AlertTriangle, AlertCircle, Info, ExternalLink, Loader2, Trash2 } from 'lucide-react';
import type { HMSError, Permission } from '../api/client';
import { api } from '../api/client';
@ -874,17 +874,17 @@ const ERROR_DESCRIPTIONS: Record<string, string> = {
'18FF_C00A': 'Please observe the nozzle of the right extruder. If the filament has been extruded, select \'Continue\'; if not, please push the filament forward slightly and then select \'Retry\'.',
};
function getSeverityInfo(severity: number): { label: string; color: string; bgColor: string; Icon: typeof AlertTriangle } {
function getSeverityInfo(severity: number): { label: string; color: string; bgColor: string; buttonHoverColor: string; Icon: typeof AlertTriangle } {
switch (severity) {
case 1:
return { label: 'Fatal', color: 'text-red-500', bgColor: 'bg-red-500/20', Icon: AlertTriangle };
return { label: 'Fatal', color: 'text-red-500', bgColor: 'bg-red-500/20', buttonHoverColor: 'bg-red-500/10', Icon: AlertTriangle };
case 2:
return { label: 'Serious', color: 'text-red-400', bgColor: 'bg-red-500/15', Icon: AlertTriangle };
return { label: 'Serious', color: 'text-red-400', bgColor: 'bg-red-500/15', buttonHoverColor: 'bg-red-500/10', Icon: AlertTriangle };
case 3:
return { label: 'Warning', color: 'text-orange-400', bgColor: 'bg-orange-500/20', Icon: AlertCircle };
return { label: 'Warning', color: 'text-orange-400', bgColor: 'bg-orange-500/20', buttonHoverColor: 'bg-orange-500/10', Icon: AlertCircle };
case 4:
default:
return { label: 'Info', color: 'text-blue-400', bgColor: 'bg-blue-500/20', Icon: Info };
return { label: 'Info', color: 'text-blue-400', bgColor: 'bg-blue-500/20', buttonHoverColor: 'bg-blue-500/10', Icon: Info };
}
}
@ -912,6 +912,7 @@ function getHMSHomeUrl(): string {
export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPermission }: HMSErrorModalProps) {
const { t } = useTranslation();
const { showToast } = useToast();
const queryClient = useQueryClient();
const clearMutation = useMutation({
mutationFn: () => api.clearHMSErrors(printerId),
@ -940,6 +941,30 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
// printerStatusMutation with optimistic update
const activateActionMutation = useMutation({
mutationFn: (data: {
action: string,
print_error: string,
job_id: string | null,
}) => api.executeHMSAction(printerId, {
action: data.action,
print_error: data.print_error,
job_id: data.job_id,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['printerStatus'] });
showToast(t('hmsErrors.actionSuccess', 'Action sent to printer'), 'success');
onClose();
},
onError: (error: Error) => {
showToast(
`${t('hmsErrors.actionFailed', 'Failed to send action')}: ${error.message}`,
'error',
);
},
});
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-bambu-dark-secondary rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col">
@ -967,7 +992,7 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
) : (
<div className="space-y-3">
{knownErrors.map((error, index) => {
const { label, color, bgColor, Icon } = getSeverityInfo(error.severity);
const { label, color, bgColor, buttonHoverColor, Icon } = getSeverityInfo(error.severity);
const codeNum = parseInt(error.code.replace('0x', ''), 16) || 0;
const shortCode = getShortCode(error.attr, codeNum);
const description = ERROR_DESCRIPTIONS[shortCode];
@ -989,6 +1014,25 @@ export function HMSErrorModal({ printerName, errors, onClose, printerId, hasPerm
</span>
</div>
<p className="text-sm text-bambu-gray mb-2">{description}</p>
{error.actions && error.actions.length > 0 && (
<div className="flex flex-wrap gap-2 my-2">
{error.actions.map((action) => (
<button
key={action}
onClick={() => {
activateActionMutation.mutate({
action,
print_error: shortCode.replace("_", ""),
job_id: error.job_id ?? null,
});
}}
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg ${bgColor} ${color} hover:${buttonHoverColor} transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0`}
>
{t(`hmsErrors.actions.${action}`, action)}
</button>
))}
</div>
)}
<a
href={hmsHomeUrl}
target="_blank"

View file

@ -2586,6 +2586,42 @@ export default {
clearErrors: 'Fehler löschen',
clearSuccess: 'HMS-Fehler gelöscht',
clearFailed: 'HMS-Fehler konnten nicht gelöscht werden',
actionSuccess: 'Aktion an Drucker gesendet',
actionFailed: 'Aktion konnte nicht gesendet werden',
actions: {
RESUME_PRINTING: 'Druck fortsetzen',
RESUME_PRINTING_DEFECTS: 'Fortsetzen (Mängel akzeptabel)',
RESUME_PRINTING_PROBELM_SOLVED: 'Fortsetzen (Problem gelöst)',
STOP_PRINTING: 'Druck stoppen',
CHECK_ASSISTANT: 'Assistent öffnen',
FILAMENT_EXTRUDED: 'Filament extrudiert, weiter',
RETRY_FILAMENT_EXTRUDED: 'Noch nicht extrudiert, erneut',
CONTINUE: 'Fertig, weiter',
LOAD_VIRTUAL_TRAY: 'Filament laden',
OK_BUTTON: 'OK',
FILAMENT_LOAD_RESUME: 'Filament geladen, fortsetzen',
JUMP_TO_LIVEVIEW: 'Live-Ansicht öffnen',
NO_REMINDER_NEXT_TIME: 'Nicht mehr erinnern',
REFRESH_NOZZLE: 'Erneut prüfen',
IGNORE_NO_REMINDER_NEXT_TIME: 'Ignorieren und nicht mehr erinnern',
IGNORE_RESUME: 'Ignorieren und fortsetzen',
PROBLEM_SOLVED_RESUME: 'Problem gelöst, fortsetzen',
TURN_OFF_FIRE_ALARM: 'Verstanden, Brandalarm ausschalten',
RETRY_PROBLEM_SOLVED: 'Erneut versuchen (Problem gelöst)',
CANCLE: 'Abbrechen',
STOP_DRYING: 'Trocknen stoppen',
PROCEED: 'Fortfahren',
OK_JUMP_RACK: 'OK',
ABORT: 'Abbrechen',
DISABLE_PURIFICATION: 'Luftreinigung für diesen Druck deaktivieren',
DONT_REMIND_NEXT_TIME: 'Nicht mehr erinnern',
DBL_CHECK_CANCEL: 'Abbrechen',
DBL_CHECK_DONE: 'Fertig',
DBL_CHECK_RETRY: 'Erneut versuchen',
DBL_CHECK_RESUME: 'Fortsetzen',
DBL_CHECK_OK: 'Bestätigen',
REMOVE_CLOSE_BTN: 'Schließen',
},
},
// MQTT Debug modal

View file

@ -2601,6 +2601,42 @@ export default {
clearErrors: 'Clear Errors',
clearSuccess: 'HMS errors cleared',
clearFailed: 'Failed to clear HMS errors',
actionSuccess: 'Action sent to printer',
actionFailed: 'Failed to send action',
actions: {
RESUME_PRINTING: "Resume Printing",
RESUME_PRINTING_DEFECTS: "Resume (defects acceptable)",
RESUME_PRINTING_PROBELM_SOLVED: "Resume (problem solved)",
STOP_PRINTING: "Stop Printing",
CHECK_ASSISTANT: "Check Assistant",
FILAMENT_EXTRUDED: "Filament Extruded, Continue",
RETRY_FILAMENT_EXTRUDED: "Not Extruded Yet, Retry",
CONTINUE: "Finished, Continue",
LOAD_VIRTUAL_TRAY: "Load Filament",
OK_BUTTON: "OK",
FILAMENT_LOAD_RESUME: "Filament Loaded, Resume",
JUMP_TO_LIVEVIEW: "View Liveview",
NO_REMINDER_NEXT_TIME: "No Reminder Next Time",
REFRESH_NOZZLE: "Recheck",
IGNORE_NO_REMINDER_NEXT_TIME: "Ignore. Don't Remind Next Time",
IGNORE_RESUME: "Ignore this and Resume",
PROBLEM_SOLVED_RESUME: "Problem Solved and Resume",
TURN_OFF_FIRE_ALARM: "Got it, Turn off the Fire Alarm.",
RETRY_PROBLEM_SOLVED: "Retry (problem solved)",
CANCLE: "Cancle",
STOP_DRYING: "Stop Drying",
PROCEED: "Proceed",
OK_JUMP_RACK: "OK",
ABORT: "Abort",
DISABLE_PURIFICATION: "Disable Purification for This Print",
DONT_REMIND_NEXT_TIME: "Don't Remind Me",
DBL_CHECK_CANCEL: "Cancel",
DBL_CHECK_DONE: "Done",
DBL_CHECK_RETRY: "Retry",
DBL_CHECK_RESUME: "Resume",
DBL_CHECK_OK: "Confirm",
REMOVE_CLOSE_BTN: "Close",
}
},
// MQTT Debug modal

View file

@ -2589,6 +2589,42 @@ export default {
clearErrors: 'Borrar errores',
clearSuccess: 'Errores HMS borrados',
clearFailed: 'Error al borrar los errores HMS',
actionSuccess: 'Acción enviada a la impresora',
actionFailed: 'No se pudo enviar la acción',
actions: {
RESUME_PRINTING: 'Reanudar impresión',
RESUME_PRINTING_DEFECTS: 'Reanudar (defectos aceptables)',
RESUME_PRINTING_PROBELM_SOLVED: 'Reanudar (problema resuelto)',
STOP_PRINTING: 'Detener impresión',
CHECK_ASSISTANT: 'Ver asistente',
FILAMENT_EXTRUDED: 'Filamento extruido, continuar',
RETRY_FILAMENT_EXTRUDED: 'Aún no extruido, reintentar',
CONTINUE: 'Finalizado, continuar',
LOAD_VIRTUAL_TRAY: 'Cargar filamento',
OK_BUTTON: 'OK',
FILAMENT_LOAD_RESUME: 'Filamento cargado, reanudar',
JUMP_TO_LIVEVIEW: 'Ver en vivo',
NO_REMINDER_NEXT_TIME: 'No recordar la próxima vez',
REFRESH_NOZZLE: 'Volver a comprobar',
IGNORE_NO_REMINDER_NEXT_TIME: 'Ignorar y no recordar',
IGNORE_RESUME: 'Ignorar y reanudar',
PROBLEM_SOLVED_RESUME: 'Problema resuelto, reanudar',
TURN_OFF_FIRE_ALARM: 'Entendido, apagar alarma de incendio',
RETRY_PROBLEM_SOLVED: 'Reintentar (problema resuelto)',
CANCLE: 'Cancelar',
STOP_DRYING: 'Detener secado',
PROCEED: 'Continuar',
OK_JUMP_RACK: 'OK',
ABORT: 'Cancelar',
DISABLE_PURIFICATION: 'Desactivar purificación para esta impresión',
DONT_REMIND_NEXT_TIME: 'No recordarme',
DBL_CHECK_CANCEL: 'Cancelar',
DBL_CHECK_DONE: 'Hecho',
DBL_CHECK_RETRY: 'Reintentar',
DBL_CHECK_RESUME: 'Reanudar',
DBL_CHECK_OK: 'Confirmar',
REMOVE_CLOSE_BTN: 'Cerrar',
},
},
// MQTT Debug modal

View file

@ -2575,6 +2575,42 @@ export default {
clearErrors: 'Effacer les erreurs',
clearSuccess: 'Erreurs HMS effacées',
clearFailed: 'Échec de l\'effacement des erreurs HMS',
actionSuccess: 'Action envoyée à l\'imprimante',
actionFailed: 'Échec de l\'envoi de l\'action',
actions: {
RESUME_PRINTING: 'Reprendre l\'impression',
RESUME_PRINTING_DEFECTS: 'Reprendre (défauts acceptables)',
RESUME_PRINTING_PROBELM_SOLVED: 'Reprendre (problème résolu)',
STOP_PRINTING: 'Arrêter l\'impression',
CHECK_ASSISTANT: 'Ouvrir l\'assistant',
FILAMENT_EXTRUDED: 'Filament extrudé, continuer',
RETRY_FILAMENT_EXTRUDED: 'Pas encore extrudé, réessayer',
CONTINUE: 'Terminé, continuer',
LOAD_VIRTUAL_TRAY: 'Charger le filament',
OK_BUTTON: 'OK',
FILAMENT_LOAD_RESUME: 'Filament chargé, reprendre',
JUMP_TO_LIVEVIEW: 'Voir en direct',
NO_REMINDER_NEXT_TIME: 'Ne plus rappeler la prochaine fois',
REFRESH_NOZZLE: 'Revérifier',
IGNORE_NO_REMINDER_NEXT_TIME: 'Ignorer et ne plus rappeler',
IGNORE_RESUME: 'Ignorer et reprendre',
PROBLEM_SOLVED_RESUME: 'Problème résolu, reprendre',
TURN_OFF_FIRE_ALARM: 'Compris, désactiver l\'alarme incendie',
RETRY_PROBLEM_SOLVED: 'Réessayer (problème résolu)',
CANCLE: 'Annuler',
STOP_DRYING: 'Arrêter le séchage',
PROCEED: 'Continuer',
OK_JUMP_RACK: 'OK',
ABORT: 'Annuler',
DISABLE_PURIFICATION: 'Désactiver la purification pour cette impression',
DONT_REMIND_NEXT_TIME: 'Ne plus me rappeler',
DBL_CHECK_CANCEL: 'Annuler',
DBL_CHECK_DONE: 'Terminé',
DBL_CHECK_RETRY: 'Réessayer',
DBL_CHECK_RESUME: 'Reprendre',
DBL_CHECK_OK: 'Confirmer',
REMOVE_CLOSE_BTN: 'Fermer',
},
},
// MQTT Debug modal

View file

@ -2574,6 +2574,42 @@ export default {
clearErrors: 'Cancella errori',
clearSuccess: 'Errori HMS cancellati',
clearFailed: 'Impossibile cancellare gli errori HMS',
actionSuccess: 'Azione inviata alla stampante',
actionFailed: 'Impossibile inviare l\'azione',
actions: {
RESUME_PRINTING: 'Riprendi stampa',
RESUME_PRINTING_DEFECTS: 'Riprendi (difetti accettabili)',
RESUME_PRINTING_PROBELM_SOLVED: 'Riprendi (problema risolto)',
STOP_PRINTING: 'Ferma stampa',
CHECK_ASSISTANT: 'Apri assistente',
FILAMENT_EXTRUDED: 'Filamento estruso, continua',
RETRY_FILAMENT_EXTRUDED: 'Non ancora estruso, riprova',
CONTINUE: 'Completato, continua',
LOAD_VIRTUAL_TRAY: 'Carica filamento',
OK_BUTTON: 'OK',
FILAMENT_LOAD_RESUME: 'Filamento caricato, riprendi',
JUMP_TO_LIVEVIEW: 'Visualizza in diretta',
NO_REMINDER_NEXT_TIME: 'Non ricordare la prossima volta',
REFRESH_NOZZLE: 'Ricontrolla',
IGNORE_NO_REMINDER_NEXT_TIME: 'Ignora e non ricordare',
IGNORE_RESUME: 'Ignora e riprendi',
PROBLEM_SOLVED_RESUME: 'Problema risolto, riprendi',
TURN_OFF_FIRE_ALARM: 'Capito, spegni allarme antincendio',
RETRY_PROBLEM_SOLVED: 'Riprova (problema risolto)',
CANCLE: 'Annulla',
STOP_DRYING: 'Ferma essiccazione',
PROCEED: 'Procedi',
OK_JUMP_RACK: 'OK',
ABORT: 'Annulla',
DISABLE_PURIFICATION: 'Disattiva purificazione per questa stampa',
DONT_REMIND_NEXT_TIME: 'Non ricordarmelo',
DBL_CHECK_CANCEL: 'Annulla',
DBL_CHECK_DONE: 'Fatto',
DBL_CHECK_RETRY: 'Riprova',
DBL_CHECK_RESUME: 'Riprendi',
DBL_CHECK_OK: 'Conferma',
REMOVE_CLOSE_BTN: 'Chiudi',
},
},
// MQTT Debug modal

View file

@ -2586,6 +2586,42 @@ export default {
clearErrors: 'エラーをクリア',
clearSuccess: 'HMSエラーをクリアしました',
clearFailed: 'HMSエラーのクリアに失敗しました',
actionSuccess: 'アクションをプリンターに送信しました',
actionFailed: 'アクションの送信に失敗しました',
actions: {
RESUME_PRINTING: '印刷を再開',
RESUME_PRINTING_DEFECTS: '再開(不具合を許容)',
RESUME_PRINTING_PROBELM_SOLVED: '再開(問題解決)',
STOP_PRINTING: '印刷を停止',
CHECK_ASSISTANT: 'アシスタントを開く',
FILAMENT_EXTRUDED: 'フィラメント排出済み、続行',
RETRY_FILAMENT_EXTRUDED: 'まだ排出されていない、再試行',
CONTINUE: '完了、続行',
LOAD_VIRTUAL_TRAY: 'フィラメントを装填',
OK_BUTTON: 'OK',
FILAMENT_LOAD_RESUME: 'フィラメント装填完了、再開',
JUMP_TO_LIVEVIEW: 'ライブビューを表示',
NO_REMINDER_NEXT_TIME: '次回は通知しない',
REFRESH_NOZZLE: '再確認',
IGNORE_NO_REMINDER_NEXT_TIME: '無視して次回は通知しない',
IGNORE_RESUME: '無視して再開',
PROBLEM_SOLVED_RESUME: '問題解決、再開',
TURN_OFF_FIRE_ALARM: '了解、火災警報をオフ',
RETRY_PROBLEM_SOLVED: '再試行(問題解決)',
CANCLE: 'キャンセル',
STOP_DRYING: '乾燥を停止',
PROCEED: '続行',
OK_JUMP_RACK: 'OK',
ABORT: '中止',
DISABLE_PURIFICATION: 'この印刷では空気清浄を無効化',
DONT_REMIND_NEXT_TIME: '今後表示しない',
DBL_CHECK_CANCEL: 'キャンセル',
DBL_CHECK_DONE: '完了',
DBL_CHECK_RETRY: '再試行',
DBL_CHECK_RESUME: '再開',
DBL_CHECK_OK: '確認',
REMOVE_CLOSE_BTN: '閉じる',
},
},
// MQTT Debug modal

View file

@ -2437,7 +2437,43 @@ export default {
clearInstructions: '오류를 해제하려면 프린터에서 오류를 지우세요.',
clearErrors: '오류 지우기',
clearSuccess: 'HMS 오류가 지워졌습니다',
clearFailed: 'HMS 오류 지우기 실패'
clearFailed: 'HMS 오류 지우기 실패',
actionSuccess: '프린터에 작업을 전송함',
actionFailed: '작업 전송 실패',
actions: {
RESUME_PRINTING: '인쇄 재개',
RESUME_PRINTING_DEFECTS: '재개 (결함 허용)',
RESUME_PRINTING_PROBELM_SOLVED: '재개 (문제 해결됨)',
STOP_PRINTING: '인쇄 중지',
CHECK_ASSISTANT: '도우미 보기',
FILAMENT_EXTRUDED: '필라멘트 압출됨, 계속',
RETRY_FILAMENT_EXTRUDED: '아직 압출되지 않음, 다시 시도',
CONTINUE: '완료, 계속',
LOAD_VIRTUAL_TRAY: '필라멘트 로드',
OK_BUTTON: '확인',
FILAMENT_LOAD_RESUME: '필라멘트 로드됨, 재개',
JUMP_TO_LIVEVIEW: '실시간 보기',
NO_REMINDER_NEXT_TIME: '다음에 알리지 않음',
REFRESH_NOZZLE: '다시 확인',
IGNORE_NO_REMINDER_NEXT_TIME: '무시 및 다시 알리지 않음',
IGNORE_RESUME: '무시하고 재개',
PROBLEM_SOLVED_RESUME: '문제 해결됨, 재개',
TURN_OFF_FIRE_ALARM: '확인, 화재 경보 끄기',
RETRY_PROBLEM_SOLVED: '다시 시도 (문제 해결됨)',
CANCLE: '취소',
STOP_DRYING: '건조 중지',
PROCEED: '계속',
OK_JUMP_RACK: '확인',
ABORT: '중단',
DISABLE_PURIFICATION: '이 인쇄에 대해 공기 정화 비활성화',
DONT_REMIND_NEXT_TIME: '알리지 않음',
DBL_CHECK_CANCEL: '취소',
DBL_CHECK_DONE: '완료',
DBL_CHECK_RETRY: '다시 시도',
DBL_CHECK_RESUME: '재개',
DBL_CHECK_OK: '확인',
REMOVE_CLOSE_BTN: '닫기',
},
},
mqttDebug: {
title: 'MQTT 디버그 로그',

View file

@ -2574,6 +2574,42 @@ export default {
clearErrors: 'Limpar Erros',
clearSuccess: 'Erros HMS limpos',
clearFailed: 'Falha ao limpar erros HMS',
actionSuccess: 'Ação enviada à impressora',
actionFailed: 'Falha ao enviar ação',
actions: {
RESUME_PRINTING: 'Retomar impressão',
RESUME_PRINTING_DEFECTS: 'Retomar (defeitos aceitáveis)',
RESUME_PRINTING_PROBELM_SOLVED: 'Retomar (problema resolvido)',
STOP_PRINTING: 'Parar impressão',
CHECK_ASSISTANT: 'Ver assistente',
FILAMENT_EXTRUDED: 'Filamento extrudado, continuar',
RETRY_FILAMENT_EXTRUDED: 'Ainda não extrudado, tentar novamente',
CONTINUE: 'Finalizado, continuar',
LOAD_VIRTUAL_TRAY: 'Carregar filamento',
OK_BUTTON: 'OK',
FILAMENT_LOAD_RESUME: 'Filamento carregado, retomar',
JUMP_TO_LIVEVIEW: 'Ver ao vivo',
NO_REMINDER_NEXT_TIME: 'Não lembrar da próxima vez',
REFRESH_NOZZLE: 'Verificar novamente',
IGNORE_NO_REMINDER_NEXT_TIME: 'Ignorar e não lembrar',
IGNORE_RESUME: 'Ignorar e retomar',
PROBLEM_SOLVED_RESUME: 'Problema resolvido, retomar',
TURN_OFF_FIRE_ALARM: 'Entendido, desligar alarme de incêndio',
RETRY_PROBLEM_SOLVED: 'Tentar novamente (problema resolvido)',
CANCLE: 'Cancelar',
STOP_DRYING: 'Parar secagem',
PROCEED: 'Prosseguir',
OK_JUMP_RACK: 'OK',
ABORT: 'Abortar',
DISABLE_PURIFICATION: 'Desativar purificação para esta impressão',
DONT_REMIND_NEXT_TIME: 'Não me lembrar',
DBL_CHECK_CANCEL: 'Cancelar',
DBL_CHECK_DONE: 'Concluído',
DBL_CHECK_RETRY: 'Tentar novamente',
DBL_CHECK_RESUME: 'Retomar',
DBL_CHECK_OK: 'Confirmar',
REMOVE_CLOSE_BTN: 'Fechar',
},
},
// MQTT Debug modal

View file

@ -2590,6 +2590,42 @@ export default {
clearErrors: 'Hataları Temizle',
clearSuccess: 'HMS hataları temizlendi',
clearFailed: 'HMS hataları temizlenemedi',
actionSuccess: 'Eylem yazıcıya gönderildi',
actionFailed: 'Eylem gönderilemedi',
actions: {
RESUME_PRINTING: 'Baskıyı sürdür',
RESUME_PRINTING_DEFECTS: 'Sürdür (kusurlar kabul edilebilir)',
RESUME_PRINTING_PROBELM_SOLVED: 'Sürdür (sorun çözüldü)',
STOP_PRINTING: 'Baskıyı durdur',
CHECK_ASSISTANT: 'Asistanı aç',
FILAMENT_EXTRUDED: 'Filament ekstrude edildi, devam et',
RETRY_FILAMENT_EXTRUDED: 'Henüz ekstrude edilmedi, tekrar dene',
CONTINUE: 'Tamamlandı, devam et',
LOAD_VIRTUAL_TRAY: 'Filament yükle',
OK_BUTTON: 'Tamam',
FILAMENT_LOAD_RESUME: 'Filament yüklendi, sürdür',
JUMP_TO_LIVEVIEW: 'Canlı görünümü aç',
NO_REMINDER_NEXT_TIME: 'Bir daha hatırlatma',
REFRESH_NOZZLE: 'Tekrar kontrol et',
IGNORE_NO_REMINDER_NEXT_TIME: 'Yok say ve bir daha hatırlatma',
IGNORE_RESUME: 'Yok say ve sürdür',
PROBLEM_SOLVED_RESUME: 'Sorun çözüldü, sürdür',
TURN_OFF_FIRE_ALARM: 'Anlaşıldı, yangın alarmını kapat',
RETRY_PROBLEM_SOLVED: 'Tekrar dene (sorun çözüldü)',
CANCLE: 'İptal',
STOP_DRYING: 'Kurutmayı durdur',
PROCEED: 'Devam et',
OK_JUMP_RACK: 'Tamam',
ABORT: 'İptal',
DISABLE_PURIFICATION: 'Bu baskı için arıtmayı devre dışı bırak',
DONT_REMIND_NEXT_TIME: 'Beni hatırlatma',
DBL_CHECK_CANCEL: 'İptal',
DBL_CHECK_DONE: 'Tamam',
DBL_CHECK_RETRY: 'Tekrar dene',
DBL_CHECK_RESUME: 'Sürdür',
DBL_CHECK_OK: 'Onayla',
REMOVE_CLOSE_BTN: 'Kapat',
},
},
// MQTT Hata Ayıklama modali

View file

@ -2574,6 +2574,42 @@ export default {
clearErrors: '清除错误',
clearSuccess: 'HMS 错误已清除',
clearFailed: '清除 HMS 错误失败',
actionSuccess: '已向打印机发送操作',
actionFailed: '操作发送失败',
actions: {
RESUME_PRINTING: '恢复打印',
RESUME_PRINTING_DEFECTS: '恢复 (缺陷可接受)',
RESUME_PRINTING_PROBELM_SOLVED: '恢复 (问题已解决)',
STOP_PRINTING: '停止打印',
CHECK_ASSISTANT: '查看助手',
FILAMENT_EXTRUDED: '已挤出耗材,继续',
RETRY_FILAMENT_EXTRUDED: '尚未挤出,重试',
CONTINUE: '已完成,继续',
LOAD_VIRTUAL_TRAY: '加载耗材',
OK_BUTTON: '确定',
FILAMENT_LOAD_RESUME: '耗材已加载,恢复',
JUMP_TO_LIVEVIEW: '查看实时画面',
NO_REMINDER_NEXT_TIME: '下次不再提醒',
REFRESH_NOZZLE: '重新检查',
IGNORE_NO_REMINDER_NEXT_TIME: '忽略,下次不再提醒',
IGNORE_RESUME: '忽略并恢复',
PROBLEM_SOLVED_RESUME: '问题已解决,恢复',
TURN_OFF_FIRE_ALARM: '知道了,关闭火警',
RETRY_PROBLEM_SOLVED: '重试 (问题已解决)',
CANCLE: '取消',
STOP_DRYING: '停止干燥',
PROCEED: '继续',
OK_JUMP_RACK: '确定',
ABORT: '终止',
DISABLE_PURIFICATION: '本次打印禁用空气净化',
DONT_REMIND_NEXT_TIME: '不再提醒',
DBL_CHECK_CANCEL: '取消',
DBL_CHECK_DONE: '完成',
DBL_CHECK_RETRY: '重试',
DBL_CHECK_RESUME: '恢复',
DBL_CHECK_OK: '确认',
REMOVE_CLOSE_BTN: '关闭',
},
},
// MQTT Debug modal

View file

@ -2574,6 +2574,42 @@ export default {
clearErrors: '清除錯誤',
clearSuccess: 'HMS 錯誤已清除',
clearFailed: '清除 HMS 錯誤失敗',
actionSuccess: '已向印表機傳送動作',
actionFailed: '動作傳送失敗',
actions: {
RESUME_PRINTING: '恢復列印',
RESUME_PRINTING_DEFECTS: '恢復 (瑕疵可接受)',
RESUME_PRINTING_PROBELM_SOLVED: '恢復 (問題已解決)',
STOP_PRINTING: '停止列印',
CHECK_ASSISTANT: '檢視助理',
FILAMENT_EXTRUDED: '已擠出耗材,繼續',
RETRY_FILAMENT_EXTRUDED: '尚未擠出,重試',
CONTINUE: '已完成,繼續',
LOAD_VIRTUAL_TRAY: '載入耗材',
OK_BUTTON: '確定',
FILAMENT_LOAD_RESUME: '耗材已載入,恢復',
JUMP_TO_LIVEVIEW: '檢視即時畫面',
NO_REMINDER_NEXT_TIME: '下次不再提醒',
REFRESH_NOZZLE: '重新檢查',
IGNORE_NO_REMINDER_NEXT_TIME: '忽略,下次不再提醒',
IGNORE_RESUME: '忽略並恢復',
PROBLEM_SOLVED_RESUME: '問題已解決,恢復',
TURN_OFF_FIRE_ALARM: '知道了,關閉火警',
RETRY_PROBLEM_SOLVED: '重試 (問題已解決)',
CANCLE: '取消',
STOP_DRYING: '停止乾燥',
PROCEED: '繼續',
OK_JUMP_RACK: '確定',
ABORT: '中止',
DISABLE_PURIFICATION: '本次列印停用空氣淨化',
DONT_REMIND_NEXT_TIME: '不再提醒',
DBL_CHECK_CANCEL: '取消',
DBL_CHECK_DONE: '完成',
DBL_CHECK_RETRY: '重試',
DBL_CHECK_RESUME: '恢復',
DBL_CHECK_OK: '確認',
REMOVE_CLOSE_BTN: '關閉',
},
},
// MQTT Debug modal

View file

@ -0,0 +1,81 @@
import asyncio
import json
import requests
HMS_ACTIONS_JSON_PATH = "backend/app/data/hms_actions.json"
HMS_REQUEST_URL = "https://e.bambulab.com/hms/GetActionImage.php"
HMS_ID_TO_ACTION_NAME_MAP: dict[int, str] = {
2: "RESUME_PRINTING",
3: "RESUME_PRINTING_DEFECTS",
4: "RESUME_PRINTING_PROBELM_SOLVED",
5: "STOP_PRINTING",
6: "CHECK_ASSISTANT",
7: "FILAMENT_EXTRUDED",
8: "RETRY_FILAMENT_EXTRUDED",
9: "CONTINUE",
10: "LOAD_VIRTUAL_TRAY",
11: "OK_BUTTON",
12: "FILAMENT_LOAD_RESUME",
13: "JUMP_TO_LIVEVIEW",
23: "NO_REMINDER_NEXT_TIME",
24: "REFRESH_NOZZLE",
25: "IGNORE_NO_REMINDER_NEXT_TIME",
27: "IGNORE_RESUME",
28: "PROBLEM_SOLVED_RESUME",
29: "TURN_OFF_FIRE_ALARM",
34: "RETRY_PROBLEM_SOLVED",
35: "STOP_DRYING",
37: "CANCLE", # Note: "CANCLE" is intentionally misspelled in the BambuStudio source code
39: "REMOVE_CLOSE_BTN",
41: "PROCEED",
49: "OK_JUMP_RACK",
51: "ABORT",
54: "DISABLE_PURIFICATION",
57: "DONT_REMIND_NEXT_TIME",
10000: "DBL_CHECK_CANCEL",
10001: "DBL_CHECK_DONE",
10002: "DBL_CHECK_RETRY",
10003: "DBL_CHECK_RESUME",
10004: "DBL_CHECK_OK",
}
async def main():
error_to_action_map: dict[str, list[str]] = {}
# get the json response from the url
response = requests.get(HMS_REQUEST_URL)
if response.status_code == 200:
data = response.json()
ready_data = {}
for item in data["data"]:
# error_code = item["ecode"][:4] + "_" + item["ecode"][4:]
mapped_actions = []
for hms_id in item["actions"]:
if hms_id not in HMS_ID_TO_ACTION_NAME_MAP:
print(f"Warning: Unrecognized HMS action ID {hms_id} for error code {item['ecode']}")
else:
mapped_actions.append(HMS_ID_TO_ACTION_NAME_MAP.get(hms_id, f"UNKNOWN_ACTION_{hms_id}"))
print(f"ecode: {item['ecode']}, actions: {mapped_actions}, device: {item['device']}")
if item["device"] not in ready_data:
ready_data[item["device"]] = {}
ready_data[item["device"]][item["ecode"]] = mapped_actions
# ready_data.append(
# {
# "ecode": item["ecode"],
# "actions": mapped_actions,
# "device": item["device"],
# }
# )
with open(HMS_ACTIONS_JSON_PATH, "w") as f:
json.dump(ready_data, f, indent=4)
else:
print("Failed to fetch data")
print(error_to_action_map)
# autogenerate
if __name__ == "__main__":
asyncio.run(main())

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-BSFuaWEQ.js"></script>
<script type="module" crossorigin src="/assets/index-B8n3e67k.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BKwIZ5yr.css">
</head>
<body>