mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
Pin forgectrl: the crash watch; its catalog case and the drill's latch bit
forgectrl 161686f adds the head-accelerometer crash watch (two tiers on the LIS2HH12's interrupt generators, armed inside the laser's armed window). The catalog grows cooling.crash-watch-plumbing - the unarmed half of the watch provable without a button press: accel_watch stays at watch through an unarmed session, three zeros read as the three crash gates off, an out-of-range threshold is refused - and _COOL_COVERS widens to src/accel.*. The armed-tier bench drill is a BRINGUP next-work item. The de-risk drill script's CTRL7 write moves from bit 0 (4D_IG1) to bit 2 (LIR1), the latch the datasheet actually puts there; the drill's findings did not depend on latching (a continuous gravity signal).
This commit is contained in:
+6
-30
@@ -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),
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user