diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index bc606fb..b25beb6 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -1225,36 +1225,12 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`. 5. **Update system Phase 5 — recovery refresh.** The remaining phase of `docs/UPDATE-SYSTEM.md` (a refreshed recovery image in boot0); Phases 0–4 are done. -6. **Head accelerometer crash detector, and the head IRQ (planned; - exploratory).** The factory's head crash detector is the head - accelerometer's own on-chip interrupt generator, armed per job from the - HA* header thresholds and read by polling the sensor; the head IRQ is the - coarse aggregate path to that event and the others (hall, beam). Both are - one mechanism (the facts bank, "The head MCU flag register and HEAD_IRQ" - and "The head accelerometer"), which is why these two items are one. The - detector is adopted, not measured from scratch: the earlier plan to - bench-measure a filter is moot now that the HA* thresholds are known to be - LIS2HH12 register values at a known full scale. - - The detector arms the LIS2HH12 interrupt generator over i2c-3 (per-axis - threshold, duration) and polls IG_SRC1 for a latched per-axis trip, on - the factory's two-tier shape (an alert that pauses, an abort that fails). - The rail-contact signature in the facts bank sets the first threshold in - register units; a pause on contact is the first use. ForgeFIRM already - reads the head accel raw for liveness, so this shares the bus, not new - hardware. The readout path is settled by the de-risk drill - (`scripts/bench/accel_crash_probe.py`, on the bench page; the record is - in CAMPAIGN-LOG): the detector is **forgectrl-only**. The IG registers - (0x30 to 0x35) program and poll over i2c-dev (I2C_SLAVE_FORCE) while - `st_accel` stays bound, and the liveness raw reads keep working through - an armed window. Keeping the factory full scale (no CTRL4 write) keeps - the coexistence clean, and the armed detector must own the ODR: the - facts bank ("The head accelerometer") has the power-down behavior. The - gravity axis rules the thresholds: X and Y arm below 1 g, a Z threshold - must sit above 1 g plus margin. Owed: the forgectrl detector itself, - per-state (idle/run) thresholds seeded from a captured cut header's HA* - values, and the two tiers wired into the existing feed-hold and - stop-plus-latch paths. +6. **Crash-watch bench commissioning, and the head IRQ (exploratory).** + Commission the crash watch on the bench, on the next image: a knock + inside an armed window must walk both tiers (`BUMP` holds and + releases after the quiet polls; `CRASH` stops motion and locks the + latch for the session), and judge the shipped thresholds against + that knock. Owed for the head IRQ, only if a coarse hardware interrupt is wanted instead of the poll: arm the accel bit in the head MCU (reg 0x03/0x04), diff --git a/forgetest/forgetest/suite/cooling.py b/forgetest/forgetest/suite/cooling.py index ca20036..041fff0 100644 --- a/forgetest/forgetest/suite/cooling.py +++ b/forgetest/forgetest/suite/cooling.py @@ -12,6 +12,7 @@ from .. import hw _COOL_COVERS = [("forgectrl", "src/cool.*"), ("forgectrl", "src/coolfmt.*"), ("forgectrl", "src/diag.*"), ("forgectrl", "src/gates.*"), ("forgectrl", "src/airflow.*"), + ("forgectrl", "src/accel.*"), ("forgectrl", "src/settings.*"), ("forgectrl", "src/status.*"), ("forgectrl", "src/ui/**"), ("grblhal-glowforge", "src/glowforge_cooling.*"), @@ -836,6 +837,84 @@ def fire_watch_tiers(ctx): ctx.check(fc.wait_idle(60, abort=ctx.aborted), "machine did not return to idle") +CRASH_KEYS = ("cool_accel_x_alert", "cool_accel_y_alert", "cool_accel_abort") +CRASH_GATES = ["crash_abort", "crash_x_alert", "crash_y_alert"] + + +@test("cooling.crash-watch-plumbing", title="The crash watch stays off the accelerometer unarmed and its gates say off", + subsystem="cooling", kind="auto", mode="grbl", est_min=3, + covers=_COOL_COVERS, requires=["kernel.latch-locked-idle"], + steps=["Machine idle, lid closed. The test writes the three crash thresholds and restores " + "them; two M8/M9 sessions, no fire, no motion."], + description="The head-accelerometer crash watch's plumbing, provable without an armed " + "window: /cool/status carries accel_watch at watch with the factory-seeded " + "thresholds standing; an unarmed run session never arms the watch (the " + "coexistence contract: liveness and homing own the accel outside the armed " + "window), so accel_watch reads watch through it; the three thresholds at 0 " + "read as the three crash gates off by value (gates_off names them) with the " + "session OK; a threshold past the register range is refused; restored, the " + "gates read ok. The tiers themselves (BUMP, CRASH) trip only on a physical " + "knock inside an armed window and are commissioned at the bench, not here.") +def crash_watch_plumbing(ctx): + fc = ctx.forgectrl + ev = ctx.evidence + before = fc.settings() + orig = {k: before.get(k, "") for k in CRASH_KEYS} + ev["orig"] = orig + ctx.log("original: %s", orig) + gates = (before.get("gates") or {}) + for k in CRASH_KEYS: + g = gates.get(k) or {} + ctx.check(g.get("gate", "").startswith("crash_"), "settings gates has no %s row: %s", k, g) + c0 = _cool(fc) + ctx.check(c0.get("accel_watch") == "watch", + "accel_watch %r at idle with thresholds set, expected watch", c0.get("accel_watch")) + + st, body = fc.post("/settings", params={"cool_accel_x_alert": "300"}) + ctx.check(st != 200 or body.get("cool_accel_x_alert") != "300", + "a threshold past the register range (300) was accepted") + + restored = False + with ctx.grbl() as grbl: + ctx.check(grbl.status_report()["state"].startswith("Idle"), "controller is not idle") + try: + # Leg 1: an unarmed session never arms the watch. + c = _run_session(ctx, grbl, fc, lambda c: c.get("phase") == "run", "unarmed session") + ev["unarmed"] = c + ctx.check(c.get("accel_watch") == "watch", + "accel_watch %r in an unarmed run session, expected watch", c.get("accel_watch")) + ctx.check(c.get("verdict") == "OK", "unarmed session not OK: %s", c) + + # Leg 2: the three thresholds at zero are the gates off. + _set_gates(ctx, fc, {k: "0" for k in CRASH_KEYS}) + c = _run_session(ctx, grbl, fc, + lambda c: c.get("verdict") == "OK" and + set(CRASH_GATES) <= set(c.get("gates_off") or []), + "watch off") + ev["off"] = c + ctx.check(set(CRASH_GATES) <= set(c.get("gates_off") or []), + "gates_off %s does not name the three crash gates", c.get("gates_off")) + ctx.check(c.get("verdict") == "OK", "the watch at zero did not read OK: %s", c) + + # Restore, and prove the gates read ok again. + _set_gates(ctx, fc, orig) + restored = True + c = _run_session(ctx, grbl, fc, + lambda c: not (set(CRASH_GATES) & set(c.get("gates_off") or [])), + "restored") + ev["restored"] = c + ctx.check(not (set(CRASH_GATES) & set(c.get("gates_off") or [])), + "the crash gates stayed off after the restore: %s", c.get("gates_off")) + finally: + if not restored: + st, body = fc.post("/settings", params=orig) + ctx.log("restore on failure: POST /settings %s -> %s", orig, st) + after = fc.settings() + ctx.check(all(after.get(k, "") == orig[k] for k in CRASH_KEYS), + "settings not restored: %s", {k: after.get(k) for k in CRASH_KEYS}) + ctx.check(fc.wait_idle(60, abort=ctx.aborted), "machine did not return to idle") + + @test("cooling.critical-tier", title="The coolant critical line is a fault above the ceiling's pause", subsystem="cooling", kind="auto", mode="grbl", est_min=3, covers=_COOL_COVERS + [("forgectrl", "src/main.c")], requires=["cooling.gate-off"], diff --git a/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc b/meta-forgefirm/recipes-forgefirm/forgectrl/forgectrl-pin.inc index 341ba9d..821fc33 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 = "c77f040b954e531ca01b460d5567e23d811737a8" +SRCREV = "161686fddab446270c63bd3c4cfec169f1b14277" PV = "0.1.0" diff --git a/scripts/bench/accel_crash_probe.py b/scripts/bench/accel_crash_probe.py index 9c0e849..08f4bb6 100644 --- a/scripts/bench/accel_crash_probe.py +++ b/scripts/bench/accel_crash_probe.py @@ -70,7 +70,8 @@ WHO_AM_I = 0x0F WHO_AM_I_LIS2HH12 = 0x41 CTRL1 = 0x20 CTRL4 = 0x23 # bits 5:4 = FS (00=+/-2g, 10=+/-4g, 11=+/-8g) -CTRL7 = 0x26 # bit 0 = LIR1 (latch IG_SRC1, read-to-clear) +CTRL7 = 0x26 # bit 2 = LIR1 (latch IG_SRC1, read-to-clear; + # bit 3 = LIR2, bits 1:0 are the 4D enables) OUT_X_L = 0x28 IG_CFG1 = 0x30 # AOI,6D,ZHIE,ZLIE,YHIE,YLIE,XHIE,XLIE IG_SRC1 = 0x31 # IA(6),ZH,ZL,YH,YL,XH,XL @@ -182,7 +183,7 @@ def main(): # Arm IG1: latch the source (CTRL7 LIR1), set the thresholds and # duration, then enable the axes last. Preserve CTRL7's other bits. ctrl7 = rd(fd, CTRL7) - wr(fd, CTRL7, ctrl7 | 0x01) + wr(fd, CTRL7, ctrl7 | 0x04) wr(fd, IG_THS_X1, ths) wr(fd, IG_THS_Y1, ths) wr(fd, IG_THS_Z1, ths)