mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-19 02:51:42 -07:00
setup.focus: the head's place judged against the stops its manual home declared, read from the home anchor
head_at_home placed the gantry stops at minus manual_home_x and manual_home_y as the settings read at the burn. A manual home reads them once: it declares the head at the stops at minus the offsets, and with an offset it moves the head to X0 Y0, that far in front of them. A write of the offsets after the home restarts no controller and keeps the X/Y reference. So a home made with 5 and 3, the offsets then cleared, had the head at X0 Y0 judged home 5 and 3 mm in front of the stops, and the burn started: the false PASS2172ae4set out to close. The other way round, offsets set after a home made with none failed a head that was at the stops. The home anchor holds the stops as the home declared them: /status reports the anchor's X and Y plus the step counters. head_at_home now takes the stops from the X and Y of the anchor (baseline.ANCHOR_PATH) when /status reports X and Y referenced by a manual home, and judges the head against them within HOME_TOL_MM. An anchor that is gone, or that is not a manual home of X and Y, fails the test before any start. The settings are read for the remedy alone. With an offset, the remedy names how the push ends: release the motors, push the head against the stops, and end the release with Energize motors ($ME), not with Set home here, which moves the head off the stops to X0 Y0 again. The remedy before said only to release the motors and push the head, and a release left standing makes the burn ask home-push, which the test does not answer. Without an offset it stays "home it there". The judge only refuses more: no energy or motion path changes. Proof: tests/test_setup_burns.py, the Unit tests step of forgetest-ci.yml (python -B -m unittest discover -s tests), 24 tests OK on Windows with python 3.14 and on Ubuntu 24.04 with python 3.12. BurnRunTests plants the anchor of a home with no offset beside the fake's /status, and test_a_manual_home_offset_puts_the_stops_off_x0_y0 plants the anchor of each home: a home made with 5 and 3 fails the head at X0 Y0 and passes it at X-5 Y-3; the offsets cleared after that home still fail the head at X0 Y0 before any start (no wizard start, no lever); the offsets set after a home made with none pass the head at X0 Y0; an anchor that is gone, or that reads as the startup's Z-only reference, fails before any start. That last pair replaces the leg where an offset that did not read as a number failed: the stops no longer come from the settings, and the rule that leg held (no stops placed, nothing starts) holds on the anchor. test_setup_dark.py (21) and test_manifest.py (39, with the guard on the suite's package imports, which setup_burns.py's import of baseline.py passes) pass. Negative controls on scratch copies: head_at_home as it was fails the case at the offsets cleared after the home (Failed not raised), and so do the stops read from the settings; an anchor that cannot be read taken as X0 Y0 fails it at the anchor that is gone; an anchor of any axes and any source fails it at the startup anchor. A probe of the failure text: with the offsets 5 and 3 it names Energize motors ($ME), not Set home here; with none it says "home it there". pyflakes reports no undefined name. Acceptance: setup.focus's fingerprint moves (setup_burns.py's shared text and the test's own description); the other 42 do not move (source_sha compared before and after). baseline.py is a shared judge already, in every hash. Against the campaign this series already needs, that adds no run:850498fmoved all 43 once.
This commit is contained in:
@@ -22,6 +22,7 @@ the real numbers go in.
|
||||
import re
|
||||
import time
|
||||
|
||||
from .. import baseline
|
||||
from .. import hw
|
||||
from ..catalog import test
|
||||
from .setup_dark import run_check, Restore, FALLBACK_WINDOW, LENS_WINDOW_KEYS, dark
|
||||
@@ -44,9 +45,10 @@ BURN_COVERS = [("forgectrl", "src/wizlive.*"), ("forgectrl", "src/wizrun.h"),
|
||||
# Every burn asks first that the gantry stops are installed and the head
|
||||
# is at its home against them; the bench's head stands there, and every
|
||||
# burn's tail brings it back. head_at_home reads the head's place from
|
||||
# /status before the wizard starts and judges it against the stops, within
|
||||
# HOME_TOL_MM, when a manual home references X and Y: at X0 Y0 with no
|
||||
# manual home offset, at minus manual_home_x and manual_home_y with one.
|
||||
# /status before the wizard starts and judges it, within HOME_TOL_MM,
|
||||
# against the stops as the manual home that references X and Y declared
|
||||
# them (its home anchor): X0 Y0 for a home with no manual home offset,
|
||||
# minus manual_home_x and manual_home_y for a home made with them.
|
||||
HOME = "The head is home"
|
||||
HOME_TOL_MM = 0.05
|
||||
HOME_OFFSET_KEYS = ("manual_home_x", "manual_home_y")
|
||||
@@ -85,22 +87,44 @@ class Witnesses:
|
||||
return {"laser_on_max": self.laser_on_max, "hv_max": self.hv_max, "reads": self.reads}
|
||||
|
||||
|
||||
def home_offsets(ctx):
|
||||
"""manual_home_x and manual_home_y as the settings hold them, in mm:
|
||||
how far in front of the gantry stops a manual home puts X0 Y0 (blank
|
||||
is 0). A value that does not read as a number of 0 or more fails the
|
||||
test: the stops cannot be placed without it."""
|
||||
s = ctx.forgectrl.settings()
|
||||
out = []
|
||||
for key in HOME_OFFSET_KEYS:
|
||||
raw = s.get(key)
|
||||
def anchor_stops():
|
||||
"""Where the manual home that references X and Y declared the gantry
|
||||
stops, in /status coordinates: the X and Y of the controller's home
|
||||
anchor (baseline.ANCHOR_PATH), to which /status adds the step
|
||||
counters. None when the anchor does not read as a manual home of X
|
||||
and Y. The settings do not place the stops: a manual home reads
|
||||
manual_home_x and manual_home_y once, and a change to them after the
|
||||
home leaves its reference as it is."""
|
||||
try:
|
||||
with open(baseline.ANCHOR_PATH, encoding="ascii", errors="replace") as f:
|
||||
fields = f.read().split()
|
||||
x, y, axes, source = float(fields[0]), float(fields[1]), int(fields[3]), fields[4]
|
||||
except (OSError, ValueError, IndexError):
|
||||
return None
|
||||
if (axes & 3) != 3 or source != "manual":
|
||||
return None
|
||||
return x, y
|
||||
|
||||
|
||||
def home_offset_remedy(ctx):
|
||||
"""What brings the head to its home against the stops, from what the
|
||||
next manual home reads: with no manual home offset, a home there;
|
||||
with one, Set home here moves the head off the stops to X0 Y0, so the
|
||||
head is pushed there with the motors released, and the release ends
|
||||
with Energize motors."""
|
||||
s = ctx.forgectrl.settings() or {}
|
||||
raw = [str(s.get(key) or "").strip() for key in HOME_OFFSET_KEYS]
|
||||
|
||||
def no_offset(v):
|
||||
try:
|
||||
v = float(raw) if raw not in (None, "") else 0.0
|
||||
except (TypeError, ValueError):
|
||||
v = None
|
||||
ctx.check(v is not None and v >= 0, "%s reads %r: the gantry stops cannot be placed from it", key, raw)
|
||||
out.append(v)
|
||||
return tuple(out)
|
||||
return v == "" or float(v) == 0
|
||||
except ValueError:
|
||||
return False
|
||||
if no_offset(raw[0]) and no_offset(raw[1]):
|
||||
return "home it there before the burn"
|
||||
return ("with manual_home_x %s and manual_home_y %s, Set home here moves the head off the stops to X0 Y0; "
|
||||
"release the motors, push the head against the stops, and end the release with Energize motors "
|
||||
"($ME), not with Set home here, before the burn" % (raw[0] or "blank", raw[1] or "blank"))
|
||||
|
||||
|
||||
def head_at_home(ctx):
|
||||
@@ -108,13 +132,13 @@ def head_at_home(ctx):
|
||||
/status under the campaign's own controller: the wizard restarts the
|
||||
controller before its home prompt, and a fresh controller drops the
|
||||
X/Y reference, so a read at that prompt places nothing. When X and Y
|
||||
are referenced by a manual home, the head is judged against the
|
||||
gantry stops, where the wizard draws from, within HOME_TOL_MM: a
|
||||
manual home declares the head at the stops at minus manual_home_x
|
||||
and manual_home_y, X0 Y0 with no offset, and with an offset it moves
|
||||
the head to X0 Y0, that far in front of them. Unreferenced, the
|
||||
head's place rests on the operator's word at the ready gate, and the
|
||||
record says so."""
|
||||
are referenced by a manual home, the head is judged within
|
||||
HOME_TOL_MM against the gantry stops, where the wizard draws from,
|
||||
as that home declared them (anchor_stops): X0 Y0 for a home with no
|
||||
offset, and minus manual_home_x and manual_home_y for a home made
|
||||
with them, which moves the head to X0 Y0, that far in front of the
|
||||
stops. Unreferenced, the head's place rests on the operator's word
|
||||
at the ready gate, and the record says so."""
|
||||
fc = ctx.forgectrl
|
||||
read = {}
|
||||
|
||||
@@ -137,19 +161,16 @@ def head_at_home(ctx):
|
||||
ctx.log("the head's place rests on the operator's word at the ready gate (X and Y unreferenced: "
|
||||
"homed_axes %s, home_source %s, pos %s)", axes, source, pos)
|
||||
return
|
||||
ox, oy = home_offsets(ctx)
|
||||
stops = rec["stops"] = {"x": 0.0 - ox, "y": 0.0 - oy}
|
||||
found = anchor_stops()
|
||||
ctx.check(found is not None, "the home anchor (%s) does not read as the manual home /status reports: the "
|
||||
"gantry stops cannot be placed before the burn", baseline.ANCHOR_PATH)
|
||||
stops = rec["stops"] = {"x": found[0], "y": found[1]}
|
||||
x, y = pos.get("x"), pos.get("y")
|
||||
at_home = (isinstance(x, (int, float)) and isinstance(y, (int, float))
|
||||
and abs(x - stops["x"]) <= HOME_TOL_MM and abs(y - stops["y"]) <= HOME_TOL_MM)
|
||||
if ox or oy:
|
||||
remedy = ("manual_home_x %g and manual_home_y %g put the stops at X%.2f Y%.2f, and a manual home "
|
||||
"leaves the head at X0 Y0, that far in front of them; release the motors and push the "
|
||||
"head against the stops before the burn" % (ox, oy, stops["x"], stops["y"]))
|
||||
else:
|
||||
remedy = "home it there before the burn"
|
||||
ctx.check(at_home, "the head is not at its home against the gantry stops: /status reads pos %s with X "
|
||||
"and Y referenced by a manual home; %s", pos, remedy)
|
||||
"and Y referenced by a manual home that declared the stops at X%.2f Y%.2f; %s", pos, stops["x"],
|
||||
stops["y"], "" if at_home else home_offset_remedy(ctx))
|
||||
ctx.log("the head is home on record: pos %s, X and Y referenced by a manual home, the stops at X%.2f Y%.2f",
|
||||
pos, stops["x"], stops["y"])
|
||||
|
||||
@@ -346,9 +367,9 @@ def focus_window_ok(ctx, r, before):
|
||||
"burn writes."],
|
||||
description="The focus burn as a fresh head runs it. Before anything starts, the head's "
|
||||
"place is read from /status and recorded: judged against the gantry stops when "
|
||||
"a manual home references X and Y (X0 Y0, or minus manual_home_x and "
|
||||
"manual_home_y when they are set), else left to the operator's word at the "
|
||||
"ready gate. The "
|
||||
"a manual home references X and Y (where that home declared them: X0 Y0, or "
|
||||
"minus the manual_home_x and manual_home_y it read), else left to the "
|
||||
"operator's word at the ready gate. The "
|
||||
"edge setting is cleared, so the burn starts from the bench reference's edge, "
|
||||
"while the lens window stays the one the Lens travel check wrote (or the "
|
||||
"fallback). POST "
|
||||
|
||||
@@ -21,7 +21,7 @@ import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import helpers # noqa: E402
|
||||
from forgetest import catalog # noqa: E402
|
||||
from forgetest import baseline, catalog # noqa: E402
|
||||
from forgetest.runner import Aborted, Context, Failed, Run # noqa: E402
|
||||
from forgetest.suite import setup_burns, setup_dark # noqa: E402
|
||||
|
||||
@@ -291,14 +291,19 @@ class BurnRunTests(unittest.TestCase):
|
||||
os.environ["GF_SYSFS_ROOT"] = self.sysfs + "/"
|
||||
self.fake = helpers.FakeForgectrl().start()
|
||||
# the head at its home against the gantry stops, X and Y referenced
|
||||
# by a manual home (the fake's pos is X0 Y0 Z0)
|
||||
# by a manual home with no offset (the fake's pos is X0 Y0 Z0), its
|
||||
# anchor declaring the stops at X0 Y0
|
||||
self.fake.state["status"].update({"homed": True, "homed_axes": 7, "home_source": "manual"})
|
||||
self.saved_anchor = baseline.ANCHOR_PATH
|
||||
baseline.ANCHOR_PATH = os.path.join(self.sysfs, "grblhal.homed")
|
||||
self.anchor("0.000 0.000 0.000 7 manual")
|
||||
self.daemon = ScriptedDaemon(self.fake)
|
||||
self.poll = setup_dark.POLL_S
|
||||
setup_dark.POLL_S = 0.01
|
||||
|
||||
def tearDown(self):
|
||||
setup_dark.POLL_S = self.poll
|
||||
baseline.ANCHOR_PATH = self.saved_anchor
|
||||
self.fake.stop()
|
||||
if self.saved_root is None:
|
||||
os.environ.pop("GF_SYSFS_ROOT", None)
|
||||
@@ -312,6 +317,12 @@ class BurnRunTests(unittest.TestCase):
|
||||
with open(path, "w") as f:
|
||||
f.write(value + "\n")
|
||||
|
||||
def anchor(self, line):
|
||||
"""The controller's home anchor as a home writes it: X Y Z, the
|
||||
axes it references, and what set it."""
|
||||
with open(baseline.ANCHOR_PATH, "w") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
def run_test(self, tid):
|
||||
t = catalog.load_suite()[tid]
|
||||
run = self.run = Run("test", t.id, t.title) # kept for a run that fails
|
||||
@@ -451,11 +462,13 @@ class BurnRunTests(unittest.TestCase):
|
||||
self.assert_found()
|
||||
|
||||
def test_a_manual_home_offset_puts_the_stops_off_x0_y0(self):
|
||||
# manual_home_x 5 and manual_home_y 3: a manual home declares the
|
||||
# stops X-5 Y-3 and leaves the head at X0 Y0, in front of them,
|
||||
# where the wizard does not draw from. X0 Y0 fails before any start,
|
||||
# with no controller lever touched; the head at the stops is home.
|
||||
# A manual home made with manual_home_x 5 and manual_home_y 3
|
||||
# declares the stops X-5 Y-3 (its anchor) and leaves the head at
|
||||
# X0 Y0, in front of them, where the wizard does not draw from. X0
|
||||
# Y0 fails before any start, with no controller lever touched; the
|
||||
# head at the stops is home.
|
||||
self.fake.state["settings"].update({"manual_home_x": "5", "manual_home_y": "3"})
|
||||
self.anchor("-5.000 -3.000 0.000 7 manual")
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.focus")
|
||||
self.assertIn("not at its home", str(cm.exception))
|
||||
@@ -467,12 +480,34 @@ class BurnRunTests(unittest.TestCase):
|
||||
run = self.run_test("setup.focus")
|
||||
self.assertTrue(run.evidence["head_at_start"]["judged"])
|
||||
self.assertEqual(self.daemon.order, ["laser.focus"])
|
||||
# an offset that does not read as a number places no stops: nothing starts
|
||||
self.fake.state["settings"]["manual_home_x"] = "five"
|
||||
# The offsets cleared after that home leave its reference as it is:
|
||||
# the stops are still X-5 Y-3, and the head at X0 Y0 still fails
|
||||
# before any start.
|
||||
for k in ("manual_home_x", "manual_home_y"):
|
||||
self.fake.state["settings"].pop(k)
|
||||
self.fake.state["status"]["pos"] = {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.focus")
|
||||
self.assertIn("manual_home_x reads 'five'", str(cm.exception))
|
||||
self.assertIn("not at its home", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["laser.focus"])
|
||||
# The offsets set after a home made with none: the head at X0 Y0 is
|
||||
# at the stops that home declared, and the burn runs.
|
||||
self.fake.state["settings"].update({"manual_home_x": "5", "manual_home_y": "3"})
|
||||
self.anchor("0.000 0.000 0.000 7 manual")
|
||||
run = self.run_test("setup.focus")
|
||||
self.assertEqual(run.evidence["head_at_start"]["stops"], {"x": 0.0, "y": 0.0})
|
||||
self.assertEqual(self.daemon.order, ["laser.focus", "laser.focus"])
|
||||
# An anchor that is gone, or that is not the manual home /status
|
||||
# reports, places no stops: nothing starts.
|
||||
os.unlink(baseline.ANCHOR_PATH)
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.focus")
|
||||
self.assertIn("home anchor", str(cm.exception))
|
||||
self.anchor("0.000 0.000 0.000 4 startup")
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test("setup.focus")
|
||||
self.assertIn("home anchor", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["laser.focus", "laser.focus"])
|
||||
|
||||
def test_an_unreferenced_head_runs_on_the_operators_word(self):
|
||||
# A fresh controller's counters-only position places nothing: the
|
||||
|
||||
Reference in New Issue
Block a user