mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
Catalog: motion.port-jog and motion.tray leave; two leftovers of the audit
Two catalog tests leave for the host proofs that carry them; the catalog goes from 80 to 78 (78 @test in forgetest/forgetest/suite/). - motion.port-jog. forgectrl's tests/host_routes.sh (7b2f629 on e69d967) runs the controller port's routes against the port's stand-in: a jog refused without the token, a jog that is no number 400 with nothing on the port, a numeric jog on the port as G91 G21, cancel, release and energize each one line, GET /motion/state passing the port's state on with state and sender, GET /motion/sender 200. The port beside a sender, its polls, the cancel, the client first and the bound are grblHAL's ctlport_test.py and forgectrl's grblport_test; motion.release keeps the physical currents. The helpers before it (_port_state, _port_idle, _words) stay: bedsize, laser and motion.job read them. - motion.tray. host_routes.sh holds POST /motion/tray (tray=in on the port, tray=up 400) and the panel page's switch (id="traybtn", trayToggle, /motion/tray); grblHAL's tray_test.py the mode, its offset, its persistence and its frame; forgectrl's tray_test the daemon's side. suite/tray.py held only it: the module and its import go. The coverage lint then named forgectrl src/tray.c and src/tray.h, and nothing else; grblhal-glowforge's src/glowforge_tray.* stays covered by another catalog test, so it gets no row. One HOST_TESTED row takes forgectrl's, to forgectrl:tests/tray_test.c on forgectrl:.github/workflows/build.yml, which the pin c274f19 runs (./build/tray_test). Two of the audit's leftovers, in the same change: - test_manifest.py's ImplementationHashTests sat after the __main__ guard, so running the file directly skipped it. It is above the guard now, byte for byte: python3 -m unittest test_manifest.py and python3 test_manifest.py (from tests/, where its helpers import resolves) each run its 7 cases, and 34 in all; before, the direct run ran none of them. - laser_lifecycle_test.py's test_lid_open_in_wait said an alarm is raised; the docstring now says what the test asserts: the lid-open cancel is reported and no alarm is raised. The assertions are unchanged and the harness was not run. The coverage lint on the tree manifest finds no uncovered path and no host-tested row problem across the 78 tests; its one finding is setup.check-lens's empty src/lensfind.* entry. pyflakes reports nothing on motion.py, manifest.py, test_manifest.py or laser_lifecycle_test.py. The full forgetest suite was not run. Fingerprints, measured against the parent: 17 tests move with motion.py's shared text: the 11 left in motion.py, and homing.manual, laser's four and setup.check-envelope, whose modules read it. The pending pin bumps from forgectrl c274f19 and forgeext 2d0391e already owe all 17; this deletion adds none to the owed list. The two leftovers move no fingerprint. releases/ is unchanged.
This commit is contained in:
@@ -162,6 +162,9 @@ HOST_TESTED = [
|
|||||||
# The event stream: a button edge, a lease changing hands, telemetry
|
# The event stream: a button edge, a lease changing hands, telemetry
|
||||||
# (the live order, the take before the free, is tests/host_lease.sh).
|
# (the live order, the take before the free, is tests/host_lease.sh).
|
||||||
("forgectrl", "src/events.*", "forgectrl:tests/events_test.c", "forgectrl:.github/workflows/build.yml"),
|
("forgectrl", "src/events.*", "forgectrl:tests/events_test.c", "forgectrl:.github/workflows/build.yml"),
|
||||||
|
# The crumb tray's mode as the daemon keeps it: its offset and what it
|
||||||
|
# refuses (the route and the panel's switch are tests/host_routes.sh).
|
||||||
|
("forgectrl", "src/tray.*", "forgectrl:tests/tray_test.c", "forgectrl:.github/workflows/build.yml"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# This repository's root, when the package runs from a checkout of it; a
|
# This repository's root, when the package runs from a checkout of it; a
|
||||||
|
|||||||
@@ -23,5 +23,4 @@ from . import cloud # noqa: F401,E402
|
|||||||
from . import exthost # noqa: F401,E402
|
from . import exthost # noqa: F401,E402
|
||||||
from . import homeoff # noqa: F401,E402
|
from . import homeoff # noqa: F401,E402
|
||||||
from . import bedsize # noqa: F401,E402
|
from . import bedsize # noqa: F401,E402
|
||||||
from . import tray # noqa: F401,E402
|
|
||||||
from . import tlsrec # noqa: F401,E402
|
from . import tlsrec # noqa: F401,E402
|
||||||
|
|||||||
@@ -1753,171 +1753,6 @@ def _words(body):
|
|||||||
return body if isinstance(body, str) else json.dumps(body)
|
return body if isinstance(body, str) else json.dumps(body)
|
||||||
|
|
||||||
|
|
||||||
@test("motion.port-jog", title="The controller port: the panel jogs beside a connected Grbl client",
|
|
||||||
subsystem="motion", kind="auto", mode="grbl", est_min=4,
|
|
||||||
covers=_MOTION_COVERS + [("forgectrl", "src/grblport.*"), ("forgectrl", "src/main.c"),
|
|
||||||
("forgectrl", "src/status.*")],
|
|
||||||
requires=["motion.jog-roundtrip"],
|
|
||||||
steps=["Bed clear; the head needs 180 mm of free travel toward +X. Nobody touches the gantry."],
|
|
||||||
description="With the suite connected as the Grbl client, POST /motion/jog moves the head by "
|
|
||||||
"what was asked (the kernel's counters are the witness), the client is told "
|
|
||||||
"([MSG:Panel jog]) and is not displaced: its next line still draws ok. "
|
|
||||||
"GET /motion/state agrees with the kernel. With the client polling the way "
|
|
||||||
"LightBurn does ('?' and an end of line), no port jog is refused for it, a 30 mm "
|
|
||||||
"jog runs whole, and every poll draws its ok. POST /motion/cancel stops a long jog "
|
|
||||||
"short. The client goes first: its line during a port jog cancels the jog and "
|
|
||||||
"draws ok, never an error. A jog past the per-request bound is refused with "
|
|
||||||
"nothing moved. POST /motion/release takes the step currents to 0 and /status "
|
|
||||||
"says so, a jog is then refused in words, and POST /motion/energize restores "
|
|
||||||
"the hold currents. The head is returned to where it was found.")
|
|
||||||
def port_jog(ctx):
|
|
||||||
ev = ctx.evidence
|
|
||||||
fc = hw.Forgectrl()
|
|
||||||
with ctx.grbl() as g:
|
|
||||||
clean_slate(ctx, g)
|
|
||||||
x0, y0 = kernel_start(ctx)
|
|
||||||
ctx.sleep(0.6) # past the port's sender-quiet interval
|
|
||||||
|
|
||||||
# A bounded jog, beside the client.
|
|
||||||
st, body = fc.post("/motion/jog", params={"x": "10", "feed": "2400"})
|
|
||||||
ctx.check(st == 200, "POST /motion/jog x=10 -> %s %s", st, _words(body))
|
|
||||||
_port_idle(ctx, fc)
|
|
||||||
x1, y1 = kernel_xy_mm(ctx)
|
|
||||||
ps = _port_state(fc)
|
|
||||||
ev["jog"] = {"kernel": [x0, x1], "port_mpos": ps.get("mpos"), "sender": ps.get("sender")}
|
|
||||||
ctx.log("port jog +10: kernel X %.3f -> %.3f, port state %s", x0, x1, ps)
|
|
||||||
ctx.check(abs((x1 - x0) - 10.0) < 0.1, "the kernel moved %.3f mm, not 10", x1 - x0)
|
|
||||||
ctx.check(abs(y1 - y0) < 0.05, "Y moved on an X jog: %.3f -> %.3f", y0, y1)
|
|
||||||
ctx.check(ps.get("sender") is True, "the port does not see the connected client: %s", ps)
|
|
||||||
text = drain_text(g, 0.5)
|
|
||||||
ctx.check("Panel jog" in text, "the client was not told of the panel's jog: %r", text)
|
|
||||||
r = g.command("G4 P0")
|
|
||||||
ctx.check("ok" in r and not any(x.startswith("error") for x in r),
|
|
||||||
"the client was displaced or refused after a port jog: %s", r)
|
|
||||||
|
|
||||||
# The client polls the way LightBurn does: '?' with an end of line
|
|
||||||
# behind it. The '?' is a realtime character; the end of line is an
|
|
||||||
# empty line, and an empty line is not the client speaking. No jog
|
|
||||||
# is refused for it, a jog runs whole under it, each poll draws its ok.
|
|
||||||
ctx.sleep(0.6)
|
|
||||||
g.drain()
|
|
||||||
polls, refused = 0, []
|
|
||||||
for _ in range(8):
|
|
||||||
g.send_raw(b"?\n")
|
|
||||||
polls += 1
|
|
||||||
st, body = fc.post("/motion/jog", params={"x": "0.5", "feed": "3000"})
|
|
||||||
if st != 200:
|
|
||||||
refused.append(_words(body))
|
|
||||||
ctx.sleep(0.12)
|
|
||||||
ctx.check(not refused, "%d of 8 port jogs were refused under a '?'+EOL poll: %s", len(refused), refused[:2])
|
|
||||||
_port_idle(ctx, fc)
|
|
||||||
xa = kernel_xy_mm(ctx)[0]
|
|
||||||
st, body = fc.post("/motion/jog", params={"x": "30", "feed": "1200"}) # 1.5 s: several polls long
|
|
||||||
ctx.check(st == 200, "the jog under the poll -> %s %s", st, _words(body))
|
|
||||||
in_jog = 0
|
|
||||||
deadline = time.time() + 6
|
|
||||||
while time.time() < deadline:
|
|
||||||
g.send_raw(b"?\n")
|
|
||||||
polls += 1
|
|
||||||
ctx.sleep(0.25)
|
|
||||||
ps = _port_state(fc)
|
|
||||||
if ps["state"] == "Idle" and not ps["port_jog"]:
|
|
||||||
break
|
|
||||||
in_jog += 1
|
|
||||||
_port_idle(ctx, fc)
|
|
||||||
xb = kernel_xy_mm(ctx)[0]
|
|
||||||
oks = sum(1 for line in drain_text(g, 1.0).splitlines() if line.strip() == "ok")
|
|
||||||
ev["poll"] = {"polls": polls, "oks": oks, "polls_in_jog": in_jog, "kernel": [xa, xb]}
|
|
||||||
ctx.log("'?'+EOL poll: 8 of 8 jogs accepted; a 30 mm jog moved %.3f with %d polls into it; "
|
|
||||||
"%d polls drew %d oks", xb - xa, in_jog, polls, oks)
|
|
||||||
ctx.check(in_jog >= 3, "only %d polls landed inside the jog: the case did not test it", in_jog)
|
|
||||||
ctx.check(abs((xb - xa) - 30.0) < 0.1, "a status poll cut the port jog short: moved %.3f of 30", xb - xa)
|
|
||||||
ctx.check(oks == polls, "%d polls drew %d oks", polls, oks)
|
|
||||||
x1 = xb
|
|
||||||
|
|
||||||
# Past the bound: refused at the door, nothing moved.
|
|
||||||
st, body = fc.post("/motion/jog", params={"x": "101"})
|
|
||||||
ev["past_bound"] = [st, _words(body)]
|
|
||||||
ctx.check(st == 400, "a 101 mm jog -> %s %s, expected 400", st, _words(body))
|
|
||||||
ctx.check(abs(kernel_xy_mm(ctx)[0] - x1) < 0.01, "a refused jog moved the head")
|
|
||||||
|
|
||||||
# The cancel stops a long jog short.
|
|
||||||
ctx.sleep(0.6)
|
|
||||||
st, body = fc.post("/motion/jog", params={"x": "40", "feed": "600"})
|
|
||||||
ctx.check(st == 200, "the long jog -> %s %s", st, _words(body))
|
|
||||||
ctx.sleep(1.0)
|
|
||||||
st, body = fc.post("/motion/cancel")
|
|
||||||
ctx.check(st == 200, "POST /motion/cancel -> %s %s", st, _words(body))
|
|
||||||
_port_idle(ctx, fc)
|
|
||||||
x2 = kernel_xy_mm(ctx)[0]
|
|
||||||
ev["cancel"] = [x1, x2]
|
|
||||||
ctx.log("canceled a 40 mm jog at %.3f mm", x2 - x1)
|
|
||||||
ctx.check(1.0 < x2 - x1 < 35.0, "the cancel did not stop the jog short: moved %.3f of 40", x2 - x1)
|
|
||||||
|
|
||||||
# The client goes first: its line cancels the port's jog and draws ok.
|
|
||||||
# A fast jog, so the cancel is a real deceleration: the core stays in
|
|
||||||
# the jog state for the length of it, and a client line read in that
|
|
||||||
# window is the error:9 the hold exists to prevent. A slow jog stops
|
|
||||||
# at once and would pass with no hold at all.
|
|
||||||
g.drain()
|
|
||||||
ctx.sleep(0.6)
|
|
||||||
st, body = fc.post("/motion/jog", params={"x": "100", "feed": "6000"})
|
|
||||||
ctx.check(st == 200, "the fast jog -> %s %s", st, _words(body))
|
|
||||||
ctx.sleep(0.3)
|
|
||||||
r = g.command("G4 P0", timeout=15)
|
|
||||||
_port_idle(ctx, fc)
|
|
||||||
x3 = kernel_xy_mm(ctx)[0]
|
|
||||||
ev["sender_first"] = {"reply": r, "kernel": [x2, x3]}
|
|
||||||
ctx.log("the client's line during a port jog drew %s; the jog stopped at %.3f of 100", r, x3 - x2)
|
|
||||||
ctx.check("ok" in r and not any(x.startswith("error") for x in r),
|
|
||||||
"the client's line during a port jog drew %s, not ok", r)
|
|
||||||
ctx.check(2.0 < x3 - x2 < 95.0, "the client's line did not cancel the port jog: moved %.3f", x3 - x2)
|
|
||||||
|
|
||||||
# The panel's own: the release and its end, by their routes.
|
|
||||||
ctx.sleep(0.6)
|
|
||||||
st, body = fc.post("/motion/release")
|
|
||||||
ctx.check(st == 200, "POST /motion/release -> %s %s", st, _words(body))
|
|
||||||
ctx.sleep(0.5)
|
|
||||||
ev["released"] = {"currents": list(_step_currents()),
|
|
||||||
"status": fc.status().get("motors_released"),
|
|
||||||
"port": _port_state(fc).get("released")}
|
|
||||||
ctx.check(ev["released"]["currents"] == ["0", "0"],
|
|
||||||
"the step currents read %s after the release", ev["released"]["currents"])
|
|
||||||
ctx.check(ev["released"]["status"] is True and ev["released"]["port"] is True,
|
|
||||||
"the release is not reported: %s", ev["released"])
|
|
||||||
k0 = _kernel_position()
|
|
||||||
st, body = fc.post("/motion/jog", params={"x": "1"})
|
|
||||||
ev["jog_released"] = [st, _words(body)]
|
|
||||||
ctx.check(st == 409 and "released" in _words(body),
|
|
||||||
"a jog while released -> %s %s", st, _words(body))
|
|
||||||
ctx.check(_kernel_position() == k0, "the kernel played something while released")
|
|
||||||
st, body = fc.post("/motion/energize")
|
|
||||||
ctx.check(st == 200, "POST /motion/energize -> %s %s", st, _words(body))
|
|
||||||
ctx.sleep(0.5)
|
|
||||||
ev["energized"] = {"currents": list(_step_currents()),
|
|
||||||
"status": fc.status().get("motors_released"),
|
|
||||||
"faults": hw.sysfs_int("cnc/faults")}
|
|
||||||
ctx.check(tuple(ev["energized"]["currents"]) == HOLD_CURRENTS and ev["energized"]["status"] is False,
|
|
||||||
"not energized: %s", ev["energized"])
|
|
||||||
ctx.check(ev["energized"]["faults"] == 0, "a driver fault on energize: %s", ev["energized"]["faults"])
|
|
||||||
|
|
||||||
# Back to where the head was found, by the kernel's own measure.
|
|
||||||
ctx.sleep(0.6)
|
|
||||||
for _ in range(3): # one request moves 100 mm at most
|
|
||||||
back = x0 - kernel_xy_mm(ctx)[0]
|
|
||||||
if abs(back) < 0.05:
|
|
||||||
break
|
|
||||||
st, body = fc.post("/motion/jog", params={"x": "%.3f" % max(-100.0, min(100.0, back)),
|
|
||||||
"feed": "2400"})
|
|
||||||
ctx.check(st == 200, "the return jog -> %s %s", st, _words(body))
|
|
||||||
_port_idle(ctx, fc)
|
|
||||||
ctx.sleep(0.6)
|
|
||||||
check_kernel_returned(ctx, ev, (x0, y0), tag="port")
|
|
||||||
machine_idle(ctx)
|
|
||||||
ctx.log("PASS: the port jogged beside the client, the cancel and the client's own line each stopped "
|
|
||||||
"a jog short, the bound held, and the release and the energize went through their routes")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- the job runner
|
# ---------------------------------------------------------------- the job runner
|
||||||
|
|
||||||
def _job(fc):
|
def _job(fc):
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
# Copyright 2026 514 LLC d/b/a OpenGlow
|
|
||||||
# Written by Scott Wiederhold
|
|
||||||
# https://community.openglow.org
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
"""The crumb tray's mode, on the machine.
|
|
||||||
|
|
||||||
Its own module, so that no other test's fingerprint moves. With the tray
|
|
||||||
out, the GRBL controller's Z is the focal height above the floor of the cut
|
|
||||||
area: its Z position and its Z range move up by tray_offset_mm on the lens
|
|
||||||
step grid, and nothing moves. The mode persists as the controller's marker
|
|
||||||
in the data directory. That it survives a controller restart is the
|
|
||||||
driver's harness case (tray_test.py): a restart here would drop the X and
|
|
||||||
Y reference, and the machine would not be handed back as it was found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
from ..catalog import test
|
|
||||||
from .. import hw
|
|
||||||
from .motion import (_kernel_position, _port_state, _words, clean_slate, drain_text, machine_idle)
|
|
||||||
|
|
||||||
TRAY_MARKER = "/data/forgefirm/tray.out"
|
|
||||||
LENS_SPM = 2.922 # $102: the lens screw's half-steps per mm
|
|
||||||
SETUP_CARD = "laser.focus" # a setup burn: it asks before it moves anything
|
|
||||||
|
|
||||||
|
|
||||||
def _grid_mm(offset_mm):
|
|
||||||
"""The offset on the lens step grid, the controller's way (lroundf)."""
|
|
||||||
return math.floor(offset_mm * LENS_SPM + 0.5) / LENS_SPM
|
|
||||||
|
|
||||||
|
|
||||||
def _lens(fc):
|
|
||||||
return fc.status().get("lens") or {}
|
|
||||||
|
|
||||||
|
|
||||||
def _judge(ctx, fc, ev, key, mode, found, shift):
|
|
||||||
"""The machine reads the tray `mode`, with Z and the reach moved by
|
|
||||||
`shift` mm from what the test found."""
|
|
||||||
s = fc.status()
|
|
||||||
lens, pos = s.get("lens") or {}, s.get("pos") or {}
|
|
||||||
port = _port_state(fc)
|
|
||||||
rec = ev[key] = {"lens": lens, "pos_z": pos.get("z"), "port_tray": port.get("tray"),
|
|
||||||
"port_z": port.get("mpos", [None] * 3)[2], "marker": os.path.exists(TRAY_MARKER),
|
|
||||||
"home": [s.get("homed_axes"), s.get("home_source")]}
|
|
||||||
ctx.log("%s: %s", key, rec)
|
|
||||||
ctx.check(lens.get("tray") == mode and port.get("tray") == mode,
|
|
||||||
"%s: the status says tray %s and the port %s, not %s", key, lens.get("tray"), port.get("tray"), mode)
|
|
||||||
ctx.check(rec["marker"] == (mode == "out"), "%s: the marker %s is %s with the tray %s", key, TRAY_MARKER,
|
|
||||||
"present" if rec["marker"] else "absent", mode)
|
|
||||||
for k in ("reach_min", "reach_max"):
|
|
||||||
ctx.check(lens.get(k) is not None and abs(lens[k] - (found["lens"][k] + shift)) < 0.011,
|
|
||||||
"%s: %s reads %s, not %.2f", key, k, lens.get(k), found["lens"][k] + shift)
|
|
||||||
ctx.check(pos.get("z") is not None and abs(pos["z"] - (found["pos_z"] + shift)) < 0.006,
|
|
||||||
"%s: the status Z reads %s, not %.2f", key, pos.get("z"), found["pos_z"] + shift)
|
|
||||||
ctx.check(rec["port_z"] is not None and abs(rec["port_z"] - (found["port_z"] + shift)) < 0.002,
|
|
||||||
"%s: the controller's Z reads %s, not %.3f", key, rec["port_z"], found["port_z"] + shift)
|
|
||||||
ctx.check(rec["home"] == found["home"], "%s: the reference changed across the switch: %s, not %s",
|
|
||||||
key, rec["home"], found["home"])
|
|
||||||
|
|
||||||
|
|
||||||
def _tray_route(fc, mode):
|
|
||||||
st, body = fc.post("/motion/tray", params={"tray": mode})
|
|
||||||
return st, _words(body)
|
|
||||||
|
|
||||||
|
|
||||||
def _card_refused(ctx, fc, ev):
|
|
||||||
"""A setup card is refused while the tray is out. The machine is idle a
|
|
||||||
moment after the suite's Grbl client closes; until then the refusal is
|
|
||||||
the idle rule's. A card that started anyway is ended at once: it asks
|
|
||||||
before it moves anything."""
|
|
||||||
path = "/wiz/%s/start" % SETUP_CARD
|
|
||||||
deadline = time.time() + 20
|
|
||||||
while True:
|
|
||||||
st, body = fc.post(path)
|
|
||||||
words = _words(body)
|
|
||||||
if st == 200:
|
|
||||||
fc.post("/wiz/%s/abort" % SETUP_CARD)
|
|
||||||
ctx.wait_for(lambda: not (fc.get("/wiz/dark")[1] or {}).get("running"), 20, poll=0.5)
|
|
||||||
if st != 409 or "not idle" not in words or time.time() > deadline:
|
|
||||||
break
|
|
||||||
ctx.sleep(0.5)
|
|
||||||
ev["setup_card"] = [st, words]
|
|
||||||
ctx.log("a setup card started with the tray out -> %s %s", st, words)
|
|
||||||
ctx.check(st == 409 and "crumb tray" in words, "a setup card started with the tray out -> %s %s", st, words)
|
|
||||||
|
|
||||||
|
|
||||||
@test("motion.tray", title="The crumb tray: tray out moves Z and its range up by the offset, and nothing moves",
|
|
||||||
subsystem="motion", kind="auto", mode="grbl", est_min=2,
|
|
||||||
covers=[("grblhal-glowforge", "src/glowforge_tray.*"), ("grblhal-glowforge", "src/glowforge_homing.*"),
|
|
||||||
("grblhal-glowforge", "src/ctlport.*"), ("grblhal-glowforge", "src/glowforge_io.*"),
|
|
||||||
("forgectrl", "src/tray.*"), ("forgectrl", "src/grblport.*"), ("forgectrl", "src/status.*"),
|
|
||||||
("forgectrl", "src/main.c"), ("forgectrl", "src/wizdark.*"), ("forgectrl", "src/ui/panel.js"),
|
|
||||||
("forgectrl", "src/ui/index.html")],
|
|
||||||
requires=["motion.port-jog"],
|
|
||||||
steps=["Nothing moves; the crumb tray can stay where it is."],
|
|
||||||
description="The tray mode is switched to the other one and back, through the panel's route and "
|
|
||||||
"through M103 from a Grbl client, both ways. Each switch changes the numbers and "
|
|
||||||
"moves nothing: /status says the tray, its Z and the lens reach move by the offset on "
|
|
||||||
"the lens step grid (1.35 in is 100 half-steps, 34.22 mm), the reference and what set it "
|
|
||||||
"stand, the controller port's state "
|
|
||||||
"agrees, the marker in the data directory follows, the client is told [MSG:Tray in] or "
|
|
||||||
"[MSG:Tray out] and its next line draws ok, and the kernel's position and byte counters "
|
|
||||||
"do not move. In the other mode, a jog to the Z the lens read before the switch is "
|
|
||||||
"refused (error:15): the two Z ranges never overlap; back in the mode found, the same "
|
|
||||||
"jog is taken. A setup card started with the tray out is refused in the operator's "
|
|
||||||
"words, with nothing sent. The page "
|
|
||||||
"the panel serves carries the switch. The tray mode is put back as found in a finally. "
|
|
||||||
"The controller is not restarted: that would drop the X and Y reference. That the mode "
|
|
||||||
"survives a restart, the errors of M103, and its wait for the move before it are the "
|
|
||||||
"driver's harness cases.")
|
|
||||||
def tray(ctx):
|
|
||||||
fc = ctx.forgectrl
|
|
||||||
ev = ctx.evidence
|
|
||||||
s0 = fc.status()
|
|
||||||
found_mode = (s0.get("lens") or {}).get("tray")
|
|
||||||
ctx.check(found_mode in ("in", "out"), "the status carries no tray mode: %s", s0.get("lens"))
|
|
||||||
other = "out" if found_mode == "in" else "in"
|
|
||||||
p_found = "P1" if found_mode == "out" else "P0"
|
|
||||||
p_other = "P1" if other == "out" else "P0"
|
|
||||||
shift = _grid_mm((s0.get("lens") or {}).get("tray_offset_mm") or 34.29) * (1 if other == "out" else -1)
|
|
||||||
machine_idle(ctx)
|
|
||||||
found = {"lens": s0.get("lens"), "pos_z": (s0.get("pos") or {}).get("z"),
|
|
||||||
"port_z": _port_state(fc)["mpos"][2], "kernel": _kernel_position(),
|
|
||||||
"home": [s0.get("homed_axes"), s0.get("home_source")]}
|
|
||||||
ev["found"] = {"tray": found_mode, "shift_mm": round(shift, 4), **found}
|
|
||||||
ctx.log("found the tray %s; switching to %s moves Z %+.3f mm", found_mode, other, shift)
|
|
||||||
ctx.check(found["pos_z"] is not None, "the status carries no Z")
|
|
||||||
|
|
||||||
st, page = fc.get("/", raw=True)
|
|
||||||
text = page.decode("utf-8", "replace") if isinstance(page, bytes) else str(page)
|
|
||||||
for want in ('id="traybtn"', "function trayToggle", "/motion/tray?tray="):
|
|
||||||
ctx.check(st == 200 and want in text, "the panel page carries no %s", want)
|
|
||||||
|
|
||||||
switched = False
|
|
||||||
try:
|
|
||||||
if found_mode == "out":
|
|
||||||
_card_refused(ctx, fc, ev)
|
|
||||||
with ctx.grbl() as g:
|
|
||||||
clean_slate(ctx, g)
|
|
||||||
ctx.sleep(0.6) # past the port's sender-quiet interval
|
|
||||||
|
|
||||||
# Through the panel's route.
|
|
||||||
st, words = _tray_route(fc, other)
|
|
||||||
ctx.check(st == 200, "POST /motion/tray tray=%s -> %s %s", other, st, words)
|
|
||||||
switched = True
|
|
||||||
said = drain_text(g, 0.5)
|
|
||||||
ctx.check("[MSG:Tray %s]" % other in said, "the client was not told the tray is %s: %r", other, said)
|
|
||||||
_judge(ctx, fc, ev, "route_" + other, other, found, shift)
|
|
||||||
r = g.command("G4 P0")
|
|
||||||
ctx.check(r[-1:] == ["ok"], "the client's line after the switch drew %s", r)
|
|
||||||
|
|
||||||
# The Z the lens read before the switch is outside this mode's range.
|
|
||||||
jog = "$J=G90 G21 Z%.3f F200" % found["port_z"]
|
|
||||||
r = g.command(jog)
|
|
||||||
ev["jog_" + other] = [jog, r]
|
|
||||||
ctx.check(r[-1:] == ["error:15"], "with the tray %s, %s drew %s, not error:15", other, jog, r)
|
|
||||||
g.command("") # the empty line that acknowledges the error
|
|
||||||
|
|
||||||
# Back through M103, from the client.
|
|
||||||
r = g.command("M103 " + p_found)
|
|
||||||
ev["m103_" + found_mode] = r
|
|
||||||
ctx.check(r[-1:] == ["ok"] and "[MSG:Tray %s]" % found_mode in r,
|
|
||||||
"M103 %s drew %s", p_found, r)
|
|
||||||
switched = False
|
|
||||||
_judge(ctx, fc, ev, "m103_" + found_mode, found_mode, found, 0.0)
|
|
||||||
r = g.command(jog)
|
|
||||||
ev["jog_" + found_mode] = [jog, r]
|
|
||||||
ctx.check(r[-1:] == ["ok"], "with the tray %s again, %s drew %s", found_mode, jog, r)
|
|
||||||
machine_idle(ctx)
|
|
||||||
|
|
||||||
# And the other way: M103 out, the route back.
|
|
||||||
r = g.command("M103 " + p_other)
|
|
||||||
ev["m103_" + other] = r
|
|
||||||
ctx.check(r[-1:] == ["ok"] and "[MSG:Tray %s]" % other in r, "M103 %s drew %s", p_other, r)
|
|
||||||
switched = True
|
|
||||||
_judge(ctx, fc, ev, "m103_" + other, other, found, shift)
|
|
||||||
|
|
||||||
if other == "out":
|
|
||||||
_card_refused(ctx, fc, ev)
|
|
||||||
|
|
||||||
st, words = _tray_route(fc, found_mode)
|
|
||||||
ctx.check(st == 200, "POST /motion/tray tray=%s -> %s %s", found_mode, st, words)
|
|
||||||
switched = False
|
|
||||||
_judge(ctx, fc, ev, "route_" + found_mode, found_mode, found, 0.0)
|
|
||||||
finally:
|
|
||||||
if switched:
|
|
||||||
# The route is refused for a moment after the suite's Grbl client closes.
|
|
||||||
first = sys.exc_info()[1]
|
|
||||||
took = ctx.wait_for(lambda: _tray_route(fc, found_mode)[0] == 200 or None, 20, poll=0.5)
|
|
||||||
ctx.log("put the tray back %s -> %s", found_mode, "taken" if took is not None else "not taken")
|
|
||||||
ev["put_back"] = took is not None
|
|
||||||
if first is None:
|
|
||||||
ctx.check(took is not None, "the tray mode is left %s, not put back %s", other, found_mode)
|
|
||||||
machine_idle(ctx)
|
|
||||||
k1 = _kernel_position()
|
|
||||||
ev["kernel"] = [found["kernel"], k1]
|
|
||||||
ctx.check(k1 == found["kernel"], "the kernel moved across the switches: %s -> %s", found["kernel"], k1)
|
|
||||||
ctx.log("PASS: the tray mode switched both ways through the route and through M103, Z and the reach "
|
|
||||||
"moved by %.3f mm and back, nothing moved, and it is %s as found", abs(shift), found_mode)
|
|
||||||
@@ -425,10 +425,6 @@ class CatalogTests(unittest.TestCase):
|
|||||||
self.assertEqual(len(t.source_sha), 64)
|
self.assertEqual(len(t.source_sha), 64)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
|
|
||||||
|
|
||||||
class ImplementationHashTests(unittest.TestCase):
|
class ImplementationHashTests(unittest.TestCase):
|
||||||
"""A test's implementation hash is its own function plus the code its
|
"""A test's implementation hash is its own function plus the code its
|
||||||
module shares: a body edit moves one test, a helper edit moves them
|
module shares: a body edit moves one test, a helper edit moves them
|
||||||
@@ -530,3 +526,7 @@ def two(ctx):
|
|||||||
for t in catalog.all_tests(reg):
|
for t in catalog.all_tests(reg):
|
||||||
path = inspect.getsourcefile(t.fn)
|
path = inspect.getsourcefile(t.fn)
|
||||||
self.assertIn(t.id, catalog.module_parts(path)[1], t.id)
|
self.assertIn(t.id, catalog.module_parts(path)[1], t.id)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|||||||
@@ -629,8 +629,8 @@ def test_button_wait_arms():
|
|||||||
|
|
||||||
def test_lid_open_in_wait():
|
def test_lid_open_in_wait():
|
||||||
"""Rule 6b: the lid opening during the button wait cancels the job:
|
"""Rule 6b: the lid opening during the button wait cancels the job:
|
||||||
the reason is reported, an alarm is raised, the window never opens.
|
the lid-open cancel is reported, no alarm is raised, the window never
|
||||||
A press with the lid still open must not arm either."""
|
opens. A press with the lid still open must not arm either."""
|
||||||
s = Session("lid-in-wait", disarm_s=60, switches=SW_CLOSED)
|
s = Session("lid-in-wait", disarm_s=60, switches=SW_CLOSED)
|
||||||
try:
|
try:
|
||||||
s.send_raw("M4 S100")
|
s.send_raw("M4 S100")
|
||||||
|
|||||||
Reference in New Issue
Block a user