mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-16 01:29:55 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0d4302d73 | ||
|
|
884d014d2c | ||
|
|
a9f0ad0d51 | ||
|
|
2dde10cf43 | ||
|
|
f41464cc58 | ||
|
|
20c6256527 | ||
|
|
b7a595ced7 | ||
|
|
5c93e49c8c | ||
|
|
650483c085 | ||
|
|
7b8f72b632 |
@@ -247,8 +247,11 @@ TOOLS = [
|
||||
{"id": "z-envelope-test", "title": "Z envelope harness", "script": "z_envelope_test.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "The Z soft limit belongs to the driver, not to $20: an unreferenced Z is collapsed to where the "
|
||||
"lens stands, and neither a $20 nor a $132 write frees it. A CI harness (the grblHAL repo): needs "
|
||||
"the host-built null-sink controller, not the machine, so it is not a bench-page tool."},
|
||||
"lens stands, and neither a $20 nor a $132 write frees it. A referenced lens (forgectrl's marker "
|
||||
"and the lens settings) opens it to the window the settings hold, the fallback or the focus "
|
||||
"card's stops: the ends of the reach run, two half-steps past either end alarms. A CI harness "
|
||||
"(the grblHAL repo): needs the host-built null-sink controller, not the machine, so it is not a "
|
||||
"bench-page tool."},
|
||||
{"id": "raster-dry", "title": "Dry top-speed raster per XY microstep mode", "script": "raster_dry.py",
|
||||
"safety": "dry", "where": "board", "ported": True,
|
||||
"args": [_arg("modes", "choice", "8 16 32", "the modes to run, in order", ["8", "16", "32", "8 16 32"])],
|
||||
|
||||
@@ -21,7 +21,7 @@ from ..catalog import test
|
||||
from .commission_dark import run_check, Restore
|
||||
|
||||
SHEET_COVERS = [("forgectrl", "src/wizlive.*"), ("forgectrl", "src/wizrun.h"),
|
||||
("forgectrl", "src/sheet.*"),
|
||||
("forgectrl", "src/lens.*"), ("forgectrl", "src/sheet.*"),
|
||||
("forgectrl", "src/font_hershey.*"), ("forgectrl", "src/jobstream.*"),
|
||||
("forgectrl", "src/curverec.*"), ("forgectrl", "src/wizdark.*"),
|
||||
("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"),
|
||||
@@ -71,6 +71,22 @@ def preview_ok(ctx, wid):
|
||||
ctx.check(st == 200 and re.search(rb"^M[34] S\d", body or b"", re.M) is not None,
|
||||
"no program for %s (%s)", wid, st)
|
||||
ctx.evidence["program_lines"] = body.count(b"\n")
|
||||
# Every Z the program commands is a height the head reaches in the
|
||||
# window the machine holds now: the focus card's own numbers once it
|
||||
# has them, and the conservative fallback on a head whose travel is
|
||||
# not known yet. The controller refuses a Z outside it with ALARM:2,
|
||||
# and it refuses it while the body is still burning, so the card dies
|
||||
# where the parser is and not where the bad line is.
|
||||
lens = (fc.status() or {}).get("lens") or {}
|
||||
lo, hi = lens.get("reach_min"), lens.get("reach_max")
|
||||
zs = [float(m) for m in re.findall(rb"^G0 Z(-?[0-9.]+)", body or b"", re.M)]
|
||||
ctx.evidence.setdefault("program_z", {})[wid] = {
|
||||
"z": zs, "reach": [lo, hi], "stops_found": lens.get("stops_found")}
|
||||
ctx.check(lo is not None and hi is not None, "/status carries no lens reach: %s", lens)
|
||||
if lo is not None and hi is not None:
|
||||
out = [z for z in zs if z < lo - 0.01 or z > hi + 0.01]
|
||||
ctx.check(not out, "%s commands Z outside the lens window %.2f..%.2f: %s",
|
||||
wid, lo, hi, out)
|
||||
|
||||
|
||||
def emission_ok(ctx, last):
|
||||
@@ -89,6 +105,61 @@ def file_card(ctx, wid):
|
||||
ev.setdefault("cards", {})[wid] = {k: ev.pop(k) for k in CARD_EVIDENCE if k in ev}
|
||||
|
||||
|
||||
# The lens settings: the head's numbers, which a fresh machine does not
|
||||
# have. Cleared before the cards so the run is a fresh machine's.
|
||||
LENS_SETTINGS = CARD_SETTINGS[:3]
|
||||
FALLBACK_WINDOW = (10, 12)
|
||||
|
||||
|
||||
def fresh_lens(ctx):
|
||||
"""The lens as a fresh machine has it: the head's numbers unknown, so
|
||||
the frame runs in the fallback window and the focus card has to find
|
||||
the stops and write them before its ladder burns. The machine's own
|
||||
numbers come back with the restore at the end of the run."""
|
||||
fc = ctx.forgectrl
|
||||
for k in LENS_SETTINGS:
|
||||
st, _ = fc.post("/settings", params={k: ""})
|
||||
ctx.check(st == 200, "clearing %s -> %s", k, st)
|
||||
lens = (fc.status() or {}).get("lens") or {}
|
||||
ctx.evidence["lens_fresh"] = lens
|
||||
ctx.check(lens.get("stops_found") is False and (lens.get("below"), lens.get("above")) == FALLBACK_WINDOW,
|
||||
"the lens window is not the fallback after the clear: %s", lens)
|
||||
|
||||
|
||||
def focus_window_ok(ctx):
|
||||
"""The window the focus card burned its ladder in is the one it wrote
|
||||
before its controller started, and the one the machine holds now: the
|
||||
result's window is the stops it found (or the fallback), the settings
|
||||
hold that window, every ladder height lies in its reach, and the
|
||||
program served now agrees with /status."""
|
||||
fc = ctx.forgectrl
|
||||
r = ((ctx.evidence.get("cards") or {}).get("laser.focus") or {}).get("result") or {}
|
||||
w, stops = r.get("window") or {}, r.get("stops") or {}
|
||||
s = fc.settings() or {}
|
||||
held = (str(s.get("lens_stop_below_steps")), str(s.get("lens_stop_above_steps")))
|
||||
ctx.check(held == (str(w.get("below")), str(w.get("above"))),
|
||||
"the settings hold the window %s, the ladder ran in %s/%s", held, w.get("below"), w.get("above"))
|
||||
if stops.get("found"):
|
||||
ctx.check((w.get("below"), w.get("above")) == (stops.get("below"), stops.get("above")),
|
||||
"the ladder's window %s is not the stops found %s", w, stops)
|
||||
else:
|
||||
ctx.check((w.get("below"), w.get("above")) == FALLBACK_WINDOW,
|
||||
"stops not found (%s), but the ladder's window is %s", stops.get("why"), w)
|
||||
zs = r.get("ladder_z") or []
|
||||
lo, hi = w.get("reach_min"), w.get("reach_max")
|
||||
out = [z for z in zs if lo is None or hi is None or z < lo - 0.01 or z > hi + 0.01]
|
||||
ctx.check(len(zs) == 12 and not out, "ladder heights outside the reach %s..%s: %s", lo, hi, out or zs)
|
||||
ev = ctx.evidence
|
||||
before = dict(ev.get("program_z") or {}).get("laser.focus")
|
||||
preview_ok(ctx, "laser.focus")
|
||||
ev["focus_window"] = {"window": w, "stops": stops, "ladder_z": zs,
|
||||
"settings": {k: s.get(k) for k in LENS_SETTINGS},
|
||||
"program_z_before": before,
|
||||
"program_z_after": (ev.get("program_z") or {}).get("laser.focus"),
|
||||
"preview_bytes": ev.pop("preview_bytes", None),
|
||||
"program_lines": ev.pop("program_lines", None)}
|
||||
|
||||
|
||||
def thickness_answer(ctx):
|
||||
"""The thickness the placement is answered with, in the machine's
|
||||
units (the number prompts read ui_units), and the millimeters the
|
||||
@@ -167,15 +238,22 @@ def burn(ctx, wid, answers, want):
|
||||
"Nothing else: the test answers every prompt itself and puts back every setting the "
|
||||
"cards write. It runs about half an hour, mostly the coolant settle and the dark "
|
||||
"tails."],
|
||||
description="One run over the sheet's seven wizards on one piece of wood. "
|
||||
description="One run over the sheet's seven wizards on one piece of wood, as a fresh "
|
||||
"machine runs them: the three lens settings are cleared first, so the frame "
|
||||
"runs in the fallback window and the focus card has to find the stops and write "
|
||||
"them before its ladder burns. "
|
||||
"POST /wiz/sheet.place/start references the lens, takes the controller in "
|
||||
"loopback posture, jogs +X 10 and -X 10, and sets the origin at the head "
|
||||
"(Full sheet; the thickness answered in the machine's units, 3.2 mm or "
|
||||
"0.125 in). Then, each after the press and each with its preview "
|
||||
"and program served by GET /wiz/sheet.svg and /wiz/sheet.gcode: the frame "
|
||||
"and program served by GET /wiz/sheet.svg and /wiz/sheet.gcode, every Z the "
|
||||
"program commands inside the reach /status reports: the frame "
|
||||
"(180 x 130 mm) and the header band at the mark dose; the focus card (the "
|
||||
"lens homed on its bottom stop, the hall edge counted, twelve lines down the "
|
||||
"travel; the pick is line 11, the thickness kept); the floor card (twelve "
|
||||
"lens on its hall edge, the stops found by the head accelerometer and written "
|
||||
"as the window before the controller starts, twelve lines from the top of the "
|
||||
"window to the bottom; the pick is line 11, the thickness kept; after it the "
|
||||
"settings, the result's window, and the served program must hold the window "
|
||||
"the ladder ran in); the floor card (twelve "
|
||||
"rungs from 2 to 24 percent with the floor and the curve off for the job; the "
|
||||
"pick is 8, the floor written is 10); the dose card (seven rungs sampled at "
|
||||
"25 Hz, the curve fit on 20 s of dark); the corner card (five gammas reloaded "
|
||||
@@ -191,6 +269,9 @@ def sheet(ctx):
|
||||
"each. The bench presses when the button lights.")
|
||||
place(ctx)
|
||||
with Restore(ctx, CARD_SETTINGS):
|
||||
fresh_lens(ctx)
|
||||
for wid, answers, want in CARDS:
|
||||
burn(ctx, wid, answers, want)
|
||||
ctx.log("PASS: the sheet's seven wizards ran on one piece; settings restored")
|
||||
if wid == "laser.focus":
|
||||
focus_window_ok(ctx)
|
||||
ctx.log("PASS: the sheet's seven wizards ran on one piece as a fresh machine's; settings restored")
|
||||
|
||||
@@ -1329,3 +1329,64 @@ def flow_under_load(ctx):
|
||||
ctx.check(dt is not None, "the window did not close after M2")
|
||||
ctx.log("PASS: lit through the window, raw rise %.1f C, laser %.1f C off, judged %.1f C against %.1f "
|
||||
"(dT %.1f), window closed %.1f s after M2", raw, share, rise, float(limit), float(dtc), dt)
|
||||
|
||||
|
||||
@test("cooling.fan-duty-readback", title="A fan duty a device lost is read back and put back inside a tick",
|
||||
subsystem="cooling", kind="auto", mode="grbl", est_min=2,
|
||||
covers=_COOL_COVERS + [("kernel-module-glowforge", "src/head*")],
|
||||
requires=["kernel.latch-locked-idle"],
|
||||
description="M8 opens a run session and every fan reads its run duty back from its device. "
|
||||
"The head's air-assist register is then written back to the idle duty behind the "
|
||||
"engine's back (what a head reset or a dropped transaction leaves) and the exhaust "
|
||||
"PWM to zero: within a few ticks each reads its run duty again, the log names each "
|
||||
"loss and its put-back, and the session stays OK with no airflow fault (a fan put "
|
||||
"back gets its spin-up grace). Then M9 and the idle duties.")
|
||||
def fan_duty_readback(ctx):
|
||||
fc = ctx.forgectrl
|
||||
ev = ctx.evidence
|
||||
run = {"head/air_assist_pwm": 1023, "thermal/exhaust_pwm": 65535, "thermal/intake_pwm": 43278}
|
||||
with ctx.grbl() as g:
|
||||
c = _hold_session(ctx, g, fc, lambda c: c.get("phase") == "run", "readback", 20)
|
||||
try:
|
||||
ctx.check(c.get("phase") == "run", "no run session opened: %s", c)
|
||||
ctx.sleep(2)
|
||||
before = {k: hw.sysfs_int(k) for k in run}
|
||||
ctx.log("duties at run: %s", before)
|
||||
ev["run_duties"] = before
|
||||
ctx.check(all(before.get(k) == v for k, v in run.items()),
|
||||
"not every fan reads its run duty: %s", before)
|
||||
hw.sysfs_write("head/air_assist_pwm", 204)
|
||||
hw.sysfs_write("thermal/exhaust_pwm", 0)
|
||||
lost = {k: hw.sysfs_int(k) for k in ("head/air_assist_pwm", "thermal/exhaust_pwm")}
|
||||
ctx.log("written behind the engine's back: %s", lost)
|
||||
back = {}
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < 6:
|
||||
ctx.sleep(0.5)
|
||||
back = {k: hw.sysfs_int(k) for k in ("head/air_assist_pwm", "thermal/exhaust_pwm")}
|
||||
if back.get("head/air_assist_pwm") == 1023 and back.get("thermal/exhaust_pwm") == 65535:
|
||||
break
|
||||
took = round(time.time() - t0, 1)
|
||||
ev["put_back"] = {"duties": back, "after_s": took}
|
||||
ctx.log("put back after %s s: %s", took, back)
|
||||
ctx.check(back.get("head/air_assist_pwm") == 1023, "the air-assist duty was not put back: %s", back)
|
||||
ctx.check(back.get("thermal/exhaust_pwm") == 65535, "the exhaust duty was not put back: %s", back)
|
||||
ctx.sleep(2)
|
||||
ctx.check(_tail_has(fc, "head/air_assist_pwm read 204 with 1023 commanded: put back"),
|
||||
"the air-assist loss is not named in the log")
|
||||
ctx.check(_tail_has(fc, "thermal/exhaust_pwm read 0 with 65535 commanded: put back"),
|
||||
"the exhaust loss is not named in the log")
|
||||
for _ in range(8):
|
||||
ctx.sleep(1)
|
||||
c = _cool(fc)
|
||||
if c.get("verdict") != "OK":
|
||||
break
|
||||
ev["verdict_after"] = {"verdict": c.get("verdict"), "hold": c.get("hold"), "reason": c.get("reason")}
|
||||
ctx.check(c.get("verdict") == "OK" and not c.get("hold"),
|
||||
"the session did not stay OK after the put-back: %s %s", c.get("verdict"), c.get("reason"))
|
||||
finally:
|
||||
g.command("M9")
|
||||
_session_ended(ctx, fc, "readback")
|
||||
idle = {k: hw.sysfs_int(k) for k in run}
|
||||
ev["idle_duties"] = idle
|
||||
ctx.log("duties at idle: %s", idle)
|
||||
|
||||
@@ -56,6 +56,7 @@ def auth(ctx):
|
||||
("/mode", {"controller": "grbl"}), ("/settings", {"ui_units": "mm"}),
|
||||
("/diag/flow-verify", None), ("/diag/abort", None),
|
||||
("/update/apply", None), ("/boot", {"target": "a"}),
|
||||
("/update/check", None), ("/update/dismiss", {"version": "v0.0.0"}),
|
||||
("/system/reboot", None), ("/restore/factory", None),
|
||||
("/restore/factory-return", {"confirm": "1"}), ("/system/ssh", {"enable": "1"}),
|
||||
("/wiz/advisories/accept", None), ("/wiz/account", None),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""update.* - the A/B slot inventory and the firmware verification path."""
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
import shutil
|
||||
@@ -8,6 +9,7 @@ from ..catalog import test
|
||||
from .. import hw
|
||||
|
||||
_UPDATE_COVERS = [("forgectrl", "src/update.c"), ("forgectrl", "src/update.h"),
|
||||
("forgectrl", "src/relcheck.c"), ("forgectrl", "src/relcheck.h"),
|
||||
("ffboot", "**")]
|
||||
|
||||
|
||||
@@ -117,3 +119,76 @@ def slots_and_signature(ctx):
|
||||
except OSError:
|
||||
pass
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
|
||||
|
||||
_VERSION_RX = re.compile(r"^v?\d+\.\d+\.\d+")
|
||||
|
||||
|
||||
@test("update.release-check", title="Release check answers from the releases API; the alert dismissal is per release",
|
||||
subsystem="update", kind="auto", est_min=1,
|
||||
covers=_UPDATE_COVERS + [("forgectrl", "src/ui/panel.js"), ("forgectrl", "src/ui/index.html")],
|
||||
requires=["forgectrl.auth"],
|
||||
description="GET /update/release answers the kept answer of the daily check (available, version, "
|
||||
"current, new, checked, dismissed). POST /update/check asks the releases API now and "
|
||||
"answers the same shape; a machine with no route to the API answers 502, which the drill "
|
||||
"records and steps over. A published release is a v<semver> tag with the firmware "
|
||||
"file's size and its notes, and `new` follows the version order: a development build "
|
||||
"sees every release as newer. POST /update/dismiss marks that version dismissed, an "
|
||||
"empty version undoes it, and a version that is not a tag is refused. The dismissal "
|
||||
"the machine had before the drill is put back.")
|
||||
def release_check(ctx):
|
||||
fc = ctx.forgectrl
|
||||
ev = ctx.evidence
|
||||
keys = ("available", "version", "current", "new", "checked", "dismissed", "detail", "bytes")
|
||||
|
||||
st, before = fc.get("/update/release")
|
||||
ctx.log("GET /update/release -> %s %s", st, {k: before.get(k) for k in keys} if isinstance(before, dict) else before)
|
||||
ctx.check(st == 200 and isinstance(before, dict) and "available" in before and "checked" in before,
|
||||
"GET /update/release -> %s", st)
|
||||
ev["before"] = {k: before.get(k) for k in keys}
|
||||
prior = before.get("version", "") if before.get("dismissed") else ""
|
||||
|
||||
st, now = fc.post("/update/check")
|
||||
ctx.log("POST /update/check -> %s %s", st, {k: now.get(k) for k in keys} if isinstance(now, dict) else now)
|
||||
if st == 502:
|
||||
ctx.log("the machine has no route to the releases API; the kept answer stands")
|
||||
now = before
|
||||
else:
|
||||
ctx.check(st == 200 and isinstance(now, dict) and "available" in now, "POST /update/check -> %s", st)
|
||||
ctx.check(isinstance(now.get("checked"), int) and now["checked"] >= before.get("checked", 0),
|
||||
"the check did not move `checked`: %s", now.get("checked"))
|
||||
ev["after_check"] = {k: now.get(k) for k in keys}
|
||||
|
||||
if now.get("available"):
|
||||
ctx.check(_VERSION_RX.match(str(now.get("version", ""))) is not None,
|
||||
"the release version is not a v<semver> tag: %r", now.get("version"))
|
||||
ctx.check(isinstance(now.get("bytes"), int) and now["bytes"] > 0,
|
||||
"the firmware file's size is missing: %r", now.get("bytes"))
|
||||
ctx.check(isinstance(now.get("notes"), str), "the notes are not a string")
|
||||
ctx.check(isinstance(now.get("new"), bool), "`new` is not a bool")
|
||||
if not _VERSION_RX.match(str(now.get("current", ""))):
|
||||
ctx.check(now.get("new") is True,
|
||||
"a development build (%r) must see release %s as newer", now.get("current"), now["version"])
|
||||
else:
|
||||
ctx.log("no release available: %s", now.get("detail"))
|
||||
ctx.check(isinstance(now.get("detail"), str) and now["detail"], "an unavailable release names no reason")
|
||||
|
||||
try:
|
||||
st, body = fc.post("/update/dismiss", params={"version": "v0.0.1; rm -rf /"})
|
||||
ctx.log("POST /update/dismiss version=<not a tag> -> %s %s", st, body)
|
||||
ctx.check(st == 400, "a version that is not a tag was not refused: %s", st)
|
||||
|
||||
tag = now["version"] if now.get("available") else "v0.0.0"
|
||||
st, body = fc.post("/update/dismiss", params={"version": tag})
|
||||
ctx.log("POST /update/dismiss version=%s -> %s %s", tag, st,
|
||||
{k: body.get(k) for k in keys} if isinstance(body, dict) else body)
|
||||
ctx.check(st == 200 and isinstance(body, dict), "POST /update/dismiss -> %s", st)
|
||||
if now.get("available"):
|
||||
ctx.check(body.get("dismissed") is True, "the dismissal of %s was not recorded: %r", tag, body.get("dismissed"))
|
||||
st, body = fc.post("/update/dismiss", params={"version": ""})
|
||||
ctx.log("POST /update/dismiss version= -> %s", st)
|
||||
ctx.check(st == 200 and isinstance(body, dict) and body.get("dismissed") is False,
|
||||
"the empty version did not undo the dismissal: %s %r", st, body.get("dismissed") if isinstance(body, dict) else body)
|
||||
finally:
|
||||
st, body = fc.post("/update/dismiss", params={"version": prior})
|
||||
ctx.log("dismissal put back to %r -> %s", prior, st)
|
||||
|
||||
@@ -206,6 +206,100 @@
|
||||
border-left: 4px solid var(--og-red);
|
||||
}
|
||||
|
||||
/* ---- The release dialog -------------------------------------------- */
|
||||
#rel-view h3 {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--og-dim);
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
.relnotes {
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--og-line);
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.relnotes h1,
|
||||
.relnotes h2,
|
||||
.relnotes h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 10px 0 4px;
|
||||
}
|
||||
.relnotes h1:first-child,
|
||||
.relnotes h2:first-child,
|
||||
.relnotes h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.relnotes p,
|
||||
.relnotes ul {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
.relnotes ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.relnotes code {
|
||||
font-size: 12px;
|
||||
}
|
||||
.relnotes table {
|
||||
border-collapse: collapse;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
.relnotes th,
|
||||
.relnotes td {
|
||||
border: 1px solid var(--og-line);
|
||||
padding: 2px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.relwarn {
|
||||
background: var(--og-warn-bg);
|
||||
border: 1px solid var(--og-warn);
|
||||
color: var(--og-warn-txt);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.relsteps {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 22px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.relsteps li {
|
||||
color: var(--og-dim);
|
||||
}
|
||||
.relsteps li.cur {
|
||||
color: var(--og-txt);
|
||||
font-weight: 600;
|
||||
}
|
||||
.relsteps li.done {
|
||||
color: var(--og-ok);
|
||||
}
|
||||
.relsteps li.done::after {
|
||||
content: ' \2713';
|
||||
}
|
||||
.progress {
|
||||
--bs-progress-bg: var(--og-line-soft);
|
||||
--bs-progress-bar-bg: var(--og-action);
|
||||
--bs-progress-height: 10px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
#rel-phase {
|
||||
min-height: 1.4em;
|
||||
font-size: 12.5px;
|
||||
margin: 0;
|
||||
}
|
||||
#rel-err {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
/* ---- Chrome: header, tabs, footer ---------------------------------- */
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
@@ -17,7 +17,44 @@ from forgetest.runner import Context, Run # noqa: E402
|
||||
from forgetest.suite import commission_sheet # noqa: E402
|
||||
|
||||
LIVE = ["sheet.frame", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner", "cooling.flow-load"]
|
||||
# The focus card's program is read again after it ran: the served ladder
|
||||
# must then lie in the window the card wrote.
|
||||
PREVIEWS = ["sheet.frame", "laser.focus", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner",
|
||||
"cooling.flow-load"]
|
||||
EMISSION = {"hv_max": 610, "laser_on_samples": 900, "thermopile_delta": 600, "lit_s": 30.0}
|
||||
SPM = 2.922 # the lens screw, half-steps per mm ($102)
|
||||
EDGE_DEFAULT = 3.35
|
||||
|
||||
|
||||
def lens_block(s):
|
||||
"""The /status lens block as the daemon builds it from the settings: a
|
||||
stop count from 1 to 40 stands, anything else is the fallback."""
|
||||
def count(key, fallback):
|
||||
try:
|
||||
n = float(s.get(key, ""))
|
||||
except ValueError:
|
||||
return fallback, False
|
||||
return (int(n), True) if 1 <= n <= 40 else (fallback, False)
|
||||
try:
|
||||
edge = float(s.get("lens_hall_edge_z_mm", ""))
|
||||
except ValueError:
|
||||
edge = EDGE_DEFAULT
|
||||
below, a = count("lens_stop_below_steps", 10)
|
||||
above, b = count("lens_stop_above_steps", 12)
|
||||
return {"edge_z": round(edge, 2), "below": below, "above": above, "stops_found": a and b,
|
||||
"reach_min": round(edge - below / SPM, 2), "reach_max": round(edge + above / SPM, 2)}
|
||||
|
||||
|
||||
def ladder_z(edge, below, above):
|
||||
"""The focus ladder the daemon builds: twelve whole half-steps from
|
||||
the top of the window to the bottom."""
|
||||
return [round(edge + round(above - (above + below) * i / 11) / SPM, 2) for i in range(12)]
|
||||
|
||||
|
||||
# The window the focus card finds and writes before its controller starts.
|
||||
STOPS = {"lens_stop_below_steps": "14", "lens_stop_above_steps": "20"}
|
||||
FOCUS_WINDOW = {"below": 14, "above": 20, "reach_min": round(EDGE_DEFAULT - 14 / SPM, 2),
|
||||
"reach_max": round(EDGE_DEFAULT + 20 / SPM, 2)}
|
||||
|
||||
# Each wizard as the daemon runs it: its prompts in order (id, kind,
|
||||
# options) and the result it ends with. A "jog" prompt repeats until the
|
||||
@@ -37,7 +74,9 @@ SCRIPT = {
|
||||
"steps_per_mm": 2.922, "max_height_mm": 13.81,
|
||||
"focus_range_mm": {"min": 2.17, "max": 13.81},
|
||||
"stops": {"found": True, "below": 14, "above": 20, "contact_below": 16,
|
||||
"contact_above": 22, "why": ""}, "emission": EMISSION}),
|
||||
"contact_above": 22, "why": ""},
|
||||
"window": dict(FOCUS_WINDOW), "ladder_z": ladder_z(EDGE_DEFAULT, 14, 20),
|
||||
"emission": EMISSION}),
|
||||
"laser.floor": ([("floor-arm", "continue", ["Continue"]),
|
||||
("floor-pick", "choice", ["2", "4", "6", "8", "None"])],
|
||||
{"faintest_density": 8.0, "floor_density": 10.0, "emission": EMISSION}),
|
||||
@@ -50,10 +89,11 @@ SCRIPT = {
|
||||
{"lit_s": 30.0, "dose_raw_s": 12000, "peak_c": 0.72, "k_density": 2.14e-5,
|
||||
"k_cw": 2.78e-5, "emission": EMISSION}),
|
||||
}
|
||||
# What each card writes when it ends.
|
||||
# What a card writes when it starts (the focus card's window, before its
|
||||
# controller starts) and when it ends.
|
||||
START_WRITES = {"laser.focus": dict(STOPS)}
|
||||
WRITES = {
|
||||
"laser.focus": {"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14",
|
||||
"lens_stop_above_steps": "20"},
|
||||
"laser.focus": {"lens_hall_edge_z_mm": "6.96"},
|
||||
"laser.floor": {"laser_floor_density": "10"},
|
||||
"laser.dose-curve": {"laser_dose_curve": "10:0.44,100:100"},
|
||||
"laser.corner": {"laser_corner_gamma": "1.5"},
|
||||
@@ -115,9 +155,11 @@ class ScriptedDaemon:
|
||||
self.versions = {}
|
||||
self.answers = {} # wid -> [values]
|
||||
self.order = [] # the wizards as started
|
||||
self.settings_at_start = {} # wid -> the settings as the wizard found them
|
||||
self.previews = []
|
||||
self.queue = []
|
||||
self.seq = 0
|
||||
self.stray_z = None # a Z the served program commands beyond the reach, when set
|
||||
fake.on_get = self.on_get
|
||||
fake.on_post = self.on_post
|
||||
|
||||
@@ -130,6 +172,10 @@ class ScriptedDaemon:
|
||||
return True
|
||||
|
||||
def on_get(self, path, q):
|
||||
s = self.fake.state["settings"]
|
||||
if path == "/status":
|
||||
# the lens block follows the settings, as the daemon's does
|
||||
return 200, dict(self.fake.state["status"], lens=lens_block(s))
|
||||
if path == "/wiz/dark":
|
||||
return 200, dict(self.dark)
|
||||
if path == "/wiz":
|
||||
@@ -140,8 +186,18 @@ class ScriptedDaemon:
|
||||
return 404, {"error": "no such card"}
|
||||
return 200, b"<svg xmlns='http://www.w3.org/2000/svg'></svg>", "image/svg+xml"
|
||||
if path == "/wiz/sheet.gcode":
|
||||
# the frame and the text are M4 at the mark dose, as the daemon serves them
|
||||
return 200, b"; mock\nM4 S400\nG0 X60 Y42\nG1 X120 Y42 F3000\nM5\n", "text/plain"
|
||||
# the frame and the text are M4 at the mark dose, as the daemon
|
||||
# serves them; the head's Z is the edge, the tail goes back to
|
||||
# it, and the focus card's ladder spans the window the settings
|
||||
# hold now
|
||||
lens = lens_block(s)
|
||||
lines = ["; mock", "G0 Z%.2f" % lens["edge_z"], "M4 S400", "G0 X60 Y42", "G1 X120 Y42 F3000"]
|
||||
if q.get("card") == "laser.focus":
|
||||
lines += ["G0 Z%.2f" % z for z in ladder_z(lens["edge_z"], lens["below"], lens["above"])]
|
||||
if self.stray_z is not None:
|
||||
lines.append("G0 Z%.2f" % self.stray_z)
|
||||
lines += ["M5", "G0 Z%.2f" % lens["edge_z"], "G0 X0 Y0", "M2"]
|
||||
return 200, ("\n".join(lines) + "\n").encode(), "text/plain"
|
||||
return None
|
||||
|
||||
def on_post(self, path, form):
|
||||
@@ -151,6 +207,8 @@ class ScriptedDaemon:
|
||||
if wid not in SCRIPT:
|
||||
return 404, {"error": "no such wizard"}
|
||||
self.order.append(wid)
|
||||
self.settings_at_start[wid] = dict(s)
|
||||
s.update(START_WRITES.get(wid, {}))
|
||||
self.queue = list(SCRIPT[wid][0])
|
||||
self.dark.update({"id": wid, "running": True, "phase": "starting", "result": None, "error": ""})
|
||||
self.open_next()
|
||||
@@ -199,7 +257,7 @@ class SheetRunTests(unittest.TestCase):
|
||||
self.assertEqual(d.answers["laser.focus"], ["Continue", "11", "Keep"])
|
||||
self.assertEqual(d.answers["laser.floor"], ["Continue", "8"])
|
||||
self.assertEqual(d.answers["laser.corner"], ["Continue", "1.50"])
|
||||
self.assertEqual(d.previews, LIVE)
|
||||
self.assertEqual(d.previews, PREVIEWS)
|
||||
cards = run.evidence["cards"]
|
||||
self.assertEqual(sorted(cards), sorted(["sheet.place"] + LIVE))
|
||||
for wid in LIVE:
|
||||
@@ -208,6 +266,24 @@ class SheetRunTests(unittest.TestCase):
|
||||
self.assertNotIn("emission", run.evidence)
|
||||
self.assertEqual(cards["sheet.place"]["result"]["origin_x"], 100)
|
||||
self.assertEqual(run.evidence["settings_before"], FOUND)
|
||||
# A fresh machine's run: the lens settings were gone before the
|
||||
# frame, the frame's program ran in the fallback window, the focus
|
||||
# card's window was in the settings when the floor card started,
|
||||
# and the window check filed what it compared.
|
||||
for k in commission_sheet.LENS_SETTINGS:
|
||||
self.assertNotIn(k, d.settings_at_start["sheet.frame"], k)
|
||||
self.assertEqual(run.evidence["lens_fresh"]["stops_found"], False)
|
||||
self.assertEqual(run.evidence["program_z"]["sheet.frame"]["reach"], [-0.07, 7.46])
|
||||
floor_start = d.settings_at_start["laser.floor"]
|
||||
self.assertEqual({k: floor_start[k] for k in commission_sheet.LENS_SETTINGS},
|
||||
{"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14", "lens_stop_above_steps": "20"})
|
||||
fw = run.evidence["focus_window"]
|
||||
self.assertEqual(fw["window"], FOCUS_WINDOW)
|
||||
self.assertEqual(fw["settings"]["lens_stop_below_steps"], "14")
|
||||
self.assertEqual(len(fw["ladder_z"]), 12)
|
||||
self.assertEqual(fw["program_z_before"]["reach"], [-0.07, 7.46]) # the fallback ladder
|
||||
self.assertEqual(fw["program_z_after"]["reach"], [2.17, 13.8]) # the written window at the new edge
|
||||
self.assertEqual(len(fw["program_z_after"]["z"]), 14) # head, twelve lines, tail
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k) # restored
|
||||
|
||||
@@ -261,6 +337,44 @@ class SheetRunTests(unittest.TestCase):
|
||||
finally:
|
||||
SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], dict(SCRIPT_DARK, emission=EMISSION))
|
||||
|
||||
def test_a_focus_window_the_settings_do_not_hold_fails_the_run(self):
|
||||
# The defect the bench found: the focus card burned its ladder over
|
||||
# the stops it found while the controller's Z limit stood on the
|
||||
# settings, which did not hold them. A daemon whose focus result
|
||||
# names a window the settings do not hold fails the run before the
|
||||
# floor card, and the settings still go back.
|
||||
from forgetest.runner import Failed
|
||||
saved = dict(START_WRITES["laser.focus"])
|
||||
START_WRITES["laser.focus"] = {}
|
||||
try:
|
||||
t = catalog.load_suite()["commission.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
with self.assertRaises(Failed) as cm:
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertIn("the settings hold", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["sheet.place", "sheet.frame", "laser.focus"])
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k)
|
||||
finally:
|
||||
START_WRITES["laser.focus"] = saved
|
||||
|
||||
def test_a_program_z_beyond_the_reach_fails_before_the_burn(self):
|
||||
# A served program that commands a Z the lens does not reach is
|
||||
# refused before the card starts: nothing burns.
|
||||
from forgetest.runner import Failed
|
||||
self.daemon.stray_z = 10.6
|
||||
t = catalog.load_suite()["commission.sheet"]
|
||||
run = Run("test", t.id, t.title)
|
||||
run.unattended = True
|
||||
with self.assertRaises(Failed) as cm:
|
||||
t.fn(Context(run, None, t))
|
||||
self.assertIn("outside the lens window", str(cm.exception))
|
||||
self.assertIn("10.6", str(cm.exception))
|
||||
self.assertEqual(self.daemon.order, ["sheet.place"])
|
||||
for k, v in FOUND.items():
|
||||
self.assertEqual(self.fake.state["settings"][k], v, k)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# only SRCREV and PV here - the image manifest leaves *-pin.inc out of the
|
||||
# layer content hash because the component entry already identifies the
|
||||
# pinned source (forgefirm-image-manifest.bbclass).
|
||||
SRCREV = "a9e7073b2325c8c9d89d490fe6d067abfac22da5"
|
||||
PV = "0.1.17"
|
||||
SRCREV = "2380e07cce024e40f6097bdbe8fdb2034b5bd624"
|
||||
PV = "0.1.24"
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
# it to equal the rootfs stamp, the .fw meta-version and the release tag,
|
||||
# and the acceptance test image.health compares the stamp on the running
|
||||
# machine with the manifest's.
|
||||
FORGEFIRM_RELEASE ?= "0.0.2"
|
||||
FORGEFIRM_RELEASE ?= "0.0.4"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
# ForgeFIRM acceptance - 20260911172215 (dev)
|
||||
|
||||
- Image: `20260911172215 (dev)` (forgefirm-image-dev)
|
||||
- Manifest identity: `fb24c3f4508ae54592f8d0b98ed0b2a07a933274980d8c1e4e5a967618ef9bd8`
|
||||
- Catalog: `ceabc830345fc4cac4832de691c0571e93a5c4e155666ce91601c844c40942fc`
|
||||
- Campaign: `c-20260911182256-078f` opened 2026-09-11T18:22:56Z
|
||||
- Exported: 2026-09-11T18:54:20Z
|
||||
- **Release authorized: YES**
|
||||
- Tests: 83 total, 83 satisfied (57 inherited), 0 required
|
||||
|
||||
| Test | Kind | Result | Run at | Campaign | Inherited |
|
||||
|---|---|---|---|---|---|
|
||||
| `image.health` | auto, core | PASS | 2026-09-11T18:22:57Z | `c-20260911182256-078f` | no |
|
||||
| `image.license-bundle` | auto | PASS | 2026-09-11T17:29:26Z | `c-20260911172921-9426` | yes |
|
||||
| `kernel.latch-locked-idle` | auto, core | PASS | 2026-09-11T18:22:58Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.k1-k2` | auto, core, takeover | PASS | 2026-09-11T18:23:19Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.deadman-close` | auto, core, takeover | PASS | 2026-09-11T18:23:29Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.backtrack-bounds` | auto, core, takeover | PASS | 2026-09-11T18:23:48Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.fire-line` | auto, core, takeover | PASS | 2026-09-11T18:24:32Z | `c-20260911182256-078f` | no |
|
||||
| `kernel.resume-lead` | auto, takeover | PASS | 2026-09-11T17:31:23Z | `c-20260911172921-9426` | yes |
|
||||
| `kernel.pic-soc-load` | auto | PASS | 2026-09-11T17:31:26Z | `c-20260911172921-9426` | yes |
|
||||
| `forgectrl.auth` | auto | PASS | 2026-09-11T17:31:27Z | `c-20260911172921-9426` | yes |
|
||||
| `forgectrl.settings-bounds` | auto | PASS | 2026-09-11T17:31:27Z | `c-20260911172921-9426` | yes |
|
||||
| `forgectrl.panel-serves` | auto | PASS | 2026-09-11T17:29:23Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.gate-blocks-controllers` | auto, takeover | PASS | 2026-09-11T17:31:46Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.override-until-reboot` | auto | PASS | 2026-09-11T17:31:46Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.advisories-rehash` | auto, takeover | PASS | 2026-09-11T17:31:55Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.account-login` | auto, takeover | PASS | 2026-09-11T17:32:44Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.https-only-writes` | auto | PASS | 2026-09-11T17:32:45Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.ssh-until-reboot` | auto | PASS | 2026-09-11T17:32:46Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.cloud-disabled-surface` | auto | PASS | 2026-09-11T17:32:47Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.factory-return` | auto | PASS | 2026-09-11T17:32:49Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.machine-name` | auto | PASS | 2026-09-11T17:32:50Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.first-run-flow` | operator, takeover | PASS | 2026-09-11T17:54:32Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.first-run-page` | operator, takeover | PASS | 2026-09-11T17:55:49Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.cert-page` | auto | PASS | 2026-09-11T17:32:50Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.what-changed` | auto, takeover | PASS | 2026-09-11T17:32:59Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.record-export` | auto | PASS | 2026-09-11T17:34:22Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.mirror` | auto, takeover | PASS | 2026-09-11T17:34:54Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-switches` | operator | PASS | 2026-09-11T17:56:07Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-sensors` | auto | PASS | 2026-09-11T17:34:34Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-airflow` | auto, takeover | PASS | 2026-09-11T17:36:26Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-cameras` | operator | PASS | 2026-09-11T17:56:22Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-motion` | auto, takeover | PASS | 2026-09-11T17:37:06Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-flow-verify` | auto, takeover | PASS | 2026-09-11T17:44:42Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.cloud-header-capture` | operator, takeover | PASS | 2026-09-11T17:59:41Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.sheet` | live, takeover | PASS | 2026-09-11T18:17:29Z | `c-20260911172921-9426` | yes |
|
||||
| `logs.tree-tail-export` | auto | PASS | 2026-09-11T17:33:40Z | `c-20260911172921-9426` | yes |
|
||||
| `logs.routing` | auto | PASS | 2026-09-11T17:44:46Z | `c-20260911172921-9426` | yes |
|
||||
| `logs.level-settings` | auto | PASS | 2026-09-11T17:44:46Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.pacing` | auto | PASS | 2026-09-11T17:36:41Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.jog-roundtrip` | auto | PASS | 2026-09-11T17:45:00Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.microstep-modes` | auto | PASS | 2026-09-11T17:45:25Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.liveness-probe` | auto | PASS | 2026-09-11T17:45:33Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.gate-waits-for-lid` | operator | PASS | 2026-09-11T18:24:48Z | `c-20260911182256-078f` | no |
|
||||
| `motion.cancel-abort` | auto | PASS | 2026-09-11T17:45:40Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.deadman` | auto | PASS | 2026-09-11T17:46:09Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.button-hold-resume` | operator | PASS | 2026-09-11T18:24:59Z | `c-20260911182256-078f` | no |
|
||||
| `motion.lid-cancel-home` | operator | PASS | 2026-09-11T18:25:15Z | `c-20260911182256-078f` | no |
|
||||
| `motion.interlock-cancel-home` | operator | PASS | 2026-09-11T18:25:24Z | `c-20260911182256-078f` | no |
|
||||
| `motion.lid-policy-hold` | operator | PASS | 2026-09-11T18:25:37Z | `c-20260911182256-078f` | no |
|
||||
| `motion.step-timing-under-load` | auto | PASS | 2026-09-11T17:46:40Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.aa-offset-calibrate` | auto | PASS | 2026-09-11T17:38:38Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.flow-verify` | auto | PASS | 2026-09-11T17:41:29Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.fans-quiet-after-motion` | auto | PASS | 2026-09-11T17:47:35Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.gate-off` | auto | PASS | 2026-09-11T17:35:02Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.floor-and-warm-up` | auto | PASS | 2026-09-11T17:49:02Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.tec-drive` | auto | PASS | 2026-09-11T17:49:10Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.fire-watch-tiers` | auto | PASS | 2026-09-11T17:49:25Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.crash-watch-plumbing` | auto | PASS | 2026-09-11T17:49:32Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.critical-tier` | auto | PASS | 2026-09-11T17:49:41Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.fan-gate-trips` | auto | PASS | 2026-09-11T17:35:45Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.flow-under-load` | live | PASS | 2026-09-11T18:41:47Z | `c-20260911182256-078f` | no |
|
||||
| `laser.power-floor` | auto | PASS | 2026-09-11T17:49:42Z | `c-20260911172921-9426` | yes |
|
||||
| `laser.emission-witness` | live, core | PASS | 2026-09-11T18:40:15Z | `c-20260911182256-078f` | no |
|
||||
| `laser.m5-rapid-dark` | live | PASS | 2026-09-11T18:42:25Z | `c-20260911182256-078f` | no |
|
||||
| `laser.disarm-in-hold` | live | PASS | 2026-09-11T18:43:51Z | `c-20260911182256-078f` | no |
|
||||
| `laser.armed-kill` | live | PASS | 2026-09-11T18:44:43Z | `c-20260911182256-078f` | no |
|
||||
| `laser.arm-wait-lid` | operator | PASS | 2026-09-11T18:25:57Z | `c-20260911182256-078f` | no |
|
||||
| `laser.pause-resume-lid-cancel` | live | PASS | 2026-09-11T18:45:12Z | `c-20260911182256-078f` | no |
|
||||
| `camera.snapshot` | auto | PASS | 2026-09-11T17:49:57Z | `c-20260911172921-9426` | yes |
|
||||
| `camera.sensor-profile` | auto | PASS | 2026-09-11T17:50:10Z | `c-20260911172921-9426` | yes |
|
||||
| `camera.h264-stream` | auto | PASS | 2026-09-11T17:50:23Z | `c-20260911172921-9426` | yes |
|
||||
| `camera.frame-health` | auto | PASS | 2026-09-11T17:50:36Z | `c-20260911172921-9426` | yes |
|
||||
| `camera.lid-privacy` | operator | PASS | 2026-09-11T18:26:12Z | `c-20260911182256-078f` | no |
|
||||
| `camera.key-read` | auto | PASS | 2026-09-11T17:50:37Z | `c-20260911172921-9426` | yes |
|
||||
| `update.slots-and-signature` | auto | PASS | 2026-09-11T17:32:49Z | `c-20260911172921-9426` | yes |
|
||||
| `cloud.mode-switch` | operator | PASS | 2026-09-11T17:58:28Z | `c-20260911172921-9426` | yes |
|
||||
| `cloud.service-protocol` | operator | PASS | 2026-09-11T18:46:13Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.lid-interlock-abort` | live | PASS | 2026-09-11T18:47:21Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.lid-during-button-wait` | operator | PASS | 2026-09-11T18:26:51Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.verdict-hold` | operator | PASS | 2026-09-11T18:38:58Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.pause-resume` | live | PASS | 2026-09-11T18:49:33Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.oversize-stream` | live | PASS | 2026-09-11T18:52:19Z | `c-20260911182256-078f` | no |
|
||||
| `cloud.paused-lid-cancel` | live | PASS | 2026-09-11T18:52:48Z | `c-20260911182256-078f` | no |
|
||||
|
||||
Artifact sha256: `d7bb5d900f9237711a17ae207df90025605340c2eb2eaf34f9ed508cc3a899a6`
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
# ForgeFIRM acceptance - 20260912180956 (dev)
|
||||
|
||||
- Image: `20260912180956 (dev)` (forgefirm-image-dev)
|
||||
- Manifest identity: `8bb5b6f39ca4e0a5afca9b620aa38980c4ede8cdfda240b24bbc01fd6c28fa05`
|
||||
- Catalog: `f906de649b7533e3713309f141afacbc06420f96824936a60b70febd71bb2df7`
|
||||
- Campaign: `c-20260912182427-e1ee` opened 2026-09-12T18:24:27Z
|
||||
- Exported: 2026-09-12T19:21:35Z
|
||||
- **Release authorized: YES**
|
||||
- Tests: 85 total, 85 satisfied (50 inherited), 0 required
|
||||
|
||||
| Test | Kind | Result | Run at | Campaign | Inherited |
|
||||
|---|---|---|---|---|---|
|
||||
| `image.health` | auto, core | PASS | 2026-09-12T18:24:28Z | `c-20260912182427-e1ee` | no |
|
||||
| `image.license-bundle` | auto | PASS | 2026-09-12T16:56:02Z | `c-20260912165539-ac51` | yes |
|
||||
| `kernel.latch-locked-idle` | auto, core | PASS | 2026-09-12T18:24:29Z | `c-20260912182427-e1ee` | no |
|
||||
| `kernel.k1-k2` | auto, core, takeover | PASS | 2026-09-12T18:24:50Z | `c-20260912182427-e1ee` | no |
|
||||
| `kernel.deadman-close` | auto, core, takeover | PASS | 2026-09-12T18:25:02Z | `c-20260912182427-e1ee` | no |
|
||||
| `kernel.backtrack-bounds` | auto, core, takeover | PASS | 2026-09-12T18:25:21Z | `c-20260912182427-e1ee` | no |
|
||||
| `kernel.fire-line` | auto, core, takeover | PASS | 2026-09-12T18:26:04Z | `c-20260912182427-e1ee` | no |
|
||||
| `kernel.resume-lead` | auto, takeover | PASS | 2026-09-11T17:31:23Z | `c-20260911172921-9426` | yes |
|
||||
| `kernel.pic-soc-load` | auto | PASS | 2026-09-11T17:31:26Z | `c-20260911172921-9426` | yes |
|
||||
| `forgectrl.auth` | auto | PASS | 2026-09-12T16:58:12Z | `c-20260912165539-ac51` | yes |
|
||||
| `forgectrl.settings-bounds` | auto | PASS | 2026-09-12T16:58:19Z | `c-20260912165539-ac51` | yes |
|
||||
| `forgectrl.panel-serves` | auto | PASS | 2026-09-12T16:55:54Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.gate-blocks-controllers` | auto, takeover | PASS | 2026-09-12T16:58:44Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.override-until-reboot` | auto | PASS | 2026-09-12T16:58:44Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.advisories-rehash` | auto, takeover | PASS | 2026-09-12T16:58:53Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.account-login` | auto, takeover | PASS | 2026-09-12T16:59:43Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.https-only-writes` | auto | PASS | 2026-09-12T16:59:44Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.ssh-until-reboot` | auto | PASS | 2026-09-12T16:59:46Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.cloud-disabled-surface` | auto | PASS | 2026-09-12T16:59:46Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.factory-return` | auto | PASS | 2026-09-12T16:59:49Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.machine-name` | auto | PASS | 2026-09-11T17:32:50Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.first-run-flow` | operator, takeover | PASS | 2026-09-12T17:10:18Z | `c-20260912170819-8dec` | yes |
|
||||
| `commission.first-run-page` | operator, takeover | PASS | 2026-09-11T17:55:49Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.cert-page` | auto | PASS | 2026-09-12T16:59:50Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.what-changed` | auto, takeover | PASS | 2026-09-12T16:59:59Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.record-export` | auto | PASS | 2026-09-12T17:01:26Z | `c-20260912165539-ac51` | yes |
|
||||
| `commission.mirror` | auto, takeover | PASS | 2026-09-11T17:34:54Z | `c-20260911172921-9426` | yes |
|
||||
| `commission.check-switches` | operator | PASS | 2026-09-12T17:10:23Z | `c-20260912170819-8dec` | yes |
|
||||
| `commission.check-sensors` | auto | PASS | 2026-09-12T18:26:17Z | `c-20260912182427-e1ee` | no |
|
||||
| `commission.check-airflow` | auto, takeover | PASS | 2026-09-12T18:27:51Z | `c-20260912182427-e1ee` | no |
|
||||
| `commission.check-cameras` | operator | PASS | 2026-09-12T17:10:54Z | `c-20260912170819-8dec` | yes |
|
||||
| `commission.check-motion` | auto, takeover | PASS | 2026-09-12T18:28:17Z | `c-20260912182427-e1ee` | no |
|
||||
| `commission.check-flow-verify` | auto, takeover | PASS | 2026-09-12T17:16:41Z | `c-20260912170819-8dec` | yes |
|
||||
| `commission.cloud-header-capture` | operator, takeover | PASS | 2026-09-12T17:29:14Z | `c-20260912170819-8dec` | yes |
|
||||
| `commission.sheet` | live, takeover | PASS | 2026-09-12T19:02:59Z | `c-20260912182427-e1ee` | no |
|
||||
| `logs.tree-tail-export` | auto | PASS | 2026-09-12T17:00:43Z | `c-20260912165539-ac51` | yes |
|
||||
| `logs.routing` | auto | PASS | 2026-09-11T17:44:46Z | `c-20260911172921-9426` | yes |
|
||||
| `logs.level-settings` | auto | PASS | 2026-09-12T17:16:42Z | `c-20260912170819-8dec` | yes |
|
||||
| `motion.pacing` | auto | PASS | 2026-09-11T17:36:41Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.jog-roundtrip` | auto | PASS | 2026-09-11T17:45:00Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.microstep-modes` | auto | PASS | 2026-09-12T17:17:07Z | `c-20260912170819-8dec` | yes |
|
||||
| `motion.liveness-probe` | auto | PASS | 2026-09-11T17:45:33Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.gate-waits-for-lid` | operator | PASS | 2026-09-12T17:17:23Z | `c-20260912170819-8dec` | yes |
|
||||
| `motion.cancel-abort` | auto | PASS | 2026-09-11T17:45:40Z | `c-20260911172921-9426` | yes |
|
||||
| `motion.deadman` | auto | PASS | 2026-09-12T17:17:53Z | `c-20260912170819-8dec` | yes |
|
||||
| `motion.button-hold-resume` | operator | PASS | 2026-09-11T18:24:59Z | `c-20260911182256-078f` | yes |
|
||||
| `motion.lid-cancel-home` | operator | PASS | 2026-09-11T18:25:15Z | `c-20260911182256-078f` | yes |
|
||||
| `motion.interlock-cancel-home` | operator | PASS | 2026-09-11T18:25:24Z | `c-20260911182256-078f` | yes |
|
||||
| `motion.lid-policy-hold` | operator | PASS | 2026-09-11T18:25:37Z | `c-20260911182256-078f` | yes |
|
||||
| `motion.step-timing-under-load` | auto | PASS | 2026-09-11T17:46:40Z | `c-20260911172921-9426` | yes |
|
||||
| `cooling.aa-offset-calibrate` | auto | PASS | 2026-09-12T18:29:50Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.flow-verify` | auto | PASS | 2026-09-12T18:32:33Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.fans-quiet-after-motion` | auto | PASS | 2026-09-12T18:33:39Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.gate-off` | auto | PASS | 2026-09-12T18:26:26Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.floor-and-warm-up` | auto | PASS | 2026-09-12T18:35:06Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.tec-drive` | auto | PASS | 2026-09-12T18:35:15Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.fire-watch-tiers` | auto | PASS | 2026-09-12T18:35:31Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.crash-watch-plumbing` | auto | PASS | 2026-09-12T18:35:38Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.critical-tier` | auto | PASS | 2026-09-12T18:35:47Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.fan-gate-trips` | auto | PASS | 2026-09-12T18:27:10Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.flow-under-load` | live | PASS | 2026-09-12T19:05:38Z | `c-20260912182427-e1ee` | no |
|
||||
| `cooling.fan-duty-readback` | auto | PASS | 2026-09-12T18:36:02Z | `c-20260912182427-e1ee` | no |
|
||||
| `laser.power-floor` | auto | PASS | 2026-09-12T17:20:57Z | `c-20260912170819-8dec` | yes |
|
||||
| `laser.emission-witness` | live, core | PASS | 2026-09-12T18:50:46Z | `c-20260912182427-e1ee` | no |
|
||||
| `laser.m5-rapid-dark` | live | PASS | 2026-09-12T19:06:30Z | `c-20260912182427-e1ee` | no |
|
||||
| `laser.disarm-in-hold` | live | PASS | 2026-09-12T19:08:04Z | `c-20260912182427-e1ee` | no |
|
||||
| `laser.armed-kill` | live | PASS | 2026-09-12T19:08:58Z | `c-20260912182427-e1ee` | no |
|
||||
| `laser.arm-wait-lid` | operator | PASS | 2026-09-12T18:36:21Z | `c-20260912182427-e1ee` | no |
|
||||
| `laser.pause-resume-lid-cancel` | live | PASS | 2026-09-12T19:09:43Z | `c-20260912182427-e1ee` | no |
|
||||
| `camera.snapshot` | auto | PASS | 2026-09-12T17:10:39Z | `c-20260912170819-8dec` | yes |
|
||||
| `camera.sensor-profile` | auto | PASS | 2026-09-12T17:21:13Z | `c-20260912170819-8dec` | yes |
|
||||
| `camera.h264-stream` | auto | PASS | 2026-09-12T17:21:27Z | `c-20260912170819-8dec` | yes |
|
||||
| `camera.frame-health` | auto | PASS | 2026-09-12T17:21:40Z | `c-20260912170819-8dec` | yes |
|
||||
| `camera.lid-privacy` | operator | PASS | 2026-09-12T17:21:56Z | `c-20260912170819-8dec` | yes |
|
||||
| `camera.key-read` | auto | PASS | 2026-09-12T17:21:57Z | `c-20260912170819-8dec` | yes |
|
||||
| `update.slots-and-signature` | auto | PASS | 2026-09-12T16:59:48Z | `c-20260912165539-ac51` | yes |
|
||||
| `update.release-check` | auto | PASS | 2026-09-12T17:21:58Z | `c-20260912170819-8dec` | yes |
|
||||
| `cloud.mode-switch` | operator | PASS | 2026-09-12T18:38:00Z | `c-20260912182427-e1ee` | no |
|
||||
| `cloud.service-protocol` | operator | PASS | 2026-09-12T19:10:50Z | `c-20260912182427-e1ee` | no |
|
||||
| `cloud.lid-interlock-abort` | live | PASS | 2026-09-12T19:11:59Z | `c-20260912182427-e1ee` | no |
|
||||
| `cloud.lid-during-button-wait` | operator | PASS | 2026-09-12T17:24:21Z | `c-20260912170819-8dec` | yes |
|
||||
| `cloud.verdict-hold` | operator | PASS | 2026-09-12T18:40:33Z | `c-20260912182427-e1ee` | no |
|
||||
| `cloud.pause-resume` | live | PASS | 2026-09-12T19:14:09Z | `c-20260912182427-e1ee` | no |
|
||||
| `cloud.oversize-stream` | live | PASS | 2026-09-12T19:16:55Z | `c-20260912182427-e1ee` | no |
|
||||
| `cloud.paused-lid-cancel` | live | PASS | 2026-09-12T19:17:30Z | `c-20260912182427-e1ee` | no |
|
||||
|
||||
Artifact sha256: `9650b1ed9862eefab808ca32cdaa8f6b9b6debd8ee640adde9d18dcdfc03ba25`
|
||||
@@ -0,0 +1,5 @@
|
||||
ForgeFIRM v0.0.4 is the first public beta release.
|
||||
|
||||
Expect bugs. When you find one, open an issue in this repository or report it on the community forum at https://community.openglow.org so it can be fixed.
|
||||
|
||||
Every release below 0.1.0 is a beta release. Installation, usage, and safety are documented at https://docs.forgefirm.org/.
|
||||
@@ -25,6 +25,19 @@ no root) and drives it over TCP:
|
||||
does not free Z either
|
||||
6. X and Y are still free after both writes
|
||||
|
||||
A referenced lens opens the envelope to the window the shared settings
|
||||
hold, the same keys the daemon's wizards build their Z from. forgectrl
|
||||
leaves the lens on its hall edge and a marker in the state directory
|
||||
before the controller starts; the driver takes it as it loads its
|
||||
settings and places the edge on its step grid:
|
||||
|
||||
7. the marker with the edge alone: Z sits at the edge's grid step, the
|
||||
fallback window (10 below, 12 above) runs to its ends, and two
|
||||
half-steps past either end alarms
|
||||
8. the marker with the focus card's stops (14 below, 20 above): the
|
||||
wider window runs to its ends, two half-steps past either alarms
|
||||
9. a stop count out of range (41) falls back on its side alone
|
||||
|
||||
The binary keeps its settings in EEPROM.DAT in the working directory, so
|
||||
each run starts from defaults in a temporary directory and leaves
|
||||
nothing behind.
|
||||
@@ -53,13 +66,24 @@ def fail(msg):
|
||||
|
||||
|
||||
class Session:
|
||||
"""One null-sink controller process with its own settings store."""
|
||||
"""One null-sink controller process with its own settings store.
|
||||
`conf` (key: value) is the shared config the driver reads; with
|
||||
`referenced` the lens marker forgectrl leaves is in the state
|
||||
directory, so the driver takes the lens reference at its start."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, conf=None, referenced=False):
|
||||
self.workdir = tempfile.mkdtemp(prefix="z-envelope-")
|
||||
env = dict(os.environ, GF_STATE_DIR=self.workdir, FFLOG_STDERR="1")
|
||||
for key in ("GFSINK", "GF_SWITCH_FILE", "GF_VERDICT_FILE"):
|
||||
env.pop(key, None)
|
||||
conf_path = os.path.join(self.workdir, "forgefirm.conf")
|
||||
with open(conf_path, "w") as f:
|
||||
for k, v in (conf or {}).items():
|
||||
f.write("%s = %s\n" % (k, v))
|
||||
env["GFHOME_CONF"] = conf_path
|
||||
if referenced:
|
||||
with open(os.path.join(self.workdir, "lens.home"), "w") as f:
|
||||
f.write("edge 0 3 3\n")
|
||||
self.proc = subprocess.Popen([BIN, "-p", str(PORT)],
|
||||
cwd=self.workdir, env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
@@ -113,6 +137,23 @@ class Session:
|
||||
m = re.search(r"<(\w+)", self.send("?"))
|
||||
return m.group(1) if m else "?"
|
||||
|
||||
def mpos_z(self):
|
||||
"""The reported machine Z (no work offset is set in a session)."""
|
||||
m = re.search(r"[MW]Pos:(-?[\d.]+),(-?[\d.]+),(-?[\d.]+)", self.send("?"))
|
||||
if not m:
|
||||
fail("no position in the status report")
|
||||
return float(m.group(3))
|
||||
|
||||
def wait_idle(self, timeout=15.0):
|
||||
"""A move that ran plays out before the next line is judged: the
|
||||
soft limit on a moving machine holds first and alarms after."""
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
if self.state() == "Idle":
|
||||
return
|
||||
time.sleep(0.1)
|
||||
fail("the controller did not return to Idle")
|
||||
|
||||
def move(self, gcode):
|
||||
"""Run one move. Returns True when the soft limit blocked it.
|
||||
An alarm needs a reset and an unlock before the next move."""
|
||||
@@ -160,6 +201,60 @@ def free_in_xy(s, when):
|
||||
check(ran, "X and Y free %s" % when, "an X or Y move was blocked %s" % when)
|
||||
|
||||
|
||||
def window_holds(s, steps, below, above, what):
|
||||
"""The envelope of a referenced lens whose edge sits on grid step
|
||||
`steps`: the ends of the reach (`below` and `above` half-steps from
|
||||
the edge) run, two half-steps past either end alarms. The controller
|
||||
keeps a half-step of slack beyond the reach, so one past is not
|
||||
judged here; two is outside on every head."""
|
||||
spm = float(s.setting("$102"))
|
||||
top, bottom = (steps + above) / spm, (steps - below) / spm
|
||||
s.wait_idle()
|
||||
check(not s.move("G0 Z%.3f" % top), "%s: the top of the reach, Z %.3f, runs" % (what, top),
|
||||
"%s: Z %.3f, the top of the reach, was blocked" % (what, top))
|
||||
s.wait_idle()
|
||||
check(not s.move("G0 Z%.3f" % bottom), "%s: the bottom of the reach, Z %.3f, runs" % (what, bottom),
|
||||
"%s: Z %.3f, the bottom of the reach, was blocked" % (what, bottom))
|
||||
over, under = (steps + above + 2) / spm, (steps - below - 2) / spm
|
||||
s.wait_idle()
|
||||
check(s.move("G0 Z%.3f" % over), "%s: two half-steps over the top, Z %.3f, alarms" % (what, over),
|
||||
"%s: Z %.3f, two half-steps over the top, ran" % (what, over))
|
||||
s.wait_idle()
|
||||
check(s.move("G0 Z%.3f" % under), "%s: two half-steps under the bottom, Z %.3f, alarms" % (what, under),
|
||||
"%s: Z %.3f, two half-steps under the bottom, ran" % (what, under))
|
||||
|
||||
|
||||
def referenced_cases():
|
||||
"""The lens referenced by forgectrl's marker: the envelope is the
|
||||
window the shared settings hold, as the daemon's wizards read it."""
|
||||
edge = 3.35
|
||||
conf = {"lens_hall_edge_z_mm": "%.2f" % edge}
|
||||
s = Session(conf=conf, referenced=True)
|
||||
try:
|
||||
spm = float(s.setting("$102"))
|
||||
steps = int(round(edge * spm))
|
||||
z = s.mpos_z()
|
||||
check(abs(z - steps / spm) < 0.002,
|
||||
"referenced: Z sits at the edge's grid step, %.3f" % z,
|
||||
"referenced: Z reads %.3f, not the edge's grid step %.3f" % (z, steps / spm))
|
||||
window_holds(s, steps, 10, 12, "the fallback window")
|
||||
free_in_xy(s, "with the lens referenced")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
s = Session(conf=dict(conf, lens_stop_below_steps="14", lens_stop_above_steps="20"), referenced=True)
|
||||
try:
|
||||
window_holds(s, steps, 14, 20, "the focus card's window")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
s = Session(conf=dict(conf, lens_stop_below_steps="41", lens_stop_above_steps="20"), referenced=True)
|
||||
try:
|
||||
window_holds(s, steps, 10, 20, "a below count out of range")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.isfile(BIN):
|
||||
fail("no controller binary at %s" % BIN)
|
||||
@@ -189,6 +284,7 @@ def main():
|
||||
free_in_xy(s, "after the settings writes")
|
||||
finally:
|
||||
s.close()
|
||||
referenced_cases()
|
||||
print("PASS z_envelope_test")
|
||||
|
||||
|
||||
|
||||
+15
-2
@@ -365,6 +365,19 @@ else
|
||||
DOCS_TAG_CMD="git -C $DOCS_DIR push origin v$VERSION"
|
||||
fi
|
||||
|
||||
# The release notes: releases/v<version>/notes.md when the release carries
|
||||
# one (staged beside the assets, not uploaded as one), GitHub's generated
|
||||
# notes otherwise.
|
||||
NOTES_OPT="--generate-notes"
|
||||
if [ -f "$REPO/releases/v$VERSION/notes.md" ]; then
|
||||
cp "$REPO/releases/v$VERSION/notes.md" "$STAGE/notes.md"
|
||||
NOTES_OPT="--notes-file notes.md"
|
||||
echo "notes: releases/v$VERSION/notes.md"
|
||||
else
|
||||
rm -f "$STAGE/notes.md"
|
||||
echo "notes: generated by GitHub"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
== release v$VERSION staged ==
|
||||
@@ -377,7 +390,7 @@ Pre-publish checklist (docs.forgefirm.org, Developers, "Release flow"):
|
||||
Publish (from a directory with an authenticated gh):
|
||||
cd "$STAGE"
|
||||
gh release create "v$VERSION" --repo openglow-org/forgefirm \\
|
||||
--title "ForgeFIRM v$VERSION" --generate-notes $PRERELEASE \\
|
||||
--title "ForgeFIRM v$VERSION" $NOTES_OPT $PRERELEASE \\
|
||||
$ASSETS
|
||||
|
||||
Push the documentation tag with it:
|
||||
@@ -387,7 +400,7 @@ EOF
|
||||
if [ "$PUBLISH" = "1" ]; then
|
||||
command -v gh >/dev/null || die "--publish requested but gh is not on PATH"
|
||||
( cd "$STAGE" && gh release create "v$VERSION" --repo openglow-org/forgefirm \
|
||||
--title "ForgeFIRM v$VERSION" --generate-notes $PRERELEASE \
|
||||
--title "ForgeFIRM v$VERSION" $NOTES_OPT $PRERELEASE \
|
||||
$ASSETS ) \
|
||||
|| die "gh release create failed"
|
||||
echo "== published v$VERSION =="
|
||||
|
||||
Reference in New Issue
Block a user