Browse Source

fix(printer): bed-jog "Home Z" could crash bed into toolhead on H2C/H2D/H2S/X1 (#1052)

  Critical safety fix. The bed-jog dialog's "Home Z" button sent a bare
  `G28 Z` over gcode_line. On Bambu printers where the Z endstop is at
  the top (bed moves UP into it — H2C, H2D, H2S, X1 family), `G28 Z`
  skips the toolhead-park step that a full `G28` runs first, so the bed
  rises at full speed with nothing getting out of the way. The reporter
  only escaped damage because the toolhead happened to be parked on the
  purge chute.

  The /printers/{id}/home-axes endpoint and BambuClient.home_axes() now
  always send bare `G28` regardless of the axes argument, triggering the
  firmware's safe multi-step routine (park toolhead → home XY → home Z).
  The axes argument is kept for API compat but ignored; invalid values
  still return 400.

  Frontend retitles the button "Auto Home" and updates the dialog copy
  in all 7 locales so users aren't surprised when X/Y motion happens
  before Z. Parameterized regression test asserts z/xy/all all produce
  bare G28.
maziggy 1 month ago
parent
commit
7026a6de77

File diff suppressed because it is too large
+ 1 - 0
CHANGELOG.md


+ 22 - 11
backend/app/api/routes/printers.py

@@ -2497,19 +2497,30 @@ async def bed_jog(
 @router.post("/{printer_id}/home-axes")
 @router.post("/{printer_id}/home-axes")
 async def home_axes(
 async def home_axes(
     printer_id: int,
     printer_id: int,
-    axes: str = Query("z", description="Axes to home: 'z', 'xy', or 'all'"),
+    axes: str = Query(
+        "all",
+        description="Legacy; accepted values are 'z' | 'xy' | 'all'. Always runs the printer's full auto-home sequence — see below.",
+    ),
     _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
     _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_CONTROL),
     db: AsyncSession = Depends(get_db),
     db: AsyncSession = Depends(get_db),
 ):
 ):
-    """Home one or more axes via G28."""
+    """Run the printer's full auto-home sequence via bare `G28`.
+
+    Bambu printers (H2C / H2D / H2S / X1 family) home the Z axis by moving
+    the BED UP toward an endstop at the top of travel. If the toolhead is
+    not already parked out of the way, a bare `G28 Z` will crash the bed
+    into the toolhead — #1052 reported exactly that on H2C: the bed rose
+    without stopping at a safe height because `G28 Z` skipped the
+    toolhead-park step that a full `G28` runs first.
+
+    The endpoint therefore ignores the `axes` argument and always sends a
+    bare `G28`, which the firmware expands into a safe multi-step sequence
+    (park toolhead → home XY → home Z). The argument is kept only for
+    backward-compat with existing clients; sending an invalid value still
+    returns 400 so typos surface instead of silently proceeding.
+    """
     axes = axes.lower()
     axes = axes.lower()
-    if axes == "z":
-        gcode = "G28 Z"
-    elif axes == "xy":
-        gcode = "G28 X Y"
-    elif axes == "all":
-        gcode = "G28"
-    else:
+    if axes not in ("z", "xy", "all"):
         raise HTTPException(400, "axes must be 'z', 'xy', or 'all'")
         raise HTTPException(400, "axes must be 'z', 'xy', or 'all'")
 
 
     result = await db.execute(select(Printer).where(Printer.id == printer_id))
     result = await db.execute(select(Printer).where(Printer.id == printer_id))
@@ -2521,10 +2532,10 @@ async def home_axes(
     if not client:
     if not client:
         raise HTTPException(400, "Printer not connected")
         raise HTTPException(400, "Printer not connected")
 
 
-    if not client.send_gcode(gcode):
+    if not client.send_gcode("G28"):
         raise HTTPException(500, "Failed to send home command")
         raise HTTPException(500, "Failed to send home command")
 
 
-    return {"success": True, "message": f"Home {axes} command sent"}
+    return {"success": True, "message": "Full auto-home sequence sent"}
 
 
 
 
 @router.post("/{printer_id}/hms/clear")
 @router.post("/{printer_id}/hms/clear")

+ 7 - 9
backend/app/services/bambu_mqtt.py

@@ -4079,17 +4079,15 @@ class BambuMQTTClient:
         return True
         return True
 
 
     def home_axes(self, axes: str = "XYZ") -> bool:
     def home_axes(self, axes: str = "XYZ") -> bool:
-        """Home the specified axes.
+        """Run the printer's full auto-home sequence.
 
 
-        Args:
-            axes: Axes to home (e.g., "XYZ", "X", "XY", "Z")
-
-        Returns:
-            True if command was sent, False otherwise
+        The ``axes`` argument is ignored: a bare ``G28`` is always sent so
+        Bambu firmware runs its safe multi-step routine (park toolhead →
+        home XY → home Z). Partial-axis variants like ``G28 Z`` skip the
+        toolhead-park step and can crash the bed into the toolhead on H2C
+        / H2D / H2S / X1 where Z-home moves the bed UP — see #1052.
         """
         """
-        # G28 homes all axes, G28 X Y Z homes specific axes
-        axes_param = " ".join(axes.upper())
-        return self.send_gcode(f"G28 {axes_param}")
+        return self.send_gcode("G28")
 
 
     def move_axis(self, axis: str, distance: float, speed: int = 3000) -> bool:
     def move_axis(self, axis: str, distance: float, speed: int = 3000) -> bool:
         """Move an axis by a relative distance.
         """Move an axis by a relative distance.

+ 6 - 6
backend/tests/unit/test_bed_jog.py

@@ -95,11 +95,11 @@ class TestHomeAxesAPI:
         assert response.status_code == 400
         assert response.status_code == 400
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
-    @pytest.mark.parametrize(
-        "axes,expected",
-        [("z", "G28 Z"), ("xy", "G28 X Y"), ("all", "G28")],
-    )
-    async def test_home_axes_success(self, async_client: AsyncClient, printer_factory, axes, expected):
+    @pytest.mark.parametrize("axes", ["z", "xy", "all"])
+    async def test_home_axes_always_runs_full_home(self, async_client: AsyncClient, printer_factory, axes):
+        # Regression for #1052: regardless of the axes argument, the endpoint must send a bare
+        # `G28` so the printer's safe auto-home sequence (toolhead park → XY home → Z home) runs.
+        # Sending `G28 Z` alone on H2C/H2D/H2S/X1 can crash the bed into the toolhead.
         printer = await printer_factory(name="P1")
         printer = await printer_factory(name="P1")
         mock_client = MagicMock()
         mock_client = MagicMock()
         mock_client.send_gcode.return_value = True
         mock_client.send_gcode.return_value = True
@@ -107,7 +107,7 @@ class TestHomeAxesAPI:
             mock_pm.get_client.return_value = mock_client
             mock_pm.get_client.return_value = mock_client
             response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes={axes}")
             response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes={axes}")
             assert response.status_code == 200
             assert response.status_code == 200
-            mock_client.send_gcode.assert_called_once_with(expected)
+            mock_client.send_gcode.assert_called_once_with("G28")
 
 
     @pytest.mark.asyncio
     @pytest.mark.asyncio
     async def test_home_axes_not_connected(self, async_client: AsyncClient, printer_factory):
     async def test_home_axes_not_connected(self, async_client: AsyncClient, printer_factory):

+ 3 - 3
frontend/src/i18n/locales/de.ts

@@ -341,10 +341,10 @@ export default {
       down: 'Platte runter',
       down: 'Platte runter',
       disabledWhilePrinting: 'Während des Drucks deaktiviert',
       disabledWhilePrinting: 'Während des Drucks deaktiviert',
       notHomedTitle: 'Drucker ist nicht referenziert',
       notHomedTitle: 'Drucker ist nicht referenziert',
-      notHomedMessage: 'Die Z-Achse wurde seit dem letzten Druck nicht referenziert. Referenzieren Sie Z zuerst für eine sichere Positionierung oder bewegen Sie trotzdem — die Software-Endschalter werden dabei umgangen.',
-      homeZ: 'Z referenzieren',
+      notHomedMessage: 'Der Drucker wurde seit dem letzten Druck nicht referenziert. Führen Sie zuerst die automatische Referenzfahrt aus (parkt den Werkzeugkopf und referenziert dann X, Y und Z) oder bewegen Sie trotzdem — die Software-Endschalter werden dabei umgangen.',
+      homeZ: 'Automatische Referenzfahrt',
       moveAnyway: 'Trotzdem bewegen',
       moveAnyway: 'Trotzdem bewegen',
-      homingStarted: 'Z-Achse wird referenziert…',
+      homingStarted: 'Drucker wird automatisch referenziert…',
     },
     },
     // Permissions
     // Permissions
     permission: {
     permission: {

+ 3 - 3
frontend/src/i18n/locales/en.ts

@@ -341,10 +341,10 @@ export default {
       down: 'Move plate down',
       down: 'Move plate down',
       disabledWhilePrinting: 'Disabled while printing',
       disabledWhilePrinting: 'Disabled while printing',
       notHomedTitle: 'Printer is not homed',
       notHomedTitle: 'Printer is not homed',
-      notHomedMessage: 'The Z axis has not been homed since the last print. Home Z first for safe positioning, or move anyway — soft endstops will be bypassed.',
-      homeZ: 'Home Z',
+      notHomedMessage: 'The printer has not been homed since the last print. Run auto-home first for safe positioning (parks the toolhead, then homes X, Y, and Z), or move anyway — soft endstops will be bypassed.',
+      homeZ: 'Auto Home',
       moveAnyway: 'Move anyway',
       moveAnyway: 'Move anyway',
-      homingStarted: 'Homing Z axis…',
+      homingStarted: 'Auto-homing printer…',
     },
     },
     // Permissions
     // Permissions
     permission: {
     permission: {

+ 3 - 3
frontend/src/i18n/locales/fr.ts

@@ -341,10 +341,10 @@ export default {
       down: 'Descendre le plateau',
       down: 'Descendre le plateau',
       disabledWhilePrinting: 'Désactivé pendant l\'impression',
       disabledWhilePrinting: 'Désactivé pendant l\'impression',
       notHomedTitle: 'Imprimante non référencée',
       notHomedTitle: 'Imprimante non référencée',
-      notHomedMessage: 'L\'axe Z n\'a pas été référencé depuis la dernière impression. Référencez Z d\'abord pour un positionnement sûr, ou déplacez quand même — les butées logicielles seront ignorées.',
-      homeZ: 'Référencer Z',
+      notHomedMessage: 'L\'imprimante n\'a pas été référencée depuis la dernière impression. Lancez la référence automatique d\'abord pour un positionnement sûr (parque la tête d\'outil, puis référence X, Y et Z), ou déplacez quand même — les butées logicielles seront ignorées.',
+      homeZ: 'Référence automatique',
       moveAnyway: 'Déplacer quand même',
       moveAnyway: 'Déplacer quand même',
-      homingStarted: 'Référencement de l\'axe Z…',
+      homingStarted: 'Référencement automatique en cours…',
     },
     },
     // Permissions
     // Permissions
     permission: {
     permission: {

+ 3 - 3
frontend/src/i18n/locales/it.ts

@@ -341,10 +341,10 @@ export default {
       down: 'Sposta piano giù',
       down: 'Sposta piano giù',
       disabledWhilePrinting: 'Disabilitato durante la stampa',
       disabledWhilePrinting: 'Disabilitato durante la stampa',
       notHomedTitle: 'Stampante non azzerata',
       notHomedTitle: 'Stampante non azzerata',
-      notHomedMessage: 'L\'asse Z non è stato azzerato dall\'ultima stampa. Azzera Z prima per un posizionamento sicuro, oppure muovi comunque — i finecorsa software verranno ignorati.',
-      homeZ: 'Azzera Z',
+      notHomedMessage: 'La stampante non è stata azzerata dall\'ultima stampa. Esegui prima l\'azzeramento automatico per un posizionamento sicuro (parcheggia la testa di stampa, poi azzera X, Y e Z), oppure muovi comunque — i finecorsa software verranno ignorati.',
+      homeZ: 'Azzeramento automatico',
       moveAnyway: 'Muovi comunque',
       moveAnyway: 'Muovi comunque',
-      homingStarted: 'Azzeramento asse Z…',
+      homingStarted: 'Azzeramento automatico in corso…',
     },
     },
     // Permissions
     // Permissions
     permission: {
     permission: {

+ 3 - 3
frontend/src/i18n/locales/ja.ts

@@ -340,10 +340,10 @@ export default {
       down: 'プレートを下へ',
       down: 'プレートを下へ',
       disabledWhilePrinting: '印刷中は無効',
       disabledWhilePrinting: '印刷中は無効',
       notHomedTitle: 'プリンターがホーミングされていません',
       notHomedTitle: 'プリンターがホーミングされていません',
-      notHomedMessage: '前回の印刷以降、Z軸がホーミングされていません。安全な位置決めのためにまずZをホーミングするか、このまま移動してください — ソフトエンドストップはバイパスされます。',
-      homeZ: 'Zをホーミング',
+      notHomedMessage: '前回の印刷以降、プリンターがホーミングされていません。安全な位置決めのためにまずオートホーミングを実行するか(ツールヘッドをパークしてからX・Y・Zをホーミングします)、このまま移動してください — ソフトエンドストップはバイパスされます。',
+      homeZ: 'オートホーミング',
       moveAnyway: 'このまま移動',
       moveAnyway: 'このまま移動',
-      homingStarted: 'Z軸をホーミング中…',
+      homingStarted: 'プリンターをオートホーミング中…',
     },
     },
     // Permissions
     // Permissions
     permission: {
     permission: {

+ 3 - 3
frontend/src/i18n/locales/pt-BR.ts

@@ -341,10 +341,10 @@ export default {
       down: 'Mover mesa para baixo',
       down: 'Mover mesa para baixo',
       disabledWhilePrinting: 'Desativado durante a impressão',
       disabledWhilePrinting: 'Desativado durante a impressão',
       notHomedTitle: 'Impressora não referenciada',
       notHomedTitle: 'Impressora não referenciada',
-      notHomedMessage: 'O eixo Z não foi referenciado desde a última impressão. Referencie Z primeiro para um posicionamento seguro, ou mova assim mesmo — os fins de curso de software serão ignorados.',
-      homeZ: 'Referenciar Z',
+      notHomedMessage: 'A impressora não foi referenciada desde a última impressão. Execute a referência automática primeiro para um posicionamento seguro (estaciona o cabeçote, depois referencia X, Y e Z), ou mova assim mesmo — os fins de curso de software serão ignorados.',
+      homeZ: 'Referência automática',
       moveAnyway: 'Mover assim mesmo',
       moveAnyway: 'Mover assim mesmo',
-      homingStarted: 'Referenciando eixo Z…',
+      homingStarted: 'Referenciando impressora automaticamente…',
     },
     },
     // Permissions
     // Permissions
     permission: {
     permission: {

+ 3 - 3
frontend/src/i18n/locales/zh-CN.ts

@@ -341,10 +341,10 @@ export default {
       down: '热床下移',
       down: '热床下移',
       disabledWhilePrinting: '打印中已禁用',
       disabledWhilePrinting: '打印中已禁用',
       notHomedTitle: '打印机未归零',
       notHomedTitle: '打印机未归零',
-      notHomedMessage: '自上次打印以来 Z 轴尚未归零。请先归零 Z 以确保安全定位,或者直接移动 — 软限位将被绕过。',
-      homeZ: '归零 Z',
+      notHomedMessage: '打印机自上次打印以来尚未归零。请先执行自动归零以确保安全定位(先停放喷头,然后归零 X、Y 和 Z),或者直接移动 — 软限位将被绕过。',
+      homeZ: '自动归零',
       moveAnyway: '强制移动',
       moveAnyway: '强制移动',
-      homingStarted: 'Z 轴归零中…',
+      homingStarted: '打印机自动归零中…',
     },
     },
     // Permissions
     // Permissions
     permission: {
     permission: {

+ 3 - 3
frontend/src/i18n/locales/zh-TW.ts

@@ -341,10 +341,10 @@ export default {
       down: '熱床下移',
       down: '熱床下移',
       disabledWhilePrinting: '列印中已停用',
       disabledWhilePrinting: '列印中已停用',
       notHomedTitle: '印表機未歸零',
       notHomedTitle: '印表機未歸零',
-      notHomedMessage: '自上次列印以來 Z 軸尚未歸零。請先歸零 Z 以確保安全定位,或者直接移動 — 軟限位將被繞過。',
-      homeZ: '歸零 Z',
+      notHomedMessage: '印表機自上次列印以來尚未歸零。請先執行自動歸零以確保安全定位(先停放噴頭,然後歸零 X、Y 和 Z),或者直接移動 — 軟限位將被繞過。',
+      homeZ: '自動歸零',
       moveAnyway: '強制移動',
       moveAnyway: '強制移動',
-      homingStarted: 'Z 軸歸零中…',
+      homingStarted: '印表機自動歸零中…',
     },
     },
     // Permissions
     // Permissions
     permission: {
     permission: {

+ 1 - 1
frontend/src/pages/PrintersPage.tsx

@@ -4768,7 +4768,7 @@ function PrinterCard({
             <div className="flex flex-col gap-2">
             <div className="flex flex-col gap-2">
               <button
               <button
                 onClick={() => {
                 onClick={() => {
-                  homeAxesMutation.mutate('z');
+                  homeAxesMutation.mutate('all');
                   setShowNotHomedModal(null);
                   setShowNotHomedModal(null);
                 }}
                 }}
                 className="w-full px-3 py-2 rounded-lg text-xs font-medium bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30 transition-colors"
                 className="w-full px-3 py-2 rounded-lg text-xs font-medium bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30 transition-colors"

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DbBotyQl.js


+ 1 - 1
static/index.html

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

Some files were not shown because too many files changed in this diff