mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-29 01:21:16 -07:00
tray_test.py and motion.tray: the crumb tray's mode on the null-sink controller and on the machine
- scripts/bench/tray_test.py, run by the driver's CI: M103 P1 and P0 move Z and its envelope by tray_offset_mm on the lens step grid, tell the sender, write and remove the marker beside the shared config, and ship no step; a missing P, P2, P0.5, P-1, and an axis word are errors that change nothing; the switch behind a move waits for the move; the port's tray op with a sender connected, its count exact, refused out of form, during a program, and at a package's M-code; the mode across a soft reset and a killed controller, a start over the marker taking the lens reference in the tray-out frame; the offset's range; an unreferenced lens; a camera home in both modes handing the runner the same park. The kernel-idle half of the barrier needs the pulse device: a null sink is always idle. - scripts/bench/ctlport_test.py: the dark case carries tray out and in under an open armed window. - forgetest: motion.tray in its own module, suite/tray.py, so no other test's fingerprint moves. It switches the mode both ways through the panel's route and through M103 from a Grbl client and judges each switch: /status's mode, Z, and reach moved by the offset's grid, the port's state, the marker, the reference and its source unchanged, the client told, the kernel's counters unmoved. A jog to the Z the lens read before a switch is refused in the other mode and taken back in the mode found; a setup card started with the tray out is refused in the operator's words; the panel page carries the switch. The mode goes back as found in a finally. It does not restart the controller, which would drop the X and Y reference: persistence is tray_test.py's. - The bench registry and README list tray_test.py. Proof: tray_test.py ALL PASS against the driver with the mode, and six driver copies each missing one part of it fail. The driver's whole CI harness list passes. The forgetest unit suite: 500 tests OK. The coverage lint over the working trees: nothing uncovered, 126 tests. On the bench reference, with the binaries hot-deployed on the fx18 image: motion.tray PASS, and FAIL against the driver whose anchor lost its source word on the second switch. Acceptance. motion.tray is new; the suite and bench files are in no other test's fingerprint.
This commit is contained in:
@@ -36,6 +36,7 @@ page's takeover does that; from a host, stop them first.
|
||||
| `ctlport_test.py` | Host-side controller port and line-multiplexer harness (null-sink controller): a scripted sender that counts every `ok` and `error` runs beside the port's one client on `grbl.ctl`. A port jog's status goes to the port and never to the sender, a port error does not reach the sender's parser, a sender line cancels a port jog and gets its own `ok` (a line queued right behind the port's included), plus the refusals, the single client, a CR LF sender, a soft reset, and the client as the dead-man. `python3 ctlport_test.py <grblHAL_glowforge>`. |
|
||||
| `cool_report_test.py` | Host-side cooling report harness (null-sink controller, a stand-in for forgectrl's listener on `FORGECTRL_PORT`): every `POST /cool/state` carries the secret the supervisor hands the controller at its spawn (`GF_REPORT_SECRET`) as `X-ForgeFIRM-Report`; with no secret the header is absent; a value that is not 32 hex digits, a CR LF with a header behind it included, never reaches the wire; and the homing runner the controller starts is handed the secret (it reports in the controller's place during a gfcloud `$H`), and none when the controller has none or a malformed one. `python3 cool_report_test.py <grblHAL_glowforge>`. |
|
||||
| `manual_home_test.py` | Host-side manual homing and motor release harness (null-sink controller, `GFSINK_DUMP` and `GFSINK_ATTR_LOG`): `$H` under `homing_mode = manual` ships nothing and declares X0 Y0, and with a manual home offset declares minus it and jogs dark to the origin before it answers, a jog cancel ending the move short; while the motors are released (`$MD`) every motion source is refused and ships nothing, `$X` and a soft reset do not unlock it, and only `$ME` and a manual `$H` write the energize. Imports its sender and port client from `ctlport_test.py`. `python3 manual_home_test.py <grblHAL_glowforge>`. |
|
||||
| `tray_test.py` | Host-side crumb tray harness (null-sink controller, `GFSINK_DUMP`): `M103 P1` and `P0` move Z and its envelope by `tray_offset_mm` on the lens step grid, tell the sender, write and remove the `tray.out` marker beside the shared config, and ship no step; the errors of `M103`; the switch waits for the move before it; the port's `tray` op and its refusals; the mode across a soft reset and a controller restart; the offset's range; an unreferenced lens; a camera home in both modes. Imports its sender and port client from `ctlport_test.py`. `python3 tray_test.py <grblHAL_glowforge>`. |
|
||||
| `live_fire_drills.py` | **LIVE LASER** drills, on the board (the bench page) or from a LAN host (`GF_HOST`): `live_fire_drills.py <drill> [S] [F]` - `witness` (emission witness, lid-IR peaks vs the ambient baseline, HV current, job-based disarm on M2), `hold` (disarm grace in Hold), `faultpos` (armed job refuses a stale origin after an underrun), `ircut` (lid-IR characterization cut at S/F), `pthresh` (laser power-threshold ladder: 13 constant-power rungs from 2 % to 30 % of full on scrap; the lowest rung that marks is the tube's striking threshold and reads directly as the `$35` value - requires `$35` = 0 for the run), `dladder` (density ladder at a chosen base period), `pcurve` (laser performance-curve ladder: one 100 mm line per level at 10 mm/s under M3, the laser off between rungs and a mid-ladder rung repeated at the end; reads `pic/hv_current` and the head thermopile `head/beam_detect_analog` (a scatter detector in the beam path upstream of the final mirror, so it sees the beam, not the material) from sysfs at ~25 Hz on the board, brackets each rung on the controller's Run/Idle states, and reports per rung the current with a clipped-at-1023 flag, the thermopile delta over its laser-off baseline and in-line drift, then the normalized curve, monotonicity, a line fit with its threshold intercept and the repeat-rung drift; JSON record with the raw trace in the bench data directory; rungs follow `laser_power_model`, a comma list overrides; a curve measurement wants `$35` = 0), `dpatch [F] [pitch] [length]` (depth witness for the density dose curve: two rows of small serpentine-filled patches, row A CW at feeds giving relative doses 1.0 to 0.25 of the reference feed, row B at the reference feed at 100/80/60/45/30 % density; the operator matches each row-B patch to the row-A patch of equal depth, which reads the density's light fraction off the material beside the thermopile's prediction; JSON record), `m4feeds [S] [F1] [F2]` (the density time base across feeds: one out-and-back line pair per feed at the same S under M4 density, one armed run; the operator reads within-line evenness and reversal darkness at both feeds - M4's velocity scaling is what holds dose per mm through the accel), `m4corner [S] [F]` (M4 velocity-scaled power into corners: a corner-heavy vector pattern at 30 % under M4 density, one armed run; the operator confirms every commanded segment marks - the floor makes a dropout unreachable - and the drill asserts the arm report, one discharge window and dark after), `m5dark` (the rapids after an M5 ship dark: one 20 mm line at M3 S400, M5, dwell, rapid back, dwell, rapid forward; PASS when the 25 Hz current trace shows one discharge segment and reads dark after the M5 and `laser_on_sampled` never re-lights; the catalog's `laser.m5-rapid-dark` is its port), `flowload` (cooling under laser load, one armed run per invocation, the conf keys it writes put back at the end, the pump never commanded off: `t1` reproduces the flow-check trip with the check on at its defaults and two 30 x 4 mm CW fills at F1500 starting on the press with no dark dwell, and reports the engine's rise/dT verdict beside the 25 Hz trace of both coolant sensors, the current, the digital witness and the heater output in 5 s bins across the window, with the shape at fire start; `t2 <secs> [pct]` runs with the check off and one fill of about `secs` lit seconds at CW or at `pct` density, and reports the lag to each sensor, the rise per raw-second of `hv_current` and what a full 50 s window would add against the 1.6 C margin; `fit` fits rise against dose over every t2 record; JSON records), `expstop` (armed kill on the expected-stop path; needs the panel token - `GF_TOKEN`, or the board's token file) and `ctrlstart` (the separate controller restart after it). Every drill waits for the operator's physical arm press; eye protection, fire watch, extinguisher, and exhaust are mandatory. |
|
||||
| `pacing_test.py` | Protocol-loop pacing check (runs on the board, dry motion): idle and parked-in-Hold states are coarse-paced, active motion is tight-paced, and a feed-hold/resume mid-move preserves position with no feeder starve. |
|
||||
| `planner_blocks_test.py` | Host-side planner buffer depth harness (null-sink controller): `$398` written to 400 and to 1000, the controller restarted on the same settings store each time, and it must answer on the port, report the depth in its status report and run a move. A byte-wide ring index once made 255 blocks or more spin the start. Runs in the grblHAL repo's CI. |
|
||||
|
||||
@@ -33,8 +33,8 @@ job after it.
|
||||
8. the port's client is the dead-man: closing it cancels its jog
|
||||
9. every port operation ships dark. Under an open armed window, with M3
|
||||
modal and S above zero, a port jog, a canceled port jog, the refused
|
||||
jogs, and the panel operations add steps to the shipped stream and not
|
||||
one FIRE tick. A jog block carries the modal spindle state, so the
|
||||
jogs, and the panel operations (the tray's mode among them) add steps
|
||||
to the shipped stream and not one FIRE tick. A jog block carries the modal spindle state, so the
|
||||
stream's mask on jogging is the only thing that keeps a jog dark, and
|
||||
this is what holds the port to it
|
||||
10. "sender out" keeps senders from the network out, and only on an idle
|
||||
@@ -593,6 +593,8 @@ def test_port_dark():
|
||||
replies["release"] = s.port.request("release") # refused: a laser job is armed
|
||||
replies["energize"] = s.port.request("energize")
|
||||
replies["home"] = s.port.request("home") # manual: declares X0 Y0, moves nothing
|
||||
replies["tray out"] = s.port.request("tray out") # the Z frame moves, the head does not
|
||||
replies["tray in"] = s.port.request("tray in")
|
||||
replies["jog-after-home"] = s.port.request("jog G91 X6 F1200")
|
||||
s.sender.wait_state("Idle")
|
||||
time.sleep(0.5)
|
||||
@@ -600,6 +602,8 @@ def test_port_dark():
|
||||
steps1, fire1 = s.ticks()
|
||||
if replies["jog"] != "ok" or replies["jog-long"] != "ok" or replies["jog-after-home"] != "ok":
|
||||
fail("[dark] a port jog under the open window was refused: %r" % replies)
|
||||
if replies["tray out"] != "ok" or replies["tray in"] != "ok":
|
||||
fail("[dark] the tray op under the open window was refused: %r" % replies)
|
||||
if steps1 <= steps0:
|
||||
fail("[dark] the port jogs shipped no step: the case would prove nothing")
|
||||
if s.sender.saw("laser disarmed"):
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2026 514 LLC d/b/a OpenGlow
|
||||
# Written by Scott Wiederhold
|
||||
# https://community.openglow.org
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Host-side verification of the crumb tray's mode (glowforge_tray.c).
|
||||
|
||||
With the tray out the work sits on the floor of the cut area, and Z is the
|
||||
focal height above that floor: the Z position and the Z envelope move up by
|
||||
tray_offset_mm on the lens step grid (1.35" is 100 half-steps). M103 P1 sets
|
||||
the tray out, M103 P0 sets it in, and the controller port's "tray out|in"
|
||||
runs the same line for the panel. Runs the native grblHAL_glowforge binary
|
||||
in null-sink mode, with GFSINK_DUMP capturing the shipped stream, a scripted
|
||||
sender on the Grbl socket, and the port client of ctlport_test.py:
|
||||
|
||||
1. M103 P1 and P0 shift the reported Z by the offset's whole half-steps,
|
||||
tell the sender [MSG:Tray out] and [MSG:Tray in], keep Z referenced,
|
||||
write and remove the marker beside the shared config, and ship no
|
||||
step tick; asking for the mode already set changes nothing and says so
|
||||
2. the envelope moves with the position: with the tray out the ends of
|
||||
the lens's reach, shifted, run and two half-steps past them are
|
||||
refused, and a jog to a tray-in Z is refused; with the tray in again
|
||||
the same jog runs
|
||||
3. a missing P, a P other than 0 or 1, and an axis word on the line are
|
||||
errors (28, 39, 31) that change nothing
|
||||
4. the M-code is a barrier: behind a move it waits for the move to end,
|
||||
and the frame changes only once the machine is idle
|
||||
5. the port's tray op switches the mode with a sender connected, the
|
||||
sender's response count stays exact and it is told the mode; the op
|
||||
is refused out of form (error:invalid), during a program
|
||||
(busy:state), and while a job waits at a package's M-code
|
||||
(busy:mcode)
|
||||
6. the mode survives a soft reset, and a controller restart: a start with
|
||||
the marker present takes the lens reference in the tray-out frame,
|
||||
and one without it in the tray-in frame
|
||||
7. tray_offset_mm sets the shift, held to 13 to 60 mm
|
||||
8. an unreferenced lens shifts the same way, and stays pinned where it
|
||||
reads
|
||||
9. a camera home with the tray out declares Z the offset higher, and
|
||||
hands the runner the same park: the lens parks in the same place
|
||||
|
||||
The kernel-idle half of the barrier (the wait a package's M-code makes)
|
||||
needs the pulse device: a null sink is always idle.
|
||||
|
||||
Usage: tray_test.py [path/to/grblHAL_glowforge]
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ctlport_test as ct # noqa: E402
|
||||
|
||||
BIN = ct.BIN
|
||||
fail = ct.fail
|
||||
PORT = 2395
|
||||
SPM = 2.922
|
||||
EDGE = 3.35 # the edge's grid step is 10
|
||||
EDGE_K = 10
|
||||
SHIFT_K = 100 # the default offset, 34.29 mm
|
||||
DOWN, UP = 10, 12 # the fallback window
|
||||
|
||||
|
||||
def zk(k):
|
||||
return k / SPM
|
||||
|
||||
|
||||
class Session:
|
||||
"""One null-sink controller over its own state directory. `conf` is the
|
||||
shared config; with `referenced` the lens marker forgectrl leaves is
|
||||
there, so the controller takes the lens reference at its start."""
|
||||
|
||||
def __init__(self, conf=None, referenced=True):
|
||||
self.workdir = tempfile.mkdtemp(prefix="tray-")
|
||||
self.conf = os.path.join(self.workdir, "forgefirm.conf")
|
||||
self.dump = os.path.join(self.workdir, "stream.bin")
|
||||
self.marker = os.path.join(self.workdir, "tray.out")
|
||||
self.write_conf(conf or {})
|
||||
if referenced:
|
||||
with open(os.path.join(self.workdir, "lens.home"), "w") as f:
|
||||
f.write("edge 0 3 3\n")
|
||||
verdict = os.path.join(self.workdir, "cooling.state")
|
||||
self.stop = threading.Event()
|
||||
self.pub = threading.Thread(target=ct.publish_verdicts, args=(verdict, self.stop), daemon=True)
|
||||
self.pub.start()
|
||||
env = dict(os.environ, GFHOME_CONF=self.conf, GF_STATE_DIR=self.workdir,
|
||||
GF_VERDICT_FILE=verdict, GFSINK_DUMP=self.dump, FFLOG_STDERR="1")
|
||||
env.pop("GFSINK", None)
|
||||
env.pop("GF_SWITCH_FILE", None)
|
||||
self.env = env
|
||||
self.start()
|
||||
|
||||
def write_conf(self, conf):
|
||||
with open(self.conf, "w") as f:
|
||||
f.write("cool_fan_grace_s = 0\nlaser_disarm_s = 1\nlens_hall_edge_z_mm = %.2f\n" % EDGE)
|
||||
for k, v in conf.items():
|
||||
f.write("%s = %s\n" % (k, v))
|
||||
|
||||
def start(self):
|
||||
self.proc = subprocess.Popen([BIN, "-p", str(PORT)], cwd=self.workdir, env=self.env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
self.sender = ct.Sender(PORT)
|
||||
path = os.path.join(self.workdir, "grbl.ctl")
|
||||
for _ in range(50):
|
||||
if os.path.exists(path):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
fail("the controller did not create %s" % path)
|
||||
self.port = ct.PortClient(path)
|
||||
# The lens reference is taken on the first pass of the realtime hook.
|
||||
time.sleep(0.3)
|
||||
|
||||
def restart(self, hard):
|
||||
"""End the controller (SIGKILL when hard) and start another over the
|
||||
same state directory and shared config."""
|
||||
self.port.close()
|
||||
self.sender.close()
|
||||
if hard:
|
||||
self.proc.kill()
|
||||
else:
|
||||
self.proc.send_signal(signal.SIGINT)
|
||||
self.proc.wait(10)
|
||||
self.start()
|
||||
|
||||
def state(self):
|
||||
return self.port.state()
|
||||
|
||||
def z(self):
|
||||
return self.state()["mpos"][2]
|
||||
|
||||
def tray(self):
|
||||
return self.state()["tray"]
|
||||
|
||||
def ticks(self):
|
||||
"""Step ticks shipped so far (a byte with bit 7 set is a power byte)."""
|
||||
try:
|
||||
with open(self.dump, "rb") as f:
|
||||
data = f.read()
|
||||
except OSError:
|
||||
return 0
|
||||
return sum(1 for b in data if not b & 0x80 and b & 0x25)
|
||||
|
||||
def messages(self, text, want=None):
|
||||
"""How many [MSG:<text>] lines the sender has had. With `want`, wait
|
||||
up to a second for that many: the port answers at once, and the
|
||||
sender's lines go out on the controller's next pass."""
|
||||
end = time.time() + (1.0 if want is not None else 0)
|
||||
while True:
|
||||
n = self.sender.saw("[MSG:%s]" % text)
|
||||
if want is None or n >= want or time.time() > end:
|
||||
return n
|
||||
time.sleep(0.02)
|
||||
|
||||
def quiet(self):
|
||||
time.sleep(0.45)
|
||||
|
||||
def close(self):
|
||||
self.port.close()
|
||||
self.sender.close()
|
||||
self.proc.send_signal(signal.SIGINT)
|
||||
try:
|
||||
self.proc.wait(5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
self.stop.set()
|
||||
self.pub.join(2)
|
||||
shutil.rmtree(self.workdir, ignore_errors=True)
|
||||
|
||||
|
||||
def expect_z(s, k, what):
|
||||
z = s.z()
|
||||
if abs(z - zk(k)) > 0.002:
|
||||
fail("[%s] Z reads %.3f, not %.3f (%d half-steps)" % (what, z, zk(k), k))
|
||||
|
||||
|
||||
def refused(s, line):
|
||||
"""Send a line that must be refused, and return its error, acknowledged
|
||||
with an empty line (the core holds a parser error against the next
|
||||
g-code line until then)."""
|
||||
r = s.sender.send(line)
|
||||
if r.startswith("error"):
|
||||
if s.sender.send("") != "ok":
|
||||
fail("the empty line that acknowledges %s did not get ok" % r)
|
||||
return r
|
||||
return None
|
||||
|
||||
|
||||
def jog_to(s, k):
|
||||
"""An absolute jog to half-step k: its response."""
|
||||
r = s.sender.send("$J=G90 G21 Z%.4f F200" % zk(k))
|
||||
if r == "ok":
|
||||
s.sender.wait_state("Idle")
|
||||
elif s.sender.send("") != "ok":
|
||||
fail("the empty line after a refused jog did not get ok")
|
||||
return r
|
||||
|
||||
|
||||
def test_switch(s):
|
||||
expect_z(s, EDGE_K, "switch")
|
||||
st = s.state()
|
||||
if st["tray"] != "in" or not st["homed"] & 4:
|
||||
fail("[switch] not tray in with Z referenced at the start: %r" % st)
|
||||
ticks = s.ticks()
|
||||
if s.sender.send("M103 P1") != "ok":
|
||||
fail("[switch] M103 P1 was refused")
|
||||
st = s.state()
|
||||
if st["tray"] != "out" or not st["homed"] & 4:
|
||||
fail("[switch] not tray out with Z referenced after M103 P1: %r" % st)
|
||||
expect_z(s, EDGE_K + SHIFT_K, "switch")
|
||||
if not s.messages("Tray out", 1):
|
||||
fail("[switch] the sender was not told [MSG:Tray out]")
|
||||
if not os.path.exists(s.marker):
|
||||
fail("[switch] M103 P1 left no marker beside the shared config")
|
||||
if s.sender.send("M103 P1") != "ok" or s.tray() != "out":
|
||||
fail("[switch] a second M103 P1 was refused or changed the mode")
|
||||
expect_z(s, EDGE_K + SHIFT_K, "switch: the mode already set")
|
||||
if s.messages("Tray out", 2) != 2:
|
||||
fail("[switch] the mode already set was not told again")
|
||||
if s.sender.send("M103 P0") != "ok" or s.tray() != "in":
|
||||
fail("[switch] M103 P0 did not set the tray in")
|
||||
expect_z(s, EDGE_K, "switch: back in")
|
||||
if os.path.exists(s.marker) or not s.messages("Tray in", 1):
|
||||
fail("[switch] M103 P0 left the marker or did not tell the sender")
|
||||
time.sleep(0.3)
|
||||
if s.ticks() != ticks:
|
||||
fail("[switch] the switches shipped %d step ticks" % (s.ticks() - ticks))
|
||||
print("PASS [switch]: M103 P1 moved Z up %d half-steps and P0 back, the sender was told, the "
|
||||
"marker followed, and no step shipped" % SHIFT_K)
|
||||
|
||||
|
||||
def test_envelope(s):
|
||||
s.sender.send("M103 P1")
|
||||
base = EDGE_K + SHIFT_K
|
||||
for k, want, what in ((base + UP, "ok", "the top of the reach"),
|
||||
(base + UP + 2, "error:15", "two half-steps over the top"),
|
||||
(base - DOWN, "ok", "the bottom of the reach"),
|
||||
(base - DOWN - 2, "error:15", "two half-steps under the bottom"),
|
||||
(EDGE_K + UP, "error:15", "a tray-in Z")):
|
||||
r = jog_to(s, k)
|
||||
if r != want:
|
||||
fail("[envelope] tray out: a jog to %s (Z %.3f) got %r, not %s" % (what, zk(k), r, want))
|
||||
if jog_to(s, base) != "ok":
|
||||
fail("[envelope] the jog back to the edge was refused")
|
||||
s.sender.send("M103 P0")
|
||||
expect_z(s, EDGE_K, "envelope")
|
||||
if jog_to(s, EDGE_K + UP) != "ok":
|
||||
fail("[envelope] tray in: the jog to the same tray-in Z was refused")
|
||||
if jog_to(s, EDGE_K) != "ok":
|
||||
fail("[envelope] the jog back to the edge was refused")
|
||||
print("PASS [envelope]: with the tray out the shifted reach ran to its ends, past them and a "
|
||||
"tray-in Z were refused; with the tray in the tray-in Z ran")
|
||||
|
||||
|
||||
def test_errors(s):
|
||||
ticks = s.ticks()
|
||||
for line, want in (("M103", "error:28"), ("M103 P2", "error:39"), ("M103 P0.5", "error:39"),
|
||||
("M103 P-1", "error:39"), ("M103 P1 Z5", "error:31"), ("M103 P1 X5", "error:31")):
|
||||
r = refused(s, line)
|
||||
if r != want:
|
||||
fail("[errors] %r got %r, not %s" % (line, r, want))
|
||||
if s.tray() != "in" or os.path.exists(s.marker):
|
||||
fail("[errors] %r changed the mode" % line)
|
||||
expect_z(s, EDGE_K, "errors")
|
||||
time.sleep(0.3)
|
||||
if s.ticks() != ticks:
|
||||
fail("[errors] the refused lines shipped %d step ticks" % (s.ticks() - ticks))
|
||||
print("PASS [errors]: a missing P, P2, P0.5, P-1 and an axis word were refused and changed nothing")
|
||||
|
||||
|
||||
def test_barrier(s):
|
||||
"""Behind a move the switch waits for it to end."""
|
||||
x0 = s.state()["mpos"][0]
|
||||
s.sender.send("G91 G1 X10 F300") # about two seconds
|
||||
n = s.sender.count()
|
||||
s.sender.sock.sendall(b"M103 P1\n")
|
||||
seen_run = False
|
||||
end = time.time() + 10
|
||||
while time.time() < end:
|
||||
st = s.port.state()
|
||||
if st["state"] == "Run":
|
||||
seen_run = True
|
||||
if st["tray"] != "in" or abs(st["mpos"][2] - zk(EDGE_K)) > 0.002:
|
||||
fail("[barrier] the frame changed while the move before M103 ran: %r" % st)
|
||||
if st["tray"] == "out":
|
||||
break
|
||||
time.sleep(0.05)
|
||||
if not seen_run:
|
||||
fail("[barrier] the move was never seen running, so the case proves nothing")
|
||||
st = s.port.state()
|
||||
if st["tray"] != "out" or st["state"] != "Idle" or abs(st["mpos"][0] - x0 - 10) > 0.01:
|
||||
fail("[barrier] the switch did not land after the move ended: %r" % st)
|
||||
for _ in range(100):
|
||||
if s.sender.count() > n:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
s.sender.send("G90")
|
||||
s.sender.send("M103 P0")
|
||||
print("PASS [barrier]: M103 behind a move changed the frame only once the move had ended")
|
||||
|
||||
|
||||
def test_port(s):
|
||||
s.sender.wait_state("Idle")
|
||||
s.quiet()
|
||||
before = s.sender.count()
|
||||
told = s.messages("Tray out")
|
||||
r = s.port.request("tray out")
|
||||
if r != "ok" or s.tray() != "out":
|
||||
fail("[port] tray out got %r" % r)
|
||||
expect_z(s, EDGE_K + SHIFT_K, "port")
|
||||
if s.port.request("tray out") != "ok":
|
||||
fail("[port] a second tray out was refused")
|
||||
if s.messages("Tray out", told + 2) != told + 2:
|
||||
fail("[port] the sender was not told the mode each time")
|
||||
if s.port.request("tray sideways") != "error:invalid":
|
||||
fail("[port] a tray op out of form was not refused")
|
||||
if s.sender.count() != before:
|
||||
fail("[port] the sender got %d responses for the port's tray ops" % (s.sender.count() - before))
|
||||
|
||||
s.sender.send("G91 G1 X-10 F300")
|
||||
time.sleep(0.4)
|
||||
r = s.port.request("tray in")
|
||||
if r != "busy:state" or s.tray() != "out":
|
||||
fail("[port] tray in during a program got %r" % r)
|
||||
s.sender.wait_state("Idle")
|
||||
s.sender.send("G90")
|
||||
|
||||
if s.port.request("mcodes 160") != "ok":
|
||||
fail("[port] the M-code table was refused")
|
||||
n = s.sender.count()
|
||||
s.sender.sock.sendall(b"M160 P1\n")
|
||||
for _ in range(100):
|
||||
st = s.state()
|
||||
if st.get("mcode"):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
fail("[port] no M-code waits: %r" % s.state())
|
||||
r = s.port.request("tray in")
|
||||
if r != "busy:mcode" or s.tray() != "out":
|
||||
fail("[port] tray in while a job waits at an M-code got %r" % r)
|
||||
if s.port.request("mcode_result %d ok" % st["mcode"]["seq"]) != "ok":
|
||||
fail("[port] the M-code's answer was refused")
|
||||
for _ in range(100):
|
||||
if s.sender.count() > n:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
s.port.request("mcodes -")
|
||||
s.quiet()
|
||||
before = s.sender.count()
|
||||
if s.port.request("tray in") != "ok" or s.tray() != "in":
|
||||
fail("[port] tray in was refused")
|
||||
expect_z(s, EDGE_K, "port")
|
||||
if s.sender.count() != before or s.sender.send("G0 X0") != "ok":
|
||||
fail("[port] the sender's count was disturbed by the tray op")
|
||||
s.sender.wait_state("Idle")
|
||||
print("PASS [port]: tray out and in through the port with a sender connected, its count exact "
|
||||
"and the mode told; refused out of form, during a program, and at a package's M-code")
|
||||
|
||||
|
||||
def test_persistence(s):
|
||||
s.sender.send("M103 P1")
|
||||
s.sender.realtime(b"\x18")
|
||||
time.sleep(1.0)
|
||||
s.sender.send("$X")
|
||||
if s.tray() != "out":
|
||||
fail("[persistence] a soft reset set the tray in")
|
||||
expect_z(s, EDGE_K + SHIFT_K, "persistence: soft reset")
|
||||
s.restart(hard=True)
|
||||
st = s.state()
|
||||
if st["tray"] != "out" or not st["homed"] & 4:
|
||||
fail("[persistence] a restart over the marker came up %r" % st)
|
||||
expect_z(s, EDGE_K + SHIFT_K, "persistence: restart with the tray out")
|
||||
if jog_to(s, EDGE_K + SHIFT_K + UP) != "ok" or jog_to(s, EDGE_K + UP) != "error:15":
|
||||
fail("[persistence] the restarted controller's envelope is not the tray-out one")
|
||||
jog_to(s, EDGE_K + SHIFT_K)
|
||||
if s.port.request("tray in") != "ok":
|
||||
fail("[persistence] tray in was refused after the restart")
|
||||
expect_z(s, EDGE_K, "persistence: in after the restart")
|
||||
s.restart(hard=False)
|
||||
if s.tray() != "in":
|
||||
fail("[persistence] a restart with no marker came up tray out")
|
||||
expect_z(s, EDGE_K, "persistence: restart with the tray in")
|
||||
print("PASS [persistence]: the tray out survived a soft reset and a controller kill, the new "
|
||||
"controller referenced Z in the tray-out frame, and a start with no marker in the tray-in frame")
|
||||
|
||||
|
||||
def test_offset():
|
||||
for key, k in (("20", 58), ("5", 38), ("99", 175)):
|
||||
s = Session(conf={"tray_offset_mm": key})
|
||||
try:
|
||||
s.sender.send("M103 P1")
|
||||
expect_z(s, EDGE_K + k, "offset %s" % key)
|
||||
s.sender.send("M103 P0")
|
||||
expect_z(s, EDGE_K, "offset %s: back in" % key)
|
||||
finally:
|
||||
s.close()
|
||||
print("PASS [offset]: tray_offset_mm 20 moved Z 58 half-steps, 5 was held to 13 mm (38), "
|
||||
"99 to 60 mm (175)")
|
||||
|
||||
|
||||
def test_unreferenced():
|
||||
s = Session(referenced=False)
|
||||
try:
|
||||
z0 = s.z()
|
||||
k0 = round(z0 * SPM)
|
||||
s.sender.send("M103 P1")
|
||||
expect_z(s, k0 + SHIFT_K, "unreferenced")
|
||||
for step in ("1", "-1"):
|
||||
r = s.sender.send("$J=G91 G21 Z%s F200" % step)
|
||||
if r != "error:15":
|
||||
fail("[unreferenced] a Z jog of %s got %r: the pinned envelope did not follow" % (step, r))
|
||||
s.sender.send("")
|
||||
s.sender.send("M103 P0")
|
||||
expect_z(s, k0, "unreferenced: back in")
|
||||
finally:
|
||||
s.close()
|
||||
print("PASS [unreferenced]: an unreferenced lens shifted the same way and stayed pinned where it read")
|
||||
|
||||
|
||||
def test_camera_home():
|
||||
s = Session(conf={"homing_mode": "gfcloud"})
|
||||
park = os.path.join(s.workdir, "park.log")
|
||||
s.write_conf({"homing_mode": "gfcloud", "gfcloud_home_cmd": "echo $GFHOME_PARK_HALF_STEPS >> %s" % park})
|
||||
try:
|
||||
z = {}
|
||||
for mode in ("P0", "P1"):
|
||||
s.sender.send("M103 " + mode)
|
||||
if s.sender.send("$H") != "ok":
|
||||
fail("[camera-home] $H with M103 %s was refused" % mode)
|
||||
s.sender.wait_state("Idle")
|
||||
z[mode] = s.z()
|
||||
with open(park) as f:
|
||||
parks = f.read().split()
|
||||
if len(parks) != 2 or parks[0] != parks[1]:
|
||||
fail("[camera-home] the runner was handed parks %r, not the same one twice" % parks)
|
||||
k = EDGE_K + int(parks[0])
|
||||
if abs(z["P0"] - zk(k)) > 0.002 or abs(z["P1"] - zk(k + SHIFT_K)) > 0.002:
|
||||
fail("[camera-home] Z after the homes read %r, not %.3f and %.3f" % (z, zk(k), zk(k + SHIFT_K)))
|
||||
if jog_to(s, EDGE_K + SHIFT_K + UP) != "ok" or jog_to(s, EDGE_K + UP) != "error:15":
|
||||
fail("[camera-home] the envelope after a tray-out home is not the tray-out one")
|
||||
s.sender.send("M103 P0")
|
||||
finally:
|
||||
s.close()
|
||||
print("PASS [camera-home]: the park handed to the runner was %s half-steps in both modes, and Z "
|
||||
"after the home read %.3f tray in, %.3f tray out" % (parks[0], z["P0"], z["P1"]))
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(BIN):
|
||||
fail("no controller binary at %s" % BIN)
|
||||
s = Session()
|
||||
try:
|
||||
if s.sender.send("$102") != "ok" or not s.sender.saw("$102=2.922"):
|
||||
fail("$102 is not 2.922: this harness needs the default")
|
||||
test_switch(s)
|
||||
test_envelope(s)
|
||||
test_errors(s)
|
||||
test_barrier(s)
|
||||
test_port(s)
|
||||
test_persistence(s)
|
||||
finally:
|
||||
s.close()
|
||||
test_offset()
|
||||
test_unreferenced()
|
||||
test_camera_home()
|
||||
print("ALL PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user