mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
forgetest: cloud.mode-switch opens the lid behind the controller's start, ahead of the hunt
The supervisor holds every controller spawn until the enclosure is closed (forgectrl 0.1.25), and the test opened the lid before it asked for the cloud controller: POST /mode answered "waiting, the lid is open" and the controller never came up. The round trip now switches with the lid closed, polls /mode five times a second, and opens the lid the moment the controller is running. The client requests its connect-time hunt a few seconds after its start, right behind its session, so the hunt still finds the lid open. The order is recorded and judged: the hunt's request line must not be in the client's log when the lid reads open (hunt_before_lid_open), and the test refuses to start with the lid open. The catalog text tells the operator to open the lid at once, with a hand ready on it. Proof. Host: test_cloud_suite drives the round trip with the hunt landing only once the lid reads open, as on the bench, plus the lost race (the hunt requested before the lid opened fails the test with "before the lid was open") and the start with the lid open refused; 8 mode-switch cases green. Bench reference (dev image 20260915001814, forgectrl 0.1.25): cloud.mode-switch PASS in 118 s, the lid open 4 s before the client requested its hunt, no refusal before the hunt's end, the lens homed, the exhaust row unjudged, 5 service motions after the lid closed, $H under gfhome homed in 48.4 s with 9 motion windows. No catalog consequence beyond the test itself: its covers map is unchanged.
This commit is contained in:
@@ -87,6 +87,9 @@ FORGECTRL_LOG = "/data/log/forgefirm/forgectrl/forgectrl.log"
|
|||||||
LIMITS_MARK = "job limits from the header: "
|
LIMITS_MARK = "job limits from the header: "
|
||||||
EFFECTIVE_MARK = "effective limits: coolant ceiling "
|
EFFECTIVE_MARK = "effective limits: coolant ceiling "
|
||||||
SESSION_MARKS = ("authenticate_machine SUCCESS", "ws_connect ESTABLISHED")
|
SESSION_MARKS = ("authenticate_machine SUCCESS", "ws_connect ESTABLISHED")
|
||||||
|
# The service's connect-time hunt, as the client logs its request: a few
|
||||||
|
# seconds after the controller starts, right behind the session.
|
||||||
|
HUNT_REQUEST = "service action request: hunt"
|
||||||
RETURN_MAX_MM = 600.0 # the head comes back from the home corner across the bed
|
RETURN_MAX_MM = 600.0 # the head comes back from the home corner across the bed
|
||||||
|
|
||||||
|
|
||||||
@@ -145,7 +148,7 @@ def return_head(ctx, feed=2400):
|
|||||||
"head not back at the start after the return jog: %s", pos)
|
"head not back at the start after the return jog: %s", pos)
|
||||||
|
|
||||||
|
|
||||||
def wait_mode(ctx, fc, want_mode, want_controller="running", timeout=90):
|
def wait_mode(ctx, fc, want_mode, want_controller="running", timeout=90, poll=1.0):
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
last = None
|
last = None
|
||||||
while time.time() - t0 < timeout:
|
while time.time() - t0 < timeout:
|
||||||
@@ -157,7 +160,7 @@ def wait_mode(ctx, fc, want_mode, want_controller="running", timeout=90):
|
|||||||
return m
|
return m
|
||||||
if m.get("controller") == "motion-fault":
|
if m.get("controller") == "motion-fault":
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(poll)
|
||||||
return last
|
return last
|
||||||
|
|
||||||
|
|
||||||
@@ -240,17 +243,22 @@ def gfhome_homing(ctx, ev, g):
|
|||||||
covers=_HOMING_PATH + [("forgectrl", "src/cool.*"), ("forgectrl", "src/airflow.*"),
|
covers=_HOMING_PATH + [("forgectrl", "src/cool.*"), ("forgectrl", "src/airflow.*"),
|
||||||
("grblhal-glowforge", "src/**")],
|
("grblhal-glowforge", "src/**")],
|
||||||
requires=["forgectrl.auth", "motion.pacing"], actions=["lid"],
|
requires=["forgectrl.auth", "motion.pacing"], actions=["lid"],
|
||||||
steps=["Bed clear; cloud credentials configured; the machine on the network. The test "
|
steps=["Bed clear, lid closed; cloud credentials configured; the machine on the network. "
|
||||||
"turns cloud mode and the gfcloud homing on itself when they are off, and puts "
|
"The test turns cloud mode and the gfcloud homing on itself when they are off, and "
|
||||||
"the settings back at the end.",
|
"puts the settings back at the end.",
|
||||||
"Open the lid when told and leave it open through the cloud client's connect and its "
|
"Open the lid the moment you are told, with a hand ready on it: the cloud client's "
|
||||||
"hunt; close it when told. Nothing else: the switch back and the $H homing run on "
|
"first hunt begins a few seconds after its controller starts and must find the lid "
|
||||||
"their own, and the head ends parked at the home corner."],
|
"open. Leave it open through the connect and the hunt; close it when told. Nothing "
|
||||||
|
"else: the switch back and the $H homing run on their own, and the head ends parked "
|
||||||
|
"at the home corner."],
|
||||||
description="One round trip with the two service-driven motions on it. POST /mode switches "
|
description="One round trip with the two service-driven motions on it. POST /mode switches "
|
||||||
"to the cloud controller: gfcloud comes up under supervision, authenticates and "
|
"to the cloud controller with the lid closed (no controller starts with the "
|
||||||
|
"enclosure open): gfcloud comes up under supervision, authenticates and "
|
||||||
"establishes its service session (its own log lines are the evidence; the "
|
"establishes its service session (its own log lines are the evidence; the "
|
||||||
"connect-time firmware probe is recorded when configured). Its connect-time hunt "
|
"connect-time firmware probe is recorded when configured). The lid is opened the "
|
||||||
"runs with the lid OPEN, as the factory's does - it completes, nothing before its "
|
"moment the controller is up, before the client requests its connect-time hunt "
|
||||||
|
"(a hunt requested before the lid was open fails the test), so the hunt runs "
|
||||||
|
"with the lid OPEN, as the factory's does - it completes, nothing before its "
|
||||||
"end is refused for the lid, the lens homes - and with the extraction fans off it "
|
"end is refused for the lid, the lens homes - and with the extraction fans off it "
|
||||||
"is measured by the airflow gates but not judged (no AIRFLOW, the exhaust row "
|
"is measured by the airflow gates but not judged (no AIRFLOW, the exhaust row "
|
||||||
"reads unjudged); the camera service survives the switch. The lid closed, the "
|
"reads unjudged); the camera service survives the switch. The lid closed, the "
|
||||||
@@ -275,17 +283,32 @@ def mode_switch(ctx):
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
lamp0 = hw.sysfs_read("pic/lid_led")
|
lamp0 = hw.sysfs_read("pic/lid_led")
|
||||||
|
ctx.check(ctx.switch("lid") is True, "close the lid first: no controller starts with it open")
|
||||||
|
|
||||||
ctx.act("lid", "open", text="Leave it open: the cloud client connects and hunts with the lid open.")
|
# The switch is made with the enclosure closed (the supervisor holds
|
||||||
|
# every spawn until it is). The lid opens the moment the controller is
|
||||||
|
# up: the client requests its connect-time hunt a few seconds after
|
||||||
|
# its start, right behind its session, and the hunt must find the lid
|
||||||
|
# open. The poll is tight so the window is not spent waiting.
|
||||||
log_offset = log_size(GFCLOUD_LOG)
|
log_offset = log_size(GFCLOUD_LOG)
|
||||||
st, body = fc.post("/mode", data={"controller": "cloud"})
|
st, body = fc.post("/mode", data={"controller": "cloud"})
|
||||||
ctx.log("POST /mode controller=cloud -> %s %s", st, body)
|
ctx.log("POST /mode controller=cloud -> %s %s", st, body)
|
||||||
ctx.check(st == 200, "mode switch to cloud refused: %s %s", st, body)
|
ctx.check(st == 200, "mode switch to cloud refused: %s %s", st, body)
|
||||||
m = wait_mode(ctx, fc, "cloud", timeout=90)
|
m = wait_mode(ctx, fc, "cloud", timeout=90, poll=0.2)
|
||||||
ev["mode_cloud"] = m
|
ev["mode_cloud"] = m
|
||||||
ctx.log("mode after switch: %s", m)
|
ctx.log("mode after switch: %s", m)
|
||||||
ctx.check(m and m.get("mode") == "cloud" and m.get("controller") == "running",
|
ctx.check(m and m.get("mode") == "cloud" and m.get("controller") == "running",
|
||||||
"cloud controller did not come up: %s", m)
|
"cloud controller did not come up: %s", m)
|
||||||
|
ctx.act("lid", "open", text="Now, at once: the cloud client's first hunt begins a few seconds "
|
||||||
|
"after its controller starts and must find the lid open. Leave it open through the "
|
||||||
|
"connect and the hunt.")
|
||||||
|
# The order is the proof: the hunt's request is not in the log yet
|
||||||
|
# when the lid reads open.
|
||||||
|
ev["hunt_before_lid_open"] = log_has(log_offset, HUNT_REQUEST)
|
||||||
|
ctx.check(not ev["hunt_before_lid_open"],
|
||||||
|
"the client requested its hunt before the lid was open (the hunt follows the "
|
||||||
|
"controller's start by a few seconds): the lid opened too late, so a hunt with the "
|
||||||
|
"lid open is not proven")
|
||||||
# the client's own session lines are the evidence of a live cloud session
|
# the client's own session lines are the evidence of a live cloud session
|
||||||
session = wait_session(ctx, log_offset)
|
session = wait_session(ctx, log_offset)
|
||||||
probe = None
|
probe = None
|
||||||
|
|||||||
@@ -558,12 +558,16 @@ class CloudSuiteTests(unittest.TestCase):
|
|||||||
HUNT_RUN_SAMPLE = {"phase": "run", "verdict": "ok", "armed": False,
|
HUNT_RUN_SAMPLE = {"phase": "run", "verdict": "ok", "armed": False,
|
||||||
"fan_gates": {"exhaust": {"state": "unjudged", "reading": 0, "floor": 500}}}
|
"fan_gates": {"exhaust": {"state": "unjudged", "reading": 0, "floor": 500}}}
|
||||||
|
|
||||||
def mode_switch_setup(self, hunt_lines=None, home_complete=True):
|
def mode_switch_setup(self, hunt_lines=None, home_complete=True, lid_late=False):
|
||||||
"""The fakes a mode-switch run needs: grbl to answer $H, the lid
|
"""The fakes a mode-switch run needs: grbl to answer $H, the lid
|
||||||
lamp attr, homing_mode = gfcloud, the service lines landing on
|
lamp attr, homing_mode = gfcloud, the service lines landing on
|
||||||
the switch to cloud (the hunt reads as a run to the cooling
|
the switch to cloud - the client's start at once, its session and
|
||||||
engine while it lasts), the re-hunt on the lid close, and gfhome
|
hunt once the lid reads open, as on the bench where the lid opens
|
||||||
finishing the homing after $H."""
|
behind the controller's start and ahead of the hunt (the hunt
|
||||||
|
reads as a run to the cooling engine while it lasts) - the re-hunt
|
||||||
|
on the lid close, and gfhome finishing the homing after $H. With
|
||||||
|
lid_late the hunt is requested before the lid opens: the race the
|
||||||
|
test must call."""
|
||||||
self.grbl = helpers.FakeGrbl().start()
|
self.grbl = helpers.FakeGrbl().start()
|
||||||
os.makedirs(self.sysfs + "pic", exist_ok=True)
|
os.makedirs(self.sysfs + "pic", exist_ok=True)
|
||||||
self._attr("pic/lid_led", "236")
|
self._attr("pic/lid_led", "236")
|
||||||
@@ -575,22 +579,35 @@ class CloudSuiteTests(unittest.TestCase):
|
|||||||
hunt_part = hunt_lines(hunt_part)
|
hunt_part = hunt_lines(hunt_part)
|
||||||
fc = self.fc
|
fc = self.fc
|
||||||
|
|
||||||
|
def hunt():
|
||||||
|
self.append(hunt_part, delay=0.0)
|
||||||
|
time.sleep(4.0) # the hunt outlasts the session wait on the bench
|
||||||
|
fc.state["cool"] = {"phase": "idle", "armed": False, "hold": False}
|
||||||
|
|
||||||
def on_post(path, form):
|
def on_post(path, form):
|
||||||
if path == "/mode" and form.get("controller") == "cloud":
|
if path == "/mode" and form.get("controller") == "cloud":
|
||||||
fc.state["cool"] = dict(self.HUNT_RUN_SAMPLE)
|
fc.state["cool"] = dict(self.HUNT_RUN_SAMPLE)
|
||||||
|
|
||||||
def land():
|
def land():
|
||||||
self.append(pre, delay=0.0)
|
self.append(pre, delay=0.0)
|
||||||
|
if lid_late:
|
||||||
|
return
|
||||||
|
t0 = time.time()
|
||||||
|
while fc.state["status"]["switches"]["lid"] and time.time() - t0 < 5:
|
||||||
|
time.sleep(0.02)
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
self.append(hunt_part, delay=0.0)
|
hunt()
|
||||||
time.sleep(4.0) # the hunt outlasts the session wait on the bench
|
|
||||||
fc.state["cool"] = {"phase": "idle", "armed": False, "hold": False}
|
|
||||||
threading.Thread(target=land, daemon=True).start()
|
threading.Thread(target=land, daemon=True).start()
|
||||||
elif path == "/mode" and form.get("controller") == "grbl":
|
elif path == "/mode" and form.get("controller") == "grbl":
|
||||||
self.grbl.state = "Idle"
|
self.grbl.state = "Idle"
|
||||||
return None
|
return None
|
||||||
self.fc.on_post = on_post
|
self.fc.on_post = on_post
|
||||||
|
|
||||||
|
def lid_opened():
|
||||||
|
if lid_late:
|
||||||
|
self.append(hunt_part, delay=0.0)
|
||||||
|
self.lid(False)
|
||||||
|
|
||||||
def lid_closed():
|
def lid_closed():
|
||||||
self.lid(True)
|
self.lid(True)
|
||||||
self.append(close_part, delay=0.2)
|
self.append(close_part, delay=0.2)
|
||||||
@@ -605,7 +622,7 @@ class CloudSuiteTests(unittest.TestCase):
|
|||||||
f.write(b"2026-08-17T09:46:00.000000+00:00 gfhome[2300] INFO homing complete "
|
f.write(b"2026-08-17T09:46:00.000000+00:00 gfhome[2300] INFO homing complete "
|
||||||
b"(service quiet 8s, 3 motion windows)\n")
|
b"(service quiet 8s, 3 motion windows)\n")
|
||||||
self.home_hook = homing
|
self.home_hook = homing
|
||||||
return {"Open the lid.": lambda: self.lid(False), "Close the lid.": lid_closed}
|
return {"Open the lid.": lid_opened, "Close the lid.": lid_closed}
|
||||||
|
|
||||||
def wait_home_command(self):
|
def wait_home_command(self):
|
||||||
while "$H" not in self.grbl.sent:
|
while "$H" not in self.grbl.sent:
|
||||||
@@ -633,6 +650,29 @@ class CloudSuiteTests(unittest.TestCase):
|
|||||||
self.assertEqual(self.script.asked, [])
|
self.assertEqual(self.script.asked, [])
|
||||||
self.assertTrue(any("PASS:" in l for l in run.lines), run.lines[-5:])
|
self.assertTrue(any("PASS:" in l for l in run.lines), run.lines[-5:])
|
||||||
|
|
||||||
|
def test_mode_switch_opens_the_lid_behind_the_controller_and_ahead_of_the_hunt(self):
|
||||||
|
# No controller starts with the enclosure open: the switch is made
|
||||||
|
# lid closed, the lid opens once the controller is up, and the
|
||||||
|
# hunt's request lands after that - the order the test records.
|
||||||
|
hooks = self.mode_switch_setup()
|
||||||
|
threading.Thread(target=self.wait_home_command, daemon=True).start()
|
||||||
|
run = self.run_test(cloud.mode_switch, hooks=hooks, test_id="cloud.mode-switch")
|
||||||
|
self.assertFalse(run.evidence["hunt_before_lid_open"])
|
||||||
|
acts = [(a["channel"], a["state"]) for a in run.evidence["actions"]]
|
||||||
|
self.assertEqual(acts, [("lid", "open"), ("lid", "close")])
|
||||||
|
i_post = next(i for i, l in enumerate(run.lines) if "POST /mode controller=cloud" in l)
|
||||||
|
i_open = next(i for i, l in enumerate(run.lines) if "ACT lid open" in l)
|
||||||
|
self.assertLess(i_post, i_open)
|
||||||
|
|
||||||
|
def test_mode_switch_refuses_to_start_with_the_lid_open(self):
|
||||||
|
hooks = self.mode_switch_setup()
|
||||||
|
self.lid(False)
|
||||||
|
self.assertFails(cloud.mode_switch, "close the lid first", hooks=hooks)
|
||||||
|
|
||||||
|
def test_mode_switch_fails_when_the_hunt_was_requested_before_the_lid_opened(self):
|
||||||
|
hooks = self.mode_switch_setup(lid_late=True)
|
||||||
|
self.assertFails(cloud.mode_switch, "before the lid was open", hooks=hooks)
|
||||||
|
|
||||||
def test_mode_switch_fails_when_the_hunt_is_refused_for_the_lid(self):
|
def test_mode_switch_fails_when_the_hunt_is_refused_for_the_lid(self):
|
||||||
def refused(hunt_part):
|
def refused(hunt_part):
|
||||||
i = next(i for i, l in enumerate(hunt_part) if "z_axis:home starting z homing cycle" in l)
|
i = next(i for i, l in enumerate(hunt_part) if "z_axis:home starting z homing cycle" in l)
|
||||||
|
|||||||
Reference in New Issue
Block a user