mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 01:01:12 -07:00
forgetest: cloud tests stay in cloud mode; the page can ignore prerequisites
The cloud job tests (lid-abort, lid-during-button-wait, hunt-lid-open,
pause-resume) run in cloud mode and leave the machine there: enter_cloud
reuses a live session (pid-scoped from the client's own websocket state
lines) and switches once from GRBL mode, declaring the change to the
baseline; nothing switches back. Each test judges the log from its own
window, the print by its own "print [id]: finished" line, and waits the
service's follow-up moves out (wait_quiet) before it ends. hunt-lid-open
restarts the cloud client through the supervisor's stop/start lever for
a fresh connect. The former switch-back is what failed the last bench run
of hunt-lid-open (409 machine is not idle: the service was still
re-finding the head after the lid closed).
The baseline is mode-aware: in cloud mode the client owns the GRBL
controller's init values, the lid lamp, and the position counters; the
mode itself is preserved unless the run declared the change
(Context.mode_changed); controller_mode is never handed back as a bare
setting (a bare write left the persisted mode out of step with the live
one). The undeclared-change restore now uses the captured state, which
the post pass never saw before.
The acceptance page gets an "Ignore prerequisites" switch (remembered by
the browser): POST /start {ignore_requires} starts a test whose requires
are unmet, and the run records the unmet prerequisites in its evidence
and log; they stay required for the release. The cloud tests' requires
no longer chain through cloud.mode-switch.
Proof: tests/test_cloud_suite.py replays the four tests on the bench's
own gfcloud excerpts (fixtures/) and the run loop's emitted pause/resume
lines under the real runner Context against a fake forgectrl; baseline
mode tests and the server override test; the whole suite (98) and the
coverage lint pass. Catalog consequence: cloud.* fingerprints move with
the module; the catalog hash moves with the requires.
This commit is contained in:
@@ -16,6 +16,17 @@ restores it after (on every exit path), so a test cannot hand the next one
|
||||
The lid lamp is fixed too, at forgectrl's `lid_lamp_idle` setting (unset =
|
||||
236): the daemon asserts it at start and at every controller spawn.
|
||||
|
||||
The controller mode in force decides what the baseline owns. In GRBL mode
|
||||
everything above applies. In cloud mode the cloud client owns what it
|
||||
configures for itself - the GRBL controller's init values (it writes its
|
||||
own from each pulse header), the lid lamp (its lid-image level), and the
|
||||
position counters (re-zeroed at every service action, so not a preserved
|
||||
value) - and the baseline checks the rest: the safety readbacks, the
|
||||
latch, the ring, the module defaults, the diagnostics tools, the cooling
|
||||
engine. The mode itself is preserved: a run hands back the mode it
|
||||
found, unless it declared a deliberate change (Context.mode_changed) -
|
||||
the cloud tests enter cloud mode once and stay there.
|
||||
|
||||
Everything found off-baseline is a "leftover": logged, kept in the run's
|
||||
evidence, and surfaced on the page. The pre-run pass attributes leftovers
|
||||
to the previous run; the post-run pass attributes them to the run itself.
|
||||
@@ -53,6 +64,20 @@ FIXED_SYSFS = [
|
||||
("thermal/tec_on", "0"),
|
||||
]
|
||||
|
||||
# The subset the GRBL controller writes at its start: checked and restored
|
||||
# in GRBL mode only. The cloud client sets its own values for these from
|
||||
# every pulse header (step_freq 10 kHz, the run currents) and hands the
|
||||
# hold currents back at idle; forcing the GRBL values under it would be
|
||||
# the baseline configuring another controller's machine.
|
||||
GRBL_CONTROLLER_SYSFS = ("cnc/motor_lock", "cnc/x_mode", "cnc/y_mode", "cnc/x_decay", "cnc/y_decay",
|
||||
"cnc/step_freq", "pic/x_step_current", "pic/y_step_current")
|
||||
|
||||
# Settings the baseline never hands back as bare settings: controller_mode
|
||||
# is the persisted mirror of the live mode (the mode item restores it
|
||||
# through POST /mode, which keeps the two in step; a bare settings write
|
||||
# would leave the runtime in one mode and the boot in the other).
|
||||
UNPRESERVED_SETTINGS = ("controller_mode",)
|
||||
|
||||
# Read-only readbacks with their idle values (no direct restore: the state
|
||||
# comes right through forgectrl - see restore_forgectrl - or is fatal).
|
||||
IDLE_READBACKS = [
|
||||
@@ -149,6 +174,7 @@ class Baseline:
|
||||
self.abort = abort or (lambda: False)
|
||||
self.captured = None
|
||||
self.forgectrl = None
|
||||
self.mode = None # the controller mode in force (per enforce)
|
||||
|
||||
def log(self, msg):
|
||||
self._log("baseline: " + msg)
|
||||
@@ -228,7 +254,8 @@ class Baseline:
|
||||
cap["sysfs"][attr] = hw.sysfs_read(attr)
|
||||
st, body = self.fc_get("/settings")
|
||||
if st == 200 and isinstance(body, dict):
|
||||
cap["settings"] = {k: v for k, v in body.items() if isinstance(v, str)}
|
||||
cap["settings"] = {k: v for k, v in body.items()
|
||||
if isinstance(v, str) and k not in UNPRESERVED_SETTINGS}
|
||||
st, body = self.fc_get("/mode")
|
||||
if st == 200 and isinstance(body, dict):
|
||||
cap["mode"] = body.get("mode")
|
||||
@@ -241,7 +268,8 @@ class Baseline:
|
||||
phase is 'pre' or 'post' (log wording only). captured is the
|
||||
preserved state to hand back (post) - None compares nothing."""
|
||||
left = []
|
||||
self._forgectrl_side(left)
|
||||
self.mode = None
|
||||
self._forgectrl_side(left, captured)
|
||||
self._kernel_side(left)
|
||||
self._lamp_side(left)
|
||||
self._preserved(left, captured)
|
||||
@@ -259,11 +287,15 @@ class Baseline:
|
||||
time.sleep(1.0)
|
||||
return None
|
||||
|
||||
def _forgectrl_side(self, left):
|
||||
def cloud_mode(self):
|
||||
return self.mode == "cloud"
|
||||
|
||||
def _forgectrl_side(self, left, captured=None):
|
||||
st, mode = self.fc_get("/mode")
|
||||
if st != 200 or not isinstance(mode, dict):
|
||||
self.log("forgectrl not answering - service-side checks skipped")
|
||||
return
|
||||
self.mode = mode.get("mode")
|
||||
# a diagnostic left running seizes the thermal hardware: abort it
|
||||
st, d = self.fc_get("/diag/status")
|
||||
if st == 200 and isinstance(d, dict) and d.get("running"):
|
||||
@@ -278,11 +310,14 @@ class Baseline:
|
||||
CAM_IDLE_S)
|
||||
if w is None:
|
||||
left.append(Leftover("cam.running", True, False, "failed: still running"))
|
||||
# supervisor: the captured mode, controller running, motion verified
|
||||
want = (self.captured or {}).get("mode") or mode.get("mode") or "grbl"
|
||||
# supervisor: the mode the run found (or declared), controller
|
||||
# running, motion verified. A mode the run changed without
|
||||
# declaring it is handed back through the switch.
|
||||
want = (captured or {}).get("mode") or mode.get("mode") or "grbl"
|
||||
if mode.get("mode") != want:
|
||||
st, body = self.fc_post("/mode", data={"controller": want})
|
||||
mode = self.wait_settled() or mode
|
||||
self.mode = mode.get("mode")
|
||||
left.append(Leftover("mode", mode.get("mode"), want,
|
||||
"restored" if mode.get("mode") == want else "failed: %s %s" % (st, body)))
|
||||
if mode.get("controller") == "motion-fault":
|
||||
@@ -336,7 +371,10 @@ class Baseline:
|
||||
"waited" if w is not None else "failed: still %s" % found))
|
||||
|
||||
def _lamp_side(self, left):
|
||||
"""The lid lamp at forgectrl's idle level (the lid_lamp_idle setting)."""
|
||||
"""The lid lamp at forgectrl's idle level (the lid_lamp_idle setting).
|
||||
In cloud mode the cloud client owns the lamp (its lid-image level)."""
|
||||
if self.cloud_mode():
|
||||
return
|
||||
st, body = self.fc_get("/settings")
|
||||
if st != 200 or not isinstance(body, dict):
|
||||
return
|
||||
@@ -373,6 +411,8 @@ class Baseline:
|
||||
left.append(Leftover("pulse ring", "%d unplayed bytes" % residue, "0 (nothing queued)",
|
||||
"unrestorable: the next run would replay them first"))
|
||||
for attr, want in FIXED_SYSFS:
|
||||
if self.cloud_mode() and attr in GRBL_CONTROLLER_SYSFS:
|
||||
continue
|
||||
got = hw.sysfs_read(attr)
|
||||
if got is None or got == want:
|
||||
continue
|
||||
@@ -461,9 +501,11 @@ class Baseline:
|
||||
except OSError as e:
|
||||
act = "failed: %s" % e
|
||||
left.append(Leftover(attr, now, was, act))
|
||||
# In cloud mode the counters are the cloud client's: every service
|
||||
# action re-zeroes them at its start, so they preserve nothing.
|
||||
was = captured.get("position")
|
||||
now = read_position()
|
||||
if was is not None and now is not None and now != was:
|
||||
if was is not None and now is not None and now != was and not self.cloud_mode():
|
||||
left.append(Leftover("position", now, was, self._return_head(was, now)))
|
||||
was = captured.get("settings")
|
||||
if was:
|
||||
|
||||
@@ -63,6 +63,10 @@ pre#log{background:#1d1e26;color:#d7dae0;font-family:ui-monospace,Consolas,monos
|
||||
.err{color:var(--red);font-size:13px;margin:6px 0}
|
||||
.note{background:#fdf3e3;border:1px solid #eccb90;border-radius:6px;padding:8px 12px;margin:8px 0;font-size:13px}
|
||||
.ack{display:block;margin:8px 0;font-size:12.5px}
|
||||
.switch{display:flex;align-items:center;gap:8px;margin:10px 0 0;font-size:13px}
|
||||
.switch input{width:16px;height:16px;margin:0}
|
||||
.switch .on{color:var(--warn);font-weight:600}
|
||||
.req.over{color:var(--dim)}
|
||||
.grp{margin-top:6px}
|
||||
.tool .argrow{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0}
|
||||
.tool .argrow label{font-size:12px;color:var(--dim)}
|
||||
@@ -89,7 +93,10 @@ pre#log{background:#1d1e26;color:#d7dae0;font-family:ui-monospace,Consolas,monos
|
||||
<button class='danger' onclick='doInvalidate()'>Invalidate all results</button>
|
||||
</div>
|
||||
<div id='actmsg'></div>
|
||||
<p class='hint'>A release is authorized when a campaign is open on this image and every catalog test is satisfied - by a PASS in the campaign, or (never for the core) by an earlier PASS whose domain fingerprint is unchanged. A FAIL ends the campaign. Invalidate-all forces a full campaign; give the reason.</p>
|
||||
<label class='switch'><input type='checkbox' id='ignreq' onchange='setIgnoreReq(this.checked)'>
|
||||
<span>Ignore prerequisites - Start any test on its own, whatever its <i>requires</i> list says</span>
|
||||
<span id='ignreqon'></span></label>
|
||||
<p class='hint'>A release is authorized when a campaign is open on this image and every catalog test is satisfied - by a PASS in the campaign, or (never for the core) by an earlier PASS whose domain fingerprint is unchanged. A FAIL ends the campaign. Invalidate-all forces a full campaign; give the reason. A test's <i>requires</i> list only orders the runs: with the switch above on, any test starts alone and its record notes which prerequisites were unmet - the release still needs every test satisfied.</p>
|
||||
</div>
|
||||
<div id='groups'></div>
|
||||
</div>
|
||||
@@ -114,7 +121,10 @@ pre#log{background:#1d1e26;color:#d7dae0;font-family:ui-monospace,Consolas,monos
|
||||
var TOKEN='__TOKEN__';
|
||||
var state=null, catalog=null, catalogHash=null, bench=null, tab='acceptance', openDetails={};
|
||||
var lastRunKey=null;
|
||||
var ignoreReq=false;try{ignoreReq=window.localStorage.getItem('forgetest.ignoreReq')==='1'}catch(e){}
|
||||
function $(id){return document.getElementById(id)}
|
||||
function setIgnoreReq(on){ignoreReq=!!on;try{window.localStorage.setItem('forgetest.ignoreReq',ignoreReq?'1':'0')}catch(e){}
|
||||
$('ignreq').checked=ignoreReq;$('ignreqon').innerHTML=ignoreReq?"<span class='on'>ON - prerequisites are not enforced</span>":'';if(state&&catalog)renderGroups()}
|
||||
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]})}
|
||||
function api(method,path,body,cb){var x=new XMLHttpRequest();x.open(method,path,true);x.setRequestHeader('X-ForgeFIRM-Token',TOKEN);
|
||||
if(body!==undefined&&body!==null){x.setRequestHeader('Content-Type','application/json')}
|
||||
@@ -151,8 +161,10 @@ function renderGroups(){var groups={},order=[];catalog.forEach(function(t){if(!g
|
||||
if(s.required&&s.status!=='running')st+="<br><span class='req'>required: "+esc(s.reason)+"</span>";
|
||||
var last=s.last?(esc(s.last.result)+' '+esc(fmtTs(s.last.ts))):'-';
|
||||
if(s.status==='inherited'&&s.origin)last+="<br><span class='tid'>from "+esc(s.origin.campaign)+" on "+esc(s.origin.image)+"</span>";
|
||||
var canStart=!busy&&s.requires_met!==false;var why=busy?'a run is in progress':(!s.requires_met?('needs: '+(s.missing_requires||[]).join(', ')):'');
|
||||
var unmet=s.requires_met===false;var canStart=!busy&&(!unmet||ignoreReq);
|
||||
var why=busy?'a run is in progress':(unmet?((ignoreReq?'prerequisites overridden - needs: ':'needs: ')+(s.missing_requires||[]).join(', ')):'');
|
||||
var startBtn="<button class='pri' "+(canStart?'':'disabled')+" title='"+esc(why)+"' onclick='startTest(\""+esc(t.id)+"\")'>Start</button>";
|
||||
if(unmet)startBtn+="<div class='req"+(ignoreReq?' over':'')+"'>"+(ignoreReq?'unmet: ':'needs: ')+esc((s.missing_requires||[]).join(', '))+"</div>";
|
||||
var det="<div class='details"+(openDetails[t.id]?' on':'')+"' id='det-"+esc(t.id)+"'>"+esc(t.description||'')+
|
||||
(t.steps&&t.steps.length?"<br><b>Operator steps:</b><ol>"+t.steps.map(function(x){return '<li>'+esc(x)+'</li>'}).join('')+"</ol>":'')+
|
||||
"<b>Requires:</b> "+esc((t.requires||[]).join(', ')||'-')+"<br><b>Covers:</b> "+esc((t.covers||[]).map(function(c){return c[0]+':'+c[1]}).join(', ')||'-')+
|
||||
@@ -179,6 +191,7 @@ function renderRun(){var r=state.running||state.last_run;var key=r?(r.kind+':'+r
|
||||
var benchNeedsRefresh=false;
|
||||
function findTest(id){for(var i=0;i<catalog.length;i++)if(catalog[i].id===id)return catalog[i];return null}
|
||||
function startTest(id){var t=findTest(id);var body={test:id};
|
||||
if(ignoreReq)body.ignore_requires=true;
|
||||
if(t&&t.kind==='live'){if(!confirmLive())return;body.ack_live=true}
|
||||
api('POST','/start',body,function(s,d){setMsg('actmsg',d.message||d.error,s!==200)})}
|
||||
function confirmLive(){return window.confirm('LIVE LASER TEST.\n\nConfirm before starting:\n - eye protection on, everyone in the room\n - fire watch present, extinguisher at hand\n - exhaust running, lid closed, scrap in place\n - you will press the physical button to arm when prompted\n\nStart the test?')}
|
||||
@@ -208,6 +221,7 @@ function startTool(id){var t=null;bench.tools.forEach(function(x){if(x.id===id)t
|
||||
(t.args||[]).forEach(function(a){var e=$('arg-'+id+'-'+a.name);if(e)args[a.name]=e.value});
|
||||
var body={tool:id,args:args};if(t.safety==='live'){if(!confirmLive())return;body.ack_live=true}
|
||||
api('POST','/bench/start',body,function(s,d){setMsg('benchmsg',d.message||d.error,s!==200);if(s===200)benchNeedsRefresh=true})}
|
||||
setIgnoreReq(ignoreReq);
|
||||
poll();
|
||||
</script></body></html>
|
||||
"""
|
||||
|
||||
@@ -203,6 +203,16 @@ class Context:
|
||||
self.log("position counters re-zeroed at the starting position; the baseline "
|
||||
"expects (0,0,0) at the end")
|
||||
|
||||
def mode_changed(self, mode):
|
||||
"""Declare a deliberate controller-mode change for the operator:
|
||||
the run leaves the machine in `mode` and the baseline keeps it
|
||||
there instead of switching back to the mode the run found (the
|
||||
cloud tests enter cloud mode once and stay)."""
|
||||
cap = self.run.baseline_captured
|
||||
if cap is not None and cap.get("mode") != mode:
|
||||
self.log("controller mode changed to %s for the operator; the baseline keeps it", mode)
|
||||
cap["mode"] = mode
|
||||
|
||||
|
||||
class Takeover:
|
||||
"""Hardware takeover: the controller is stopped through the supervisor,
|
||||
@@ -391,7 +401,12 @@ class Runner:
|
||||
return art
|
||||
|
||||
# -- starting -----------------------------------------------------------
|
||||
def start_test(self, test_id, ack_live=False):
|
||||
def start_test(self, test_id, ack_live=False, ignore_requires=False):
|
||||
"""Start a test. `requires` gates the start unless the operator
|
||||
set ignore_requires: the test then runs alone, and the run's
|
||||
evidence records which prerequisites were unmet (the release gate
|
||||
needs every test satisfied anyway, so nothing is hidden - the
|
||||
record just says the order was the operator's)."""
|
||||
t = _catalog.get(test_id, self.registry)
|
||||
if t is None:
|
||||
return False, "unknown test"
|
||||
@@ -400,8 +415,9 @@ class Runner:
|
||||
return False, "a run is in progress"
|
||||
state, _ = self.state()
|
||||
ts = state["tests"][t.id]
|
||||
if not ts["requires_met"]:
|
||||
return False, "prerequisites not satisfied: %s" % ", ".join(ts["missing_requires"])
|
||||
missing = list(ts["missing_requires"])
|
||||
if missing and not ignore_requires:
|
||||
return False, "prerequisites not satisfied: %s" % ", ".join(missing)
|
||||
if t.kind == "live" and not ack_live:
|
||||
return False, "live test: acknowledge eye protection, fire watch, and exhaust first"
|
||||
campaign = self._open_campaign_if_needed(state)
|
||||
@@ -409,6 +425,9 @@ class Runner:
|
||||
self.last = self.current
|
||||
self.current = run
|
||||
run.log("start %s (%s, %s) in campaign %s" % (t.id, t.kind, t.hardware, campaign["id"]))
|
||||
if missing:
|
||||
run.log("prerequisites overridden by the operator - not satisfied: %s" % ", ".join(missing))
|
||||
run.evidence["prerequisites"] = {"overridden": True, "missing": missing, "ts": now_ts()}
|
||||
if t.kind == "live":
|
||||
run.evidence["operator"] = {"ack_live": True, "ts": now_ts()}
|
||||
th = threading.Thread(target=self._exec_test, args=(t, run, campaign), daemon=True,
|
||||
|
||||
@@ -15,7 +15,7 @@ Routes
|
||||
GET /result?test&ts one full result record (log, evidence)
|
||||
GET /log the raw JSONL
|
||||
GET /export/acceptance.json | .md the last export
|
||||
POST /start {test, ack_live} start an acceptance test
|
||||
POST /start {test, ack_live, ignore_requires} start an acceptance test
|
||||
POST /bench/start {tool, args, ack_live}
|
||||
POST /answer {prompt_id, value}
|
||||
POST /abort
|
||||
@@ -219,7 +219,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return
|
||||
try:
|
||||
if path == "/start":
|
||||
ok, msg = r.start_test(str(body.get("test", "")), ack_live=bool(body.get("ack_live")))
|
||||
ok, msg = r.start_test(str(body.get("test", "")), ack_live=bool(body.get("ack_live")),
|
||||
ignore_requires=bool(body.get("ignore_requires")))
|
||||
self._send(200 if ok else 409, {"ok": ok, "message": msg})
|
||||
elif path == "/bench/start":
|
||||
args = body.get("args") or {}
|
||||
|
||||
+319
-191
@@ -1,5 +1,7 @@
|
||||
"""cloud.* - the controller mode switch and the optional Glowforge web-service
|
||||
mode (gfcloud daemon, gfhome homing runner)."""
|
||||
mode (gfcloud daemon, gfhome homing runner). The mode-switch test makes the
|
||||
grbl -> cloud -> grbl round trip; the job-behavior tests run in cloud mode
|
||||
and leave the machine there (see enter_cloud)."""
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
@@ -198,7 +200,7 @@ def mode_switch(ctx):
|
||||
|
||||
@test("cloud.gfhome-homing", title="Glowforge web-service homing ($H with homing_mode=gfcloud)",
|
||||
subsystem="cloud", kind="operator", est_min=5,
|
||||
covers=_CLOUD_COVERS + [("grblhal-glowforge", "src/**")], requires=["cloud.mode-switch"],
|
||||
covers=_CLOUD_COVERS + [("grblhal-glowforge", "src/**")], requires=[],
|
||||
steps=["homing_mode = gfcloud and cloud credentials configured; bed clear, lid closed.",
|
||||
"Watch the gantry: the service drives it to the corner with camera corrections.",
|
||||
"The machine ends homed, the head parked at the home corner (the position "
|
||||
@@ -249,6 +251,24 @@ def gfhome_homing(ctx):
|
||||
|
||||
|
||||
# ---- lid / button behavior of a cloud job (the factory's) ------------------
|
||||
#
|
||||
# These tests run IN cloud mode and stay there: enter_cloud() reuses a live
|
||||
# cloud session when the machine is already in cloud mode (the operator
|
||||
# switched once, on the panel or through an earlier test) and switches -
|
||||
# once, declaring the change to the baseline - only when it finds GRBL
|
||||
# mode. Nothing switches back; the operator does, when done. Each test
|
||||
# judges the gfcloud log from its own window (the offset enter_cloud
|
||||
# returns) and waits for the service's deferred moves (the re-hunt after a
|
||||
# lid close, the hunt after a print) to finish before it ends, so the next
|
||||
# run - or the operator - gets a quiet machine.
|
||||
|
||||
WS_MARKS = ("RX-EVENT: ready", "RX-EVENT: closed", "RECONNECTING", "CLOSING")
|
||||
ACTIVITY_MARKS = ("start motion", "start return home", "starting run", "starting z homing cycle")
|
||||
LOG_TAIL_BYTES = 4 << 20
|
||||
QUIET_S = 8 # the re-hunt's motions are ~4 s apart (a lid image between them)
|
||||
QUIET_TIMEOUT_S = 180
|
||||
HUNT_TIMEOUT_S = 180
|
||||
|
||||
|
||||
def log_lines_since(path, offset):
|
||||
"""New gfcloud log lines since offset (each 'ISO-time gfcloud[pid] LEVEL where message')."""
|
||||
@@ -260,6 +280,12 @@ def log_lines_since(path, offset):
|
||||
return []
|
||||
|
||||
|
||||
def log_tail(path, max_bytes=LOG_TAIL_BYTES):
|
||||
"""The last max_bytes of the log, as lines (the first may be partial)."""
|
||||
size = log_size(path)
|
||||
return log_lines_since(path, max(0, size - max_bytes))
|
||||
|
||||
|
||||
def line_time(line):
|
||||
"""Seconds (float) from the log line's ISO timestamp, or None."""
|
||||
try:
|
||||
@@ -296,47 +322,150 @@ def wait_log(ctx, offset, needles, timeout, poll=0.5):
|
||||
return found
|
||||
|
||||
|
||||
def enter_cloud(ctx):
|
||||
"""Switch to the cloud controller and wait for its service session.
|
||||
Returns (log offset at the switch, lid lamp level before)."""
|
||||
fc = ctx.forgectrl
|
||||
st, m0 = fc.get("/mode")
|
||||
ctx.check(st == 200 and isinstance(m0, dict) and m0.get("mode") == "grbl",
|
||||
"start this test in grbl mode (now %s)", m0)
|
||||
lamp0 = hw.sysfs_read("pic/lid_led")
|
||||
offset = log_size(GFCLOUD_LOG)
|
||||
st, body = fc.post("/mode", data={"controller": "cloud"})
|
||||
ctx.check(st == 200, "mode switch to cloud refused: %s %s", st, body)
|
||||
m = wait_mode(ctx, fc, "cloud", timeout=90)
|
||||
ctx.check(m and m.get("mode") == "cloud" and m.get("controller") == "running",
|
||||
"cloud controller did not come up: %s", m)
|
||||
def action_finish_index(lines, action):
|
||||
"""Index of the first '<action> [id]: finished with event ...' line, or None."""
|
||||
return next((i for i, ln in enumerate(lines)
|
||||
if (action + " [") in ln and "finished with event" in ln), None)
|
||||
|
||||
|
||||
def wait_action_finished(ctx, offset, action, timeout, poll=0.5):
|
||||
"""The action's own terminal line ('<action> [id]: finished with event
|
||||
":completed"' / '":cancelled"'), or None within timeout."""
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
ctx.checkpoint()
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
i = action_finish_index(lines, action)
|
||||
if i is not None:
|
||||
return lines[i]
|
||||
time.sleep(poll)
|
||||
return None
|
||||
|
||||
|
||||
def message(line):
|
||||
"""The message part of a log line (after 'ISO-time gfcloud[pid]')."""
|
||||
return line.split(" ", 2)[-1] if line else None
|
||||
|
||||
|
||||
def session_live(pid):
|
||||
"""(live, detail): the running cloud client (gfcloud[pid]) has a live
|
||||
service session when its last websocket state line is 'ready' - a
|
||||
later 'closed'/'RECONNECTING'/'CLOSING' means it is not connected
|
||||
now. Without any line for that pid the newest lines of the log stand
|
||||
in (the client may log under a wrapper's pid)."""
|
||||
tag = "gfcloud[%s]" % pid if pid else None
|
||||
last_pid = last_any = None
|
||||
for ln in log_tail(GFCLOUD_LOG):
|
||||
for m in WS_MARKS:
|
||||
if m in ln:
|
||||
last_any = m
|
||||
if tag and tag in ln:
|
||||
last_pid = m
|
||||
last = last_pid if last_pid is not None else last_any
|
||||
where = "pid %s" % pid if last_pid is not None else "newest lines"
|
||||
return last == "RX-EVENT: ready", "%s: last websocket state %s" % (where, last)
|
||||
|
||||
|
||||
def wait_quiet(ctx, offset, quiet_s=None, timeout=None):
|
||||
"""The service's moves are over: the machine idle and no new service
|
||||
activity in the log (a motion, a park, a run, a lens homing) for
|
||||
quiet_s (default QUIET_S; timeout QUIET_TIMEOUT_S). False on timeout."""
|
||||
quiet_s = QUIET_S if quiet_s is None else quiet_s
|
||||
timeout = QUIET_TIMEOUT_S if timeout is None else timeout
|
||||
fc = ctx.forgectrl
|
||||
t0 = time.time()
|
||||
n_seen = -1
|
||||
last_change = t0
|
||||
while time.time() - t0 < timeout:
|
||||
ctx.checkpoint()
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
n = sum(1 for ln in lines if any(m in ln for m in ACTIVITY_MARKS))
|
||||
if n != n_seen:
|
||||
n_seen = n
|
||||
last_change = time.time()
|
||||
if fc.status().get("state") == "idle" and time.time() - last_change >= quiet_s:
|
||||
return True
|
||||
time.sleep(0.5)
|
||||
return False
|
||||
|
||||
|
||||
def wait_session(ctx, offset, timeout=120):
|
||||
session = []
|
||||
while time.time() - t0 < 120:
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
ctx.checkpoint()
|
||||
session = session_lines(GFCLOUD_LOG, offset)
|
||||
if session_established(session):
|
||||
break
|
||||
return session
|
||||
time.sleep(2)
|
||||
ctx.check(session_established(session), "the cloud client never established its service session")
|
||||
ctx.log("cloud session established")
|
||||
return offset, lamp0
|
||||
return session
|
||||
|
||||
|
||||
def leave_cloud(ctx, lamp0):
|
||||
"""Back to grbl; the head returns to where the run found it."""
|
||||
def fresh_cloud_connect(ctx):
|
||||
"""A NEW cloud client with a fresh service session (its connect-time
|
||||
hunt follows): in cloud mode the controller is restarted through the
|
||||
supervisor's stop/start lever; in GRBL mode the mode is switched (and
|
||||
the change declared - the machine stays in cloud mode). Returns the log
|
||||
offset from before the connect, so the caller sees the whole session."""
|
||||
fc = ctx.forgectrl
|
||||
st, body = fc.post("/mode", data={"controller": "grbl"})
|
||||
ctx.check(st == 200, "mode switch back to grbl refused: %s %s", st, body)
|
||||
m = wait_mode(ctx, fc, "grbl", timeout=120)
|
||||
ctx.check(m and m.get("mode") == "grbl" and m.get("controller") == "running",
|
||||
"grbl controller did not come back: %s", m)
|
||||
ctx.sleep(3)
|
||||
ctx.counters_rezeroed()
|
||||
return_head(ctx)
|
||||
lamp1 = hw.sysfs_read("pic/lid_led")
|
||||
if lamp0 is not None and lamp1 != lamp0:
|
||||
hw.sysfs_write("pic/lid_led", lamp0)
|
||||
st, m = fc.get("/mode")
|
||||
ctx.check(st == 200 and isinstance(m, dict), "GET /mode -> %s", st)
|
||||
offset = log_size(GFCLOUD_LOG)
|
||||
if m.get("mode") == "cloud":
|
||||
ctx.log("cloud mode: restarting the cloud client for a fresh connect (was pid %s)", m.get("pid"))
|
||||
st, body = fc.post("/controller/stop")
|
||||
ctx.check(st == 200, "controller stop refused: %s %s", st, body)
|
||||
m = wait_mode(ctx, fc, "cloud", want_controller="standby", timeout=30)
|
||||
ctx.check(m and m.get("controller") == "standby", "the cloud client did not stop: %s", m)
|
||||
st, body = fc.post("/controller/start")
|
||||
ctx.check(st == 200, "controller start refused: %s %s", st, body)
|
||||
else:
|
||||
ctx.log("%s mode: switching to cloud", m.get("mode"))
|
||||
st, body = fc.post("/mode", data={"controller": "cloud"})
|
||||
ctx.check(st == 200, "mode switch to cloud refused: %s %s", st, body)
|
||||
m = wait_mode(ctx, fc, "cloud", timeout=90)
|
||||
ctx.check(m and m.get("mode") == "cloud" and m.get("controller") == "running",
|
||||
"cloud controller did not come up: %s", m)
|
||||
ctx.mode_changed("cloud")
|
||||
session = wait_session(ctx, offset)
|
||||
ctx.check(session_established(session),
|
||||
"the cloud client never established its service session (no credentials, no "
|
||||
"network, or the service refused)")
|
||||
ctx.log("cloud session established (pid %s)", m.get("pid"))
|
||||
return offset
|
||||
|
||||
|
||||
def enter_cloud(ctx):
|
||||
"""Cloud mode with a live service session and a quiet machine. An
|
||||
existing cloud session is reused; from GRBL mode the switch is made
|
||||
once (its connect-time hunt is waited out) and the machine stays in
|
||||
cloud mode. Returns the log offset where the test's own window begins."""
|
||||
fc = ctx.forgectrl
|
||||
st, m = fc.get("/mode")
|
||||
ctx.check(st == 200 and isinstance(m, dict), "GET /mode -> %s", st)
|
||||
if m.get("mode") == "cloud" and m.get("controller") == "running":
|
||||
live, detail = session_live(m.get("pid"))
|
||||
ctx.check(live, "cloud mode is up (pid %s) but the client has no live service session (%s) - "
|
||||
"check credentials and network, or restart the controller", m.get("pid"), detail)
|
||||
ctx.log("cloud mode already up (pid %s), service session live - reusing it", m.get("pid"))
|
||||
offset = log_size(GFCLOUD_LOG)
|
||||
else:
|
||||
offset = fresh_cloud_connect(ctx)
|
||||
hunt = wait_action_finished(ctx, offset, "hunt", HUNT_TIMEOUT_S)
|
||||
ctx.check(hunt, "the service sent no connect-time hunt (or it never finished) within %d s",
|
||||
HUNT_TIMEOUT_S)
|
||||
ctx.log("connect-time hunt: %s", message(hunt))
|
||||
ctx.check(wait_quiet(ctx, offset), "the cloud client was still running service moves after %d s",
|
||||
QUIET_TIMEOUT_S)
|
||||
return log_size(GFCLOUD_LOG)
|
||||
|
||||
|
||||
def settle_cloud(ctx, offset):
|
||||
"""End of a cloud test: the service's follow-up moves (a hunt after a
|
||||
print, the re-hunt after a lid close) done and the machine idle."""
|
||||
ctx.check(wait_quiet(ctx, offset), "the service was still moving the head %d s after the test",
|
||||
QUIET_TIMEOUT_S)
|
||||
ctx.log("cloud mode stays up; the machine is quiet")
|
||||
|
||||
|
||||
def wait_print_running(ctx, offset, timeout):
|
||||
@@ -367,13 +496,14 @@ APP_PRINT_CUE = ("In the Glowforge app: scrap on the bed, lid closed, a SMALL en
|
||||
|
||||
CANCELLED = 'finished with event ":cancelled"'
|
||||
COMPLETED = 'finished with event ":completed"'
|
||||
CLOUD_STEP = ("Cloud credentials configured; the machine in cloud mode (the test switches once from "
|
||||
"GRBL mode and stays in cloud mode; switch back on the panel when done).")
|
||||
|
||||
|
||||
@test("cloud.lid-abort", title="Lid open during a cloud print: stop, park with the lid open, cancelled",
|
||||
subsystem="cloud", kind="live", est_min=8,
|
||||
covers=_CLOUD_COVERS, requires=["cloud.mode-switch", "laser.emission-witness"],
|
||||
steps=["Cloud credentials configured; the app open in a browser; scrap on the bed and a small "
|
||||
"engrave/score job ready.",
|
||||
covers=_CLOUD_COVERS, requires=["laser.emission-witness"],
|
||||
steps=[CLOUD_STEP, "The app open in a browser; scrap on the bed and a small engrave/score job ready.",
|
||||
"Print from the app and press the button when it lights; open the lid a few seconds "
|
||||
"into the run."],
|
||||
description="A cloud print aborted by the lid behaves as the factory's does: the edge "
|
||||
@@ -382,161 +512,158 @@ COMPLETED = 'finished with event ":completed"'
|
||||
"closes, and the job ends ':cancelled'.")
|
||||
def lid_abort(ctx):
|
||||
ev = ctx.evidence
|
||||
offset, lamp0 = enter_cloud(ctx)
|
||||
try:
|
||||
ctx.instruct(APP_PRINT_CUE)
|
||||
got = wait_print_running(ctx, offset, 300)
|
||||
ctx.check(got, "the print never reached its run within 300 s (not started, or the button not pressed)")
|
||||
ctx.instruct("The head is moving. Open the lid NOW, then click Done. Leave it open until the head "
|
||||
"has returned to the corner.")
|
||||
needles = ["lid opened", "lid opened mid-run; stopping motion", "start return home",
|
||||
"return home complete", CANCELLED]
|
||||
got = wait_log(ctx, offset, needles, 90)
|
||||
ev["log"] = {k: (v.split(" ", 2)[-1] if v else None) for k, v in got.items()}
|
||||
for k, v in got.items():
|
||||
ctx.log(" %s: %s", k, "seen" if v else "MISSING")
|
||||
ctx.check(got["lid opened mid-run; stopping motion"], "the lid open did not stop the run")
|
||||
# The lid edge that stopped the run is the LAST "lid opened" edge line
|
||||
# before the stop line (an earlier open, e.g. to place the scrap, is
|
||||
# not the one).
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
stop_i = next((i for i, ln in enumerate(lines) if "lid opened mid-run; stopping motion" in ln), None)
|
||||
edge_line = None
|
||||
if stop_i is not None:
|
||||
edge_line = next((ln for ln in reversed(lines[:stop_i])
|
||||
if "_switch_event lid opened" in ln or ln.rstrip().endswith(" lid opened")), None)
|
||||
t_edge = line_time(edge_line) if edge_line else None
|
||||
t_stop = line_time(lines[stop_i]) if stop_i is not None else None
|
||||
if t_edge is not None and t_stop is not None:
|
||||
ev["edge_to_stop_ms"] = round((t_stop - t_edge) * 1000, 1)
|
||||
ctx.log("lid edge -> stop: %s ms", ev["edge_to_stop_ms"])
|
||||
ctx.check(ev["edge_to_stop_ms"] < 60, "stop was not edge-driven (%s ms after the lid edge)",
|
||||
ev["edge_to_stop_ms"])
|
||||
ctx.check(got["start return home"] and got["return home complete"],
|
||||
"the park did not run to completion with the lid open")
|
||||
# the machine, not the client: the job started at counters (0,0,0)
|
||||
# (cloud clears them at every job start), so a completed park reads
|
||||
# back there - stale ring bytes replayed ahead of the park would not
|
||||
ctx.check(ctx.forgectrl.wait_idle(15, abort=ctx.aborted), "machine not idle after the park")
|
||||
kpos = read_position()
|
||||
ev["kernel_counters_after_park"] = kpos
|
||||
ctx.log("kernel counters after the park: %s", kpos)
|
||||
ctx.check(kpos is not None and abs(kpos[0]) <= 3 and abs(kpos[1]) <= 3,
|
||||
"the head did not come back to the job start (kernel counters %s)", kpos)
|
||||
ctx.check(got[CANCELLED], "the print did not end ':cancelled'")
|
||||
st, cs = ctx.forgectrl.get("/cool/status")
|
||||
ev["armed_after"] = cs.get("armed") if isinstance(cs, dict) else None
|
||||
ev["latch_locked"] = latch_locked()
|
||||
ctx.check(not ev["armed_after"], "armed window still open after the abort")
|
||||
ctx.check(ev["latch_locked"], "kernel latch not locked after the abort")
|
||||
ctx.confirm("Did the head stop as soon as the lid opened and go straight home with the lid "
|
||||
"still open, and does the app show the print as cancelled?")
|
||||
ctx.instruct("Close the lid, then click Done.")
|
||||
ctx.sleep(3)
|
||||
finally:
|
||||
leave_cloud(ctx, lamp0)
|
||||
offset = enter_cloud(ctx)
|
||||
ctx.instruct(APP_PRINT_CUE)
|
||||
got = wait_print_running(ctx, offset, 300)
|
||||
ctx.check(got, "the print never reached its run within 300 s (not started, or the button not pressed)")
|
||||
ctx.instruct("The head is moving. Open the lid NOW, then click Done. Leave it open until the head "
|
||||
"has returned to the corner.")
|
||||
needles = ["lid opened", "lid opened mid-run; stopping motion", "start return home",
|
||||
"return home complete"]
|
||||
got = wait_log(ctx, offset, needles, 90)
|
||||
fin = wait_action_finished(ctx, offset, "print", 60)
|
||||
got["print finished"] = fin
|
||||
ev["log"] = {k: message(v) for k, v in got.items()}
|
||||
for k, v in got.items():
|
||||
ctx.log(" %s: %s", k, "seen" if v else "MISSING")
|
||||
ctx.check(got["lid opened mid-run; stopping motion"], "the lid open did not stop the run")
|
||||
# The lid edge that stopped the run is the LAST "lid opened" edge line
|
||||
# before the stop line (an earlier open, e.g. to place the scrap, is
|
||||
# not the one).
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
stop_i = next((i for i, ln in enumerate(lines) if "lid opened mid-run; stopping motion" in ln), None)
|
||||
edge_line = None
|
||||
if stop_i is not None:
|
||||
edge_line = next((ln for ln in reversed(lines[:stop_i])
|
||||
if "_switch_event lid opened" in ln or ln.rstrip().endswith(" lid opened")), None)
|
||||
t_edge = line_time(edge_line) if edge_line else None
|
||||
t_stop = line_time(lines[stop_i]) if stop_i is not None else None
|
||||
if t_edge is not None and t_stop is not None:
|
||||
ev["edge_to_stop_ms"] = round((t_stop - t_edge) * 1000, 1)
|
||||
ctx.log("lid edge -> stop: %s ms", ev["edge_to_stop_ms"])
|
||||
ctx.check(ev["edge_to_stop_ms"] < 60, "stop was not edge-driven (%s ms after the lid edge)",
|
||||
ev["edge_to_stop_ms"])
|
||||
ctx.check(got["start return home"] and got["return home complete"],
|
||||
"the park did not run to completion with the lid open")
|
||||
# the machine, not the client: the job started at counters (0,0,0)
|
||||
# (cloud clears them at every job start), so a completed park reads
|
||||
# back there - stale ring bytes replayed ahead of the park would not
|
||||
ctx.check(ctx.forgectrl.wait_idle(15, abort=ctx.aborted), "machine not idle after the park")
|
||||
kpos = read_position()
|
||||
ev["kernel_counters_after_park"] = kpos
|
||||
ctx.log("kernel counters after the park: %s", kpos)
|
||||
ctx.check(kpos is not None and abs(kpos[0]) <= 3 and abs(kpos[1]) <= 3,
|
||||
"the head did not come back to the job start (kernel counters %s)", kpos)
|
||||
ctx.check(fin and CANCELLED in fin, "the print did not end ':cancelled': %s",
|
||||
message(fin) or "no finish line")
|
||||
st, cs = ctx.forgectrl.get("/cool/status")
|
||||
ev["armed_after"] = cs.get("armed") if isinstance(cs, dict) else None
|
||||
ev["latch_locked"] = latch_locked()
|
||||
ctx.check(not ev["armed_after"], "armed window still open after the abort")
|
||||
ctx.check(ev["latch_locked"], "kernel latch not locked after the abort")
|
||||
ctx.confirm("Did the head stop as soon as the lid opened and go straight home with the lid "
|
||||
"still open, and does the app show the print as cancelled?")
|
||||
ctx.instruct("Close the lid, then click Done.")
|
||||
settle_cloud(ctx, offset)
|
||||
ctx.log("PASS: lid open -> stop in %s ms, park completed with the lid open, ':cancelled'",
|
||||
ev.get("edge_to_stop_ms"))
|
||||
|
||||
|
||||
@test("cloud.lid-during-button-wait", title="Lid open at the cloud button prompt cancels the print",
|
||||
subsystem="cloud", kind="operator", est_min=6,
|
||||
covers=_CLOUD_COVERS, requires=["cloud.mode-switch"],
|
||||
steps=["Cloud credentials configured; the app open; any small job ready (nothing will fire).",
|
||||
covers=_CLOUD_COVERS, requires=[],
|
||||
steps=[CLOUD_STEP, "The app open; any small job ready (nothing will fire).",
|
||||
"Print from the app; when the button lights white, do NOT press it - open the lid."],
|
||||
description="A cloud print waiting for the button is cancelled by the lid: the wait ends "
|
||||
"with the lid named as the reason, the laser latch relocks, the armed window "
|
||||
"closes, no run starts, and the job ends ':cancelled'.")
|
||||
def lid_during_button_wait(ctx):
|
||||
ev = ctx.evidence
|
||||
offset, lamp0 = enter_cloud(ctx)
|
||||
try:
|
||||
ctx.instruct("In the Glowforge app: lid closed, a small job set up. Click Done here, then press "
|
||||
"Print in the app. When the button lights white, do NOT press it.")
|
||||
got = wait_log(ctx, offset, ["waiting for button"], 300)
|
||||
ctx.check(got["waiting for button"], "the print never reached the button wait")
|
||||
ctx.instruct("The button is lit. Open the lid now (do not press the button), then click Done.")
|
||||
needles = ["button wait lid opened - relocking the laser", CANCELLED]
|
||||
got = wait_log(ctx, offset, needles, 60)
|
||||
ev["log"] = {k: bool(v) for k, v in got.items()}
|
||||
ctx.check(got["button wait lid opened - relocking the laser"], "the lid did not end the button wait")
|
||||
ctx.check(got[CANCELLED], "the print did not end ':cancelled'")
|
||||
# No run may start between the button wait and the cancel: the print
|
||||
# itself, or a park (the head never moved, there is nothing to park).
|
||||
# The connect-time hunt and the service's moves BEFORE the print are
|
||||
# legitimate runs and are outside this window.
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
wait_i = next((i for i, ln in enumerate(lines) if "waiting for button" in ln), None)
|
||||
end_i = next((i for i, ln in enumerate(lines) if wait_i is not None and i > wait_i and CANCELLED in ln),
|
||||
len(lines))
|
||||
ran = ([ln for ln in lines[wait_i:end_i] if "starting run" in ln]
|
||||
if wait_i is not None else [])
|
||||
ev["runs_started_after_wait"] = len(ran)
|
||||
ctx.check(not ran, "a run started after the lid-open cancel (%d)", len(ran))
|
||||
st, cs = ctx.forgectrl.get("/cool/status")
|
||||
ev["armed_after"] = cs.get("armed") if isinstance(cs, dict) else None
|
||||
ev["latch_locked"] = latch_locked()
|
||||
ctx.check(not ev["armed_after"], "armed window still open after the cancel")
|
||||
ctx.check(ev["latch_locked"], "kernel latch not locked after the cancel")
|
||||
ctx.confirm("Did the button go dark when the lid opened, with no motion, and does the app show "
|
||||
"the print as cancelled?")
|
||||
ctx.instruct("Close the lid, then click Done.")
|
||||
ctx.sleep(3)
|
||||
finally:
|
||||
leave_cloud(ctx, lamp0)
|
||||
offset = enter_cloud(ctx)
|
||||
ctx.instruct("In the Glowforge app: lid closed, a small job set up. Click Done here, then press "
|
||||
"Print in the app. When the button lights white, do NOT press it.")
|
||||
got = wait_log(ctx, offset, ["waiting for button"], 300)
|
||||
ctx.check(got["waiting for button"], "the print never reached the button wait")
|
||||
ctx.instruct("The button is lit. Open the lid now (do not press the button), then click Done.")
|
||||
relock = "button wait lid opened - relocking the laser"
|
||||
got = wait_log(ctx, offset, [relock], 60)
|
||||
fin = wait_action_finished(ctx, offset, "print", 60)
|
||||
ev["log"] = {relock: bool(got[relock]), "print finished": message(fin)}
|
||||
ctx.check(got[relock], "the lid did not end the button wait")
|
||||
ctx.check(fin and CANCELLED in fin, "the print did not end ':cancelled': %s",
|
||||
message(fin) or "no finish line")
|
||||
# No run may start between the button wait and the cancel: the print
|
||||
# itself, or a park (the head never moved, there is nothing to park).
|
||||
# The service's moves BEFORE the print are legitimate runs and are
|
||||
# outside this window.
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
wait_i = next((i for i, ln in enumerate(lines) if "waiting for button" in ln), None)
|
||||
end_i = action_finish_index(lines, "print")
|
||||
if end_i is None or wait_i is None or end_i < wait_i:
|
||||
end_i = len(lines)
|
||||
ran = ([ln for ln in lines[wait_i:end_i] if "starting run" in ln]
|
||||
if wait_i is not None else [])
|
||||
ev["runs_started_after_wait"] = len(ran)
|
||||
ctx.check(not ran, "a run started after the lid-open cancel (%d)", len(ran))
|
||||
st, cs = ctx.forgectrl.get("/cool/status")
|
||||
ev["armed_after"] = cs.get("armed") if isinstance(cs, dict) else None
|
||||
ev["latch_locked"] = latch_locked()
|
||||
ctx.check(not ev["armed_after"], "armed window still open after the cancel")
|
||||
ctx.check(ev["latch_locked"], "kernel latch not locked after the cancel")
|
||||
ctx.confirm("Did the button go dark when the lid opened, with no motion, and does the app show "
|
||||
"the print as cancelled?")
|
||||
ctx.instruct("Close the lid, then click Done.")
|
||||
settle_cloud(ctx, offset)
|
||||
ctx.log("PASS: lid open at the button prompt cancelled the print; latch locked, armed=false")
|
||||
|
||||
|
||||
@test("cloud.hunt-lid-open", title="A cloud hunt runs with the lid open",
|
||||
subsystem="cloud", kind="operator", est_min=5,
|
||||
covers=_CLOUD_COVERS, requires=["cloud.mode-switch"],
|
||||
steps=["Cloud credentials configured; bed clear.",
|
||||
"Open the lid BEFORE the test switches to cloud mode and leave it open through the "
|
||||
"connect-time hunt."],
|
||||
covers=_CLOUD_COVERS + [("forgectrl", "src/super.c")], requires=[],
|
||||
steps=[CLOUD_STEP, "Bed clear.",
|
||||
"Open the lid when asked and leave it open through the connect-time hunt of the fresh "
|
||||
"cloud client the test starts (in cloud mode the client is restarted; from GRBL mode "
|
||||
"the switch is made)."],
|
||||
description="The service's connect-time hunt (lens homing plus its XY hunt) is not gated by "
|
||||
"the lid: it runs and reports ':completed' with the lid open, as the factory's does.")
|
||||
"the lid: it runs and reports ':completed' with the lid open, as the factory's does. "
|
||||
"The service's moves after the lid closes again are waited out.")
|
||||
def hunt_lid_open(ctx):
|
||||
ev = ctx.evidence
|
||||
ctx.instruct("Open the lid and leave it open, then click Done.")
|
||||
sw = (ctx.forgectrl.status().get("switches") or {})
|
||||
ev["lid_before"] = sw.get("lid")
|
||||
ctx.check(not sw.get("lid"), "the lid reads closed (%s)", sw)
|
||||
offset, lamp0 = enter_cloud(ctx)
|
||||
try:
|
||||
# The hunt's own terminal line ("hunt [id]: finished with event ..."):
|
||||
# it must be :completed, and no lid refusal may precede it. Service
|
||||
# motions AFTER the hunt are rightly refused with the lid open and
|
||||
# are outside this window.
|
||||
hunt_line = None
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 180 and hunt_line is None:
|
||||
ctx.checkpoint()
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
hunt_i = next((i for i, ln in enumerate(lines) if "hunt [" in ln and "finished with event" in ln), None)
|
||||
if hunt_i is not None:
|
||||
hunt_line = lines[hunt_i]
|
||||
break
|
||||
time.sleep(0.5)
|
||||
ev["hunt_line"] = hunt_line.split(" ", 2)[-1] if hunt_line else None
|
||||
ctx.check(hunt_line, "the service sent no hunt (or it never finished) within 180 s of the session")
|
||||
refused = [ln for ln in lines[:hunt_i] if "unsafe to move" in ln]
|
||||
ev["refusals_before_hunt_end"] = len(refused)
|
||||
ctx.check(not refused, "the hunt was refused for the lid (%d 'unsafe to move')", len(refused))
|
||||
ctx.check(COMPLETED in hunt_line, "the hunt did not complete: %s", ev["hunt_line"])
|
||||
ctx.confirm("Did the lens home (Z motion) with the lid open, with no error in the app?")
|
||||
ctx.instruct("Close the lid, then click Done.")
|
||||
ctx.sleep(3)
|
||||
finally:
|
||||
leave_cloud(ctx, lamp0)
|
||||
ctx.log("PASS: the connect-time hunt ran and completed with the lid open")
|
||||
offset = fresh_cloud_connect(ctx)
|
||||
# The hunt's own terminal line ("hunt [id]: finished with event ..."):
|
||||
# it must be :completed, and no lid refusal may precede it. Service
|
||||
# motions AFTER the hunt are rightly refused with the lid open and
|
||||
# are outside this window.
|
||||
hunt_line = wait_action_finished(ctx, offset, "hunt", HUNT_TIMEOUT_S)
|
||||
ev["hunt_line"] = message(hunt_line)
|
||||
ctx.check(hunt_line, "the service sent no hunt (or it never finished) within %d s of the session",
|
||||
HUNT_TIMEOUT_S)
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
hunt_i = action_finish_index(lines, "hunt")
|
||||
refused = [ln for ln in lines[:hunt_i] if "unsafe to move" in ln]
|
||||
ev["refusals_before_hunt_end"] = len(refused)
|
||||
ctx.check(not refused, "the hunt was refused for the lid (%d 'unsafe to move')", len(refused))
|
||||
ctx.check(COMPLETED in hunt_line, "the hunt did not complete: %s", ev["hunt_line"])
|
||||
ctx.log("hunt with the lid open: %s", ev["hunt_line"])
|
||||
ctx.confirm("Did the lens home (Z motion) with the lid open, with no error in the app?")
|
||||
ctx.instruct("Close the lid, then click Done. (The service now re-finds the head: several "
|
||||
"moves with lid images between them - the test waits them out.)")
|
||||
settle_cloud(ctx, offset)
|
||||
lines = log_lines_since(GFCLOUD_LOG, offset)
|
||||
ev["motions_after_lid_close"] = sum(1 for ln in lines if "motion [" in ln and COMPLETED in ln)
|
||||
ctx.log("PASS: the connect-time hunt ran and completed with the lid open; %d service motion(s) "
|
||||
"completed after the lid closed", ev["motions_after_lid_close"])
|
||||
|
||||
|
||||
@test("cloud.pause-resume", title="Button pauses and resumes a cloud print (factory backtrack + lead)",
|
||||
subsystem="cloud", kind="live", est_min=8,
|
||||
covers=_CLOUD_COVERS + [("forgectrl", "src/main.c")],
|
||||
requires=["cloud.mode-switch", "laser.emission-witness"],
|
||||
steps=["Cloud credentials configured; the app open; scrap on the bed and a small engrave/score "
|
||||
"job (about 60 s) ready.",
|
||||
requires=["laser.emission-witness"],
|
||||
steps=[CLOUD_STEP, "The app open; scrap on the bed and a small engrave/score job (about 60 s) ready.",
|
||||
"Print from the app and press the button when it lights; a few seconds into the run "
|
||||
"press it again (pause), wait ~3 s, press again (resume); let the job finish."],
|
||||
description="Pressing the button during a cloud print pauses it the factory way - controlled "
|
||||
@@ -545,32 +672,33 @@ def hunt_lid_open(ctx):
|
||||
"latch stays unlocked and the armed window open through the pause.")
|
||||
def pause_resume(ctx):
|
||||
ev = ctx.evidence
|
||||
offset, lamp0 = enter_cloud(ctx)
|
||||
try:
|
||||
ctx.instruct(APP_PRINT_CUE)
|
||||
got = wait_print_running(ctx, offset, 300)
|
||||
ctx.check(got, "the print never reached its run within 300 s (not started, or the button not pressed)")
|
||||
ctx.instruct("The head is moving. Press the button once NOW (pause), watch the head stop and back up "
|
||||
"a few millimeters, wait about 3 seconds, press it again (resume), then click Done.")
|
||||
got = wait_log(ctx, offset, ["button pressed mid-run; pausing", "paused at",
|
||||
"button pressed while paused; resuming"], 90)
|
||||
ev["log"] = {k: bool(v) for k, v in got.items()}
|
||||
ctx.check(got["button pressed mid-run; pausing"], "the press did not pause the run")
|
||||
ctx.check(got["paused at"], "the pause did not settle (no 'paused at')")
|
||||
ctx.check(got["button pressed while paused; resuming"], "the second press did not resume")
|
||||
st, cs = ctx.forgectrl.get("/cool/status")
|
||||
ev["armed_after_resume"] = cs.get("armed") if isinstance(cs, dict) else None
|
||||
ctx.log("armed after the resume: %s", ev["armed_after_resume"])
|
||||
got = wait_log(ctx, offset, ["return home complete", COMPLETED], 300)
|
||||
ev["log_end"] = {k: bool(v) for k, v in got.items()}
|
||||
ctx.check(got[COMPLETED], "the print did not complete after the resume")
|
||||
ctx.check(got["return home complete"], "the post-print park did not complete")
|
||||
relocked = [ln for ln in log_lines_since(GFCLOUD_LOG, offset)
|
||||
if "relocking the laser" in ln or CANCELLED in ln]
|
||||
ev["relock_or_cancel_lines"] = len(relocked)
|
||||
ctx.check(not relocked, "the pause relocked or cancelled the job (%s)", relocked[:2])
|
||||
ctx.confirm("Did the head stop and back up a few millimeters (laser off) on the first press, "
|
||||
"resume on the second, and did the job finish and the app show it complete?")
|
||||
finally:
|
||||
leave_cloud(ctx, lamp0)
|
||||
offset = enter_cloud(ctx)
|
||||
ctx.instruct(APP_PRINT_CUE)
|
||||
got = wait_print_running(ctx, offset, 300)
|
||||
ctx.check(got, "the print never reached its run within 300 s (not started, or the button not pressed)")
|
||||
ctx.instruct("The head is moving. Press the button once NOW (pause), watch the head stop and back up "
|
||||
"a few millimeters, wait about 3 seconds, press it again (resume), then click Done.")
|
||||
got = wait_log(ctx, offset, ["button pressed mid-run; pausing", "paused at",
|
||||
"button pressed while paused; resuming"], 90)
|
||||
ev["log"] = {k: bool(v) for k, v in got.items()}
|
||||
ctx.check(got["button pressed mid-run; pausing"], "the press did not pause the run")
|
||||
ctx.check(got["paused at"], "the pause did not settle (no 'paused at')")
|
||||
ctx.check(got["button pressed while paused; resuming"], "the second press did not resume")
|
||||
st, cs = ctx.forgectrl.get("/cool/status")
|
||||
ev["armed_after_resume"] = cs.get("armed") if isinstance(cs, dict) else None
|
||||
ctx.log("armed after the resume: %s", ev["armed_after_resume"])
|
||||
fin = wait_action_finished(ctx, offset, "print", 300)
|
||||
got = wait_log(ctx, offset, ["return home complete"], 5)
|
||||
ev["log_end"] = {"return home complete": bool(got["return home complete"]),
|
||||
"print finished": message(fin)}
|
||||
ctx.check(fin, "the print did not finish within 300 s of the resume")
|
||||
ctx.check(COMPLETED in fin, "the print did not complete after the resume: %s", message(fin))
|
||||
ctx.check(got["return home complete"], "the post-print park did not complete")
|
||||
relocked = [ln for ln in log_lines_since(GFCLOUD_LOG, offset)
|
||||
if "relocking the laser" in ln or ("print [" in ln and CANCELLED in ln)]
|
||||
ev["relock_or_cancel_lines"] = len(relocked)
|
||||
ctx.check(not relocked, "the pause relocked or cancelled the job (%s)", relocked[:2])
|
||||
ctx.confirm("Did the head stop and back up a few millimeters (laser off) on the first press, "
|
||||
"resume on the second, and did the job finish and the app show it complete?")
|
||||
settle_cloud(ctx, offset)
|
||||
ctx.log("PASS: button pause/resume mid-print, job completed and parked")
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
2026-08-17T09:44:00.417337+00:00 gfcloud[1927] INFO cnc:set_step_freq 28160
|
||||
2026-08-17T09:44:00.418627+00:00 gfcloud[1927] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:44:00.419858+00:00 gfcloud[1927] INFO cnc:set_x_mode 8
|
||||
2026-08-17T09:44:00.421083+00:00 gfcloud[1927] INFO cnc:set_x_current 135
|
||||
2026-08-17T09:44:00.422535+00:00 gfcloud[1927] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:44:00.423794+00:00 gfcloud[1927] INFO cnc:set_y_mode 8
|
||||
2026-08-17T09:44:00.425014+00:00 gfcloud[1927] INFO cnc:set_y_current 22
|
||||
2026-08-17T09:44:00.432591+00:00 gfcloud[1927] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:44:00.434005+00:00 gfcloud[1927] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:44:00.438277+00:00 gfcloud[1927] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:44:01.785230+00:00 gfcloud[1927] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:44:01.797728+00:00 gfcloud[1927] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:44:01.800661+00:00 gfcloud[1927] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:44:01.808504+00:00 gfcloud[1927] INFO machine:_motion_locked end positions (actual/expected): X (-10313/-10313), Y (-649/-649), Z (0/0)
|
||||
2026-08-17T09:44:01.809326+00:00 gfcloud[1927] INFO machine:_motion_locked motion bytes actual:34952, expected: 34952
|
||||
2026-08-17T09:44:01.810066+00:00 gfcloud[1927] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:44:01.811352+00:00 gfcloud[1927] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:44:01.812874+00:00 gfcloud[1927] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:44:01.820631+00:00 gfcloud[1927] INFO machine:_motion_locked end positions (-10313, -649, 0)
|
||||
2026-08-17T09:44:01.822062+00:00 gfcloud[1927] INFO machine:_motion end motion
|
||||
2026-08-17T09:44:01.822829+00:00 gfcloud[1927] INFO basemachine:_finish_action motion [1576550614]: finished with event ":completed"
|
||||
2026-08-17T09:44:01.824200+00:00 gfcloud[1927] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:44:07.993647+00:00 gfcloud[1927] INFO gfuiservice:run service action request: print (ready)
|
||||
2026-08-17T09:44:07.998655+00:00 gfcloud[1927] INFO machine:_motion start motion
|
||||
2026-08-17T09:44:07.999643+00:00 gfcloud[1927] INFO gfuiservice:run print (ready) service action print
|
||||
2026-08-17T09:44:14.199666+00:00 gfcloud[1927] INFO cnc:laser_latch 0
|
||||
2026-08-17T09:44:14.208291+00:00 gfcloud[1927] INFO machine:_button_wait waiting for button
|
||||
2026-08-17T09:44:21.270846+00:00 gfcloud[1927] INFO machine:_switch_event lid opened
|
||||
2026-08-17T09:44:21.275703+00:00 gfcloud[1927] WARNING machine:_button_wait button wait lid opened - relocking the laser
|
||||
2026-08-17T09:44:21.278615+00:00 gfcloud[1927] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:44:21.288305+00:00 gfcloud[1927] INFO machine:_return_home start return home
|
||||
2026-08-17T09:44:21.291373+00:00 gfcloud[1927] INFO machine:_return_home return home: already at the job start
|
||||
2026-08-17T09:44:21.292501+00:00 gfcloud[1927] INFO machine:_motion_locked start cool down
|
||||
2026-08-17T09:44:21.305578+00:00 gfcloud[1927] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:44:21.307322+00:00 gfcloud[1927] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:44:21.309185+00:00 gfcloud[1927] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:44:21.316340+00:00 gfcloud[1927] INFO machine:_motion_locked end positions (0, 0, 0)
|
||||
2026-08-17T09:44:21.318139+00:00 gfcloud[1927] INFO machine:_motion end motion
|
||||
2026-08-17T09:44:21.318860+00:00 gfcloud[1927] INFO basemachine:_finish_action print [1576550621]: finished with event ":cancelled"
|
||||
2026-08-17T09:44:21.320214+00:00 gfcloud[1927] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:44:34.058320+00:00 gfcloud[1927] INFO machine:_switch_event lid closed
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
2026-08-17T09:44:35.789556+00:00 gfcloud[1927] INFO websocket:img_upload START
|
||||
2026-08-17T09:44:36.105516+00:00 gfcloud[1927] INFO websocket:img_upload COMPLETE
|
||||
2026-08-17T09:44:36.109421+00:00 gfcloud[1927] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:44:38.461442+00:00 gfcloud[1927] INFO gfcloud:_shutdown shutdown requested
|
||||
2026-08-17T09:44:38.910494+00:00 gfcloud[1927] INFO machine:_shutdown shutting down
|
||||
2026-08-17T09:44:38.913049+00:00 gfcloud[1927] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:44:38.950354+00:00 gfcloud[1927] INFO machine:_shutdown joining switch thread
|
||||
2026-08-17T09:44:38.956909+00:00 gfcloud[1927] INFO machine:_shutdown shut down complete
|
||||
2026-08-17T09:44:38.986287+00:00 gfcloud[1927] INFO websocket:_on_close RX-EVENT: closed (None, None)
|
||||
2026-08-17T09:44:38.989003+00:00 gfcloud[1927] INFO websocket:run CLOSING
|
||||
2026-08-17T09:44:38.992724+00:00 gfcloud[1927] INFO gfcloud:main gfcloud exit
|
||||
2026-08-17T09:45:08.449553+00:00 gfcloud[2278] INFO gfuiservice:__init__ INITIALIZED
|
||||
2026-08-17T09:45:08.451929+00:00 gfcloud[2278] INFO authentication:authenticate_machine START
|
||||
2026-08-17T09:45:08.965893+00:00 gfcloud[2278] INFO authentication:authenticate_machine SUCCESS
|
||||
2026-08-17T09:45:08.967947+00:00 gfcloud[2278] INFO websocket:ws_connect CONNECTING
|
||||
2026-08-17T09:45:09.421918+00:00 gfcloud[2278] INFO _logging:info Websocket connected
|
||||
2026-08-17T09:45:09.424224+00:00 gfcloud[2278] INFO websocket:_on_open RX-EVENT: ready
|
||||
2026-08-17T09:45:09.973615+00:00 gfcloud[2278] INFO websocket:ws_connect ESTABLISHED
|
||||
2026-08-17T09:45:09.994287+00:00 gfcloud[2278] INFO cnc:set_ignored_faults 0
|
||||
2026-08-17T09:45:09.996281+00:00 gfcloud[2278] INFO cnc:set_step_freq 10000
|
||||
2026-08-17T09:45:09.998803+00:00 gfcloud[2278] INFO cnc:set_x_mode Microstep.M_8
|
||||
2026-08-17T09:45:09.999688+00:00 gfcloud[2278] INFO cnc:set_y_mode Microstep.M_8
|
||||
2026-08-17T09:45:10.000483+00:00 gfcloud[2278] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:45:10.001229+00:00 gfcloud[2278] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:45:10.002000+00:00 gfcloud[2278] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:45:10.007267+00:00 gfcloud[2278] INFO cnc:set_y_current 33
|
||||
2026-08-17T09:45:10.008695+00:00 gfcloud[2278] INFO z_axis:reset resetting z
|
||||
2026-08-17T09:45:10.009124+00:00 gfcloud[2278] INFO z_axis:configure configuring z_enable: False, current: ZCur.LOW, mode: Microstep.HALF
|
||||
2026-08-17T09:45:10.021592+00:00 gfcloud[2278] INFO gfuiservice:run service action request: settings (ready)
|
||||
2026-08-17T09:45:10.024928+00:00 gfcloud[2278] INFO settings:send_report START
|
||||
2026-08-17T09:45:10.039856+00:00 gfcloud[2278] INFO settings:send_report COMPLETE
|
||||
2026-08-17T09:45:10.040650+00:00 gfcloud[2278] INFO gfuiservice:run settings (ready) service action dispatched
|
||||
2026-08-17T09:45:10.591813+00:00 gfcloud[2278] INFO gfuiservice:run service action request: hunt (ready)
|
||||
2026-08-17T09:45:10.596213+00:00 gfcloud[2278] INFO z_axis:home starting z homing cycle
|
||||
2026-08-17T09:45:10.599109+00:00 gfcloud[2278] INFO gfuiservice:run hunt (ready) service action hunt
|
||||
2026-08-17T09:45:10.600433+00:00 gfcloud[2278] INFO z_axis:configure configuring z_enable: True, current: ZCur.HIGH, mode: Microstep.FULL
|
||||
2026-08-17T09:45:10.607559+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: False
|
||||
2026-08-17T09:45:11.160422+00:00 gfcloud[2278] INFO z_axis:_step_until took -3 z_steps
|
||||
2026-08-17T09:45:11.161195+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: True
|
||||
2026-08-17T09:45:11.714663+00:00 gfcloud[2278] INFO z_axis:_step_until took 3 z_steps
|
||||
2026-08-17T09:45:11.717907+00:00 gfcloud[2278] INFO z_axis:home pass: 1
|
||||
2026-08-17T09:45:11.719134+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: False
|
||||
2026-08-17T09:45:12.272747+00:00 gfcloud[2278] INFO z_axis:_step_until took -3 z_steps
|
||||
2026-08-17T09:45:12.274301+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: True
|
||||
2026-08-17T09:45:12.829585+00:00 gfcloud[2278] INFO z_axis:_step_until took 3 z_steps
|
||||
2026-08-17T09:45:12.831305+00:00 gfcloud[2278] INFO z_axis:home pass: 2
|
||||
2026-08-17T09:45:12.832786+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: False
|
||||
2026-08-17T09:45:13.385567+00:00 gfcloud[2278] INFO z_axis:_step_until took -3 z_steps
|
||||
2026-08-17T09:45:13.397106+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: True
|
||||
2026-08-17T09:45:13.947958+00:00 gfcloud[2278] INFO z_axis:_step_until took 3 z_steps
|
||||
2026-08-17T09:45:13.949754+00:00 gfcloud[2278] INFO z_axis:home pass: 3
|
||||
2026-08-17T09:45:13.951244+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: False
|
||||
2026-08-17T09:45:14.504840+00:00 gfcloud[2278] INFO z_axis:_step_until took -3 z_steps
|
||||
2026-08-17T09:45:14.506743+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: True
|
||||
2026-08-17T09:45:15.060854+00:00 gfcloud[2278] INFO z_axis:_step_until took 3 z_steps
|
||||
2026-08-17T09:45:15.062536+00:00 gfcloud[2278] INFO z_axis:home pass: 4
|
||||
2026-08-17T09:45:15.063941+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: False
|
||||
2026-08-17T09:45:15.617907+00:00 gfcloud[2278] INFO z_axis:_step_until took -3 z_steps
|
||||
2026-08-17T09:45:15.619505+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: True
|
||||
2026-08-17T09:45:16.175270+00:00 gfcloud[2278] INFO z_axis:_step_until took 3 z_steps
|
||||
2026-08-17T09:45:16.177421+00:00 gfcloud[2278] INFO z_axis:home pass: 5
|
||||
2026-08-17T09:45:16.179808+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: False
|
||||
2026-08-17T09:45:16.733698+00:00 gfcloud[2278] INFO z_axis:_step_until took -3 z_steps
|
||||
2026-08-17T09:45:16.735355+00:00 gfcloud[2278] INFO z_axis:_step_until starting z_step until at_home: True
|
||||
2026-08-17T09:45:17.289377+00:00 gfcloud[2278] INFO z_axis:_step_until took 3 z_steps
|
||||
2026-08-17T09:45:17.291109+00:00 gfcloud[2278] INFO z_axis:configure configuring z_enable: None, current: ZCur.LOW, mode: Microstep.HALF
|
||||
2026-08-17T09:45:17.296301+00:00 gfcloud[2278] INFO z_axis:home homing cycle complete: [0, 0, 0, 0, 0]
|
||||
2026-08-17T09:45:17.298209+00:00 gfcloud[2278] INFO machine:_motion start motion
|
||||
2026-08-17T09:45:17.968494+00:00 gfcloud[2278] INFO cnc:set_step_freq 10000
|
||||
2026-08-17T09:45:17.969759+00:00 gfcloud[2278] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:45:17.970993+00:00 gfcloud[2278] INFO cnc:set_x_mode 8
|
||||
2026-08-17T09:45:17.972200+00:00 gfcloud[2278] INFO cnc:set_x_current 135
|
||||
2026-08-17T09:45:17.973616+00:00 gfcloud[2278] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:45:17.974870+00:00 gfcloud[2278] INFO cnc:set_y_mode 8
|
||||
2026-08-17T09:45:17.976077+00:00 gfcloud[2278] INFO cnc:set_y_current 22
|
||||
2026-08-17T09:45:17.983820+00:00 gfcloud[2278] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:45:17.985159+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:45:17.989360+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:45:19.630807+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:45:19.636283+00:00 gfcloud[2278] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:45:19.637979+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:19.651159+00:00 gfcloud[2278] INFO machine:_motion_locked end positions (actual/expected): X (0/0), Y (0/0), Z (-4/-4)
|
||||
2026-08-17T09:45:19.652103+00:00 gfcloud[2278] INFO machine:_motion_locked motion bytes actual:15874, expected: 15874
|
||||
2026-08-17T09:45:19.652852+00:00 gfcloud[2278] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:45:19.653964+00:00 gfcloud[2278] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:45:19.655305+00:00 gfcloud[2278] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:45:19.664032+00:00 gfcloud[2278] INFO machine:_motion_locked end positions (0, 0, -4)
|
||||
2026-08-17T09:45:19.664853+00:00 gfcloud[2278] INFO machine:_motion end motion
|
||||
2026-08-17T09:45:19.665575+00:00 gfcloud[2278] INFO basemachine:_finish_action hunt [1576550660]: finished with event ":completed"
|
||||
2026-08-17T09:45:19.667114+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:20.027338+00:00 gfcloud[2278] INFO gfuiservice:run service action request: lid_image (ready)
|
||||
2026-08-17T09:45:20.030024+00:00 gfcloud[2278] INFO ffmachine:_lid_image capturing Lid Image via forgectrl
|
||||
2026-08-17T09:45:20.040337+00:00 gfcloud[2278] INFO gfuiservice:run lid_image (ready) service action dispatched
|
||||
2026-08-17T09:45:21.195560+00:00 gfcloud[2278] INFO ffmachine:_lid_image uploading Lid Image
|
||||
2026-08-17T09:45:21.196529+00:00 gfcloud[2278] INFO websocket:img_upload START
|
||||
2026-08-17T09:45:21.721382+00:00 gfcloud[2278] INFO websocket:img_upload COMPLETE
|
||||
2026-08-17T09:45:21.725394+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:23.732857+00:00 gfcloud[2278] INFO gfuiservice:run service action request: motion (ready)
|
||||
2026-08-17T09:45:23.736044+00:00 gfcloud[2278] INFO machine:_motion start motion
|
||||
2026-08-17T09:45:23.737995+00:00 gfcloud[2278] INFO gfuiservice:run motion (ready) service action motion
|
||||
2026-08-17T09:45:23.738941+00:00 gfcloud[2278] INFO machine:_safe_to_move lid opened, unsafe to move
|
||||
2026-08-17T09:45:23.739814+00:00 gfcloud[2278] INFO machine:_motion end motion
|
||||
2026-08-17T09:45:23.740605+00:00 gfcloud[2278] INFO basemachine:_finish_action motion [1576550667]: finished with event ":cancelled"
|
||||
2026-08-17T09:45:23.742169+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:42.629674+00:00 gfcloud[2278] INFO machine:_switch_event lid closed
|
||||
2026-08-17T09:45:42.920194+00:00 gfcloud[2278] INFO gfuiservice:run service action request: lid_image (ready)
|
||||
2026-08-17T09:45:42.924152+00:00 gfcloud[2278] INFO ffmachine:_lid_image capturing Lid Image via forgectrl
|
||||
2026-08-17T09:45:42.927934+00:00 gfcloud[2278] INFO gfuiservice:run lid_image (ready) service action dispatched
|
||||
2026-08-17T09:45:43.989152+00:00 gfcloud[2278] INFO ffmachine:_lid_image uploading Lid Image
|
||||
2026-08-17T09:45:43.990022+00:00 gfcloud[2278] INFO websocket:img_upload START
|
||||
2026-08-17T09:45:44.312060+00:00 gfcloud[2278] INFO websocket:img_upload COMPLETE
|
||||
2026-08-17T09:45:44.315952+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:46.652633+00:00 gfcloud[2278] INFO gfuiservice:run service action request: motion (ready)
|
||||
2026-08-17T09:45:46.654950+00:00 gfcloud[2278] INFO machine:_motion start motion
|
||||
2026-08-17T09:45:46.657522+00:00 gfcloud[2278] INFO gfuiservice:run motion (ready) service action motion
|
||||
2026-08-17T09:45:48.089456+00:00 gfcloud[2278] INFO cnc:set_step_freq 10000
|
||||
2026-08-17T09:45:48.090706+00:00 gfcloud[2278] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:45:48.091970+00:00 gfcloud[2278] INFO cnc:set_x_mode 8
|
||||
2026-08-17T09:45:48.093155+00:00 gfcloud[2278] INFO cnc:set_x_current 135
|
||||
2026-08-17T09:45:48.094587+00:00 gfcloud[2278] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:45:48.095866+00:00 gfcloud[2278] INFO cnc:set_y_mode 8
|
||||
2026-08-17T09:45:48.097182+00:00 gfcloud[2278] INFO cnc:set_y_current 22
|
||||
2026-08-17T09:45:48.104758+00:00 gfcloud[2278] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:45:48.106128+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:45:48.110477+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:45:50.393091+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:45:50.398748+00:00 gfcloud[2278] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:45:50.399629+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:50.423949+00:00 gfcloud[2278] INFO machine:_motion_locked end positions (actual/expected): X (12939/12939), Y (7402/7402), Z (0/0)
|
||||
2026-08-17T09:45:50.425534+00:00 gfcloud[2278] INFO machine:_motion_locked motion bytes actual:22740, expected: 22740
|
||||
2026-08-17T09:45:50.426258+00:00 gfcloud[2278] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:45:50.427835+00:00 gfcloud[2278] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:45:50.429387+00:00 gfcloud[2278] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:45:50.437659+00:00 gfcloud[2278] INFO machine:_motion_locked end positions (12939, 7402, 0)
|
||||
2026-08-17T09:45:50.438474+00:00 gfcloud[2278] INFO machine:_motion end motion
|
||||
2026-08-17T09:45:50.439167+00:00 gfcloud[2278] INFO basemachine:_finish_action motion [1576550679]: finished with event ":completed"
|
||||
2026-08-17T09:45:50.440481+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:50.770056+00:00 gfcloud[2278] INFO gfuiservice:run service action request: lid_image (ready)
|
||||
2026-08-17T09:45:50.772800+00:00 gfcloud[2278] INFO ffmachine:_lid_image capturing Lid Image via forgectrl
|
||||
2026-08-17T09:45:50.782017+00:00 gfcloud[2278] INFO gfuiservice:run lid_image (ready) service action dispatched
|
||||
2026-08-17T09:45:51.427848+00:00 gfcloud[2278] INFO ffmachine:_lid_image uploading Lid Image
|
||||
2026-08-17T09:45:51.428456+00:00 gfcloud[2278] INFO websocket:img_upload START
|
||||
2026-08-17T09:45:51.757479+00:00 gfcloud[2278] INFO websocket:img_upload COMPLETE
|
||||
2026-08-17T09:45:51.760692+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:54.222123+00:00 gfcloud[2278] INFO gfuiservice:run service action request: motion (ready)
|
||||
2026-08-17T09:45:54.224786+00:00 gfcloud[2278] INFO machine:_motion start motion
|
||||
2026-08-17T09:45:54.226855+00:00 gfcloud[2278] INFO gfuiservice:run motion (ready) service action motion
|
||||
2026-08-17T09:45:54.557705+00:00 gfcloud[2278] INFO cnc:set_step_freq 10000
|
||||
2026-08-17T09:45:54.562210+00:00 gfcloud[2278] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:45:54.571212+00:00 gfcloud[2278] INFO cnc:set_x_mode 8
|
||||
2026-08-17T09:45:54.573373+00:00 gfcloud[2278] INFO cnc:set_x_current 135
|
||||
2026-08-17T09:45:54.596668+00:00 gfcloud[2278] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:45:54.597612+00:00 gfcloud[2278] INFO cnc:set_y_mode 8
|
||||
2026-08-17T09:45:54.598479+00:00 gfcloud[2278] INFO cnc:set_y_current 22
|
||||
2026-08-17T09:45:54.622488+00:00 gfcloud[2278] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:45:54.624223+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:45:54.629307+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:45:54.835600+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:45:54.838848+00:00 gfcloud[2278] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:45:54.839726+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:54.849011+00:00 gfcloud[2278] INFO machine:_motion_locked end positions (actual/expected): X (159/159), Y (5/5), Z (0/0)
|
||||
2026-08-17T09:45:54.849946+00:00 gfcloud[2278] INFO machine:_motion_locked motion bytes actual:1222, expected: 1222
|
||||
2026-08-17T09:45:54.850790+00:00 gfcloud[2278] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:45:54.852317+00:00 gfcloud[2278] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:45:54.854073+00:00 gfcloud[2278] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:45:54.880450+00:00 gfcloud[2278] INFO machine:_motion_locked end positions (159, 5, 0)
|
||||
2026-08-17T09:45:54.881925+00:00 gfcloud[2278] INFO machine:_motion end motion
|
||||
2026-08-17T09:45:54.882641+00:00 gfcloud[2278] INFO basemachine:_finish_action motion [1576550683]: finished with event ":completed"
|
||||
2026-08-17T09:45:54.884500+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:55.137070+00:00 gfcloud[2278] INFO gfuiservice:run service action request: lid_image (ready)
|
||||
2026-08-17T09:45:55.139449+00:00 gfcloud[2278] INFO ffmachine:_lid_image capturing Lid Image via forgectrl
|
||||
2026-08-17T09:45:55.142346+00:00 gfcloud[2278] INFO gfuiservice:run lid_image (ready) service action dispatched
|
||||
2026-08-17T09:45:55.809276+00:00 gfcloud[2278] INFO ffmachine:_lid_image uploading Lid Image
|
||||
2026-08-17T09:45:55.810155+00:00 gfcloud[2278] INFO websocket:img_upload START
|
||||
2026-08-17T09:45:56.158888+00:00 gfcloud[2278] INFO websocket:img_upload COMPLETE
|
||||
2026-08-17T09:45:56.162644+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:45:58.467590+00:00 gfcloud[2278] INFO gfuiservice:run service action request: motion (ready)
|
||||
2026-08-17T09:45:58.469426+00:00 gfcloud[2278] INFO machine:_motion start motion
|
||||
2026-08-17T09:45:58.471091+00:00 gfcloud[2278] INFO gfuiservice:run motion (ready) service action motion
|
||||
2026-08-17T09:46:00.721960+00:00 gfcloud[2278] INFO cnc:set_step_freq 28160
|
||||
2026-08-17T09:46:00.725688+00:00 gfcloud[2278] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:46:00.727477+00:00 gfcloud[2278] INFO cnc:set_x_mode 8
|
||||
2026-08-17T09:46:00.728738+00:00 gfcloud[2278] INFO cnc:set_x_current 135
|
||||
2026-08-17T09:46:00.730198+00:00 gfcloud[2278] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:46:00.731506+00:00 gfcloud[2278] INFO cnc:set_y_mode 8
|
||||
2026-08-17T09:46:00.732735+00:00 gfcloud[2278] INFO cnc:set_y_current 22
|
||||
2026-08-17T09:46:00.740459+00:00 gfcloud[2278] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:46:00.741865+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:46:00.745983+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:46:02.491897+00:00 gfcloud[2278] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:46:02.494137+00:00 gfcloud[2278] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:46:02.495155+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:46:02.530565+00:00 gfcloud[2278] INFO machine:_motion_locked end positions (actual/expected): X (-13096/-13096), Y (-7400/-7400), Z (0/0)
|
||||
2026-08-17T09:46:02.531413+00:00 gfcloud[2278] INFO machine:_motion_locked motion bytes actual:47243, expected: 47243
|
||||
2026-08-17T09:46:02.538168+00:00 gfcloud[2278] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:46:02.539603+00:00 gfcloud[2278] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:46:02.541147+00:00 gfcloud[2278] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:46:02.549578+00:00 gfcloud[2278] INFO machine:_motion_locked end positions (-13096, -7400, 0)
|
||||
2026-08-17T09:46:02.550385+00:00 gfcloud[2278] INFO machine:_motion end motion
|
||||
2026-08-17T09:46:02.551153+00:00 gfcloud[2278] INFO basemachine:_finish_action motion [1576550685]: finished with event ":completed"
|
||||
2026-08-17T09:46:02.552496+00:00 gfcloud[2278] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:46:03.007623+00:00 gfcloud[2278] INFO gfuiservice:run service action request: lid_image (ready)
|
||||
2026-08-17T09:46:03.009488+00:00 gfcloud[2278] INFO ffmachine:_lid_image capturing Lid Image via forgectrl
|
||||
2026-08-17T09:46:03.037155+00:00 gfcloud[2278] INFO gfuiservice:run lid_image (ready) service action dispatched
|
||||
2026-08-17T09:46:03.758520+00:00 gfcloud[2278] INFO ffmachine:_lid_image uploading Lid Image
|
||||
2026-08-17T09:46:03.760049+00:00 gfcloud[2278] INFO websocket:img_upload START
|
||||
2026-08-17T09:46:04.128126+00:00 gfcloud[2278] INFO websocket:img_upload COMPLETE
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
2026-08-17T09:42:09.781500+00:00 gfcloud[1522] INFO gfuiservice:run motion (ready) service action motion
|
||||
2026-08-17T09:42:11.281337+00:00 gfcloud[1522] INFO cnc:set_step_freq 28160
|
||||
2026-08-17T09:42:11.282627+00:00 gfcloud[1522] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:42:11.283850+00:00 gfcloud[1522] INFO cnc:set_x_mode 8
|
||||
2026-08-17T09:42:11.285121+00:00 gfcloud[1522] INFO cnc:set_x_current 135
|
||||
2026-08-17T09:42:11.286711+00:00 gfcloud[1522] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:42:11.287977+00:00 gfcloud[1522] INFO cnc:set_y_mode 8
|
||||
2026-08-17T09:42:11.289235+00:00 gfcloud[1522] INFO cnc:set_y_current 22
|
||||
2026-08-17T09:42:11.297425+00:00 gfcloud[1522] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:42:11.299141+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:11.303264+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:42:12.560917+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:12.572022+00:00 gfcloud[1522] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:42:12.572811+00:00 gfcloud[1522] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:42:12.580801+00:00 gfcloud[1522] INFO machine:_motion_locked end positions (actual/expected): X (-10313/-10313), Y (-649/-649), Z (0/0)
|
||||
2026-08-17T09:42:12.581620+00:00 gfcloud[1522] INFO machine:_motion_locked motion bytes actual:34952, expected: 34952
|
||||
2026-08-17T09:42:12.582366+00:00 gfcloud[1522] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:42:12.583540+00:00 gfcloud[1522] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:42:12.584928+00:00 gfcloud[1522] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:42:12.593681+00:00 gfcloud[1522] INFO machine:_motion_locked end positions (-10313, -649, 0)
|
||||
2026-08-17T09:42:12.604394+00:00 gfcloud[1522] INFO machine:_motion end motion
|
||||
2026-08-17T09:42:12.604757+00:00 gfcloud[1522] INFO basemachine:_finish_action motion [1576550503]: finished with event ":completed"
|
||||
2026-08-17T09:42:12.605824+00:00 gfcloud[1522] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:42:15.672518+00:00 gfcloud[1522] INFO gfuiservice:run service action request: print (ready)
|
||||
2026-08-17T09:42:15.676330+00:00 gfcloud[1522] INFO machine:_motion start motion
|
||||
2026-08-17T09:42:15.680419+00:00 gfcloud[1522] INFO gfuiservice:run print (ready) service action print
|
||||
2026-08-17T09:42:21.975587+00:00 gfcloud[1522] INFO cnc:laser_latch 0
|
||||
2026-08-17T09:42:22.001354+00:00 gfcloud[1522] INFO machine:_button_wait waiting for button
|
||||
2026-08-17T09:42:24.774117+00:00 gfcloud[1522] INFO machine:_switch_event button pushed
|
||||
2026-08-17T09:42:24.778509+00:00 gfcloud[1522] INFO cnc:set_step_freq 10000
|
||||
2026-08-17T09:42:24.781114+00:00 gfcloud[1522] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:42:24.783654+00:00 gfcloud[1522] INFO cnc:set_x_mode 8
|
||||
2026-08-17T09:42:24.786180+00:00 gfcloud[1522] INFO cnc:set_x_current 135
|
||||
2026-08-17T09:42:24.789088+00:00 gfcloud[1522] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:42:24.791559+00:00 gfcloud[1522] INFO cnc:set_y_mode 8
|
||||
2026-08-17T09:42:24.794380+00:00 gfcloud[1522] INFO cnc:set_y_current 22
|
||||
2026-08-17T09:42:24.815263+00:00 gfcloud[1522] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:42:24.816687+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:24.820823+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:42:25.049489+00:00 gfcloud[1522] INFO machine:_switch_event button released
|
||||
2026-08-17T09:42:30.832527+00:00 gfcloud[1522] INFO machine:_switch_event lid opened
|
||||
2026-08-17T09:42:30.838627+00:00 gfcloud[1522] WARNING machine:_run_loop lid opened mid-run; stopping motion
|
||||
2026-08-17T09:42:30.941553+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:30.947477+00:00 gfcloud[1522] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:42:30.949099+00:00 gfcloud[1522] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:42:30.964916+00:00 gfcloud[1522] INFO machine:_motion_locked end positions (actual/expected): X (8454/7573), Y (684/684), Z (3/3)
|
||||
2026-08-17T09:42:30.965704+00:00 gfcloud[1522] INFO machine:_motion_locked motion bytes actual:60649, expected: 238043
|
||||
2026-08-17T09:42:30.972180+00:00 gfcloud[1522] INFO machine:_return_home start return home
|
||||
2026-08-17T09:42:31.847247+00:00 gfcloud[1522] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:42:31.848617+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:31.852977+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:42:36.970521+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:36.976059+00:00 gfcloud[1522] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:42:36.977738+00:00 gfcloud[1522] INFO machine:_return_home return home complete
|
||||
2026-08-17T09:42:36.979696+00:00 gfcloud[1522] INFO machine:_motion_locked start cool down
|
||||
2026-08-17T09:42:36.998973+00:00 gfcloud[1522] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:42:37.000314+00:00 gfcloud[1522] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:42:37.001774+00:00 gfcloud[1522] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:42:37.010089+00:00 gfcloud[1522] INFO machine:_motion_locked end positions (0, 0, 3)
|
||||
2026-08-17T09:42:37.010885+00:00 gfcloud[1522] INFO machine:_motion end motion
|
||||
2026-08-17T09:42:37.011563+00:00 gfcloud[1522] INFO basemachine:_finish_action print [1576550507]: finished with event ":cancelled"
|
||||
2026-08-17T09:42:37.013071+00:00 gfcloud[1522] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:42:37.449180+00:00 gfcloud[1522] INFO gfuiservice:run service action request: hunt (ready)
|
||||
2026-08-17T09:42:37.451286+00:00 gfcloud[1522] INFO z_axis:home starting z homing cycle
|
||||
2026-08-17T09:42:37.452782+00:00 gfcloud[1522] INFO gfuiservice:run hunt (ready) service action hunt
|
||||
2026-08-17T09:42:37.453691+00:00 gfcloud[1522] INFO z_axis:configure configuring z_enable: True, current: ZCur.HIGH, mode: Microstep.FULL
|
||||
2026-08-17T09:42:37.458283+00:00 gfcloud[1522] INFO z_axis:_step_until starting z_step until at_home: False
|
||||
2026-08-17T09:42:37.827610+00:00 gfcloud[1522] INFO z_axis:_step_until took -2 z_steps
|
||||
2026-08-17T09:42:37.829278+00:00 gfcloud[1522] INFO z_axis:_step_until starting z_step until at_home: True
|
||||
2026-08-17T09:42:38.200493+00:00 gfcloud[1522] INFO z_axis:_step_until took 2 z_steps
|
||||
2026-08-17T09:42:38.202234+00:00 gfcloud[1522] INFO z_axis:home pass: 1
|
||||
2026-08-17T09:42:38.203788+00:00 gfcloud[1522] INFO z_axis:_step_until starting z_step until at_home: False
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
2026-08-17T09:42:15.672518+00:00 gfcloud[1522] INFO gfuiservice:run service action request: print (ready)
|
||||
2026-08-17T09:42:15.676330+00:00 gfcloud[1522] INFO machine:_motion start motion
|
||||
2026-08-17T09:42:15.680419+00:00 gfcloud[1522] INFO gfuiservice:run print (ready) service action print
|
||||
2026-08-17T09:42:21.975587+00:00 gfcloud[1522] INFO cnc:laser_latch 0
|
||||
2026-08-17T09:42:22.001354+00:00 gfcloud[1522] INFO machine:_button_wait waiting for button
|
||||
2026-08-17T09:42:24.774117+00:00 gfcloud[1522] INFO machine:_switch_event button pushed
|
||||
2026-08-17T09:42:24.778509+00:00 gfcloud[1522] INFO cnc:set_step_freq 10000
|
||||
2026-08-17T09:42:24.781114+00:00 gfcloud[1522] INFO cnc:set_x_decay 1
|
||||
2026-08-17T09:42:24.783654+00:00 gfcloud[1522] INFO cnc:set_x_mode 8
|
||||
2026-08-17T09:42:24.786180+00:00 gfcloud[1522] INFO cnc:set_x_current 135
|
||||
2026-08-17T09:42:24.789088+00:00 gfcloud[1522] INFO cnc:set_y_decay 1
|
||||
2026-08-17T09:42:24.791559+00:00 gfcloud[1522] INFO cnc:set_y_mode 8
|
||||
2026-08-17T09:42:24.794380+00:00 gfcloud[1522] INFO cnc:set_y_current 22
|
||||
2026-08-17T09:42:24.815263+00:00 gfcloud[1522] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:42:24.816687+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:24.820823+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:42:25.049489+00:00 gfcloud[1522] INFO machine:_switch_event button released
|
||||
2026-08-17T09:42:31.104201+00:00 gfcloud[1522] INFO machine:_switch_event button pushed
|
||||
2026-08-17T09:42:31.106877+00:00 gfcloud[1522] INFO machine:_run_loop button pressed mid-run; pausing
|
||||
2026-08-17T09:42:31.288114+00:00 gfcloud[1522] INFO machine:_switch_event button released
|
||||
2026-08-17T09:42:31.612040+00:00 gfcloud[1522] INFO machine:_run_loop paused at Position(x=AxisPosition(steps=1210, mm=22.68, inch=0.89), y=AxisPosition(steps=402, mm=7.54, inch=0.30), z=AxisPosition(steps=3, mm=0.0, inch=0.0), bytes=PulsPosition(total=238043, processed=61200))
|
||||
2026-08-17T09:42:34.702311+00:00 gfcloud[1522] INFO machine:_switch_event button pushed
|
||||
2026-08-17T09:42:34.704560+00:00 gfcloud[1522] INFO machine:_run_loop button pressed while paused; resuming (laser lead 1950 ticks)
|
||||
2026-08-17T09:42:34.901002+00:00 gfcloud[1522] INFO machine:_switch_event button released
|
||||
2026-08-17T09:42:58.130807+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:58.136283+00:00 gfcloud[1522] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:42:58.137979+00:00 gfcloud[1522] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:42:58.151159+00:00 gfcloud[1522] INFO machine:_motion_locked end positions (actual/expected): X (7573/7573), Y (684/684), Z (3/3)
|
||||
2026-08-17T09:42:58.152103+00:00 gfcloud[1522] INFO machine:_motion_locked motion bytes actual:238043, expected: 238043
|
||||
2026-08-17T09:42:58.160180+00:00 gfcloud[1522] INFO machine:_return_home start return home
|
||||
2026-08-17T09:42:59.047247+00:00 gfcloud[1522] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:42:59.048617+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:42:59.052977+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.RUNNING
|
||||
2026-08-17T09:43:04.170521+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:43:04.176059+00:00 gfcloud[1522] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:43:04.177738+00:00 gfcloud[1522] INFO machine:_return_home return home complete
|
||||
2026-08-17T09:43:04.179696+00:00 gfcloud[1522] INFO machine:_motion_locked start cool down
|
||||
2026-08-17T09:43:04.198973+00:00 gfcloud[1522] INFO machine:_motion_locked start idle
|
||||
2026-08-17T09:43:04.200314+00:00 gfcloud[1522] INFO cnc:set_x_current 33
|
||||
2026-08-17T09:43:04.201774+00:00 gfcloud[1522] INFO cnc:set_y_current 5
|
||||
2026-08-17T09:43:04.210089+00:00 gfcloud[1522] INFO machine:_motion_locked end positions (0, 0, 3)
|
||||
2026-08-17T09:43:04.210885+00:00 gfcloud[1522] INFO machine:_motion end motion
|
||||
2026-08-17T09:43:04.211563+00:00 gfcloud[1522] INFO basemachine:_finish_action print [1576550507]: finished with event ":completed"
|
||||
2026-08-17T09:43:04.213071+00:00 gfcloud[1522] INFO cnc:laser_latch 1
|
||||
2026-08-17T09:43:04.649180+00:00 gfcloud[1522] INFO gfuiservice:run service action request: hunt (ready)
|
||||
2026-08-17T09:43:04.651286+00:00 gfcloud[1522] INFO z_axis:home starting z homing cycle
|
||||
2026-08-17T09:43:04.652782+00:00 gfcloud[1522] INFO gfuiservice:run hunt (ready) service action hunt
|
||||
2026-08-17T09:43:11.298209+00:00 gfcloud[1522] INFO machine:_motion start motion
|
||||
2026-08-17T09:43:11.983820+00:00 gfcloud[1522] INFO machine:_run_loop starting run
|
||||
2026-08-17T09:43:13.630807+00:00 gfcloud[1522] INFO machine:_run_loop current state: MachineState.IDLE
|
||||
2026-08-17T09:43:13.636283+00:00 gfcloud[1522] INFO machine:_run_loop finished run
|
||||
2026-08-17T09:43:13.664853+00:00 gfcloud[1522] INFO machine:_motion end motion
|
||||
2026-08-17T09:43:13.665575+00:00 gfcloud[1522] INFO basemachine:_finish_action hunt [1576550560]: finished with event ":completed"
|
||||
@@ -76,3 +76,89 @@ def make_test(id, covers, always=False, requires=(), kind="auto", fn=None, subsy
|
||||
|
||||
def registry(*tests):
|
||||
return {t.id: t for t in tests}
|
||||
|
||||
|
||||
# ------------------------------------------------------- fake forgectrl
|
||||
|
||||
class FakeForgectrl:
|
||||
"""A stand-in for the machine-services daemon on localhost: canned
|
||||
JSON for the endpoints the suite reads, a mutable `state` the test
|
||||
scripts, every POST recorded, and an optional `on_post(path, form)`
|
||||
hook returning (status, body) to script the daemon's reactions.
|
||||
Point the suite at it with FORGECTRL_URL (see start/stop)."""
|
||||
|
||||
def __init__(self):
|
||||
import http.server
|
||||
import json as _json
|
||||
import threading as _threading
|
||||
import urllib.parse as _up
|
||||
self.state = {
|
||||
"mode": {"mode": "grbl", "controller": "running", "pid": 100, "motion": "verified"},
|
||||
"status": {"state": "idle", "homed": False, "diag": False, "laser_locked": True,
|
||||
"pos": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"switches": {"lid": True, "button": False, "interlock_ok": True,
|
||||
"head": True, "hv_enable": False}},
|
||||
"cool": {"phase": "idle", "armed": False, "hold": False},
|
||||
"cam": {"running": False, "clients": 0},
|
||||
"diag": {"running": False},
|
||||
"settings": {"controller_mode": "grbl", "lid_lamp_idle": ""},
|
||||
}
|
||||
self.posts = []
|
||||
self.on_post = None
|
||||
fake = self
|
||||
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def _send(self, st, body):
|
||||
data = _json.dumps(body).encode()
|
||||
self.send_response(st)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?", 1)[0]
|
||||
key = {"/mode": "mode", "/status": "status", "/cool/status": "cool", "/cam/status": "cam",
|
||||
"/diag/status": "diag", "/settings": "settings"}.get(path)
|
||||
if key is None:
|
||||
return self._send(404, {"error": "no " + path})
|
||||
self._send(200, fake.state[key])
|
||||
|
||||
def do_POST(self):
|
||||
path, _, query = self.path.partition("?")
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
raw = self.rfile.read(n).decode() if n else ""
|
||||
form = dict(_up.parse_qsl(raw)) if raw else dict(_up.parse_qsl(query))
|
||||
fake.posts.append((path, form))
|
||||
if fake.on_post:
|
||||
r = fake.on_post(path, form)
|
||||
if r is not None:
|
||||
return self._send(*r)
|
||||
if path == "/mode" and form.get("controller"):
|
||||
fake.state["mode"] = dict(fake.state["mode"], mode=form["controller"], controller="running",
|
||||
pid=fake.state["mode"].get("pid", 0) + 1)
|
||||
fake.state["settings"]["controller_mode"] = form["controller"]
|
||||
elif path == "/settings":
|
||||
fake.state["settings"].update(form)
|
||||
self._send(200, {"ok": True})
|
||||
|
||||
self._srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H)
|
||||
self._srv.daemon_threads = True
|
||||
self._srv.block_on_close = False # never wait on a lingering connection
|
||||
self.url = "http://127.0.0.1:%d" % self._srv.server_address[1]
|
||||
self._th = _threading.Thread(target=self._srv.serve_forever, daemon=True)
|
||||
|
||||
def start(self):
|
||||
self._th.start()
|
||||
os.environ["FORGECTRL_URL"] = self.url
|
||||
os.environ["FORGECTRL_TOKEN_FILE"] = os.devnull
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
self._srv.shutdown()
|
||||
self._srv.server_close()
|
||||
os.environ.pop("FORGECTRL_URL", None)
|
||||
os.environ.pop("FORGECTRL_TOKEN_FILE", None)
|
||||
|
||||
@@ -266,3 +266,95 @@ class BaselineTests(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class BaselineModeTests(BaselineTests):
|
||||
"""The baseline against a fake forgectrl: what the mode in force
|
||||
owns. Reuses the fake sysfs tree of BaselineTests; only the new
|
||||
tests run here (the inherited ones are skipped)."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
import helpers
|
||||
self.fc = helpers.FakeForgectrl().start()
|
||||
baseline.Baseline._unreachable_until = 0.0
|
||||
|
||||
def tearDown(self):
|
||||
self.fc.stop()
|
||||
super().tearDown()
|
||||
|
||||
def run(self, result=None):
|
||||
# only this class's own tests, not the base class's
|
||||
if self._testMethodName not in BaselineModeTests.__dict__:
|
||||
return
|
||||
return super().run(result)
|
||||
|
||||
def cloud(self):
|
||||
self.fc.state["mode"] = {"mode": "cloud", "controller": "running", "pid": 7, "motion": "verified"}
|
||||
self.fc.state["settings"]["controller_mode"] = "cloud"
|
||||
|
||||
def test_grbl_mode_restores_the_controller_values_and_the_lamp(self):
|
||||
self._attr("cnc/step_freq", "10000")
|
||||
self._attr("pic/lid_led", "77")
|
||||
left = self.bl().enforce("pre", captured=None)
|
||||
self.assertEqual(sorted(x.item for x in left), ["cnc/step_freq", "pic/lid_led"])
|
||||
self.assertEqual(self._read("cnc/step_freq"), "28160")
|
||||
self.assertEqual(self._read("pic/lid_led"), "236")
|
||||
|
||||
def test_cloud_mode_leaves_the_clients_config_lamp_and_counters(self):
|
||||
self.cloud()
|
||||
self._attr("cnc/step_freq", "10000") # the cloud client's tick
|
||||
self._attr("pic/x_step_current", "135")
|
||||
self._attr("pic/lid_led", "77") # its lid-image level
|
||||
b = self.bl()
|
||||
cap = b.capture()
|
||||
self.assertEqual(cap["mode"], "cloud")
|
||||
self.assertNotIn("controller_mode", cap["settings"])
|
||||
self._pos(-13096, -7400, 0) # the service re-zeroed and moved
|
||||
self._attr("cnc/streaming", "1") # NOT the client's: still restored
|
||||
left = b.enforce("post", captured=cap)
|
||||
self.assertEqual([x.item for x in left], ["cnc/streaming"])
|
||||
self.assertEqual(self._read("cnc/step_freq"), "10000")
|
||||
self.assertEqual(self._read("pic/x_step_current"), "135")
|
||||
self.assertEqual(self._read("pic/lid_led"), "77")
|
||||
self.assertEqual(self._read("cnc/streaming"), "0")
|
||||
self.assertEqual(self.fc.posts, []) # no mode switch, no settings write
|
||||
|
||||
def test_cloud_mode_still_relocks_the_latch(self):
|
||||
self.cloud()
|
||||
self._attr("cnc/interlock_circuit", "37") # bit 3 clear: unlocked
|
||||
left = self.bl().enforce("post", captured=None)
|
||||
self.assertEqual([x.item for x in left], ["laser_latch"])
|
||||
self.assertEqual(self._read("cnc/laser_latch"), "1")
|
||||
|
||||
def test_undeclared_mode_change_is_handed_back_through_the_switch(self):
|
||||
b = self.bl()
|
||||
cap = b.capture() # found in grbl
|
||||
self.assertEqual(cap["mode"], "grbl")
|
||||
self.cloud() # the run left it in cloud, silently
|
||||
left = b.enforce("post", captured=cap)
|
||||
items = {x.item: x for x in left}
|
||||
self.assertIn("mode", items)
|
||||
self.assertEqual(items["mode"].action, "restored")
|
||||
self.assertEqual(self.fc.posts, [("/mode", {"controller": "grbl"})])
|
||||
self.assertEqual(self.fc.state["mode"]["mode"], "grbl")
|
||||
|
||||
def test_declared_mode_change_is_kept(self):
|
||||
b = self.bl()
|
||||
cap = b.capture()
|
||||
self.cloud()
|
||||
cap["mode"] = "cloud" # Context.mode_changed("cloud")
|
||||
left = b.enforce("post", captured=cap)
|
||||
self.assertNotIn("mode", [x.item for x in left])
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertEqual(self.fc.state["mode"]["mode"], "cloud")
|
||||
|
||||
def test_controller_mode_setting_is_never_written_back_bare(self):
|
||||
b = self.bl()
|
||||
cap = b.capture()
|
||||
self.assertNotIn("controller_mode", cap["settings"])
|
||||
self.fc.state["settings"]["controller_mode"] = "cloud" # a switch persisted it
|
||||
self.fc.state["mode"]["mode"] = "cloud"
|
||||
cap["mode"] = "cloud"
|
||||
b.enforce("post", captured=cap)
|
||||
self.assertEqual([p for p, _ in self.fc.posts if p == "/settings"], [])
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
"""The cloud.* job-behavior tests against a fake forgectrl and a replayed
|
||||
gfcloud log: the real test functions run under the real runner Context,
|
||||
the operator prompts answered by a script that also drives the replay
|
||||
(open the lid -> the machine's lid reads open; close it -> the service's
|
||||
re-hunt lines land). The log lines are the machine's own: bench excerpts
|
||||
(the lid-abort, button-wait, and lid-open-hunt runs) and, for the pause,
|
||||
the run loop's lines as its host test emits them on the print skeleton
|
||||
of the lid-abort excerpt.
|
||||
|
||||
What is proven here: the tests find the right lines in real log noise,
|
||||
reuse an existing cloud session and never switch back, restart the client
|
||||
for a fresh hunt, judge the print's own finish line (not another action's),
|
||||
wait the service's deferred moves out, and fail for the right reasons."""
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import helpers
|
||||
from forgetest.runner import Context, Failed, Run
|
||||
from forgetest.suite import cloud
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
FIX = os.path.join(HERE, "fixtures")
|
||||
|
||||
|
||||
def fixture(name):
|
||||
with open(os.path.join(FIX, "gfcloud-%s.log" % name), "rb") as f:
|
||||
return f.read().decode().splitlines()
|
||||
|
||||
|
||||
def cut(lines, marker, count=1):
|
||||
"""(before, after) at the count-th line containing marker (the line
|
||||
itself opens `after`)."""
|
||||
n = 0
|
||||
for i, ln in enumerate(lines):
|
||||
if marker in ln:
|
||||
n += 1
|
||||
if n == count:
|
||||
return lines[:i], lines[i:]
|
||||
raise KeyError(marker)
|
||||
|
||||
|
||||
class Script:
|
||||
"""Answers every prompt with its first option; a hook per prompt
|
||||
substring drives the fake machine and the log replay."""
|
||||
|
||||
def __init__(self, run, hooks=None):
|
||||
self.run = run
|
||||
self.hooks = hooks or {}
|
||||
self.asked = []
|
||||
self.th = threading.Thread(target=self._loop, daemon=True)
|
||||
self.stop = False
|
||||
|
||||
def start(self):
|
||||
self.th.start()
|
||||
return self
|
||||
|
||||
def _loop(self):
|
||||
seen = None
|
||||
while not self.stop:
|
||||
p = self.run.prompt
|
||||
if p and p["id"] != seen:
|
||||
seen = p["id"]
|
||||
self.asked.append(p["question"])
|
||||
for key, fn in self.hooks.items():
|
||||
if key in p["question"]:
|
||||
fn()
|
||||
self.run.answer(p["id"], p["options"][0])
|
||||
time.sleep(0.02)
|
||||
|
||||
|
||||
class CloudSuiteTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp(prefix="forgetest-cloud-")
|
||||
self.log = os.path.join(self.tmp, "gfcloud.log")
|
||||
open(self.log, "wb").close()
|
||||
self.sysfs = os.path.join(self.tmp, "sysfs") + os.sep
|
||||
os.makedirs(self.sysfs + "cnc")
|
||||
self._attr("cnc/interlock_circuit", "45") # latch locked (bit 3)
|
||||
self._pos(0, 0, 3)
|
||||
os.environ["GF_SYSFS_ROOT"] = self.sysfs
|
||||
self.fc = helpers.FakeForgectrl().start()
|
||||
self.saved = (cloud.GFCLOUD_LOG, cloud.QUIET_S, cloud.QUIET_TIMEOUT_S, cloud.HUNT_TIMEOUT_S)
|
||||
cloud.GFCLOUD_LOG = self.log
|
||||
cloud.QUIET_S = 0.4
|
||||
cloud.QUIET_TIMEOUT_S = 3
|
||||
cloud.HUNT_TIMEOUT_S = 8
|
||||
self.script = None
|
||||
|
||||
def tearDown(self):
|
||||
if self.script:
|
||||
self.script.stop = True
|
||||
self.fc.stop()
|
||||
cloud.GFCLOUD_LOG, cloud.QUIET_S, cloud.QUIET_TIMEOUT_S, cloud.HUNT_TIMEOUT_S = self.saved
|
||||
os.environ.pop("GF_SYSFS_ROOT", None)
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
# -- fakes -----------------------------------------------------------
|
||||
def _attr(self, attr, val):
|
||||
with open(self.sysfs + attr, "w") as f:
|
||||
f.write(val)
|
||||
|
||||
def _pos(self, x, y, z):
|
||||
with open(self.sysfs + "cnc/position", "wb") as f:
|
||||
f.write(struct.pack("<3i2I", x, y, z, 0, 0))
|
||||
|
||||
def append(self, lines, delay=0.0):
|
||||
def go():
|
||||
if delay:
|
||||
time.sleep(delay)
|
||||
with open(self.log, "ab") as f:
|
||||
f.write(("\n".join(lines) + "\n").encode())
|
||||
if delay:
|
||||
threading.Thread(target=go, daemon=True).start()
|
||||
else:
|
||||
go()
|
||||
|
||||
def in_cloud(self, pid=2278):
|
||||
self.fc.state["mode"] = {"mode": "cloud", "controller": "running", "pid": pid, "motion": "verified"}
|
||||
self.fc.state["settings"]["controller_mode"] = "cloud"
|
||||
|
||||
def lid(self, closed):
|
||||
self.fc.state["status"]["switches"]["lid"] = bool(closed)
|
||||
|
||||
def run_test(self, fn, hooks=None, test_id="cloud.x"):
|
||||
run = Run("test", test_id, test_id)
|
||||
run.baseline_captured = {"mode": self.fc.state["mode"]["mode"], "position": [0, 0, 0]}
|
||||
ctx = Context(run, None, helpers.make_test(test_id, []))
|
||||
self.script = Script(run, hooks).start()
|
||||
try:
|
||||
fn(ctx)
|
||||
finally:
|
||||
self.script.stop = True
|
||||
return run
|
||||
|
||||
def assertFails(self, fn, needle, hooks=None):
|
||||
with self.assertRaises(Failed) as cm:
|
||||
self.run_test(fn, hooks)
|
||||
self.assertIn(needle, str(cm.exception))
|
||||
return cm.exception
|
||||
|
||||
# -- session detection -------------------------------------------------
|
||||
def test_session_live_is_per_client(self):
|
||||
lines = fixture("huntlid")
|
||||
# the old client (1927) closed and left; the new one (2278) is ready
|
||||
self.append(lines)
|
||||
self.assertTrue(cloud.session_live(2278)[0])
|
||||
self.assertFalse(cloud.session_live(1927)[0])
|
||||
# an unknown pid falls back to the newest lines (ready)
|
||||
self.assertTrue(cloud.session_live(9999)[0])
|
||||
# a drop after the ready line: not live until it is ready again
|
||||
self.append(["2026-08-17T10:00:00.000000+00:00 gfcloud[2278] INFO websocket:_on_close RX-EVENT: closed (1006, )",
|
||||
"2026-08-17T10:00:00.100000+00:00 gfcloud[2278] INFO websocket:run RECONNECTING"])
|
||||
live, detail = cloud.session_live(2278)
|
||||
self.assertFalse(live)
|
||||
self.assertIn("RECONNECTING", detail)
|
||||
self.append(["2026-08-17T10:00:06.000000+00:00 gfcloud[2278] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
self.assertTrue(cloud.session_live(2278)[0])
|
||||
|
||||
def test_enter_cloud_reuses_a_live_session_and_never_switches(self):
|
||||
self.in_cloud()
|
||||
self.append(fixture("huntlid"))
|
||||
run = self.run_test(lambda ctx: cloud.enter_cloud(ctx))
|
||||
self.assertTrue(any("reusing it" in l for l in run.lines), run.lines)
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertEqual(run.baseline_captured["mode"], "cloud")
|
||||
|
||||
def test_enter_cloud_refuses_a_dead_session(self):
|
||||
self.in_cloud(pid=1927) # the client that shut down in the excerpt
|
||||
self.append(fixture("huntlid"))
|
||||
self.assertFails(cloud.enter_cloud, "no live service session")
|
||||
|
||||
def test_enter_cloud_switches_once_from_grbl_and_declares_it(self):
|
||||
lines = fixture("huntlid")
|
||||
pre, post = cut(lines, "gfuiservice:__init__ INITIALIZED")
|
||||
self.append(pre)
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/mode":
|
||||
self.append(post, delay=0.2) # the new client's session + hunt + moves
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
run = self.run_test(lambda ctx: cloud.enter_cloud(ctx))
|
||||
self.assertEqual([p for p, _ in self.fc.posts], ["/mode"])
|
||||
self.assertEqual(self.fc.state["mode"]["mode"], "cloud")
|
||||
self.assertEqual(run.baseline_captured["mode"], "cloud") # declared: the baseline keeps it
|
||||
self.assertTrue(any("connect-time hunt" in l and ":completed" in l for l in run.lines), run.lines)
|
||||
|
||||
def test_enter_cloud_waits_for_the_service_to_stop_moving(self):
|
||||
self.in_cloud()
|
||||
self.append(fixture("huntlid"))
|
||||
# a motion in flight (never idle within the timeout) fails, and says so
|
||||
self.fc.state["status"]["state"] = "running"
|
||||
self.assertFails(cloud.enter_cloud, "still running service moves")
|
||||
|
||||
# -- the hunt with the lid open ------------------------------------------
|
||||
def test_hunt_lid_open_restarts_the_client_in_cloud_mode(self):
|
||||
self.in_cloud(pid=1927)
|
||||
lines = fixture("huntlid")
|
||||
pre, post = cut(lines, "gfuiservice:__init__ INITIALIZED")
|
||||
hunt_part, close_part = cut(post, "_switch_event lid closed")
|
||||
self.append(pre)
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/controller/stop":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="standby", pid=0)
|
||||
elif path == "/controller/start":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="running", pid=2278)
|
||||
self.append(hunt_part, delay=0.2)
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
|
||||
def lid_closed():
|
||||
self.lid(True)
|
||||
self.append(close_part, delay=0.2)
|
||||
run = self.run_test(cloud.hunt_lid_open,
|
||||
hooks={"Open the lid": lambda: self.lid(False), "Close the lid": lid_closed},
|
||||
test_id="cloud.hunt-lid-open")
|
||||
self.assertEqual([p for p, _ in self.fc.posts], ["/controller/stop", "/controller/start"])
|
||||
ev = run.evidence
|
||||
self.assertIn(":completed", ev["hunt_line"])
|
||||
self.assertEqual(ev["refusals_before_hunt_end"], 0)
|
||||
self.assertEqual(ev["motions_after_lid_close"], 3)
|
||||
self.assertEqual(self.fc.state["mode"]["mode"], "cloud") # still cloud
|
||||
self.assertTrue(any("PASS:" in l for l in run.lines))
|
||||
# the prompts, in order: open, confirm lens, close
|
||||
self.assertEqual(len(self.script.asked), 3)
|
||||
|
||||
def test_hunt_lid_open_from_grbl_switches_and_stays(self):
|
||||
lines = fixture("huntlid")
|
||||
pre, post = cut(lines, "gfuiservice:__init__ INITIALIZED")
|
||||
hunt_part, close_part = cut(post, "_switch_event lid closed")
|
||||
self.append(pre)
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/mode":
|
||||
self.append(hunt_part, delay=0.2)
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
run = self.run_test(cloud.hunt_lid_open,
|
||||
hooks={"Open the lid": lambda: self.lid(False),
|
||||
"Close the lid": lambda: (self.lid(True), self.append(close_part, delay=0.2))})
|
||||
self.assertEqual([p for p, _ in self.fc.posts], ["/mode"])
|
||||
self.assertEqual(self.fc.state["mode"]["mode"], "cloud")
|
||||
self.assertEqual(run.baseline_captured["mode"], "cloud")
|
||||
self.assertIn(":completed", run.evidence["hunt_line"])
|
||||
|
||||
def test_hunt_lid_open_needs_the_lid_open(self):
|
||||
self.in_cloud()
|
||||
self.append(fixture("huntlid"))
|
||||
self.assertFails(cloud.hunt_lid_open, "lid reads closed")
|
||||
|
||||
def test_hunt_refused_for_the_lid_fails(self):
|
||||
self.in_cloud(pid=1927)
|
||||
lines = fixture("huntlid")
|
||||
pre, post = cut(lines, "gfuiservice:__init__ INITIALIZED")
|
||||
# a refusal ahead of the hunt's end (the lid gating the hunt would look like this)
|
||||
i = next(i for i, l in enumerate(post) if "z_axis:home starting z homing cycle" in l)
|
||||
post = post[:i] + ["2026-08-17T09:45:10.595000+00:00 gfcloud[2278] INFO machine:_safe_to_move lid opened, unsafe to move"] + post[i:]
|
||||
self.append(pre)
|
||||
|
||||
def on_post(path, form):
|
||||
if path == "/controller/stop":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="standby", pid=0)
|
||||
elif path == "/controller/start":
|
||||
self.fc.state["mode"] = dict(self.fc.state["mode"], controller="running", pid=2278)
|
||||
self.append(post, delay=0.1)
|
||||
return None
|
||||
self.fc.on_post = on_post
|
||||
self.assertFails(cloud.hunt_lid_open, "refused for the lid", hooks={"Open the lid": lambda: self.lid(False)})
|
||||
|
||||
# -- pause / resume ---------------------------------------------------------
|
||||
def replay_print(self, name, at_run, at_end, tail_delay=0.3):
|
||||
"""Split a print excerpt into: up to the run's RUNNING line
|
||||
(lands when the operator says Print is done), the block landing at
|
||||
the mid-run prompt, and the rest."""
|
||||
lines = fixture(name)
|
||||
pre, rest = cut(lines, "waiting for button")
|
||||
run_pre, rest = cut(rest, "current state: MachineState.RUNNING") # the PRINT's run
|
||||
pre, rest = pre + run_pre + [rest[0]], rest[1:]
|
||||
mid, tail = cut(rest, at_end)
|
||||
return {"Click Done here": lambda: self.append(pre, delay=0.1),
|
||||
at_run: lambda: (self.append(mid, delay=0.05), self.append(tail, delay=tail_delay))}
|
||||
|
||||
def test_pause_resume_passes_on_the_machines_lines(self):
|
||||
self.in_cloud(pid=1522)
|
||||
self.append(["2026-08-17T09:41:00.000000+00:00 gfcloud[1522] INFO authentication:authenticate_machine SUCCESS",
|
||||
"2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready",
|
||||
"2026-08-17T09:41:00.900000+00:00 gfcloud[1522] INFO websocket:ws_connect ESTABLISHED"])
|
||||
hooks = self.replay_print("pause", "Press the button once NOW", "current state: MachineState.IDLE")
|
||||
self.fc.state["cool"]["armed"] = True
|
||||
run = self.run_test(cloud.pause_resume, hooks=hooks, test_id="cloud.pause-resume")
|
||||
ev = run.evidence
|
||||
self.assertEqual(ev["log"], {"button pressed mid-run; pausing": True, "paused at": True,
|
||||
"button pressed while paused; resuming": True})
|
||||
self.assertTrue(ev["armed_after_resume"])
|
||||
self.assertIn(":completed", ev["log_end"]["print finished"])
|
||||
self.assertTrue(ev["log_end"]["return home complete"])
|
||||
self.assertEqual(ev["relock_or_cancel_lines"], 0)
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertTrue(any("PASS: button pause/resume" in l for l in run.lines))
|
||||
# the post-print hunt was waited out
|
||||
self.assertTrue(any("machine is quiet" in l for l in run.lines))
|
||||
|
||||
def test_pause_resume_fails_when_the_print_is_cancelled_instead(self):
|
||||
self.in_cloud(pid=1522)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
lines = fixture("pause")
|
||||
lines = [l.replace('print [1576550507]: finished with event ":completed"',
|
||||
'print [1576550507]: finished with event ":cancelled"') for l in lines]
|
||||
pre, rest = cut(lines, "current state: MachineState.RUNNING")
|
||||
pre, rest = pre + [rest[0]], rest[1:]
|
||||
hooks = {"Click Done here": lambda: self.append(pre, delay=0.1),
|
||||
"Press the button once NOW": lambda: self.append(rest, delay=0.05)}
|
||||
self.assertFails(cloud.pause_resume, "did not complete after the resume", hooks=hooks)
|
||||
|
||||
def test_pause_resume_fails_without_the_pause_line(self):
|
||||
self.in_cloud(pid=1522)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
lines = [l for l in fixture("pause") if "button pressed mid-run" not in l]
|
||||
pre, rest = cut(lines, "current state: MachineState.RUNNING")
|
||||
pre, rest = pre + [rest[0]], rest[1:]
|
||||
# the wait for the pause lines is 90 s: shorten it through the module's poll by
|
||||
# ending the log early - the finish line arrives, but the pause never does
|
||||
saved = cloud.wait_log
|
||||
|
||||
def fast_wait_log(ctx, offset, needles, timeout, poll=0.5):
|
||||
return saved(ctx, offset, needles, min(timeout, 1.5), poll=0.1)
|
||||
cloud.wait_log = fast_wait_log
|
||||
try:
|
||||
hooks = {"Click Done here": lambda: self.append(pre, delay=0.1),
|
||||
"Press the button once NOW": lambda: self.append(rest, delay=0.05)}
|
||||
self.assertFails(cloud.pause_resume, "did not pause the run", hooks=hooks)
|
||||
finally:
|
||||
cloud.wait_log = saved
|
||||
|
||||
# -- the two lid tests, on their bench excerpts --------------------------------
|
||||
def test_lid_abort_on_the_bench_excerpt(self):
|
||||
self.in_cloud(pid=1522)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1522] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
hooks = self.replay_print("lidabort", "Open the lid NOW", "start cool down")
|
||||
run = self.run_test(cloud.lid_abort, hooks=hooks, test_id="cloud.lid-abort")
|
||||
ev = run.evidence
|
||||
self.assertLess(ev["edge_to_stop_ms"], 60)
|
||||
self.assertIn(":cancelled", ev["log"]["print finished"])
|
||||
self.assertEqual(ev["kernel_counters_after_park"], [0, 0, 3])
|
||||
self.assertTrue(ev["latch_locked"])
|
||||
self.assertFalse(ev["armed_after"])
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertTrue(any("PASS: lid open" in l for l in run.lines))
|
||||
|
||||
def test_lid_during_button_wait_on_the_bench_excerpt(self):
|
||||
self.in_cloud(pid=1927)
|
||||
self.append(["2026-08-17T09:41:00.500000+00:00 gfcloud[1927] INFO websocket:_on_open RX-EVENT: ready"])
|
||||
lines = fixture("buttonwait")
|
||||
pre, rest = cut(lines, "waiting for button")
|
||||
pre, rest = pre + [rest[0]], rest[1:]
|
||||
hooks = {"Click Done here": lambda: self.append(pre, delay=0.1),
|
||||
"Open the lid now": lambda: self.append(rest, delay=0.05)}
|
||||
run = self.run_test(cloud.lid_during_button_wait, hooks=hooks, test_id="cloud.lid-during-button-wait")
|
||||
ev = run.evidence
|
||||
self.assertEqual(ev["runs_started_after_wait"], 0)
|
||||
self.assertIn(":cancelled", ev["log"]["print finished"])
|
||||
self.assertTrue(ev["latch_locked"])
|
||||
self.assertEqual(self.fc.posts, [])
|
||||
self.assertTrue(any("PASS: lid open at the button prompt" in l for l in run.lines))
|
||||
|
||||
def test_print_finish_is_the_prints_not_another_actions(self):
|
||||
# a motion that completes before the print must not satisfy the print's finish
|
||||
lines = fixture("lidabort")
|
||||
i = cloud.action_finish_index(lines, "print")
|
||||
j = cloud.action_finish_index(lines, "motion")
|
||||
self.assertIsNotNone(i)
|
||||
self.assertIsNotNone(j)
|
||||
self.assertLess(j, i)
|
||||
self.assertIn(":cancelled", lines[i])
|
||||
self.assertIn(":completed", lines[j])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -181,6 +181,18 @@ class ServerTests(unittest.TestCase):
|
||||
st, d = self.call("POST", "/start", {"test": "fake.needs"})
|
||||
self.assertEqual(st, 409)
|
||||
self.assertIn("prerequisites", d["message"])
|
||||
# the operator's override: the test runs alone and the record says so
|
||||
st, d = self.call("POST", "/start", {"test": "fake.needs", "ignore_requires": True})
|
||||
self.assertEqual(st, 200)
|
||||
state = self.wait_idle()
|
||||
self.assertEqual(state["tests"]["fake.needs"]["status"], "pass")
|
||||
st, rec = self.call("GET", "/result?test=fake.needs")
|
||||
self.assertEqual(rec["evidence"]["prerequisites"]["missing"], ["fake.prompt"])
|
||||
self.assertTrue(rec["evidence"]["prerequisites"]["overridden"])
|
||||
self.assertTrue(any("prerequisites overridden" in l for l in rec["log"]))
|
||||
# its prerequisite is still required for the release
|
||||
self.assertFalse(state["tests"]["fake.prompt"]["satisfied"])
|
||||
self.assertFalse(state["authorized"])
|
||||
st, d = self.call("POST", "/start", {"test": "fake.live"})
|
||||
self.assertEqual(st, 409)
|
||||
self.assertIn("live", d["message"])
|
||||
|
||||
Reference in New Issue
Block a user