mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
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.
119 lines
5.9 KiB
Python
119 lines
5.9 KiB
Python
"""Unit tests for the bed-jog and home-axes endpoints (#791).
|
|
|
|
Tests:
|
|
POST /api/v1/printers/{printer_id}/bed-jog?distance=<mm>&force=<bool>
|
|
POST /api/v1/printers/{printer_id}/home-axes?axes=<z|xy|all>
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
class TestBedJogAPI:
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_not_found(self, async_client: AsyncClient):
|
|
response = await async_client.post("/api/v1/printers/99999/bed-jog?distance=10")
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_zero_distance_rejected(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="P1")
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=0")
|
|
assert response.status_code == 400
|
|
assert "distance" in response.json()["detail"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_too_large_rejected(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="P1")
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=500")
|
|
assert response.status_code == 400
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_not_connected(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="Disconnected")
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = None
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10")
|
|
assert response.status_code == 400
|
|
assert "not connected" in response.json()["detail"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_send_failure(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="P1")
|
|
mock_client = MagicMock()
|
|
mock_client.send_gcode.return_value = False
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = mock_client
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10")
|
|
assert response.status_code == 500
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_success_without_force(self, async_client: AsyncClient, printer_factory):
|
|
"""When force=false the M211 guard lines must not be emitted."""
|
|
printer = await printer_factory(name="P1")
|
|
mock_client = MagicMock()
|
|
mock_client.send_gcode.return_value = True
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = mock_client
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10&force=false")
|
|
assert response.status_code == 200
|
|
sent_gcode = mock_client.send_gcode.call_args[0][0]
|
|
assert "G91" in sent_gcode
|
|
assert "G1 Z10.00" in sent_gcode
|
|
assert "G90" in sent_gcode
|
|
assert "M211" not in sent_gcode
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bed_jog_success_with_force(self, async_client: AsyncClient, printer_factory):
|
|
"""force=true must wrap the move in M211 S0 / M211 S1."""
|
|
printer = await printer_factory(name="P1")
|
|
mock_client = MagicMock()
|
|
mock_client.send_gcode.return_value = True
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = mock_client
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=-5&force=true")
|
|
assert response.status_code == 200
|
|
sent_gcode = mock_client.send_gcode.call_args[0][0]
|
|
lines = sent_gcode.splitlines()
|
|
assert lines[0] == "M211 S0"
|
|
assert lines[-1] == "M211 S1"
|
|
assert "G1 Z-5.00" in sent_gcode
|
|
|
|
|
|
class TestHomeAxesAPI:
|
|
@pytest.mark.asyncio
|
|
async def test_home_axes_not_found(self, async_client: AsyncClient):
|
|
response = await async_client.post("/api/v1/printers/99999/home-axes?axes=z")
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_home_axes_invalid(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="P1")
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes=bogus")
|
|
assert response.status_code == 400
|
|
|
|
@pytest.mark.asyncio
|
|
@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")
|
|
mock_client = MagicMock()
|
|
mock_client.send_gcode.return_value = True
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = mock_client
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes={axes}")
|
|
assert response.status_code == 200
|
|
mock_client.send_gcode.assert_called_once_with("G28")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_home_axes_not_connected(self, async_client: AsyncClient, printer_factory):
|
|
printer = await printer_factory(name="D")
|
|
with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
|
|
mock_pm.get_client.return_value = None
|
|
response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes=z")
|
|
assert response.status_code == 400
|