mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7a595ced7 | ||
|
|
5c93e49c8c | ||
|
|
650483c085 | ||
|
|
7b8f72b632 |
@@ -247,8 +247,11 @@ TOOLS = [
|
||||
{"id": "z-envelope-test", "title": "Z envelope harness", "script": "z_envelope_test.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "The Z soft limit belongs to the driver, not to $20: an unreferenced Z is collapsed to where the "
|
||||
"lens stands, and neither a $20 nor a $132 write frees it. A CI harness (the grblHAL repo): needs "
|
||||
"the host-built null-sink controller, not the machine, so it is not a bench-page tool."},
|
||||
"lens stands, and neither a $20 nor a $132 write frees it. A referenced lens (forgectrl's marker "
|
||||
"and the lens settings) opens it to the window the settings hold, the fallback or the focus "
|
||||
"card's stops: the ends of the reach run, two half-steps past either end alarms. A CI harness "
|
||||
"(the grblHAL repo): needs the host-built null-sink controller, not the machine, so it is not a "
|
||||
"bench-page tool."},
|
||||
{"id": "raster-dry", "title": "Dry top-speed raster per XY microstep mode", "script": "raster_dry.py",
|
||||
"safety": "dry", "where": "board", "ported": True,
|
||||
"args": [_arg("modes", "choice", "8 16 32", "the modes to run, in order", ["8", "16", "32", "8 16 32"])],
|
||||
|
||||
@@ -21,7 +21,7 @@ from ..catalog import test
|
||||
from .commission_dark import run_check, Restore
|
||||
|
||||
SHEET_COVERS = [("forgectrl", "src/wizlive.*"), ("forgectrl", "src/wizrun.h"),
|
||||
("forgectrl", "src/sheet.*"),
|
||||
("forgectrl", "src/lens.*"), ("forgectrl", "src/sheet.*"),
|
||||
("forgectrl", "src/font_hershey.*"), ("forgectrl", "src/jobstream.*"),
|
||||
("forgectrl", "src/curverec.*"), ("forgectrl", "src/wizdark.*"),
|
||||
("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"),
|
||||
@@ -71,6 +71,22 @@ def preview_ok(ctx, wid):
|
||||
ctx.check(st == 200 and re.search(rb"^M[34] S\d", body or b"", re.M) is not None,
|
||||
"no program for %s (%s)", wid, st)
|
||||
ctx.evidence["program_lines"] = body.count(b"\n")
|
||||
# Every Z the program commands is a height the head reaches in the
|
||||
# window the machine holds now: the focus card's own numbers once it
|
||||
# has them, and the conservative fallback on a head whose travel is
|
||||
# not known yet. The controller refuses a Z outside it with ALARM:2,
|
||||
# and it refuses it while the body is still burning, so the card dies
|
||||
# where the parser is and not where the bad line is.
|
||||
lens = (fc.status() or {}).get("lens") or {}
|
||||
lo, hi = lens.get("reach_min"), lens.get("reach_max")
|
||||
zs = [float(m) for m in re.findall(rb"^G0 Z(-?[0-9.]+)", body or b"", re.M)]
|
||||
ctx.evidence.setdefault("program_z", {})[wid] = {
|
||||
"z": zs, "reach": [lo, hi], "stops_found": lens.get("stops_found")}
|
||||
ctx.check(lo is not None and hi is not None, "/status carries no lens reach: %s", lens)
|
||||
if lo is not None and hi is not None:
|
||||
out = [z for z in zs if z < lo - 0.01 or z > hi + 0.01]
|
||||
ctx.check(not out, "%s commands Z outside the lens window %.2f..%.2f: %s",
|
||||
wid, lo, hi, out)
|
||||
|
||||
|
||||
def emission_ok(ctx, last):
|
||||
@@ -89,6 +105,61 @@ def file_card(ctx, wid):
|
||||
ev.setdefault("cards", {})[wid] = {k: ev.pop(k) for k in CARD_EVIDENCE if k in ev}
|
||||
|
||||
|
||||
# The lens settings: the head's numbers, which a fresh machine does not
|
||||
# have. Cleared before the cards so the run is a fresh machine's.
|
||||
LENS_SETTINGS = CARD_SETTINGS[:3]
|
||||
FALLBACK_WINDOW = (10, 12)
|
||||
|
||||
|
||||
def fresh_lens(ctx):
|
||||
"""The lens as a fresh machine has it: the head's numbers unknown, so
|
||||
the frame runs in the fallback window and the focus card has to find
|
||||
the stops and write them before its ladder burns. The machine's own
|
||||
numbers come back with the restore at the end of the run."""
|
||||
fc = ctx.forgectrl
|
||||
for k in LENS_SETTINGS:
|
||||
st, _ = fc.post("/settings", params={k: ""})
|
||||
ctx.check(st == 200, "clearing %s -> %s", k, st)
|
||||
lens = (fc.status() or {}).get("lens") or {}
|
||||
ctx.evidence["lens_fresh"] = lens
|
||||
ctx.check(lens.get("stops_found") is False and (lens.get("below"), lens.get("above")) == FALLBACK_WINDOW,
|
||||
"the lens window is not the fallback after the clear: %s", lens)
|
||||
|
||||
|
||||
def focus_window_ok(ctx):
|
||||
"""The window the focus card burned its ladder in is the one it wrote
|
||||
before its controller started, and the one the machine holds now: the
|
||||
result's window is the stops it found (or the fallback), the settings
|
||||
hold that window, every ladder height lies in its reach, and the
|
||||
program served now agrees with /status."""
|
||||
fc = ctx.forgectrl
|
||||
r = ((ctx.evidence.get("cards") or {}).get("laser.focus") or {}).get("result") or {}
|
||||
w, stops = r.get("window") or {}, r.get("stops") or {}
|
||||
s = fc.settings() or {}
|
||||
held = (str(s.get("lens_stop_below_steps")), str(s.get("lens_stop_above_steps")))
|
||||
ctx.check(held == (str(w.get("below")), str(w.get("above"))),
|
||||
"the settings hold the window %s, the ladder ran in %s/%s", held, w.get("below"), w.get("above"))
|
||||
if stops.get("found"):
|
||||
ctx.check((w.get("below"), w.get("above")) == (stops.get("below"), stops.get("above")),
|
||||
"the ladder's window %s is not the stops found %s", w, stops)
|
||||
else:
|
||||
ctx.check((w.get("below"), w.get("above")) == FALLBACK_WINDOW,
|
||||
"stops not found (%s), but the ladder's window is %s", stops.get("why"), w)
|
||||
zs = r.get("ladder_z") or []
|
||||
lo, hi = w.get("reach_min"), w.get("reach_max")
|
||||
out = [z for z in zs if lo is None or hi is None or z < lo - 0.01 or z > hi + 0.01]
|
||||
ctx.check(len(zs) == 12 and not out, "ladder heights outside the reach %s..%s: %s", lo, hi, out or zs)
|
||||
ev = ctx.evidence
|
||||
before = dict(ev.get("program_z") or {}).get("laser.focus")
|
||||
preview_ok(ctx, "laser.focus")
|
||||
ev["focus_window"] = {"window": w, "stops": stops, "ladder_z": zs,
|
||||
"settings": {k: s.get(k) for k in LENS_SETTINGS},
|
||||
"program_z_before": before,
|
||||
"program_z_after": (ev.get("program_z") or {}).get("laser.focus"),
|
||||
"preview_bytes": ev.pop("preview_bytes", None),
|
||||
"program_lines": ev.pop("program_lines", None)}
|
||||
|
||||
|
||||
def thickness_answer(ctx):
|
||||
"""The thickness the placement is answered with, in the machine's
|
||||
units (the number prompts read ui_units), and the millimeters the
|
||||
@@ -167,15 +238,22 @@ def burn(ctx, wid, answers, want):
|
||||
"Nothing else: the test answers every prompt itself and puts back every setting the "
|
||||
"cards write. It runs about half an hour, mostly the coolant settle and the dark "
|
||||
"tails."],
|
||||
description="One run over the sheet's seven wizards on one piece of wood. "
|
||||
description="One run over the sheet's seven wizards on one piece of wood, as a fresh "
|
||||
"machine runs them: the three lens settings are cleared first, so the frame "
|
||||
"runs in the fallback window and the focus card has to find the stops and write "
|
||||
"them before its ladder burns. "
|
||||
"POST /wiz/sheet.place/start references the lens, takes the controller in "
|
||||
"loopback posture, jogs +X 10 and -X 10, and sets the origin at the head "
|
||||
"(Full sheet; the thickness answered in the machine's units, 3.2 mm or "
|
||||
"0.125 in). Then, each after the press and each with its preview "
|
||||
"and program served by GET /wiz/sheet.svg and /wiz/sheet.gcode: the frame "
|
||||
"and program served by GET /wiz/sheet.svg and /wiz/sheet.gcode, every Z the "
|
||||
"program commands inside the reach /status reports: the frame "
|
||||
"(180 x 130 mm) and the header band at the mark dose; the focus card (the "
|
||||
"lens homed on its bottom stop, the hall edge counted, twelve lines down the "
|
||||
"travel; the pick is line 11, the thickness kept); the floor card (twelve "
|
||||
"lens on its hall edge, the stops found by the head accelerometer and written "
|
||||
"as the window before the controller starts, twelve lines from the top of the "
|
||||
"window to the bottom; the pick is line 11, the thickness kept; after it the "
|
||||
"settings, the result's window, and the served program must hold the window "
|
||||
"the ladder ran in); the floor card (twelve "
|
||||
"rungs from 2 to 24 percent with the floor and the curve off for the job; the "
|
||||
"pick is 8, the floor written is 10); the dose card (seven rungs sampled at "
|
||||
"25 Hz, the curve fit on 20 s of dark); the corner card (five gammas reloaded "
|
||||
@@ -191,6 +269,9 @@ def sheet(ctx):
|
||||
"each. The bench presses when the button lights.")
|
||||
place(ctx)
|
||||
with Restore(ctx, CARD_SETTINGS):
|
||||
fresh_lens(ctx)
|
||||
for wid, answers, want in CARDS:
|
||||
burn(ctx, wid, answers, want)
|
||||
ctx.log("PASS: the sheet's seven wizards ran on one piece; settings restored")
|
||||
if wid == "laser.focus":
|
||||
focus_window_ok(ctx)
|
||||
ctx.log("PASS: the sheet's seven wizards ran on one piece as a fresh machine's; settings restored")
|
||||
|
||||
@@ -17,7 +17,44 @@ from forgetest.runner import Context, Run # noqa: E402
|
||||
from forgetest.suite import commission_sheet # noqa: E402
|
||||
|
||||
LIVE = ["sheet.frame", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner", "cooling.flow-load"]
|
||||
# The focus card's program is read again after it ran: the served ladder
|
||||
# must then lie in the window the card wrote.
|
||||
PREVIEWS = ["sheet.frame", "laser.focus", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner",
|
||||
"cooling.flow-load"]
|
||||
EMISSION = {"hv_max": 610, "laser_on_samples": 900, "thermopile_delta": 600, "lit_s": 30.0}
|
||||
SPM = 2.922 # the lens screw, half-steps per mm ($102)
|
||||
EDGE_DEFAULT = 3.35
|
||||
|
||||
|
||||
def lens_block(s):
|
||||
"""The /status lens block as the daemon builds it from the settings: a
|
||||
stop count from 1 to 40 stands, anything else is the fallback."""
|
||||
def count(key, fallback):
|
||||
try:
|
||||
n = float(s.get(key, ""))
|
||||
except ValueError:
|
||||
return fallback, False
|
||||
return (int(n), True) if 1 <= n <= 40 else (fallback, False)
|
||||
try:
|
||||
edge = float(s.get("lens_hall_edge_z_mm", ""))
|
||||
except ValueError:
|
||||
edge = EDGE_DEFAULT
|
||||
below, a = count("lens_stop_below_steps", 10)
|
||||
above, b = count("lens_stop_above_steps", 12)
|
||||
return {"edge_z": round(edge, 2), "below": below, "above": above, "stops_found": a and b,
|
||||
"reach_min": round(edge - below / SPM, 2), "reach_max": round(edge + above / SPM, 2)}
|
||||
|
||||
|
||||
def ladder_z(edge, below, above):
|
||||
"""The focus ladder the daemon builds: twelve whole half-steps from
|
||||
the top of the window to the bottom."""
|
||||
return [round(edge + round(above - (above + below) * i / 11) / SPM, 2) for i in range(12)]
|
||||
|
||||
|
||||
# The window the focus card finds and writes before its controller starts.
|
||||
STOPS = {"lens_stop_below_steps": "14", "lens_stop_above_steps": "20"}
|
||||
FOCUS_WINDOW = {"below": 14, "above": 20, "reach_min": round(EDGE_DEFAULT - 14 / SPM, 2),
|
||||
"reach_max": round(EDGE_DEFAULT + 20 / SPM, 2)}
|
||||
|
||||
# Each wizard as the daemon runs it: its prompts in order (id, kind,
|
||||
# options) and the result it ends with. A "jog" prompt repeats until the
|
||||
@@ -37,7 +74,9 @@ SCRIPT = {
|
||||
"steps_per_mm": 2.922, "max_height_mm": 13.81,
|
||||
"focus_range_mm": {"min": 2.17, "max": 13.81},
|
||||
"stops": {"found": True, "below": 14, "above": 20, "contact_below": 16,
|
||||
"contact_above": 22, "why": ""}, "emission": EMISSION}),
|
||||
"contact_above": 22, "why": ""},
|
||||
"window": dict(FOCUS_WINDOW), "ladder_z": ladder_z(EDGE_DEFAULT, 14, 20),
|
||||
"emission": EMISSION}),
|
||||
"laser.floor": ([("floor-arm", "continue", ["Continue"]),
|
||||
("floor-pick", "choice", ["2", "4", "6", "8", "None"])],
|
||||
{"faintest_density": 8.0, "floor_density": 10.0, "emission": EMISSION}),
|
||||
@@ -50,10 +89,11 @@ SCRIPT = {
|
||||
{"lit_s": 30.0, "dose_raw_s": 12000, "peak_c": 0.72, "k_density": 2.14e-5,
|
||||
"k_cw": 2.78e-5, "emission": EMISSION}),
|
||||
}
|
||||
# What each card writes when it ends.
|
||||
# What a card writes when it starts (the focus card's window, before its
|
||||
# controller starts) and when it ends.
|
||||
START_WRITES = {"laser.focus": dict(STOPS)}
|
||||
WRITES = {
|
||||
"laser.focus": {"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14",
|
||||
"lens_stop_above_steps": "20"},
|
||||
"laser.focus": {"lens_hall_edge_z_mm": "6.96"},
|
||||
"laser.floor": {"laser_floor_density": "10"},
|
||||
"laser.dose-curve": {"laser_dose_curve": "10:0.44,100:100"},
|
||||
"laser.corner": {"laser_corner_gamma": "1.5"},
|
||||
@@ -115,9 +155,11 @@ class ScriptedDaemon:
|
||||
self.versions = {}
|
||||
self.answers = {} # wid -> [values]
|
||||
self.order = [] # the wizards as started
|
||||
self.settings_at_start = {} # wid -> the settings as the wizard found them
|
||||
self.previews = []
|
||||
self.queue = []
|
||||
self.seq = 0
|
||||
self.stray_z = None # a Z the served program commands beyond the reach, when set
|
||||
fake.on_get = self.on_get
|
||||
fake.on_post = self.on_post
|
||||
|
||||
@@ -130,6 +172,10 @@ class ScriptedDaemon:
|
||||
return True
|
||||
|
||||
def on_get(self, path, q):
|
||||
s = self.fake.state["settings"]
|
||||
if path == "/status":
|
||||
# the lens block follows the settings, as the daemon's does
|
||||
return 200, dict(self.fake.state["status"], lens=lens_block(s))
|
||||
if path == "/wiz/dark":
|
||||
return 200, dict(self.dark)
|
||||
if path == "/wiz":
|
||||
@@ -140,8 +186,18 @@ class ScriptedDaemon:
|
||||
return 404, {"error": "no such card"}
|
||||
return 200, b"<svg xmlns='http://www.w3.org/2000/svg'></svg>", "image/svg+xml"
|
||||
if path == "/wiz/sheet.gcode":
|
||||
# the frame and the text are M4 at the mark dose, as the daemon serves them
|
||||
return 200, b"; mock\nM4 S400\nG0 X60 Y42\nG1 X120 Y42 F3000\nM5\n", "text/plain"
|
||||
# the frame and the text are M4 at the mark dose, as the daemon
|
||||
# serves them; the head's Z is the edge, the tail goes back to
|
||||
# it, and the focus card's ladder spans the window the settings
|
||||
# hold now
|
||||
lens = lens_block(s)
|
||||
lines = ["; mock", "G0 Z%.2f" % lens["edge_z"], "M4 S400", "G0 X60 Y42", "G1 X120 Y42 F3000"]
|
||||
if q.get("card") == "laser.focus":
|
||||
lines += ["G0 Z%.2f" % z for z in ladder_z(lens["edge_z"], lens["below"], lens["above"])]
|
||||
if self.stray_z is not None:
|
||||
lines.append("G0 Z%.2f" % self.stray_z)
|
||||
lines += ["M5", "G0 Z%.2f" % lens["edge_z"], "G0 X0 Y0", "M2"]
|
||||
return 200, ("\n".join(lines) + "\n").encode(), "text/plain"
|
||||
return None
|
||||
|
||||
def on_post(self, path, form):
|
||||
@@ -151,6 +207,8 @@ class ScriptedDaemon:
|
||||
if wid not in SCRIPT:
|
||||
return 404, {"error": "no such wizard"}
|
||||
self.order.append(wid)
|
||||
self.settings_at_start[wid] = dict(s)
|
||||
s.update(START_WRITES.get(wid, {}))
|
||||
self.queue = list(SCRIPT[wid][0])
|
||||
self.dark.update({"id": wid, "running": True, "phase": "starting", "result": None, "error": ""})
|
||||
self.open_next()
|
||||
@@ -199,7 +257,7 @@ class SheetRunTests(unittest.TestCase):
|
||||
self.assertEqual(d.answers["laser.focus"], ["Continue", "11", "Keep"])
|
||||
self.assertEqual(d.answers["laser.floor"], ["Continue", "8"])
|
||||
self.assertEqual(d.answers["laser.corner"], ["Continue", "1.50"])
|
||||
self.assertEqual(d.previews, LIVE)
|
||||
self.assertEqual(d.previews, PREVIEWS)
|
||||
cards = run.evidence["cards"]
|
||||
self.assertEqual(sorted(cards), sorted(["sheet.place"] + LIVE))
|
||||
for wid in LIVE:
|
||||
@@ -208,6 +266,24 @@ class SheetRunTests(unittest.TestCase):
|
||||
self.assertNotIn("emission", run.evidence)
|
||||
self.assertEqual(cards["sheet.place"]["result"]["origin_x"], 100)
|
||||
self.assertEqual(run.evidence["settings_before"], FOUND)
|
||||
# A fresh machine's run: the lens settings were gone before the
|
||||
# frame, the frame's program ran in the fallback window, the focus
|
||||
# card's window was in the settings when the floor card started,
|
||||
# and the window check filed what it compared.
|
||||
for k in commission_sheet.LENS_SETTINGS:
|
||||
self.assertNotIn(k, d.settings_at_start["sheet.frame"], k)
|
||||
self.assertEqual(run.evidence["lens_fresh"]["stops_found"], False)
|
||||
self.assertEqual(run.evidence["program_z"]["sheet.frame"]["reach"], [-0.07, 7.46])
|
||||
floor_start = d.settings_at_start["laser.floor"]
|
||||
self.assertEqual({k: floor_start[k] for k in commission_sheet.LENS_SETTINGS},
|
||||
{"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14", "lens_stop_above_steps": "20"})
|
||||
fw = run.evidence["focus_window"]
|
||||
self.assertEqual(fw["window"], FOCUS_WINDOW)
|
||||
self.assertEqual(fw["settings"]["lens_stop_below_steps"], "14")
|
||||
self.assertEqual(len(fw["ladder_z"]), 12)
|
||||
self.assertEqual(fw["program_z_before"]["reach"], [-0.07, 7.46]) # the fallback ladder
|
||||
self.assertEqual(fw["program_z_after"]["reach"], [2.17, 13.8]) # the written window at the new edge
|
||||
self.assertEqual(len(fw["program_z_after"]["z"]), 14) # head, twelve lines, tail
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k) # restored
|
||||
|
||||
@@ -261,6 +337,44 @@ class SheetRunTests(unittest.TestCase):
|
||||
finally:
|
||||
SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], dict(SCRIPT_DARK, emission=EMISSION))
|
||||
|
||||
def test_a_focus_window_the_settings_do_not_hold_fails_the_run(self):
|
||||
# The defect the bench found: the focus card burned its ladder over
|
||||
# the stops it found while the controller's Z limit stood on the
|
||||
# settings, which did not hold them. A daemon whose focus result
|
||||
# names a window the settings do not hold fails the run before the
|
||||
# floor card, and the settings still go back.
|
||||
from forgetest.runner import Failed
|
||||
saved = dict(START_WRITES["laser.focus"])
|
||||
START_WRITES["laser.focus"] = {}
|
||||
try:
|
||||
t = catalog.load_suite()["commission.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
with self.assertRaises(Failed) as cm:
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertIn("the settings hold", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["sheet.place", "sheet.frame", "laser.focus"])
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k)
|
||||
finally:
|
||||
START_WRITES["laser.focus"] = saved
|
||||
|
||||
def test_a_program_z_beyond_the_reach_fails_before_the_burn(self):
|
||||
# A served program that commands a Z the lens does not reach is
|
||||
# refused before the card starts: nothing burns.
|
||||
from forgetest.runner import Failed
|
||||
self.daemon.stray_z = 10.6
|
||||
t = catalog.load_suite()["commission.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
with self.assertRaises(Failed) as cm:
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertIn("outside the lens window", str(cm.exception))
|
||||
self.assertIn("10.6", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["sheet.place"])
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# only SRCREV and PV here - the image manifest leaves *-pin.inc out of the
|
||||
# layer content hash because the component entry already identifies the
|
||||
# pinned source (forgefirm-image-manifest.bbclass).
|
||||
SRCREV = "a9e7073b2325c8c9d89d490fe6d067abfac22da5"
|
||||
PV = "0.1.17"
|
||||
SRCREV = "864b8daea94b833ac05eda80170c54f550d54293"
|
||||
PV = "0.1.20"
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
# it to equal the rootfs stamp, the .fw meta-version and the release tag,
|
||||
# and the acceptance test image.health compares the stamp on the running
|
||||
# machine with the manifest's.
|
||||
FORGEFIRM_RELEASE ?= "0.0.2"
|
||||
FORGEFIRM_RELEASE ?= "0.0.3"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
# ForgeFIRM acceptance - 20260911172215 (dev)
|
||||
|
||||
- Image: `20260911172215 (dev)` (forgefirm-image-dev)
|
||||
- Manifest identity: `fb24c3f4508ae54592f8d0b98ed0b2a07a933274980d8c1e4e5a967618ef9bd8`
|
||||
- Catalog: `ceabc830345fc4cac4832de691c0571e93a5c4e155666ce91601c844c40942fc`
|
||||
- Campaign: `c-20260911182256-078f` opened 2026-09-11T18:22:56Z
|
||||
- Exported: 2026-09-11T18:54:20Z
|
||||
- **Release authorized: YES**
|
||||
- Tests: 83 total, 83 satisfied (57 inherited), 0 required
|
||||
|
||||
| Test | Kind | Result | Run at | Campaign | Inherited |
|
||||
|---|---|---|---|---|---|
|
||||
| `image.health` | auto, core | PASS | 2026-09-11T18:22:57Z | `c-20260911182256-078f` | no |
|
||||
| `image.license-bundle` | auto | PASS | 2026-09-11T17:29:26Z | `c-20260911172921-9426` | yes |
|
||||
| `kernel.latch-locked-idle` | auto, core | PASS | 2026-09-11T18:22:58Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.k1-k2` | auto, core, takeover | PASS | 2026-09-11T18:23:19Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.deadman-close` | auto, core, takeover | PASS | 2026-09-11T18:23:29Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.backtrack-bounds` | auto, core, takeover | PASS | 2026-09-11T18:23:48Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.fire-line` | auto, core, takeover | PASS | 2026-09-11T18:24:32Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.resume-lead` | auto, takeover | PASS | 2026-09-11T17:31:23Z | `c-20260911172921-9426` | yes |
|
||||
| `kernel.pic-soc-load` | auto | PASS | 2026-09-11T17:31:26Z | `c-20260911172921-9426` | yes |
|
||||
| `forgectrl.auth` | auto | PASS | 2026-09-11T17:31:27Z | `c-20260911172921-9426` | yes |
|
||||
| `forgectrl.settings-bounds` | auto | PASS | 2026-09-11T17:31:27Z | `c-20260911172921-9426` | yes |
|
||||
| `forgectrl.panel-serves` | auto | PASS | 2026-09-11T17:29:23Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.gate-blocks-controllers` | auto, takeover | PASS | 2026-09-11T17:31:46Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.override-until-reboot` | auto | PASS | 2026-09-11T17:31:46Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.advisories-rehash` | auto, takeover | PASS | 2026-09-11T17:31:55Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.account-login` | auto, takeover | PASS | 2026-09-11T17:32:44Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.https-only-writes` | auto | PASS | 2026-09-11T17:32:45Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.ssh-until-reboot` | auto | PASS | 2026-09-11T17:32:46Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.cloud-disabled-surface` | auto | PASS | 2026-09-11T17:32:47Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.factory-return` | auto | PASS | 2026-09-11T17:32:49Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.machine-name` | auto | PASS | 2026-09-11T17:32:50Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.first-run-flow` | operator, takeover | PASS | 2026-09-11T17:54:32Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.first-run-page` | operator, takeover | PASS | 2026-09-11T17:55:49Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.cert-page` | auto | PASS | 2026-09-11T17:32:50Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.what-changed` | auto, takeover | PASS | 2026-09-11T17:32:59Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.record-export` | auto | PASS | 2026-09-11T17:34:22Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.mirror` | auto, takeover | PASS | 2026-09-11T17:34:54Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-switches` | operator | PASS | 2026-09-11T17:56:07Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-sensors` | auto | PASS | 2026-09-11T17:34:34Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-airflow` | auto, takeover | PASS | 2026-09-11T17:36:26Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-cameras` | operator | PASS | 2026-09-11T17:56:22Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-motion` | auto, takeover | PASS | 2026-09-11T17:37:06Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-flow-verify` | auto, takeover | PASS | 2026-09-11T17:44:42Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.cloud-header-capture` | operator, takeover | PASS | 2026-09-11T17:59:41Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.sheet` | live, takeover | PASS | 2026-09-11T18:17:29Z | `c-20260911172921-9426` | yes |
|
||||
| `logs.tree-tail-export` | auto | PASS | 2026-09-11T17:33:40Z | `c-20260911172921-9426` | yes |
|
||||
| `logs.routing` | auto | PASS | 2026-09-11T17:44:46Z | `c-20260911172921-9426` | yes |
|
||||
| `logs.level-settings` | auto | PASS | 2026-09-11T17:44:46Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.pacing` | auto | PASS | 2026-09-11T17:36:41Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.jog-roundtrip` | auto | PASS | 2026-09-11T17:45:00Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.microstep-modes` | auto | PASS | 2026-09-11T17:45:25Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.liveness-probe` | auto | PASS | 2026-09-11T17:45:33Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.gate-waits-for-lid` | operator | PASS | 2026-09-11T18:24:48Z | `c-20260911182256-078f` | no |
|
||||
| `motion.cancel-abort` | auto | PASS | 2026-09-11T17:45:40Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.deadman` | auto | PASS | 2026-09-11T17:46:09Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.button-hold-resume` | operator | PASS | 2026-09-11T18:24:59Z | `c-20260911182256-078f` | no |
|
||||
| `motion.lid-cancel-home` | operator | PASS | 2026-09-11T18:25:15Z | `c-20260911182256-078f` | no |
|
||||
| `motion.interlock-cancel-home` | operator | PASS | 2026-09-11T18:25:24Z | `c-20260911182256-078f` | no |
|
||||
| `motion.lid-policy-hold` | operator | PASS | 2026-09-11T18:25:37Z | `c-20260911182256-078f` | no |
|
||||
| `motion.step-timing-under-load` | auto | PASS | 2026-09-11T17:46:40Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.aa-offset-calibrate` | auto | PASS | 2026-09-11T17:38:38Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.flow-verify` | auto | PASS | 2026-09-11T17:41:29Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.fans-quiet-after-motion` | auto | PASS | 2026-09-11T17:47:35Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.gate-off` | auto | PASS | 2026-09-11T17:35:02Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.floor-and-warm-up` | auto | PASS | 2026-09-11T17:49:02Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.tec-drive` | auto | PASS | 2026-09-11T17:49:10Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.fire-watch-tiers` | auto | PASS | 2026-09-11T17:49:25Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.crash-watch-plumbing` | auto | PASS | 2026-09-11T17:49:32Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.critical-tier` | auto | PASS | 2026-09-11T17:49:41Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.fan-gate-trips` | auto | PASS | 2026-09-11T17:35:45Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.flow-under-load` | live | PASS | 2026-09-11T18:41:47Z | `c-20260911182256-078f` | no |
|
||||
| `laser.power-floor` | auto | PASS | 2026-09-11T17:49:42Z | `c-20260911172921-9426` | yes |
|
||||
| `laser.emission-witness` | live, core | PASS | 2026-09-11T18:40:15Z | `c-20260911182256-078f` | no |
|
||||
| `laser.m5-rapid-dark` | live | PASS | 2026-09-11T18:42:25Z | `c-20260911182256-078f` | no |
|
||||
| `laser.disarm-in-hold` | live | PASS | 2026-09-11T18:43:51Z | `c-20260911182256-078f` | no |
|
||||
| `laser.armed-kill` | live | PASS | 2026-09-11T18:44:43Z | `c-20260911182256-078f` | no |
|
||||
| `laser.arm-wait-lid` | operator | PASS | 2026-09-11T18:25:57Z | `c-20260911182256-078f` | no |
|
||||
| `laser.pause-resume-lid-cancel` | live | PASS | 2026-09-11T18:45:12Z | `c-20260911182256-078f` | no |
|
||||
| `camera.snapshot` | auto | PASS | 2026-09-11T17:49:57Z | `c-20260911172921-9426` | yes |
|
||||
| `camera.sensor-profile` | auto | PASS | 2026-09-11T17:50:10Z | `c-20260911172921-9426` | yes |
|
||||
| `camera.h264-stream` | auto | PASS | 2026-09-11T17:50:23Z | `c-20260911172921-9426` | yes |
|
||||
| `camera.frame-health` | auto | PASS | 2026-09-11T17:50:36Z | `c-20260911172921-9426` | yes |
|
||||
| `camera.lid-privacy` | operator | PASS | 2026-09-11T18:26:12Z | `c-20260911182256-078f` | no |
|
||||
| `camera.key-read` | auto | PASS | 2026-09-11T17:50:37Z | `c-20260911172921-9426` | yes |
|
||||
| `update.slots-and-signature` | auto | PASS | 2026-09-11T17:32:49Z | `c-20260911172921-9426` | yes |
|
||||
| `cloud.mode-switch` | operator | PASS | 2026-09-11T17:58:28Z | `c-20260911172921-9426` | yes |
|
||||
| `cloud.service-protocol` | operator | PASS | 2026-09-11T18:46:13Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.lid-interlock-abort` | live | PASS | 2026-09-11T18:47:21Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.lid-during-button-wait` | operator | PASS | 2026-09-11T18:26:51Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.verdict-hold` | operator | PASS | 2026-09-11T18:38:58Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.pause-resume` | live | PASS | 2026-09-11T18:49:33Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.oversize-stream` | live | PASS | 2026-09-11T18:52:19Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.paused-lid-cancel` | live | PASS | 2026-09-11T18:52:48Z | `c-20260911182256-078f` | no |
|
||||
|
||||
Artifact sha256: `d7bb5d900f9237711a17ae207df90025605340c2eb2eaf34f9ed508cc3a899a6`
|
||||
@@ -25,6 +25,19 @@ no root) and drives it over TCP:
|
||||
does not free Z either
|
||||
6. X and Y are still free after both writes
|
||||
|
||||
A referenced lens opens the envelope to the window the shared settings
|
||||
hold, the same keys the daemon's wizards build their Z from. forgectrl
|
||||
leaves the lens on its hall edge and a marker in the state directory
|
||||
before the controller starts; the driver takes it as it loads its
|
||||
settings and places the edge on its step grid:
|
||||
|
||||
7. the marker with the edge alone: Z sits at the edge's grid step, the
|
||||
fallback window (10 below, 12 above) runs to its ends, and two
|
||||
half-steps past either end alarms
|
||||
8. the marker with the focus card's stops (14 below, 20 above): the
|
||||
wider window runs to its ends, two half-steps past either alarms
|
||||
9. a stop count out of range (41) falls back on its side alone
|
||||
|
||||
The binary keeps its settings in EEPROM.DAT in the working directory, so
|
||||
each run starts from defaults in a temporary directory and leaves
|
||||
nothing behind.
|
||||
@@ -53,13 +66,24 @@ def fail(msg):
|
||||
|
||||
|
||||
class Session:
|
||||
"""One null-sink controller process with its own settings store."""
|
||||
"""One null-sink controller process with its own settings store.
|
||||
`conf` (key: value) is the shared config the driver reads; with
|
||||
`referenced` the lens marker forgectrl leaves is in the state
|
||||
directory, so the driver takes the lens reference at its start."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, conf=None, referenced=False):
|
||||
self.workdir = tempfile.mkdtemp(prefix="z-envelope-")
|
||||
env = dict(os.environ, GF_STATE_DIR=self.workdir, FFLOG_STDERR="1")
|
||||
for key in ("GFSINK", "GF_SWITCH_FILE", "GF_VERDICT_FILE"):
|
||||
env.pop(key, None)
|
||||
conf_path = os.path.join(self.workdir, "forgefirm.conf")
|
||||
with open(conf_path, "w") as f:
|
||||
for k, v in (conf or {}).items():
|
||||
f.write("%s = %s\n" % (k, v))
|
||||
env["GFHOME_CONF"] = conf_path
|
||||
if referenced:
|
||||
with open(os.path.join(self.workdir, "lens.home"), "w") as f:
|
||||
f.write("edge 0 3 3\n")
|
||||
self.proc = subprocess.Popen([BIN, "-p", str(PORT)],
|
||||
cwd=self.workdir, env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
@@ -113,6 +137,23 @@ class Session:
|
||||
m = re.search(r"<(\w+)", self.send("?"))
|
||||
return m.group(1) if m else "?"
|
||||
|
||||
def mpos_z(self):
|
||||
"""The reported machine Z (no work offset is set in a session)."""
|
||||
m = re.search(r"[MW]Pos:(-?[\d.]+),(-?[\d.]+),(-?[\d.]+)", self.send("?"))
|
||||
if not m:
|
||||
fail("no position in the status report")
|
||||
return float(m.group(3))
|
||||
|
||||
def wait_idle(self, timeout=15.0):
|
||||
"""A move that ran plays out before the next line is judged: the
|
||||
soft limit on a moving machine holds first and alarms after."""
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
if self.state() == "Idle":
|
||||
return
|
||||
time.sleep(0.1)
|
||||
fail("the controller did not return to Idle")
|
||||
|
||||
def move(self, gcode):
|
||||
"""Run one move. Returns True when the soft limit blocked it.
|
||||
An alarm needs a reset and an unlock before the next move."""
|
||||
@@ -160,6 +201,60 @@ def free_in_xy(s, when):
|
||||
check(ran, "X and Y free %s" % when, "an X or Y move was blocked %s" % when)
|
||||
|
||||
|
||||
def window_holds(s, steps, below, above, what):
|
||||
"""The envelope of a referenced lens whose edge sits on grid step
|
||||
`steps`: the ends of the reach (`below` and `above` half-steps from
|
||||
the edge) run, two half-steps past either end alarms. The controller
|
||||
keeps a half-step of slack beyond the reach, so one past is not
|
||||
judged here; two is outside on every head."""
|
||||
spm = float(s.setting("$102"))
|
||||
top, bottom = (steps + above) / spm, (steps - below) / spm
|
||||
s.wait_idle()
|
||||
check(not s.move("G0 Z%.3f" % top), "%s: the top of the reach, Z %.3f, runs" % (what, top),
|
||||
"%s: Z %.3f, the top of the reach, was blocked" % (what, top))
|
||||
s.wait_idle()
|
||||
check(not s.move("G0 Z%.3f" % bottom), "%s: the bottom of the reach, Z %.3f, runs" % (what, bottom),
|
||||
"%s: Z %.3f, the bottom of the reach, was blocked" % (what, bottom))
|
||||
over, under = (steps + above + 2) / spm, (steps - below - 2) / spm
|
||||
s.wait_idle()
|
||||
check(s.move("G0 Z%.3f" % over), "%s: two half-steps over the top, Z %.3f, alarms" % (what, over),
|
||||
"%s: Z %.3f, two half-steps over the top, ran" % (what, over))
|
||||
s.wait_idle()
|
||||
check(s.move("G0 Z%.3f" % under), "%s: two half-steps under the bottom, Z %.3f, alarms" % (what, under),
|
||||
"%s: Z %.3f, two half-steps under the bottom, ran" % (what, under))
|
||||
|
||||
|
||||
def referenced_cases():
|
||||
"""The lens referenced by forgectrl's marker: the envelope is the
|
||||
window the shared settings hold, as the daemon's wizards read it."""
|
||||
edge = 3.35
|
||||
conf = {"lens_hall_edge_z_mm": "%.2f" % edge}
|
||||
s = Session(conf=conf, referenced=True)
|
||||
try:
|
||||
spm = float(s.setting("$102"))
|
||||
steps = int(round(edge * spm))
|
||||
z = s.mpos_z()
|
||||
check(abs(z - steps / spm) < 0.002,
|
||||
"referenced: Z sits at the edge's grid step, %.3f" % z,
|
||||
"referenced: Z reads %.3f, not the edge's grid step %.3f" % (z, steps / spm))
|
||||
window_holds(s, steps, 10, 12, "the fallback window")
|
||||
free_in_xy(s, "with the lens referenced")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
s = Session(conf=dict(conf, lens_stop_below_steps="14", lens_stop_above_steps="20"), referenced=True)
|
||||
try:
|
||||
window_holds(s, steps, 14, 20, "the focus card's window")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
s = Session(conf=dict(conf, lens_stop_below_steps="41", lens_stop_above_steps="20"), referenced=True)
|
||||
try:
|
||||
window_holds(s, steps, 10, 20, "a below count out of range")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.isfile(BIN):
|
||||
fail("no controller binary at %s" % BIN)
|
||||
@@ -189,6 +284,7 @@ def main():
|
||||
free_in_xy(s, "after the settings writes")
|
||||
finally:
|
||||
s.close()
|
||||
referenced_cases()
|
||||
print("PASS z_envelope_test")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user