diff --git a/forgetest/forgetest/bench.py b/forgetest/forgetest/bench.py index 11ba279..c57f84d 100644 --- a/forgetest/forgetest/bench.py +++ b/forgetest/forgetest/bench.py @@ -283,6 +283,17 @@ TOOLS = [ "wait, and a wait under an open armed window with M3 S500 ships no FIRE tick. Imports its " "sender and port client from ctlport_test.py. 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": "envelope-test", "title": "Measured envelope harness", "script": "envelope_test.py", + "safety": "dry", "where": "host", "ported": False, "args": [], + "desc": "The measured work envelope and the bed check's port op, on the null-sink controller with a " + "scripted sender and a port client: unset keys are the axis travel, envelope_x_mm and _y are the " + "far edges at a home and are held to 50 mm up to the travel plus 30 mm, the port's envelope open " + "is refused before a home and under an open armed window and lets the port's jogs reach the " + "travel plus 30 mm, envelope apply puts the keys' edges back, and the sender's first line (never " + "a status poll or an empty line), a soft reset, and the port client going away each close an " + "open envelope, the sender's line only after it closed. Imports its sender and port client from " + "ctlport_test.py. 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": "manual-home-test", "title": "Manual home and motor release harness", "script": "manual_home_test.py", "safety": "dry", "where": "host", "ported": False, "args": [], "desc": "The manual homing provider and the motor release on the null-sink controller: $H under manual " diff --git a/forgetest/forgetest/suite/__init__.py b/forgetest/forgetest/suite/__init__.py index d16b23b..c9d9225 100644 --- a/forgetest/forgetest/suite/__init__.py +++ b/forgetest/forgetest/suite/__init__.py @@ -31,3 +31,4 @@ from . import extmcode # noqa: F401,E402 from . import extwizard # noqa: F401,E402 from . import updlock # noqa: F401,E402 from . import homeoff # noqa: F401,E402 +from . import bedsize # noqa: F401,E402 diff --git a/forgetest/forgetest/suite/bedsize.py b/forgetest/forgetest/suite/bedsize.py new file mode 100644 index 0000000..5d30584 --- /dev/null +++ b/forgetest/forgetest/suite/bedsize.py @@ -0,0 +1,255 @@ +# Copyright 2026 514 LLC d/b/a OpenGlow +# Written by Scott Wiederhold +# https://community.openglow.org +# SPDX-License-Identifier: MIT + +"""The Setup page's bed check, on the machine. + +Its own module, so that no other test's fingerprint moves. The check is +driven from here inside the travel the machine already knows: a few jogs +out from the camera home, never toward an end, so it needs no operator. +Measuring the real ends is the operator's own run of the check. The +homing is the web-service session homing.cloud-offsets runs. +""" + +import json +import time + +from ..catalog import test +from .cloud import _HOMING_PATH, gfhome_homing +from .setup import read_file, record_path, write_file +from .setup_dark import DARK_COVERS, answer, dark, start + +WID = "motion.envelope" +END = "This is the end" +# The operator's answers, as the page sends them: out 60.1 mm in X and +# 61.1 mm in Y from the home, the fine steps included. A far edge is 50 mm +# at the least, so the ends are well past that and well inside the bed. +X_STEPS = ["X+10"] * 6 + ["X+1", "X+0.1", "X-1"] +Y_STEPS = ["Y+10"] * 6 + ["Y+1", "Y+0.1"] +X_OUT, Y_OUT = 60.1, 61.1 +MARGIN_MM = 1.0 +KEYS = ("envelope_x_mm", "envelope_y_mm", "homing_mode") +CLOSED = "closed the check's envelope" +CLOSED_MSG = "Bed check envelope closed: a sender line" + + +def _port(fc): + st, body = fc.get("/motion/state") + return body if st == 200 and isinstance(body, dict) else {} + + +def _drive(ctx, fc, x_steps, y_steps, hook=None, timeout=300): + """Start the check and answer each prompt from the steps in turn, then + This is the end; hook(prompt_id, n) runs before the answer to the n-th + prompt. The final status document, and the answers given.""" + # The daemon calls the machine busy for a moment after the last jog. + ctx.check(fc.wait_idle(timeout=30, abort=ctx.aborted), "the machine is not idle: %s", fc.status().get("state")) + start(ctx, fc, WID) + queue = {"x-end": list(x_steps) + [END], "y-end": list(y_steps) + [END]} + seen, given = set(), [] + t0 = time.time() + try: + while time.time() - t0 < timeout: + ctx.checkpoint() + d = dark(fc) + p = d.get("prompt") + if d.get("running") and p and p.get("seq") not in seen: + seen.add(p["seq"]) + pid = p.get("id") + ctx.check(pid in queue and queue[pid], "the check asked %s: %s (the lid must be closed)", + pid, p.get("text")) + ctx.check(p.get("kind") == "jog" and END in (p.get("options") or []), + "the %s prompt is not a jog with %r: %s", pid, END, p) + if hook: + hook(pid, len(given)) + value = queue[pid].pop(0) + st, body = answer(fc, WID, p, value) + ctx.check(st == 200, "the answer %s to %s -> %s %s", value, pid, st, body) + given.append(value) + if not d.get("running"): + return d, given + time.sleep(0.3) + except BaseException: + fc.post("/wiz/%s/abort" % WID) + raise + fc.post("/wiz/%s/abort" % WID) + ctx.fail("the bed check did not end within %d s", timeout) + + +def _jog(fc, dx=0.0, dy=0.0): + params = {"feed": "1200"} + if dx: + params["x"] = "%.3f" % dx + if dy: + params["y"] = "%.3f" % dy + return fc.post("/motion/jog", params=params) + + +def _settled(ctx, fc): + """After the suite's Grbl client closes, the daemon calls the machine + busy for a moment: the check and a settings write each wait for it.""" + ctx.wait_for(lambda: _port(fc).get("sender") is False, 20, poll=0.5) + ctx.check(fc.wait_idle(timeout=30, abort=ctx.aborted), "the machine is not idle: %s", fc.status().get("state")) + + +def _put_back(ctx, fc, found): + for k in KEYS: + v = found.get(k) or "" + + def put(): + st_, body_ = fc.post("/settings", params={k: ""}) if v == "" else fc.post("/settings", data={k: v}) + if st_ != 200: + ctx.log("restore %s=%r -> %s %s", k, v, st_, body_) + return st_ == 200 or None + took = ctx.wait_for(put, 20, poll=0.5) + ctx.log("restore %s=%r -> %s", k, v, "taken" if took is not None else "not taken") + + +def _back_home(ctx, fc, home): + """The head back where the home put it, in the port's bounded steps, so + the run ends where it began.""" + for _ in range(8): + at = _port(fc).get("mpos") or [home[0], home[1]] + dx, dy = home[0] - at[0], home[1] - at[1] + if abs(dx) < 0.005 and abs(dy) < 0.005: + return + st, body = _jog(fc, max(-100.0, min(100.0, dx)), max(-100.0, min(100.0, dy))) + if st != 200: + ctx.log("the jog back to the home -> %s %s", st, body) + ctx.wait_for(lambda: fc.status().get("state") == "idle", 10, poll=0.5) + continue + _port_idle(ctx, fc) + ctx.log("the head is not back at the home: %s", _port(fc).get("mpos")) + + +def _port_idle(ctx, fc): + ok = ctx.wait_for(lambda: (lambda s: s.get("state") == "Idle" and not s.get("port_jog"))(_port(fc)), 30, + poll=0.1) + ctx.check(ok is not None, "the port jog did not end: %s", _port(fc)) + + +def _edge(ctx, fc, ev, axis, edge): + """From where the head stands, a jog 0.5 mm short of the far edge runs + and comes back, and one 0.5 mm past it is refused with nothing moved.""" + s = _port(fc) + at = s["mpos"]["XY".index(axis)] + short, past = edge - 0.5 - at, edge + 0.5 - at + kw = (lambda d: {"dx": d}) if axis == "X" else (lambda d: {"dy": d}) + st1, b1 = _jog(fc, **kw(short)) + _port_idle(ctx, fc) + st2, b2 = _jog(fc, **kw(-short)) + _port_idle(ctx, fc) + st3, b3 = _jog(fc, **kw(past)) + _port_idle(ctx, fc) + back = _port(fc)["mpos"]["XY".index(axis)] + ev["edge_" + axis] = {"edge": edge, "short": [st1, st2], "past": [st3, b3], "at": [at, back]} + ctx.check(st1 == 200 and st2 == 200, "a jog 0.5 mm short of the %s edge %.2f and back -> %s %s, %s %s", + axis, edge, st1, b1, st2, b2) + ctx.check(st3 == 409 and "envelope" in json.dumps(b3) and abs(back - at) < 0.02, + "a jog 0.5 mm past the %s edge %.2f -> %s %s, the head at %.3f from %.3f", axis, edge, st3, b3, back, at) + + +@test("setup.check-envelope", title="The bed check measures the far edges from the operator's jogs, and a Grbl " + "client's line ends it with nothing written", + subsystem="setup", kind="auto", mode="grbl", est_min=10, + covers=DARK_COVERS + _HOMING_PATH + [("forgectrl", "src/grblport.*"), + ("grblhal-glowforge", "src/glowforge_homing.*"), + ("grblhal-glowforge", "src/ctlport.*"), + ("grblhal-glowforge", "src/driver.c")], + requires=["forgectrl.auth", "motion.pacing"], + steps=["Bed clear, lid closed; cloud credentials configured; the machine on the network. The head moves " + "at most about 62 mm out from the camera home in X and in Y."], + description="With cloud mode on, the machine is homed with the camera (the web-service session). " + "POST /wiz/motion.envelope/start runs the Bed size check; its prompts are jogs with This is the " + "end among the options, and are answered as the page would, out 60.1 mm in X and 61.1 mm in Y " + "(steps of 10, 1, and 0.1 mm). The check ends complete: the ends it reports are the home plus " + "those, the far edges 1 mm short of them are written as envelope_x_mm and envelope_y_mm, and the " + "controller holds them at once, without a home: a panel jog 0.5 mm short of each edge runs, " + "and one 0.5 mm past it is refused with nothing moved. A second run is opened, and while its " + "first prompt waits a Grbl client sends one line, which draws the controller's word that it " + "closed the envelope and then ok; the port's state then says the envelope is closed, and the " + "next answer ends the check in words, with the keys as the " + "first run left them and the edges still in force. The keys, the homing mode, and the setup " + "record are put back as found, the record under a restart, and the head is jogged back to " + "the home.") +def check_envelope(ctx): + fc = ctx.forgectrl + ev = ctx.evidence + s0 = fc.settings() or {} + ctx.check(s0.get("cloud_enabled") == "1", "cloud mode is off: a camera home needs it on") + raw = read_file(record_path()) + found = {k: s0.get(k) or "" for k in KEYS} + ev["found"] = found + home = None + try: + st, body = fc.post("/settings", data={"homing_mode": "gfcloud"}) + ctx.check(st == 200, "homing_mode=gfcloud -> %s %s", st, body) + with ctx.grbl() as g: + gfhome_homing(ctx, ev, g) + home = g.status_report().get("MPos") + ev["home"] = home + ctx.log("the camera home declared %s", home) + ctx.check(home and home[0] is not None, "no position after the home: %s", home) + _settled(ctx, fc) + + last, given = _drive(ctx, fc, X_STEPS, Y_STEPS) + r = last.get("result") or {} + ev["run1"] = {"answers": given, "error": last.get("error"), "result": r, "log": last.get("log", [])[-8:]} + ctx.log("the bed check ended: %s", json.dumps(r)) + ctx.check(not last.get("error"), "the bed check failed: %s", last.get("error")) + want_x, want_y = home[0] + X_OUT, home[1] + Y_OUT + ctx.check(abs((r.get("x_end_mm") or -1) - want_x) < 0.02 and abs((r.get("y_end_mm") or -1) - want_y) < 0.02, + "the ends are not the home plus the jogs (%.2f, %.2f): %s", want_x, want_y, r) + edge_x, edge_y = round(want_x - MARGIN_MM, 2), round(want_y - MARGIN_MM, 2) + s1 = fc.settings() or {} + ev["keys_after_run1"] = {k: s1.get(k) for k in ("envelope_x_mm", "envelope_y_mm")} + ctx.check(abs(float(s1.get("envelope_x_mm") or 0) - edge_x) < 0.011 and + abs(float(s1.get("envelope_y_mm") or 0) - edge_y) < 0.011, + "the keys are not the ends less %.1f mm (%.2f, %.2f): %s", MARGIN_MM, edge_x, edge_y, + ev["keys_after_run1"]) + ctx.check(_port(fc).get("envelope_open") is False, "the envelope is still open after the check: %s", + _port(fc)) + _edge(ctx, fc, ev, "X", edge_x) + _edge(ctx, fc, ev, "Y", edge_y) + + # A Grbl client's line while the envelope is open ends the check. + said = {} + + def client_line(pid, n): + if n: + return + ctx.check(_port(fc).get("envelope_open") is True, "the second run did not open the envelope: %s", + _port(fc)) + with ctx.grbl() as g2: + said["reply"] = g2.command("G90", timeout=10) + said["text"] = g2.drain() + said["port"] = _port(fc) + + last2, given2 = _drive(ctx, fc, ["X+1"], [], hook=client_line) + ev["run2"] = {"answers": given2, "said": said, "error": last2.get("error"), "log": last2.get("log", [])[-6:]} + ctx.log("the second run: %s", json.dumps(ev["run2"])) + ctx.check((said.get("reply") or [])[-1:] == ["ok"], "the client's line did not draw ok: %s", said) + ctx.check(any(CLOSED_MSG in l for l in (said.get("reply") or [])) or CLOSED_MSG in (said.get("text") or ""), + "the client was not told its line closed the envelope: %s", said) + ctx.check((said.get("port") or {}).get("envelope_open") is False, + "the client's line did not close the envelope: %s", said.get("port")) + ctx.check(CLOSED in (last2.get("error") or ""), "the second run did not end on the closed envelope: %s", + last2.get("error")) + s2 = fc.settings() or {} + ctx.check({k: s2.get(k) for k in ("envelope_x_mm", "envelope_y_mm")} == ev["keys_after_run1"], + "the second run wrote the keys: %s", s2) + _settled(ctx, fc) + _edge(ctx, fc, ev, "X", edge_x) + finally: + fc.post("/wiz/%s/abort" % WID) + if home and home[0] is not None: + ctx.wait_for(lambda: not dark(fc).get("running"), 30, poll=0.5) + _back_home(ctx, fc, home) + _put_back(ctx, fc, found) + # The check records itself when it completes; the record goes back + # as found under a restart, which also ends the envelope it set. + if read_file(record_path()) != raw: + with ctx.takeover(): + write_file(record_path(), raw) + ctx.log("the previous setup record is back under a restart") diff --git a/scripts/bench/envelope_test.py b/scripts/bench/envelope_test.py new file mode 100644 index 0000000..be84556 --- /dev/null +++ b/scripts/bench/envelope_test.py @@ -0,0 +1,266 @@ +#!/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 measured work envelope and the bed check's port op. + +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 + 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 + 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) + +Usage: envelope_test.py [path/to/grblHAL_glowforge] +""" +import os +import sys +import time + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import ctlport_test as ct # noqa: E402 + +fail = ct.fail +TRAVEL_X, TRAVEL_Y, EXTRA = 495.0, 279.0, 30.0 + + +def conf(s, **keys): + with open(os.path.join(s.workdir, "forgefirm.conf"), "w") as f: + f.write("cool_fan_grace_s = 0\nlaser_disarm_s = 30\nhoming_mode = manual\n") + for k, v in keys.items(): + f.write("%s = %s\n" % (k, v)) + + +def home(s): + if s.sender.send("$H", timeout=10) != "ok": + fail("manual $H refused") + s.sender.wait_state("Idle") + + +def jog_to(s, axis, mm): + """The sender's absolute jog; its response (ok or error:N), and back to 10 mm when it ran.""" + r = s.sender.send("$J=G90 G21 %s%.3f F6000" % (axis, mm)) + s.sender.wait_state("Idle") + if r == "ok": + s.sender.send("$J=G90 G21 %s10 F6000" % axis) + s.sender.wait_state("Idle") + return r + + +def edge(s, axis, inside, outside, what): + a, b = jog_to(s, axis, inside), jog_to(s, axis, outside) + if a != "ok" or b != "error:15": + fail("[%s] %s: a jog to %.2f -> %s, to %.2f -> %s" % (what, axis, inside, a, outside, b)) + + +def port_idle(s, timeout=20.0): + end = time.time() + timeout + while time.time() < end: + st = s.port.state() + if st["state"] == "Idle" and not st["port_jog"]: + return st + time.sleep(0.05) + fail("the port never saw Idle: %r" % s.port.state()) + + +def port_jog_to(s, axis, mm): + """The port's absolute jog, which leaves an open envelope open.""" + r = s.port.request("jog G90 G21 %s%.3f F6000" % (axis, mm)) + port_idle(s) + if r == "ok": + s.port.request("jog G90 G21 %s10 F6000" % axis) + port_idle(s) + return r + + +def port_edge(s, axis, inside, outside, what): + a, b = port_jog_to(s, axis, inside), port_jog_to(s, axis, outside) + if a != "ok" or b != "error:15": + fail("[%s] %s: a port jog to %.2f -> %s, to %.2f -> %s" % (what, axis, inside, a, outside, b)) + + +def envelope_open(s): + return s.port.state()["envelope_open"] + + +def open_envelope(s, what): + s.quiet() + r = s.port.request("envelope open") + if r != "ok" or not envelope_open(s): + fail("[%s] envelope open after a home -> %r, state %r" % (what, r, s.port.state())) + + +def saw_within(s, needle, seconds=2.0): + end = time.time() + seconds + while not s.sender.saw(needle) and time.time() < end: + time.sleep(0.05) + 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) + edge(s, "X", 479, 481, "measured") + edge(s, "Y", 284, 285, "measured") + 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) + before = s.sender.saw("Envelope open for the bed check") + open_envelope(s, "open") + if saw_within(s, "Envelope open for the bed check") <= before: + fail("[open] the sender got no [MSG:] for the open envelope: %r" % s.sender.other[-4:]) + port_edge(s, "X", TRAVEL_X + EXTRA - 1, TRAVEL_X + EXTRA + 1, "open") + port_edge(s, "Y", TRAVEL_Y + EXTRA - 1, TRAVEL_Y + EXTRA + 1, "open") + if not envelope_open(s): + fail("[open] the port's own jogs closed the envelope") + r = s.port.request("envelope apply") + if r != "ok" or envelope_open(s): + fail("[apply] envelope apply -> %r, state %r" % (r, s.port.state())) + edge(s, "X", 479, 481, "apply") + edge(s, "Y", 249, 251, "apply") + print("PASS [open-apply]: open, the port's jogs reach the travel plus 30 mm; apply, the keys' edges again, " + "no home") + + +def test_sender_closes(s): + conf(s, envelope_x_mm=480, envelope_y_mm=250) + home(s) + open_envelope(s, "sender") + s.sender.state() # '?' is realtime + s.sender.realtime(b"\n") # and an empty line no claim on the machine + time.sleep(0.5) + if not envelope_open(s): + fail("[sender] a status poll or an empty line closed the envelope") + before = s.sender.saw("Bed check envelope closed") + r = s.sender.send("$J=G90 G21 X%.3f F6000" % (TRAVEL_X + EXTRA - 1)) + s.sender.wait_state("Idle") + if r != "error:15": + fail("[sender] the sender's jog into the open envelope's margin -> %r: the core read it before the " + "envelope closed" % r) + if envelope_open(s) or saw_within(s, "Bed check envelope closed") <= before: + fail("[sender] the sender's line did not close the envelope: %r" % s.port.state()) + edge(s, "X", 479, 481, "sender") + # A port jog running when the sender's line arrives is canceled first. + open_envelope(s, "sender-jog") + x0 = s.port.state()["mpos"][0] + if s.port.request("jog G91 G21 X300 F6000") != "ok": + fail("[sender-jog] the long port jog was refused") + time.sleep(0.7) + # A jog line: the core holds a G-code line's parser error for the next + # one, and the edge above ended in error:15. + r = s.sender.send("$J=G90 G21 X20 F6000", timeout=10.0) + s.sender.wait_state("Idle") + st = s.port.state() + if r != "ok" or st["envelope_open"] or st["port_jog"] or abs(st["mpos"][0] - 20) > 0.01 or x0 > 20: + fail("[sender-jog] the sender's line during a port jog -> %r, state %r" % (r, st)) + print("PASS [sender]: a status poll and an empty line leave the envelope open; the sender's first line " + "closes it before the core reads it, a running port jog canceled first") + + +def test_reset_closes(s): + conf(s, envelope_x_mm=480, envelope_y_mm=250) + home(s) + open_envelope(s, "reset") + s.sender.realtime(b"\x18") + time.sleep(1.0) + if envelope_open(s): + fail("[reset] a soft reset left the envelope open") + if s.sender.state() == "Alarm": + s.sender.send("$X") + s.sender.wait_state("Idle") + edge(s, "X", 479, 481, "reset") + open_envelope(s, "client") + s.port.close() + s.port = ct.PortClient(s.path) + if envelope_open(s): + fail("[client] the port client went away and the envelope stayed open") + print("PASS [reset]: a soft reset and the port client going away each closed the open envelope") + + +def test_not_homed(): + s = ct.Session() + try: + conf(s) + s.quiet() + r = s.port.request("envelope open") + if r != "error:homed": + fail("[not-homed] envelope open before a home -> %r" % r) + print("PASS [not-homed]: envelope open before a home -> error:homed") + finally: + s.close() + + +def test_armed(): + s = ct.Session(laser=True) + try: + conf(s) + home(s) + s.sender.send("G91") + s.sender.send("M3 S500") + s.sender.send("G1 X5 F1500") + s.sender.wait_state("Idle") + time.sleep(0.5) + if not s.sender.saw("laser armed"): + fail("[armed] the laser line did not arm: the case would prove nothing") + s.quiet() + r = s.port.request("envelope open") + if r != "busy:state": + fail("[armed] envelope open under an open armed window -> %r" % r) + print("PASS [armed]: envelope open under an open armed window -> busy:state") + finally: + s.close() + + +def main(): + if not os.path.exists(ct.BIN): + 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) + finally: + s.close() + test_not_homed() + test_armed() + print("ALL PASS") + + +if __name__ == "__main__": + main()