mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
Harnesses: the null-sink cases other tests prove leave
The grblHAL harnesses keep what only a running controller shows; the cases a host unit test in grblHAL-glowforge proves, and the log and reply wording, leave. - laser_lifecycle_test: test_arm_waits_for_the_fans goes. laser_arm_test case A4 (grblHAL-glowforge) proves the fan grace stretching the arm's acknowledgment budget, on the same arm path. The docstring names it. - xy_mode_test: case_x32, case_invalid and the log checks go; the default, x16 and the low tick stay. xy_scale_test holds each mode's tick and the not-a-mode parse. - planner_blocks_test: one restart, at $398 1000; the 400 pass and the fresh-store probe go. - ctlport_test: test_poll_lf and test_crlf go (the CR LF poll stays); in test_sender_out the network turn-away goes (serial_test holds it), and the busy gating, the grant and the outlives-client part stay. test_port_dark is whole. - mcode_test: the timeout case runs on the host build's 5 s GFMCODE_WAIT_S (grblHAL-glowforge's CI configures it; the image keeps 30 s) and still shows the hold, at 5.1 s; 5 s outlasts the 3 s test_dark watches the wait; test_table refuses one form per parser branch. test_timeout_holds and test_dark stay. - envelope_test: test_unset and test_bounds go (lens_home_test holds the clamp); test_armed and the open, apply and close cases stay. - manual_home_test: the out-of-range offset block goes, a repeat of gfhome_clamp_home_mm (lens_home_test); the modal-M3 FIRE check and every release and energize case stay. - cool_report_test: one non-hex secret and one CR LF injection stay; runner-malformed and the exact header-set checks go. A bad secret is never sent and a good one is. - tray_test: test_offset keeps 20 mm; test_errors keeps a bad P and a bad axis; the frame shift, envelope, barrier, persistence, port op, unreferenced lens and camera home stay. Proof: with grblHAL-glowforge's matching commit, the eleven harnesses its CI runs pass under Linux against the null-sink build: nine on a build with a 1 s M-code wait, and mcode_test and tray_test, the two that wait at an M-code, on the 5 s build CI configures (-DGFMCODE_WAIT_S=5.0). pyflakes reports nothing in the nine changed files. Push this before grblHAL-glowforge's commit. Acceptance: none; host harnesses only.
This commit is contained in:
@@ -11,16 +11,16 @@ the route asks for it. This harness stands in for forgectrl's listener, runs
|
||||
the null-sink controller against it, and reads every report as it arrives.
|
||||
|
||||
secret every report carries X-ForgeFIRM-Report with the secret it was
|
||||
started with, and the request is otherwise as it always was
|
||||
started with
|
||||
none started with no secret, the reports carry no such header
|
||||
malformed a value that is not 32 hex digits (short, a letter out of range,
|
||||
a CR LF and a header of its own behind it) is never sent, and
|
||||
nothing of it reaches the wire
|
||||
malformed a value that is not 32 hex digits (a letter out of range, or a
|
||||
CR LF and a header of its own behind it, 32 characters long) is
|
||||
never sent, and nothing of it reaches the wire
|
||||
runner the homing runner the controller starts is handed the secret (it
|
||||
reports in the controller's place while a gfcloud $H holds the
|
||||
machine, and the controller is quiet then), and the reports
|
||||
still carry it afterwards; a controller with no secret, or with
|
||||
one that is not 32 hex digits, hands the runner none
|
||||
still carry it afterwards; a controller with no secret hands the
|
||||
runner none
|
||||
|
||||
Usage: cool_report_test.py <path-to-grblHAL_glowforge>
|
||||
"""
|
||||
@@ -170,15 +170,11 @@ def run(name, secret, check, runner=False, handed=None):
|
||||
def with_secret(name, r):
|
||||
if r["headers"].get("x-forgefirm-report") != SECRET:
|
||||
fail("[%s] a report without the secret: %s" % (name, r["headers"]))
|
||||
if set(r["headers"]) != {"host", "connection", "content-length", "x-forgefirm-report"}:
|
||||
fail("[%s] the report's headers changed: %s" % (name, sorted(r["headers"])))
|
||||
|
||||
|
||||
def without(name, r):
|
||||
if "x-forgefirm-report" in r["headers"] or "x-evil" in r["headers"] or b"Evil" in r["raw"]:
|
||||
fail("[%s] a header that must not be sent: %s" % (name, r["headers"]))
|
||||
if set(r["headers"]) != {"host", "connection", "content-length"}:
|
||||
fail("[%s] the report's headers changed: %s" % (name, sorted(r["headers"])))
|
||||
|
||||
|
||||
def main():
|
||||
@@ -186,13 +182,11 @@ def main():
|
||||
fail("no controller binary at %s" % BIN)
|
||||
run("secret", SECRET, with_secret)
|
||||
run("none", None, without)
|
||||
for i, bad in enumerate(("short", SECRET[:-1] + "g", SECRET + "0", SECRET.upper(),
|
||||
SECRET[:16] + "\r\nX-Evil: 1\r\nX-Pad: 12",
|
||||
SECRET[:16] + "\r\nX-Evil: 1\r\nX:1"), 1): # the last is 32 long
|
||||
for i, bad in enumerate((SECRET[:-1] + "g",
|
||||
SECRET[:16] + "\r\nX-Evil: 1\r\nX:1"), 1): # the second is 32 long
|
||||
run("malformed-%d" % i, bad, without)
|
||||
run("runner", SECRET, with_secret, runner=True, handed=SECRET)
|
||||
run("runner-none", None, without, runner=True)
|
||||
run("runner-malformed", SECRET[:16] + "\r\nX-Evil: 1\r\nX:1", without, runner=True)
|
||||
print("cool_report_test: all passed")
|
||||
|
||||
|
||||
|
||||
@@ -26,25 +26,23 @@ job after it.
|
||||
5. the port has one client: a second connection is refused and the first
|
||||
keeps working (it never displaces, unlike the Grbl socket); a client
|
||||
that closes and reconnects at once is served
|
||||
6. a CR LF sender keeps its count across a port jog, an empty line
|
||||
included
|
||||
7. the status hook survives a soft reset (the core puts its own report
|
||||
6. the status hook survives a soft reset (the core puts its own report
|
||||
handlers back at every reset)
|
||||
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
|
||||
7. the port's client is the dead-man: closing it cancels its jog
|
||||
8. 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 (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
|
||||
9. "sender out" keeps senders from the network out, and only on an idle
|
||||
machine with a quiet sender: it is refused while a sender line runs
|
||||
(a dwell), within 2 s of the sender's last line, and while a program
|
||||
moves; granted, it drops the sender after a message line, turns every
|
||||
sender from the network away the same way without touching the
|
||||
session, still lets one from this host in, outlives the port's client,
|
||||
and "sender in" ends it. A sender from the network is one that
|
||||
connects to this host's own address rather than the loopback
|
||||
moves; granted, it drops the sender after a message line, outlives
|
||||
the port's client, and "sender in" ends it. A sender from the network
|
||||
is one that connects to this host's own address rather than the
|
||||
loopback; one turned away while senders are kept out is serial_test.c's
|
||||
(grblHAL-glowforge)
|
||||
|
||||
Usage: ctlport_test.py [path/to/grblHAL_glowforge]
|
||||
"""
|
||||
@@ -469,10 +467,6 @@ def poll_case(tag, eol):
|
||||
s.close()
|
||||
|
||||
|
||||
def test_poll_lf():
|
||||
poll_case("poll-lf", "\n")
|
||||
|
||||
|
||||
def test_poll_crlf():
|
||||
poll_case("poll-crlf", "\r\n")
|
||||
|
||||
@@ -541,30 +535,6 @@ def test_single_client(s):
|
||||
"five reconnects right after a close were each served")
|
||||
|
||||
|
||||
def test_crlf():
|
||||
s = Session(eol="\r\n")
|
||||
try:
|
||||
for line in ("G91", "G0 X1", ""):
|
||||
if s.sender.send(line) != "ok":
|
||||
fail("[crlf] %r did not get ok" % line)
|
||||
s.sender.wait_state("Idle")
|
||||
s.quiet()
|
||||
before = s.sender.count()
|
||||
if s.port.request("jog G91 X1 F600") != "ok":
|
||||
fail("[crlf] the port jog was refused")
|
||||
s.sender.wait_state("Idle")
|
||||
for line in ("", "G0 X1", "", ""):
|
||||
if s.sender.send(line) != "ok":
|
||||
fail("[crlf] %r after the port jog did not get ok" % line)
|
||||
time.sleep(0.5)
|
||||
got = s.sender.count() - before
|
||||
if got != 4:
|
||||
fail("[crlf] 4 sender lines after the port jog got %d responses" % got)
|
||||
print("PASS [crlf]: a CR LF sender's count is exact across a port jog, empty lines included")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def test_port_dark():
|
||||
s = Session(laser=True)
|
||||
try:
|
||||
@@ -664,11 +634,6 @@ def own_address():
|
||||
return addr
|
||||
|
||||
|
||||
def generation(s):
|
||||
with open(os.path.join(s.workdir, "grbl.state")) as f:
|
||||
return json.load(f)["sender"]["generation"]
|
||||
|
||||
|
||||
def wait_closed(sender, what, timeout=3.0):
|
||||
end = time.time() + timeout
|
||||
while time.time() < end and not sender.closed:
|
||||
@@ -711,7 +676,6 @@ def test_sender_out():
|
||||
print("PASS [sender-out]: refused during a dwell, within 2 s of a line, and while a program moves")
|
||||
|
||||
time.sleep(2.3)
|
||||
gen = generation(s)
|
||||
got = s.port.request("sender out")
|
||||
if got != "ok":
|
||||
fail("[sender-out] an idle machine and a quiet sender: %r, wanted ok" % got)
|
||||
@@ -723,21 +687,6 @@ def test_sender_out():
|
||||
fail("[sender-out] the state after it: %r" % st)
|
||||
print("PASS [sender-out]: granted, it dropped the sender after a message line")
|
||||
|
||||
gen = generation(s)
|
||||
late = Sender(PORT, host=own)
|
||||
wait_closed(late, "a sender from the network")
|
||||
if not late.saw("[MSG:The machine is in use") or late.count():
|
||||
fail("[sender-out] the turned-away sender read %r" % late.other)
|
||||
time.sleep(0.3)
|
||||
if generation(s) != gen or s.port.state().get("sender") is not False:
|
||||
fail("[sender-out] turning a sender away changed the session")
|
||||
local = Sender(PORT)
|
||||
if local.closed or local.send("G21") != "ok":
|
||||
fail("[sender-out] a sender on this host was not let in")
|
||||
local.close()
|
||||
print("PASS [sender-out]: a sender from the network is turned away and the session is "
|
||||
"untouched; one from this host connects")
|
||||
|
||||
s.port.close()
|
||||
s.port = PortClient(s.path)
|
||||
if s.port.state().get("sender_out") is not True:
|
||||
@@ -773,8 +722,6 @@ def main():
|
||||
test_dead_man(s)
|
||||
finally:
|
||||
s.close()
|
||||
test_crlf()
|
||||
test_poll_lf()
|
||||
test_poll_crlf()
|
||||
test_port_dark()
|
||||
test_sender_out()
|
||||
|
||||
@@ -9,21 +9,20 @@ Runs the native grblHAL_glowforge binary in null-sink mode, with the
|
||||
scripted sender and the port client of ctlport_test.py, homing manually
|
||||
(nothing moves; X and Y become homed and the envelope is set):
|
||||
|
||||
1. unset keys: X's and Y's far edges are the travel ($130, $131)
|
||||
2. envelope_x_mm and _y set the far edges at a home, and a jog past one
|
||||
is the core's error:15
|
||||
3. a key below GFHOME_ENVELOPE_MIN_MM is held to it, and one past the
|
||||
travel plus GFHOME_ENVELOPE_EXTRA_MM is held to that
|
||||
4. the port's envelope open: refused before a home (error:homed); after
|
||||
1. envelope_x_mm and _y set the far edges at a home, and a jog past one
|
||||
is the core's error:15 (an unset key being the travel, and a key held
|
||||
to GFHOME_ENVELOPE_MIN_MM and to the travel plus
|
||||
GFHOME_ENVELOPE_EXTRA_MM, are lens_home_test.c's in grblHAL-glowforge)
|
||||
2. the port's envelope open: refused before a home (error:homed); after
|
||||
one, the port's jogs reach the travel plus 30 mm for X and Y, and the
|
||||
port's state says the envelope is open; envelope apply puts the keys'
|
||||
edges back without a home
|
||||
5. an open envelope is the port's jogs' alone: a status poll and an
|
||||
3. an open envelope is the port's jogs' alone: a status poll and an
|
||||
empty line leave it open, and the sender's first line closes it before
|
||||
the core reads the line (the line then answered ok, and the keys'
|
||||
edges in force), a port jog it meets canceled
|
||||
6. a soft reset and the port client going away each close it
|
||||
7. envelope open is refused under an open armed window (busy:state)
|
||||
4. a soft reset and the port client going away each close it
|
||||
5. envelope open is refused under an open armed window (busy:state)
|
||||
|
||||
Usage: envelope_test.py [path/to/grblHAL_glowforge]
|
||||
"""
|
||||
@@ -111,14 +110,6 @@ def saw_within(s, needle, seconds=2.0):
|
||||
return s.sender.saw(needle)
|
||||
|
||||
|
||||
def test_unset(s):
|
||||
conf(s)
|
||||
home(s)
|
||||
edge(s, "X", TRAVEL_X - 1, TRAVEL_X + 1, "unset")
|
||||
edge(s, "Y", TRAVEL_Y - 1, TRAVEL_Y + 1, "unset")
|
||||
print("PASS [unset]: with no key the far edges are the travel, %.0f and %.0f" % (TRAVEL_X, TRAVEL_Y))
|
||||
|
||||
|
||||
def test_measured(s):
|
||||
conf(s, envelope_x_mm=480, envelope_y_mm=284.5)
|
||||
home(s)
|
||||
@@ -127,14 +118,6 @@ def test_measured(s):
|
||||
print("PASS [measured]: envelope_x_mm 480 and envelope_y_mm 284.5 (past the travel) are the far edges at a home")
|
||||
|
||||
|
||||
def test_bounds(s):
|
||||
conf(s, envelope_x_mm=10, envelope_y_mm=900)
|
||||
home(s)
|
||||
edge(s, "X", 49, 51, "bounds")
|
||||
edge(s, "Y", TRAVEL_Y + EXTRA - 1, TRAVEL_Y + EXTRA + 1, "bounds")
|
||||
print("PASS [bounds]: a key below 50 mm is held to 50, one past the travel plus 30 mm to that")
|
||||
|
||||
|
||||
def test_open_apply(s):
|
||||
conf(s, envelope_x_mm=480, envelope_y_mm=250)
|
||||
home(s)
|
||||
@@ -249,9 +232,7 @@ def main():
|
||||
fail("no controller binary at %s" % ct.BIN)
|
||||
s = ct.Session()
|
||||
try:
|
||||
test_unset(s)
|
||||
test_measured(s)
|
||||
test_bounds(s)
|
||||
test_open_apply(s)
|
||||
test_sender_closes(s)
|
||||
test_reset_closes(s)
|
||||
|
||||
@@ -22,7 +22,7 @@ reported messages:
|
||||
5. arming refused while the cooling verdict blocks fire, or before
|
||||
the engine acknowledges the session: laser_arm_test.c cases C, A2
|
||||
and A3, and cooling_test.c's arm-time gate; the wait for the fans
|
||||
is this harness's
|
||||
is laser_arm_test.c case A4
|
||||
6. with a switch source present (GF_SWITCH_FILE), the arm blocks on the
|
||||
button: a press with the lid closed arms, and the lid or the
|
||||
interlock loop opening during the wait cancels the job (a clean
|
||||
@@ -558,32 +558,6 @@ def test_rx_overrun_aborts():
|
||||
s.close()
|
||||
|
||||
|
||||
def test_arm_waits_for_the_fans():
|
||||
"""Rule 5: the acknowledgment budget follows the fan grace. The
|
||||
engine acknowledges once the fans are at their floors, seconds after
|
||||
the arm; with cool_fan_grace_s at 8 the controller waits up to 13 s
|
||||
for it, and an acknowledgment at 9 s arms the job rather than
|
||||
refusing it at the old 5 s budget."""
|
||||
ack_s = 9.0
|
||||
s = Session("arm-fans", fire_ok=True, ack_after_s=ack_s,
|
||||
conf_extra="cool_fan_grace_s = 8\n")
|
||||
try:
|
||||
t0 = time.time()
|
||||
s.send_raw("M4 S100")
|
||||
s.send_raw("G1 X1 F600")
|
||||
if not wait_for(s.log, ARMED, 20, s.sock):
|
||||
fail("[arm-fans] the arm never completed with the acknowledgment at %.0f s "
|
||||
"(the budget did not follow cool_fan_grace_s)" % ack_s)
|
||||
dt = time.time() - t0
|
||||
if dt < ack_s - 0.5:
|
||||
fail("[arm-fans] armed after %.1f s, before the fans were up at %.1f s" % (dt, ack_s))
|
||||
if BLOCKED in "".join(s.log):
|
||||
fail("[arm-fans] the wait for the fans was reported as a refusal")
|
||||
print("PASS [arm-fans]: the arm waited %.1f s for the fans, then armed" % dt)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def test_sigterm_mid_job():
|
||||
"""Rule 5: a termination signal during an armed job is a STOP, not a
|
||||
"finish the job first". The supervisor's expected-stop path (mode
|
||||
@@ -980,7 +954,6 @@ def main():
|
||||
test_lid_cancels_and_returns()
|
||||
test_pause_then_lid_cancel_returns_to_the_job_start()
|
||||
test_lid_policy_hold()
|
||||
test_arm_waits_for_the_fans()
|
||||
test_sigterm_mid_job()
|
||||
print("PASS: the armed-window lifecycle holds")
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ operator's own $ME or a manual $H energizes it:
|
||||
the origin lies, never negative. With an offset the head at the stops
|
||||
is declared at minus it and jogs to the origin, dark, before $H
|
||||
answers, through the sender and through the port; the envelope starts
|
||||
at the origin, and an out-of-range offset is clamped; a jog cancel
|
||||
ends the move short of the origin and $H says where it stopped
|
||||
at the origin (an out-of-range offset clamped is lens_home_test.c's,
|
||||
gfhome_clamp_home_mm); a jog cancel ends the move short of the origin
|
||||
and $H says where it stopped
|
||||
9. a controller that starts over a released gantry (the marker in the
|
||||
state directory) comes up locked and writes no current until $ME
|
||||
8. each provider reads its own offsets and no other's: with both pairs set,
|
||||
@@ -426,15 +427,6 @@ def test_stop_block_offsets(s):
|
||||
r = refused(s, line)
|
||||
if r != "error:15":
|
||||
fail("[offsets] %s, behind the origin, got %r, not error:15" % (line, r))
|
||||
s.set_mode("manual", "manual_home_x = -10.3\nmanual_home_y = 1e9\n")
|
||||
if s.sender.send("$H", timeout=30.0) != "ok":
|
||||
fail("[offsets] $H with out-of-range offsets was refused")
|
||||
st = s.port.state()
|
||||
if st["mpos"][0] != 0 or st["mpos"][1] != 0:
|
||||
fail("[offsets] the head did not end at the origin: %r" % st["mpos"][:2])
|
||||
d = declared(s)
|
||||
if d is None or d[0] != 0 or abs(d[1] + 279.0) > 0.01:
|
||||
fail("[offsets] a negative offset and an oversized one declared %r, not (0, -279)" % (d,))
|
||||
# The panel's Set home here: the port answers once the head has stopped,
|
||||
# and the sender's count stays exact.
|
||||
s.set_mode("manual", "manual_home_x = 12.5\nmanual_home_y = 8\n")
|
||||
@@ -449,8 +441,8 @@ def test_stop_block_offsets(s):
|
||||
fail("[offsets] the sender got %d responses for the port's home" % (s.sender.count() - before))
|
||||
s.set_mode("manual")
|
||||
print("PASS [offsets]: a manual home declared (-12.5, -8) and jogged to the origin before $H "
|
||||
"answered, dark under a modal M3; the envelope started at the origin; a negative offset "
|
||||
"became 0 and an oversized one the axis travel; the port's home answered at the origin")
|
||||
"answered, dark under a modal M3; the envelope started at the origin; the port's home "
|
||||
"answered at the origin")
|
||||
|
||||
|
||||
def test_origin_jog_cancel(s):
|
||||
|
||||
@@ -24,12 +24,16 @@ sender and for the machine daemon:
|
||||
[MSG:] of its words, and a cycle start resumes it
|
||||
5. a soft reset ends the wait at once, and the controller answers the
|
||||
next line
|
||||
6. no answer in GFMCODE_WAIT_S holds the job
|
||||
6. no answer in GFMCODE_WAIT_S holds the job: 5 s in the host build
|
||||
(configured with -DGFMCODE_WAIT_S=5.0, as grblHAL-glowforge's CI
|
||||
does; the image keeps 30 s), longer than any case here waits at a
|
||||
barrier on purpose
|
||||
7. the barrier is dark: under an open armed window with M3 S500, a job
|
||||
waiting at an M-code ships no FIRE tick, the window stays open across
|
||||
the wait, and the next laser move fires again
|
||||
|
||||
Usage: mcode_test.py [path/to/grblHAL_glowforge]
|
||||
(a build configured with -DGFMCODE_WAIT_S=5.0)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
@@ -39,7 +43,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import ctlport_test as ct # noqa: E402
|
||||
|
||||
fail = ct.fail
|
||||
WAIT_S = 30.0
|
||||
WAIT_S = 5.0 # the host build's GFMCODE_WAIT_S
|
||||
|
||||
|
||||
def send_nowait(s, line):
|
||||
@@ -66,7 +70,9 @@ def test_table(s):
|
||||
r = s.port.request("mcodes " + good)
|
||||
if r != "ok":
|
||||
fail("[table] mcodes %s -> %r" % (good, r))
|
||||
for bad in ("159", "180", "160,160", "160,", ",160", "16a", "0160", "160 161", "1600", "160,,161", ""):
|
||||
# one form per refusal: out of range, twice, a separator with nothing
|
||||
# after it, not a number, and nothing at all
|
||||
for bad in ("159", "160,160", "160,", ",160", ""):
|
||||
r = s.port.request("mcodes " + bad)
|
||||
if r != "error:invalid":
|
||||
fail("[table] mcodes %r -> %r, not error:invalid" % (bad, r))
|
||||
@@ -81,8 +87,8 @@ def test_table(s):
|
||||
time.sleep(0.02)
|
||||
if s.sender.responses[n:n + 1] != ["ok"]:
|
||||
fail("[table] M165 got %r" % s.sender.responses[n:])
|
||||
print("PASS [table]: the port takes '-' and numbers from M160 to M179, the whole range; 11 other forms "
|
||||
"refused, the table left as it was")
|
||||
print("PASS [table]: the port takes '-' and numbers from M160 to M179, the whole range; a form "
|
||||
"of each refusal is refused, the table left as it was")
|
||||
|
||||
|
||||
def test_unanswered(s):
|
||||
@@ -198,9 +204,9 @@ def test_timeout_holds(s):
|
||||
wait_pending(s)
|
||||
s.sender.wait_state("Hold", timeout=WAIT_S + 5)
|
||||
dt = time.time() - t0
|
||||
if dt < WAIT_S - 1:
|
||||
if dt < WAIT_S - 0.1:
|
||||
fail("[timeout] the job was held after %.1f s, not after the wait" % dt)
|
||||
if not s.sender.saw("M160 had no answer from its extension in 30 s: the job is held"):
|
||||
if not s.sender.saw("M160 had no answer from its extension"):
|
||||
fail("[timeout] the sender got no [MSG:] of the timeout")
|
||||
s.sender.realtime(b"~")
|
||||
s.sender.wait_state("Idle", timeout=5)
|
||||
|
||||
@@ -18,12 +18,10 @@ Runs the native grblHAL_glowforge binary in null-sink mode (no hardware,
|
||||
no root) and drives it over TCP, keeping one settings store across
|
||||
restarts:
|
||||
|
||||
1. a fresh store starts and reports its default $398
|
||||
2. $398=400 is accepted and read back
|
||||
3. the controller restarted on that store answers on the port, reports
|
||||
400 blocks, and the status report shows a planner that deep
|
||||
4. the same at $398=1000, the top of the range
|
||||
5. a motion line runs to completion on the deep buffer
|
||||
1. $398=1000, the top of the range, is accepted and read back
|
||||
2. the controller restarted on that store answers on the port, reports
|
||||
1000 blocks, and the status report shows a planner that deep
|
||||
3. a motion line runs to completion on the deep buffer
|
||||
|
||||
A controller that connects and never answers after the restart is the
|
||||
failure this harness exists for. Every exit kills the controller it
|
||||
@@ -50,7 +48,6 @@ PORT = 2401
|
||||
CONNECT_S = 8.0
|
||||
ANSWER_S = 5.0
|
||||
|
||||
DEEP = 400
|
||||
TOP = 1000
|
||||
|
||||
_live = None # the controller process a failure has to take down
|
||||
@@ -199,15 +196,6 @@ def main():
|
||||
fail("no controller binary at %s" % BIN)
|
||||
workdir = tempfile.mkdtemp(prefix="planner-blocks-")
|
||||
try:
|
||||
s = Session(workdir)
|
||||
check(s.answers(), "a fresh store starts and answers",
|
||||
"a fresh store does not answer")
|
||||
print(" default $398=%s" % s.setting("$398"))
|
||||
s.stop()
|
||||
|
||||
s = restart_on(workdir, DEEP)
|
||||
s.stop()
|
||||
|
||||
s = restart_on(workdir, TOP)
|
||||
reply = s.send("G91 G1 X1 F600", 1.5)
|
||||
check("ok" in reply and "error" not in reply and "ALARM" not in reply,
|
||||
@@ -216,7 +204,7 @@ def main():
|
||||
check(s.wait_idle(), "the move completes and the controller is Idle",
|
||||
"the controller did not return to Idle after the move")
|
||||
s.stop()
|
||||
print("PASS: the planner buffer starts at %d and %d blocks" % (DEEP, TOP))
|
||||
print("PASS: the planner buffer starts at %d blocks" % TOP)
|
||||
finally:
|
||||
if _live is not None:
|
||||
_live.stop()
|
||||
|
||||
@@ -21,8 +21,8 @@ sender on the Grbl socket, and the port client of ctlport_test.py:
|
||||
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
|
||||
3. a P other than 0 or 1, and an axis word on the line, are errors (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
|
||||
@@ -33,7 +33,7 @@ sender on the Grbl socket, and the port client of ctlport_test.py:
|
||||
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
|
||||
7. tray_offset_mm sets the shift
|
||||
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
|
||||
@@ -259,8 +259,7 @@ def test_envelope(s):
|
||||
|
||||
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")):
|
||||
for line, want in (("M103 P2", "error:39"), ("M103 P1 X5", "error:31")):
|
||||
r = refused(s, line)
|
||||
if r != want:
|
||||
fail("[errors] %r got %r, not %s" % (line, r, want))
|
||||
@@ -270,7 +269,7 @@ def test_errors(s):
|
||||
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")
|
||||
print("PASS [errors]: P2 and an axis word were refused and changed nothing")
|
||||
|
||||
|
||||
def test_barrier(s):
|
||||
@@ -391,7 +390,7 @@ def test_persistence(s):
|
||||
|
||||
|
||||
def test_offset():
|
||||
for key, k in (("20", 58), ("5", 38), ("99", 175)):
|
||||
for key, k in (("20", 58),):
|
||||
s = Session(conf={"tray_offset_mm": key})
|
||||
try:
|
||||
s.sender.send("M103 P1")
|
||||
@@ -400,8 +399,7 @@ def test_offset():
|
||||
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)")
|
||||
print("PASS [offset]: tray_offset_mm 20 moved Z 58 half-steps")
|
||||
|
||||
|
||||
def test_unreferenced():
|
||||
|
||||
@@ -11,15 +11,15 @@ never takes them typed: $100/$101, the machine tick, and the kernel stop
|
||||
ramp. This harness drives the native grblHAL_glowforge binary in
|
||||
null-sink mode (no hardware, no root) over TCP, one process per config:
|
||||
|
||||
1. no key: x32, $100/$101 = 213.333, the 112640 Hz tick
|
||||
2. xy_microsteps = 16: 106.667, the 56320 Hz tick; a typed $100 is
|
||||
overwritten on the spot; $110 is left alone under a tick that
|
||||
carries it
|
||||
3. xy_microsteps = 32: 213.333, the 112640 Hz tick
|
||||
4. a value that is not a mode: x32 with a warning in the log
|
||||
5. GFSINK_RATE lowered under the mode's tick: $110/$111 are held at
|
||||
1. no key: x32, $100/$101 = 213.333, and $110 stands
|
||||
2. xy_microsteps = 16: 106.667; a typed $100 is overwritten on the
|
||||
spot; $110 is left alone under a tick that carries it
|
||||
3. GFSINK_RATE lowered under the mode's tick: $110/$111 are held at
|
||||
the feed the tick carries, one step per tick per axis
|
||||
|
||||
Each mode's derivations, and a value that is not a mode, are the host
|
||||
unit test's (xy_scale_test.c in grblHAL-glowforge).
|
||||
|
||||
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.
|
||||
@@ -142,10 +142,6 @@ def expect_scale(s, mode, spm):
|
||||
"%s is %.3f at x%d, want %.3f" % (key, got, mode, spm))
|
||||
|
||||
|
||||
def expect_log(log, needle, what):
|
||||
check(needle in log, what, "the log does not say %r:\n%s" % (needle, log))
|
||||
|
||||
|
||||
def case_default():
|
||||
s = Session()
|
||||
try:
|
||||
@@ -154,8 +150,7 @@ def case_default():
|
||||
check(near(rate, 12000.0), "$110=%.0f stands at x32" % rate,
|
||||
"$110 is %.3f at x32, want 12000" % rate)
|
||||
finally:
|
||||
log = s.close()
|
||||
expect_log(log, "x32 microsteps, 112640 Hz machine tick", "no key: x32, the 112640 Hz tick")
|
||||
s.close()
|
||||
|
||||
|
||||
def case_x16():
|
||||
@@ -172,27 +167,7 @@ def case_x16():
|
||||
check(near(rate, 12000.0), "$110=%.0f stands under a tick that carries it" % rate,
|
||||
"$110 is %.3f under x16, want 12000" % rate)
|
||||
finally:
|
||||
log = s.close()
|
||||
expect_log(log, "x16 microsteps, 56320 Hz machine tick", "x16: the 56320 Hz tick")
|
||||
|
||||
|
||||
def case_x32():
|
||||
s = Session("xy_microsteps = 32\n")
|
||||
try:
|
||||
expect_scale(s, 32, 213.333)
|
||||
finally:
|
||||
log = s.close()
|
||||
expect_log(log, "x32 microsteps, 112640 Hz machine tick", "x32: the 112640 Hz tick")
|
||||
|
||||
|
||||
def case_invalid():
|
||||
s = Session("xy_microsteps = 24\n")
|
||||
try:
|
||||
expect_scale(s, 32, 213.333)
|
||||
finally:
|
||||
log = s.close()
|
||||
expect_log(log, "xy_microsteps '24' is not 8, 16 or 32", "a value that is not a mode is refused with a warning")
|
||||
expect_log(log, "x32 microsteps, 112640 Hz machine tick", "and the machine runs at x32")
|
||||
s.close()
|
||||
|
||||
|
||||
def case_low_tick():
|
||||
@@ -211,9 +186,7 @@ def case_low_tick():
|
||||
check(near(got, 562.5, 0.05), "a typed $110=6000 comes back held at %.3f" % got,
|
||||
"a typed $110=6000 stuck at %.3f under a 1000 Hz tick" % got)
|
||||
finally:
|
||||
log = s.close()
|
||||
expect_log(log, "1000 Hz machine tick", "GFSINK_RATE took the tick to 1000 Hz")
|
||||
expect_log(log, "held at the ceiling", "the hold is logged")
|
||||
s.close()
|
||||
|
||||
|
||||
def main():
|
||||
@@ -221,8 +194,6 @@ def main():
|
||||
fail("no controller binary at %s" % BIN)
|
||||
case_default()
|
||||
case_x16()
|
||||
case_x32()
|
||||
case_invalid()
|
||||
case_low_tick()
|
||||
print("PASS xy_mode_test")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user