From 6458f6aec032b8741f5978337f0012869d641cfa Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sat, 22 Aug 2026 13:43:36 -0400 Subject: [PATCH] The ceiling at its off end stands with the default critical line forgectrl pin 368fd0c: the critical-line cross-check binds only while the ceiling is a gate. cooling.critical-tier pins that a POST setting the ceiling to its off end is accepted with the default line (the unit fake mirrors the exemption); CAMPAIGN-LOG records the gate-off failure on dev image 20260822165832 that found it, and the critical-tier pass. --- docs/CAMPAIGN-LOG.md | 16 ++++++++++++++++ docs/COOLING.md | 7 ++++--- forgetest/forgetest/suite/cooling.py | 8 ++++++++ forgetest/tests/test_cooling_suite.py | 8 ++++++-- .../forgectrl/forgectrl-pin.inc | 2 +- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/CAMPAIGN-LOG.md b/docs/CAMPAIGN-LOG.md index 966cd22..f81105a 100644 --- a/docs/CAMPAIGN-LOG.md +++ b/docs/CAMPAIGN-LOG.md @@ -3402,6 +3402,22 @@ names itself only on its rising edge and the critical fault had overwritten it. The engine now re-publishes the standing hold's reason when a critical fault clears (no new log line), and `cooling.critical-tier` checks it. +## 2026-08-22: the board temperatures on an image, and a cross-check that bound too far + +Dev image `20260822165832` (forgectrl 76115fd pinned by forgefirm 9fae47c): +`cooling.critical-tier` PASS; `cooling.gate-off` FAIL in its off leg: the +POST that sets the ceiling to its off end (60 C) came back `400 +cool_temp_critical_c must be above cool_temp_max`, because the step 3 +cross-check compared the default critical line (38 C) against the ceiling +wherever the ceiling stood. Under the settings rule every gate is off by +value on its own, so a ceiling at its off end is no ceiling and the +critical line stands alone as the fail tier: the cross-check now binds +only while the ceiling is a gate (its own table row's off end decides), +`cooling.critical-tier` pins that the off-end POST is accepted with the +default line, and the unit fake mirrors it. The test restored the +settings on its failure path as designed (the trip leg had passed: a 6 C +ceiling read `OVERTEMP` in 1 s). + ## Superseded status notes ### Shared machine services — remaining polish, as listed 2026-08-13 diff --git a/docs/COOLING.md b/docs/COOLING.md index 93781a4..d00058f 100644 --- a/docs/COOLING.md +++ b/docs/COOLING.md @@ -287,8 +287,9 @@ The engine uses the factory's coolant windows: ends with the session; the ceiling's pause keeps holding while the loop is hot, and the next session judges the line afresh. A cloud job's header carries no critical line for the coolant, so this one is always the local - setting; the settings API keeps it above the ceiling, and at its top - (70 °C) it is the gate turned off (§8a). + setting; the settings API keeps it above the ceiling while the ceiling is + a gate (a ceiling at its off end leaves the line standing alone), and at + its top (70 °C) it is the gate turned off (§8a). The **upstream** sensor gates, because it reads the coolant actually entering the tube. @@ -380,7 +381,7 @@ start of every run, so a change takes effect on your next job. | `cool_confirm_max_s` | 480 s | 60 to 3600 s | | How long a suspicion may stay unresolved before it escalates to a fault. | | `cool_temp_max` | 33 °C | 5 to 60 °C | 25 to 38 °C | Run ceiling: above it, hold. `60` turns the gate off (§8a). | | `cool_temp_resume` | 31 °C | 5 to 59 °C | 20 to 36 °C | Resume gate: below it, continue. Always kept below the ceiling. | -| `cool_temp_critical_c` | 38 °C | 6 to 70 °C | 36 to 45 °C | Critical line: a fault with no resume in the job (§5). Always kept above the ceiling; `70` turns the gate off. | +| `cool_temp_critical_c` | 38 °C | 6 to 70 °C | 36 to 45 °C | Critical line: a fault with no resume in the job (§5). Kept above the ceiling while the ceiling is a gate (a ceiling at 60 leaves the line standing alone); `70` turns the gate off. | | `cool_cooldown_s` | 15 s | 0 to 1800 s | | Smoke-clear phase at run duty after a job. | | `cool_cooldown_max_s` | 300 s | 0 to 1800 s | | Cap on the thermal cooldown phase. | | `cool_tach_exhaust_min_rpm` | 6400 rpm | 0 to 20000 | 5800 to 7000 | Exhaust fan floor at run duty (§3a). `0` turns the gate off. | diff --git a/forgetest/forgetest/suite/cooling.py b/forgetest/forgetest/suite/cooling.py index 19aa473..23a20e2 100644 --- a/forgetest/forgetest/suite/cooling.py +++ b/forgetest/forgetest/suite/cooling.py @@ -422,6 +422,14 @@ def critical_tier(ctx): after = fc.settings() ctx.check(all(after.get(k, "") == orig[k] for k in CRIT_KEYS), "the refused POST changed a setting: %s", {k: after.get(k) for k in CRIT_KEYS}) + # A ceiling at its off end is no ceiling: the default line stands + # alone, and the POST that turns the ceiling off is accepted. + st, body = fc.post("/settings", params={"cool_temp_max": str(ceil.get("hi"))}) + ev["ceiling_off_accepted"] = st + if st == 200: + fc.post("/settings", params=orig) + ctx.check(st == 200, "the ceiling at its off end %s was refused against the critical line: %s %s", + ceil.get("hi"), st, body) restored = False with ctx.grbl() as grbl: diff --git a/forgetest/tests/test_cooling_suite.py b/forgetest/tests/test_cooling_suite.py index 0a4ff2f..8a0f7a1 100644 --- a/forgetest/tests/test_cooling_suite.py +++ b/forgetest/tests/test_cooling_suite.py @@ -448,7 +448,8 @@ class CriticalTierTests(unittest.TestCase): merged.update(form) tmax = float(merged.get("cool_temp_max") or 33.0) tcrit = float(merged.get("cool_temp_critical_c") or 38.0) - if tcrit <= tmax: + ceiling_off = tmax >= self.ROWS["cool_temp_max"][3] + if not ceiling_off and tcrit <= tmax: return (400, {"error": "cool_temp_critical_c must be above cool_temp_max"}) self.fc.state["settings"].update(form) self._describe() @@ -508,8 +509,11 @@ class CriticalTierTests(unittest.TestCase): self.assertEqual(run.evidence["critical_after"]["verdict"], "OVERTEMP") self.assertEqual(run.evidence["critical_off"]["gates_off"], ["coolant_critical"]) self.assertEqual(run.evidence["restored"]["verdict"], "OK") + self.assertEqual(run.evidence["ceiling_off_accepted"], 200) posts = self.settings_posts() - self.assertEqual(posts[1], {"cool_temp_max": "6.0", "cool_temp_resume": "5.0", + # refused cross-check, the ceiling at its off end (accepted), its undo, then the low leg + self.assertEqual(posts[1], {"cool_temp_max": "60.0"}) + self.assertEqual(posts[3], {"cool_temp_max": "6.0", "cool_temp_resume": "5.0", "cool_temp_critical_c": "7.0"}) self.assertEqual(posts[-1], {"cool_temp_max": "", "cool_temp_resume": "", "cool_temp_critical_c": ""}) self.assertEqual(self.grbl.commands.count("M8"), 3) diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc index 7ef9572..148225a 100644 --- a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc +++ b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc @@ -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 = "76115fd87d0b085cd087a59ae0d58213007c42bf" +SRCREV = "368fd0c6bb4ce0eea6e9fad39ec4dfec593f914d" PV = "0.1.0"