diff --git a/forgetest/forgetest/bench.py b/forgetest/forgetest/bench.py index c57f84d..598413f 100644 --- a/forgetest/forgetest/bench.py +++ b/forgetest/forgetest/bench.py @@ -271,7 +271,8 @@ TOOLS = [ "forgectrl's listener: every report carries the secret the supervisor hands the controller at " "its spawn (GF_REPORT_SECRET), none does when there is none, a value that is not 32 hex digits " "(a CR LF with a header behind it included) never reaches the wire, and the homing runner the " - "controller starts does not inherit it. A CI harness (the grblHAL repo): needs the host-built " + "controller starts is handed it (it reports in the controller's place during a gfcloud $H), " + "and none when the controller has none. 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": "mcode-test", "title": "Package M-code barrier harness", "script": "mcode_test.py", "safety": "dry", "where": "host", "ported": False, "args": [], diff --git a/scripts/bench/README.md b/scripts/bench/README.md index d06ba78..5595a6f 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -34,7 +34,7 @@ page's takeover does that; from a host, stop them first. | `laser_lifecycle_test.py` | Host-side operator-armed-window lifecycle harness (null-sink controller): arm once per job with M5/M3 persistence, the M2 close, sender-change re-consent, the disarm grace counting down in Hold, and arm refusal under a blocking cooling verdict. Runs in the grblHAL repo's CI. | | `z_envelope_test.py` | Host-side Z envelope harness (null-sink controller): the Z soft limit belongs to the driver, not to `$20`, so the driver re-applies `sys.work_envelope`, `sys.homed` and `sys.soft_limits` for Z from the settings-changed chain. Checks that an unreferenced Z is collapsed to where the lens stands and blocks a move each way, that X and Y stay free, and that neither a `$20` write (the core clears the soft-limit mask in the setter) nor a `$132` write (which un-homes the axis as well) frees Z. Runs in the grblHAL repo's CI. | | `ctlport_test.py` | Host-side controller port and line-multiplexer harness (null-sink controller): a scripted sender that counts every `ok` and `error` runs beside the port's one client on `grbl.ctl`. A port jog's status goes to the port and never to the sender, a port error does not reach the sender's parser, a sender line cancels a port jog and gets its own `ok` (a line queued right behind the port's included), plus the refusals, the single client, a CR LF sender, a soft reset, and the client as the dead-man. `python3 ctlport_test.py `. | -| `cool_report_test.py` | Host-side cooling report harness (null-sink controller, a stand-in for forgectrl's listener on `FORGECTRL_PORT`): every `POST /cool/state` carries the secret the supervisor hands the controller at its spawn (`GF_REPORT_SECRET`) as `X-ForgeFIRM-Report`; with no secret the header is absent; a value that is not 32 hex digits, a CR LF with a header behind it included, never reaches the wire; and the homing runner the controller starts does not inherit the secret. `python3 cool_report_test.py `. | +| `cool_report_test.py` | Host-side cooling report harness (null-sink controller, a stand-in for forgectrl's listener on `FORGECTRL_PORT`): every `POST /cool/state` carries the secret the supervisor hands the controller at its spawn (`GF_REPORT_SECRET`) as `X-ForgeFIRM-Report`; with no secret the header is absent; a value that is not 32 hex digits, a CR LF with a header behind it included, never reaches the wire; and the homing runner the controller starts is handed the secret (it reports in the controller's place during a gfcloud `$H`), and none when the controller has none or a malformed one. `python3 cool_report_test.py `. | | `manual_home_test.py` | Host-side manual homing and motor release harness (null-sink controller, `GFSINK_DUMP` and `GFSINK_ATTR_LOG`): `$H` under `homing_mode = manual` ships nothing and declares X0 Y0; while the motors are released (`$MD`) every motion source is refused and ships nothing, `$X` and a soft reset do not unlock it, and only `$ME` and a manual `$H` write the energize. Imports its sender and port client from `ctlport_test.py`. `python3 manual_home_test.py `. | | `live_fire_drills.py` | **LIVE LASER** drills, on the board (the bench page) or from a LAN host (`GF_HOST`): `live_fire_drills.py [S] [F]` - `witness` (emission witness, lid-IR peaks vs the ambient baseline, HV current, job-based disarm on M2), `hold` (disarm grace in Hold), `faultpos` (armed job refuses a stale origin after an underrun), `ircut` (lid-IR characterization cut at S/F), `pthresh` (laser power-threshold ladder: 13 constant-power rungs from 2 % to 30 % of full on scrap; the lowest rung that marks is the tube's striking threshold and reads directly as the `$35` value - requires `$35` = 0 for the run), `dladder` (density ladder at a chosen base period), `pcurve` (laser performance-curve ladder: one 100 mm line per level at 10 mm/s under M3, the laser off between rungs and a mid-ladder rung repeated at the end; reads `pic/hv_current` and the head thermopile `head/beam_detect_analog` (a scatter detector in the beam path upstream of the final mirror, so it sees the beam, not the material) from sysfs at ~25 Hz on the board, brackets each rung on the controller's Run/Idle states, and reports per rung the current with a clipped-at-1023 flag, the thermopile delta over its laser-off baseline and in-line drift, then the normalized curve, monotonicity, a line fit with its threshold intercept and the repeat-rung drift; JSON record with the raw trace in the bench data directory; rungs follow `laser_power_model`, a comma list overrides; a curve measurement wants `$35` = 0), `dpatch [F] [pitch] [length]` (depth witness for the density dose curve: two rows of small serpentine-filled patches, row A CW at feeds giving relative doses 1.0 to 0.25 of the reference feed, row B at the reference feed at 100/80/60/45/30 % density; the operator matches each row-B patch to the row-A patch of equal depth, which reads the density's light fraction off the material beside the thermopile's prediction; JSON record), `m4feeds [S] [F1] [F2]` (the density time base across feeds: one out-and-back line pair per feed at the same S under M4 density, one armed run; the operator reads within-line evenness and reversal darkness at both feeds - M4's velocity scaling is what holds dose per mm through the accel), `m4corner [S] [F]` (M4 velocity-scaled power into corners: a corner-heavy vector pattern at 30 % under M4 density, one armed run; the operator confirms every commanded segment marks - the floor makes a dropout unreachable - and the drill asserts the arm report, one discharge window and dark after), `m5dark` (the rapids after an M5 ship dark: one 20 mm line at M3 S400, M5, dwell, rapid back, dwell, rapid forward; PASS when the 25 Hz current trace shows one discharge segment and reads dark after the M5 and `laser_on_sampled` never re-lights; the catalog's `laser.m5-rapid-dark` is its port), `flowload` (cooling under laser load, one armed run per invocation, the conf keys it writes put back at the end, the pump never commanded off: `t1` reproduces the flow-check trip with the check on at its defaults and two 30 x 4 mm CW fills at F1500 starting on the press with no dark dwell, and reports the engine's rise/dT verdict beside the 25 Hz trace of both coolant sensors, the current, the digital witness and the heater output in 5 s bins across the window, with the shape at fire start; `t2 [pct]` runs with the check off and one fill of about `secs` lit seconds at CW or at `pct` density, and reports the lag to each sensor, the rise per raw-second of `hv_current` and what a full 50 s window would add against the 1.6 C margin; `fit` fits rise against dose over every t2 record; JSON records), `expstop` (armed kill on the expected-stop path; needs the panel token - `GF_TOKEN`, or the board's token file) and `ctrlstart` (the separate controller restart after it). Every drill waits for the operator's physical arm press; eye protection, fire watch, extinguisher, and exhaust are mandatory. | | `pacing_test.py` | Protocol-loop pacing check (runs on the board, dry motion): idle and parked-in-Hold states are coarse-paced, active motion is tight-paced, and a feed-hold/resume mid-move preserves position with no feeder starve. | diff --git a/scripts/bench/cool_report_test.py b/scripts/bench/cool_report_test.py index 9850a0b..ca920e5 100644 --- a/scripts/bench/cool_report_test.py +++ b/scripts/bench/cool_report_test.py @@ -16,8 +16,11 @@ the null-sink controller against it, and reads every report as it arrives. malformed a value that is not 32 hex digits (short, a letter out of range, a CR LF and a header of its own behind it) is never sent, and nothing of it reaches the wire - runner the homing runner the controller starts does not inherit the - secret, and the reports still carry it afterwards + runner the homing runner the controller starts is handed the secret (it + reports in the controller's place while a gfcloud $H holds the + machine, and the controller is quiet then), and the reports + still carry it afterwards; a controller with no secret, or with + one that is not 32 hex digits, hands the runner none Usage: cool_report_test.py """ @@ -125,7 +128,7 @@ class Controller: shutil.rmtree(self.workdir, ignore_errors=True) -def run(name, secret, check, runner=False): +def run(name, secret, check, runner=False, handed=None): lis = Listener() ctl = Controller(lis, secret, runner_cmd=("env > %s" % "RUNNER_ENV") if runner else None) try: @@ -142,8 +145,11 @@ def run(name, secret, check, runner=False): time.sleep(0.3) with open(env_file) as f: runner_env = f.read() - if "GF_REPORT_SECRET" in runner_env or SECRET in runner_env: - fail("[%s] the homing runner inherited the report secret" % name) + got = [ln.split("=", 1)[1] for ln in runner_env.splitlines() if ln.startswith("GF_REPORT_SECRET=")] + if got != ([handed] if handed else []): + fail("[%s] the homing runner was handed %r, not %r" % (name, got, handed)) + if not handed and SECRET in runner_env: + fail("[%s] the secret reached the homing runner another way" % name) if "GF_STATE_DIR" not in runner_env: fail("[%s] the runner's environment was not read: %r" % (name, runner_env[:80])) s.close() @@ -184,7 +190,9 @@ def main(): SECRET[:16] + "\r\nX-Evil: 1\r\nX-Pad: 12", SECRET[:16] + "\r\nX-Evil: 1\r\nX:1"), 1): # the last is 32 long run("malformed-%d" % i, bad, without) - run("runner", SECRET, with_secret, runner=True) + run("runner", SECRET, with_secret, runner=True, handed=SECRET) + run("runner-none", None, without, runner=True) + run("runner-malformed", SECRET[:16] + "\r\nX-Evil: 1\r\nX:1", without, runner=True) print("cool_report_test: all passed")