forgetest: motion.soft-limits runs its own camera home and hands the head back

motion.soft-limits homed only when the machine was not homed already,
and then with whatever homing_mode the machine had: it counted on the
home cloud.mode-switch used to leave behind. cloud.mode-switch now drops
its camera home when it hands the head back, so the test started
unhomed, and under the operator's homing_mode = manual its $H was a
manual home, declared at once where the head stood (2.2 s, no web-service
session), and the test failed on gfhome's missing completion line. When
it did home, it left the head at the corner and the machine homed.

The test now sets homing_mode = gfcloud and unsets the camera-home
offsets for its run, so the envelope is the bed from the home corner,
and always runs its own camera home, every motion of it judged whole
(homeoff.judge_whole_motions). A setting is written when the controller
takes it (it refuses one for a moment after a Grbl client closes). At
the end the settings are put back as found and homeoff.camera_home_return
drops the home and jogs the head back to where the test found it. The
helpers are imported inside the function: homeoff imports this module.

Proof: on the bench reference, image 20260925183749 with this file
mounted, motion.soft-limits PASS: the camera home in 52.6 s, every
motion completed; X max, Y max and X min refused with ALARM:2 and no
motion, the jog past the bed refused with error 15, the move inside the
bed ran and came back; the settings put back; the head jogged back
28.95/13.03 mm to where the test found it; the baseline clean. forgetest
499 OK.

Acceptance: the test is the change; its fingerprint moves and no other
test's does.
This commit is contained in:
ScottW514
2026-09-25 15:21:45 -04:00
parent 800863fb09
commit 8570c0141e
+45 -10
View File
@@ -1202,25 +1202,53 @@ def respawn_gate(ctx):
@test("motion.soft-limits", title="After a home the bed is the X/Y envelope", @test("motion.soft-limits", title="After a home the bed is the X/Y envelope",
subsystem="motion", kind="auto", mode="grbl", est_min=3, subsystem="motion", kind="auto", mode="grbl", est_min=5,
covers=_MOTION_COVERS, requires=["motion.jog-roundtrip", "cloud.mode-switch"], covers=_MOTION_COVERS, requires=["motion.jog-roundtrip", "cloud.mode-switch"],
steps=["Bed clear, lid closed. The test homes the machine through the web service if it is " steps=["Bed clear, lid closed; cloud credentials configured; the machine on the network. The test "
"not homed (about a minute), then sends moves the controller must refuse."], "homes the machine through the web service (about a minute), sends moves the controller "
"must refuse, and hands the head back where it found it."],
description="The machine has no limit switches, so the core's $20 cannot be turned on and " description="The machine has no limit switches, so the core's $20 cannot be turned on and "
"the X/Y soft limits are the driver's: off while the position is not trusted, on " "the X/Y soft limits are the driver's: off while the position is not trusted, on "
"after a home, when the envelope is the bed ($130 by $131 from the home corner). " "after a home, when the envelope is the bed ($130 by $131 from the home corner). "
"Homed, a program move 5 mm past X max or Y max, or past the near edge, raises " "With homing_mode = gfcloud and the camera-home offsets unset for the run, $H runs "
"ALARM:2 before any motion (the kernel counters do not move), a jog past the bed " "the web-service homing session, every motion of it run whole. Homed, a program "
"is refused with error 15, and a move inside the bed runs. The Z envelope is the " "move 5 mm past X max or Y max, or past the near edge, raises ALARM:2 before any "
"lens window, as before.") "motion (the kernel counters do not move), a jog past the bed is refused with "
"error 15, and a move inside the bed runs. The Z envelope is the lens window, as "
"before. The settings are put back as found, the camera home is dropped, and the "
"head is jogged back to where the test found it by the travel the session's "
"motions logged.")
def soft_limits(ctx): def soft_limits(ctx):
# The camera-home helpers, imported here: homeoff imports this module.
from .cloud import gfhome_homing from .cloud import gfhome_homing
from .homeoff import camera_home_return, judge_whole_motions, session_mark
fc = ctx.forgectrl fc = ctx.forgectrl
ev = ctx.evidence ev = ctx.evidence
s0 = fc.settings() or {}
ctx.check(s0.get("cloud_enabled") == "1", "cloud mode is off: a camera home needs it on")
found = {k: s0.get(k) or "" for k in ("homing_mode", "gfcloud_home_x", "gfcloud_home_y")}
ev["found"] = found
session_at = None
def put(k, v):
# The controller refuses a setting for a moment after a Grbl client
# closes: each is written when it is taken. An empty value unsets it.
def write():
st_, body_ = fc.post("/settings", params={k: ""}) if v == "" else fc.post("/settings", data={k: v})
if st_ != 200:
ctx.log("%s=%r -> %s %s", k, v, st_, body_)
return st_ == 200 or None
return ctx.wait_for(write, 20, poll=0.5) is not None
try:
# A camera home at the origin: the envelope is the bed from the home corner.
for k, v in (("homing_mode", "gfcloud"), ("gfcloud_home_x", ""), ("gfcloud_home_y", "")):
ctx.check(put(k, v), "%s=%r was not taken", k, v)
with ctx.grbl() as g: with ctx.grbl() as g:
clean_slate(ctx, g) clean_slate(ctx, g)
if not fc.status().get("homed"): session_at = session_mark()
gfhome_homing(ctx, ev, g) gfhome_homing(ctx, ev, g)
judge_whole_motions(ctx, ev, session_at)
ctx.check(fc.status().get("homed"), "the machine is not homed") ctx.check(fc.status().get("homed"), "the machine is not homed")
x_travel = float(grbl_setting(g, "$130")) x_travel = float(grbl_setting(g, "$130"))
y_travel = float(grbl_setting(g, "$131")) y_travel = float(grbl_setting(g, "$131"))
@@ -1234,7 +1262,8 @@ def soft_limits(ctx):
k1 = kernel_xy_mm(ctx) k1 = kernel_xy_mm(ctx)
moved = max(abs(k1[0] - k0[0]), abs(k1[1] - k0[1])) moved = max(abs(k1[0] - k0[0]), abs(k1[1] - k0[1]))
st = g.status_report()["state"] st = g.status_report()["state"]
rec = {"cmd": cmd, "reply": lines, "alarm": "ALARM:2" in text, "moved_mm": round(moved, 3), "state": st} rec = {"cmd": cmd, "reply": lines, "alarm": "ALARM:2" in text, "moved_mm": round(moved, 3),
"state": st}
ctx.log("%s", rec) ctx.log("%s", rec)
ctx.check(rec["alarm"], "%s was not refused with ALARM:2: %s", cmd, lines) ctx.check(rec["alarm"], "%s was not refused with ALARM:2: %s", cmd, lines)
ctx.check(moved < 0.05, "%s moved the kernel %.3f mm before the alarm", cmd, moved) ctx.check(moved < 0.05, "%s moved the kernel %.3f mm before the alarm", cmd, moved)
@@ -1254,7 +1283,8 @@ def soft_limits(ctx):
ctx.check(fc.status().get("homed"), "the soft-limit alarm and the reset un-homed the machine") ctx.check(fc.status().get("homed"), "the soft-limit alarm and the reset un-homed the machine")
jog = g.command("$J=G91X%.1fF1200" % (x_travel + 5)) jog = g.command("$J=G91X%.1fF1200" % (x_travel + 5))
ev["jog"] = jog ev["jog"] = jog
ctx.check(any(l.startswith("error:15") for l in jog), "a jog past the bed was not refused with error 15: %s", jog) ctx.check(any(l.startswith("error:15") for l in jog), "a jog past the bed was not refused with error 15: %s",
jog)
# The core answers the line after an error with that error again # The core answers the line after an error with that error again
# until an empty line clears it (the sender's acknowledgment). # until an empty line clears it (the sender's acknowledgment).
g.command("") g.command("")
@@ -1268,6 +1298,11 @@ def soft_limits(ctx):
ev["kernel_back_mm"] = [round(k1[0] - k0[0], 3), round(k1[1] - k0[1], 3)] ev["kernel_back_mm"] = [round(k1[0] - k0[0], 3), round(k1[1] - k0[1], 3)]
ctx.check(max(abs(k1[0] - k0[0]), abs(k1[1] - k0[1])) < 0.1, "the head did not come back to the corner: %s", ctx.check(max(abs(k1[0] - k0[0]), abs(k1[1] - k0[1])) < 0.1, "the head did not come back to the corner: %s",
ev["kernel_back_mm"]) ev["kernel_back_mm"])
finally:
for k, v in found.items():
ctx.log("restore %s=%r -> %s", k, v, "taken" if put(k, v) else "not taken")
if session_at is not None:
camera_home_return(ctx, ev, session_at)
ctx.log("PASS: X max, Y max and X min refused with ALARM:2 and no motion, the jog refused with " ctx.log("PASS: X max, Y max and X min refused with ALARM:2 and no motion, the jog refused with "
"error 15, a move inside the bed ran") "error 15, a move inside the bed ran")