mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
docs: record what the factory does about fans, temperature and coolant
Next-work item 19 carried three open questions about the factory's operating envelope. All three are answered now, so the item states the policy instead of the guess. A fan tach alert during a cut pauses the print, taking the same transition a user pause takes, and two of the factory's three tach monitors cannot fire at all: they treat a zero limit as unconfigured and the limits arrive zero anyway. So a stalled extraction fan is caught by the temperature it causes, not by its tachometer. Every temperature alert pauses as well, and a critical fails the machine outright, which is a different state and not a pause. The scale question behind the header's ceilings is answered by the coolant family, which the factory carries twice, once in raw ADC counts where the tag named min is the hot limit, and once in millidegrees. And the factory does not verify coolant flow at all, so the cooling engine's flow check is ahead of the factory rather than behind it. The crash, tilt and beam-detect bullets gain the same grading: an alert threshold that pauses and a separate abort threshold that aborts. Also drops references to paths that resolve outside this repo, in both documents and in two bench scripts, naming the artifact instead. In CAMPAIGN-LOG that is a wording substitution only; no date, claim or measurement changes. Documentation only, so no acceptance catalog consequence.
This commit is contained in:
+151
-10
@@ -282,8 +282,7 @@ a usable witness here — it reads 0 through real cutting.
|
||||
## Lid, interlock and button policy
|
||||
|
||||
Both controller modes react the way the factory daemon does (decoded from a
|
||||
factory 2.6.0-2228 session; log archived under
|
||||
`_RESOURCES/factory-session-20260816/`, measured numbers in the facts bank).
|
||||
factory 2.6.0-2228 session; measured numbers in the facts bank).
|
||||
|
||||
- **Lid or interlock open during a job, running or paused:** motion stops
|
||||
within milliseconds of the edge, the job is **canceled and not resumable**,
|
||||
@@ -341,7 +340,7 @@ then reacquires the device and re-applies the analog config and `step_freq`.
|
||||
The runner drives the GFUIService dispatch itself (the stock `run()` loop can
|
||||
neither stop nor close the socket) and treats hunt + ≥1
|
||||
accelerometer-witnessed motion window + quiet (10 s) as complete — the modern
|
||||
v2.6.0 sequence, per `_RESOURCES/emulator.log`, is settings → hunt → lid_image
|
||||
v2.6.0 sequence, captured from a live service session, is settings → hunt → lid_image
|
||||
→ single corner move → lid_image → silence. It then re-homes
|
||||
the lens against the hall for a deterministic Z. **A quiet service without an
|
||||
accel-witnessed motion window is a failure, not a homing.**
|
||||
@@ -681,8 +680,8 @@ not a release.
|
||||
**+Y physically moves the gantry toward the FRONT.** Home corner (convention,
|
||||
for the planned limit-switch homing) = back-left (X min, Y min), workspace
|
||||
all-positive from that corner.
|
||||
- **Factory motion profile** (measured from `_RESOURCES` pulse streams with
|
||||
`puls_profile.py`): accel ≈ 700 mm/s² X / 590 mm/s² Y on v2.6.0 firmware
|
||||
- **Factory motion profile** (measured from captured factory pulse streams
|
||||
with `puls_profile.py`): accel ≈ 700 mm/s² X / 590 mm/s² Y on v2.6.0 firmware
|
||||
(2018 firmware used ≈1000); header HAxr=132/HAyr=112/HAar=133 ⇒ ≈5.3 mm/s²
|
||||
per HA unit. Travel moves peak 202 mm/s vector (≈ 8 in/s) at STfr=28160 Hz;
|
||||
prints and hunts run STfr=10000. Cut feed in the sample print: 145 mm/s. Z
|
||||
@@ -707,8 +706,8 @@ not a release.
|
||||
per-rung means are non-monotonic at the top of the ladder and the signal
|
||||
has no characterized transfer function.
|
||||
- **Factory power model** (three cloud cuts of one 1" square, same location,
|
||||
material and speed, only the UI power setting changed; captures in
|
||||
`_RESOURCES/power-settings-20260817/`): the **power byte is pinned at 127**
|
||||
material and speed, only the UI power setting changed, pulse files captured
|
||||
from each): the **power byte is pinned at 127**
|
||||
in all three runs — three occurrences each, one as the cut begins and a
|
||||
refresh every ~27 000 ticks (~2.7 s). Analog duty is never a power control.
|
||||
**Dose is FIRE-bit density on a fixed 7-tick period** (700 µs at
|
||||
@@ -808,8 +807,7 @@ not a release.
|
||||
lid-switch chain (`docs/SAFETY.md`). Doors/door1/door2 stay stable during
|
||||
motion.
|
||||
- **Factory job behavior on the lid and the button, measured on 2.6.0-2228**
|
||||
(bench session 2026-08-16, log archived at
|
||||
`_RESOURCES/factory-session-20260816/`; this is what ForgeFIRM's parity policy
|
||||
(bench session 2026-08-16; this is what ForgeFIRM's parity policy
|
||||
reproduces). Lid open mid-print: `cnc/stop` 5–6 ms after the edge, decel to
|
||||
idle in 86–91 ms, the return-home park starting ~300–340 ms after the edge and
|
||||
running to completion **with the lid still open**, the job reported
|
||||
@@ -928,6 +926,19 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
lamp-set level, not a fixed count. Even then the signal is weak — a candle
|
||||
reads like a cut — so the head camera or a real flame sensor is the honest
|
||||
path to fire detection that means something.
|
||||
|
||||
One lead worth a bench hour before building anything. The cloud ships flame
|
||||
thresholds in every pulse header, and the numbers do not look lamp-naive:
|
||||
baseline 3 counts on all four channels, alert at 275 and critical at 688 on
|
||||
the first quartile, 374 and 1022 on the second, with the third and fourth
|
||||
left at zero. Measured lamp response on this bench is 0 counts to 2, 131 to
|
||||
58, 255 to 180, so the factory's alert sits above the reading a fully lit
|
||||
lamp produces and its baseline matches the lamp-off floor. If that holds,
|
||||
the factory rides out the lamp by choosing thresholds above it rather than
|
||||
by tracking it, and the watch could be re-armed on fixed numbers after all.
|
||||
Unproven: it assumes the header's quartiles map onto the raw channels and
|
||||
share their units. Confirm both by reading the four channels while stepping
|
||||
the lamp, then compare against the header the next cloud job carries.
|
||||
5. **Limit-switch homing.** The planned second homing method (`$22` stays 0
|
||||
until it lands); printable brackets are in `3d-models/`. Also: calibrate
|
||||
`gfcloud_home_x/y` against a jog to a known reference if the factory corner
|
||||
@@ -954,7 +965,9 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
and taking the pause constants from the pulse header (`CCbp`/`CCbt`) once a
|
||||
capture confirms them. Not inducible from the bench: the
|
||||
cancel-with-a-rejected-`settings`-action case and a malformed frame (needs a
|
||||
MITM).
|
||||
MITM). The pulse header's unenforced safety envelope is item 19: it is
|
||||
listed separately because the enforcement lands in the cooling engine and
|
||||
has to hold in GRBL mode too, not only under the cloud client.
|
||||
8. **Shared machine services — remaining polish.** None of it blocking:
|
||||
- **Diagnostics as engine modes.** The flow tools still drive the thermal
|
||||
hardware themselves while the engine suspends its writes; the check
|
||||
@@ -1237,6 +1250,134 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
line does to it, and how it composes with the armed window's disarm grace
|
||||
across a long hold.
|
||||
|
||||
19. **Pulse-header envelope (cloud mode).** The pulse header is the job's
|
||||
operating envelope, and the factory refuses to cut without it: 29 of its
|
||||
tags are mandatory, a header missing one is rejected, and a known tag that
|
||||
is not header-legal is rejected too. The service fills the safety-relevant
|
||||
ones with real values per job rather than echoing back what the machine
|
||||
reported. ForgeFIRM applies thirteen tags (the three run fan duties,
|
||||
`STfr`, the X/Y current, decay and microstep set, `ZSmd`) and drops the
|
||||
rest. Nineteen of the mandatory ones are among the dropped.
|
||||
|
||||
Nothing here can put energy where it was not commanded: the hardware chain
|
||||
is the emission boundary and no header field touches it, and forgectrl runs
|
||||
its own coolant ceiling, flow verification, emission witness, liveness gate
|
||||
and silence timeout. The gap is that the envelope enforced is ForgeFIRM's
|
||||
fixed one rather than the job's, and that several failure modes the factory
|
||||
watches have no counterpart here at all. In rough order of what a failure
|
||||
would cost:
|
||||
|
||||
- **Fan tachometers gate nothing.** Every tach is read for `/status` and
|
||||
the dashboard and none is checked against a limit. A fan that stalls
|
||||
mid-cut is invisible. Exhaust is the fume path and is the one to close
|
||||
first. The tach conversions are already in the facts bank; what is
|
||||
missing is a gate and a verdict, which belongs in the cooling engine next
|
||||
to the flow check.
|
||||
|
||||
**What the factory does here is now settled, and it is not what the
|
||||
header suggests.** Three facts, and they pull in different directions.
|
||||
|
||||
First, the header windows are empty and always have been. A cut job
|
||||
carries real fan duties (`AArd` 1023, `EFrd` 65535, `IFrd` 43278); a
|
||||
motion or hunt file carries `AArd` 204 and the other two at zero, which
|
||||
is correct, since those jobs do not cut. But `AArn`, `AArx`, `EFrn`,
|
||||
`EFrx`, `IFrn` and `IFrx` are zero in both kinds, and zero in pulse files
|
||||
captured from a stock factory machine on the live service, and zero in
|
||||
that machine's own settings report. The only movement in nine years is
|
||||
that the service started setting `AArx` to 64500 on cut jobs. Nor do the
|
||||
limits arrive out of band: across every captured session, counting every
|
||||
action type, the service has ever pushed seven keys, all image, camera
|
||||
and network. No limit that bounds the machine reaches it that way.
|
||||
|
||||
Second, the factory's own monitors are mostly disabled anyway. The intake
|
||||
and exhaust tach monitors install a lenient evaluator that treats a zero
|
||||
limit as "not configured" and returns in-range, so those two are disabled
|
||||
twice over. Air assist keeps the strict evaluator and does receive
|
||||
`AArx`, so it is the one fan monitor that can fire. Because the tach
|
||||
attributes are pulse *periods* and the kernel reports 0 for a stopped or
|
||||
absent tach, that alert means "no usable tach signal", not "slow fan".
|
||||
A stalled extraction fan is not caught by tachometer on a factory
|
||||
machine; it is caught by the temperature it causes.
|
||||
|
||||
Third, and this is the part worth adopting: when a fan tach alert does
|
||||
fire during a cut, the factory **pauses the print**. It posts a
|
||||
`HARDWARE_ALERT` to the hardware task, and in the running state that
|
||||
event takes exactly the same transition as a user pause, differing only
|
||||
in one flag. Not an abort, not a warning: a pause.
|
||||
|
||||
So the policy is known even though the numbers are not. Pick thresholds
|
||||
on the bench, let a header value only tighten them, and make the verdict
|
||||
a pause rather than an abort.
|
||||
- **Only the coolant loop has a temperature ceiling.** `cool_temp_max` on
|
||||
the upstream sensor is the whole thermal gate. Board, head, interconnect,
|
||||
lid, fused and power-supply temperatures are read and displayed and gate
|
||||
nothing, though the header carries a run max, a warmup max and a critical
|
||||
max for each, and `PTmn`/`PTmx` on the supply are mandatory.
|
||||
|
||||
**The factory stops for temperature far more readily than it does for
|
||||
fans, and the scale question is answered.** Its plain temperature alerts
|
||||
raise an aggregate `alarm` condition which posts `ENVIRONMENTAL_ALERT`,
|
||||
and during a cut that pauses the print on the same transition as a user
|
||||
pause. Its `*_temp_critical` conditions are in the machine-unusable set
|
||||
and post `FAILURE` instead, which is a different state and not a pause.
|
||||
Two tiers, then: every temperature alert pauses, and a critical fails the
|
||||
machine.
|
||||
|
||||
The units are per-sensor, not universal, which is what made the header
|
||||
numbers look wrong. The coolant loop is the worked example: the factory
|
||||
carries the same six per-phase limits twice, `CT{i,w,r}{n,x}` in raw ADC
|
||||
counts and `CM{i,w,r}{n,x}` in millidegrees, and the raw family's "min"
|
||||
is the *hot* limit because the thermistors are NTCs. Run the firmware's
|
||||
own beta conversion over the compiled `CT` defaults and round numbers
|
||||
fall out (4.01 to 50.01 C idle, 1.02 to 31.04 C warmup and run), which is
|
||||
the check that the conversion is right. A stock machine's live `CM`
|
||||
values are 10 to 30 C idle and 5 to 35 C warmup and run, with 1.0 C of
|
||||
hysteresis on each end. The conversions live in `UAPI.md`.
|
||||
|
||||
What remains is per-sensor: establish each family's scale from its own
|
||||
conversion before adopting any ceiling, and remember the factory reads
|
||||
four locations where ForgeFIRM exposes one chassis sensor, so three of
|
||||
the four header ceilings have nothing here to compare against.
|
||||
- **Coolant flow is not verified by the factory either, and its heater is
|
||||
idle.** Worth knowing before treating the factory as the reference for
|
||||
the cooling engine's flow check. The loop is built for calorimetric flow
|
||||
detection, a heater in line between two thermistors, and the firmware
|
||||
carries a full closed-loop controller for it: the `CF` family, with a
|
||||
setpoint, proportional and integral gains, a differential-temperature
|
||||
readout and min/max limits on it. None of it is armed. The service never
|
||||
sends those settings, a stock machine reports none of them, and the three
|
||||
faults it would raise (`coolant_flow_alert`, `coolant_flow_fault`,
|
||||
`coolant_heater_fault`) have no raiser anywhere in the factory image. The
|
||||
factory watches one coolant thermistor against a per-phase window with
|
||||
hysteresis, and that is all. Its heater is written only at phase changes,
|
||||
from a configured per-phase value, and nothing reads the result back.
|
||||
ForgeFIRM's flow verification is therefore ahead of the factory, not
|
||||
behind it, and nothing about the factory's numbers should be used to
|
||||
weaken it.
|
||||
- **No crash or tilt abort.** The head accelerometer is used only as the
|
||||
motion-liveness probe. The factory runs two tiers off the same sensor and
|
||||
the header supplies both: a per-axis *alert* threshold, which posts
|
||||
`MACHINE_INITIATED_PAUSE` and so pauses the print, and a separate per-axis
|
||||
*abort* threshold, which posts `ABORT`. `HAxr`, `HAyr` and `HAar` arrive
|
||||
every job. The lid accelerometer and the two tilt conditions ride the same
|
||||
structure, with `lid_tilt` and `head_tilt` grouped with the fan alerts
|
||||
rather than with the accelerometer ones.
|
||||
- **Beam detect is unread.** The kernel carries the latch; no userspace
|
||||
reads it. In the factory, `beam_detect_alert` pauses the print and
|
||||
`beam_detect_abort` aborts it, and a `beam_detect_report` is uploaded
|
||||
afterward at whatever severity the report-upload condition selects. See
|
||||
also item 15, which is circling the same hardware from the other side.
|
||||
- **`MCsn` and `PDfm` are not checked.** The factory refuses a pulse file
|
||||
whose serial does not match the machine, and refuses a pulse-data format
|
||||
it does not recognize. ForgeFIRM runs both. These are cheap: two
|
||||
comparisons at the top of the job path, and a refusal is a clean abort.
|
||||
|
||||
Deciding what to adopt is part of the work, not a foregone conclusion. A
|
||||
limit that arrives from a remote service is a limit that service can raise,
|
||||
so the sane shape is probably to take the header value as a ceiling that
|
||||
can only tighten a locally configured one, never loosen it. Whatever lands
|
||||
needs acceptance coverage in the same change.
|
||||
|
||||
**Deliberately not gated:** an armed GRBL job after an underrun cuts at the
|
||||
stale origin unless homing is required (GRBL mode permits unhomed cutting; the
|
||||
underrun itself alarms and unlinks the anchor). Not in the acceptance catalog
|
||||
|
||||
@@ -60,7 +60,7 @@ per-run `clamped` stat from the operator's own job log).
|
||||
### Milestone 2 — motion quality
|
||||
|
||||
**Milestone 2 (motion quality): bench-verified 2026-08-02.** The factory
|
||||
motion constants were extracted from the `_RESOURCES` pulse files
|
||||
motion constants were extracted from captured factory pulse files
|
||||
(`scripts/bench/puls_profile.py`) and applied end-to-end:
|
||||
- grblHAL defaults now factory-true: 12000 mm/min max rate (X/Y),
|
||||
700/590 mm/s² accel (X/Y). Machine tick default 28160 Hz (the factory's
|
||||
@@ -2373,9 +2373,8 @@ release is cut.**
|
||||
bench-validated 2026-08-17 on dev image `20260817124714`.** Both controller
|
||||
modes react to the lid, the remote-interlock loop and the button the way the
|
||||
factory daemon does. The factory behavior was decoded and then recorded on
|
||||
the bench machine booted into factory 2.6.0-2228; that session's log is
|
||||
archived under `_RESOURCES/factory-session-20260816/` (with a README indexing
|
||||
its five prints) and its measured numbers are in the facts bank in `BRINGUP.md`.
|
||||
the bench machine booted into factory 2.6.0-2228; that session covered five
|
||||
prints and its measured numbers are in the facts bank in `BRINGUP.md`.
|
||||
- **What the machine does, both modes.** Lid or interlock open during a job,
|
||||
running or paused: motion stops within milliseconds of the edge, the job is
|
||||
**cancelled and not resumable**, the head returns to the position the job
|
||||
@@ -2703,8 +2702,7 @@ model itself, with the analog path as the fallback.
|
||||
|
||||
Three cloud-mode cuts of the same 1" square, same location, same material,
|
||||
same speed, changing only the Glowforge UI power setting: Precision Power 1,
|
||||
Precision Power 100, then Full Power. Captures in
|
||||
`_RESOURCES/power-settings-20260817/`.
|
||||
Precision Power 100, then Full Power, with the pulse file captured from each.
|
||||
|
||||
Pulse-file capture ships off (`LOGGING.SAVE_PULS`), and the machine's copy of
|
||||
`/data/etc/gfhome.conf` predated the key, so it was enabled for this session
|
||||
@@ -2923,8 +2921,8 @@ duty cycle of 19.53 %** — 1.371 on-ticks of every 7-tick window. Fitting the
|
||||
three captures, the factory maps its entire 1–100 scale onto density
|
||||
18.9–79.5 %, with Full Power off that line at ~99.7 %. Its "1 %" is the
|
||||
bottom of the band that does useful work, not 1 % of the physical range —
|
||||
which is why no user ever meets the dead zone. The older `_RESOURCES`
|
||||
captures run 6.5–18.8 % density on other jobs, so 18.9 % is a product
|
||||
which is why no user ever meets the dead zone. Older captured factory
|
||||
jobs run 6.5–18.8 % density, so 18.9 % is a product
|
||||
decision about cutting, not a physical floor.
|
||||
|
||||
### The fix: a minimum pulse width
|
||||
|
||||
@@ -45,8 +45,8 @@ page's takeover does that; from a host, stop them first.
|
||||
| `flow_escalate_drill.py` | Coolant starved-re-check escalation drill (runs on the board, controller running): sets the engine's confirmation budget `cool_confirm_max_s` to a short value through forgectrl's settings (`flow_escalate_drill.py [budget_s]`, default 60, the setting's minimum) and restores it after; with the pump off the job-start check reads SUSPECT, the stagnant loop cannot pass the settle gate inside the budget, and the engine must escalate to FAULT. PASS/FAIL (exit status), leaves the machine idle. |
|
||||
| `flow_sampler.py` | Board-side coolant sampler used by the flow tools (`flow_sampler.py <duration_s> <interval_s>`, prints `elapsed,raw_down,raw_up`); run on the board (dev image: `/usr/share/forgetest/bench/`) so cadence does not depend on ssh latency. |
|
||||
| `temp_calibrate.py` | Coolant temperature spot-check helper (board or host): `watch [seconds]` / `point <measured_C> [note]` / `fit` — pairs a measured temperature with averaged raw ADC readings and fits a per-machine line to sanity-check the factory curve against a thermometer. Points accumulate in `temp_calibration.json` in the bench data directory. |
|
||||
| `build-glowforge.sh` | Cross-compiles **grblHAL-glowforge** (the canonical driver repo, `../../../grblHAL-glowforge`) in the Yocto build environment, borrowing the recipe toolchain. Run: `bash <path>/build-glowforge.sh` (from Windows, launch it through the WSL distro from PowerShell; Git Bash mangles /mnt/c paths). Env: `FF_SRC_TOP`, `FF_BUILD_TOP`. This is the production controller build. |
|
||||
| `build-forgectrl.sh` | Cross-compiles **forgectrl** (the canonical control-daemon repo, `../../../forgectrl`) the same way, borrowing the toolchain from the forgectrl recipe workdir (regenerate with `bitbake forgectrl` after a clean). |
|
||||
| `build-glowforge.sh` | Cross-compiles **grblHAL-glowforge** (the canonical driver repo) in the Yocto build environment, borrowing the recipe toolchain. Run: `bash <path>/build-glowforge.sh` (from Windows, launch it through the WSL distro from PowerShell; Git Bash mangles /mnt/c paths). Env: `FF_SRC_TOP`, `FF_BUILD_TOP`. This is the production controller build. |
|
||||
| `build-forgectrl.sh` | Cross-compiles **forgectrl** (the canonical control-daemon repo) the same way, borrowing the toolchain from the forgectrl recipe workdir (regenerate with `bitbake forgectrl` after a clean). |
|
||||
| `accel_fast.py` | Direct-I2C sampler for the two head-bus LIS2HH12s (runs on the board; unbinds/rebinds st-accel around the capture, 800 Hz ODR, ~270 Hz per device polled): optional mid-capture jogs via local grblHAL TCP. CSV to /tmp/accel.csv. The head accel is i2c-3 0x1e. |
|
||||
| `bump_seek.py` | Accelerometer bump-seek homing prototype (runs on the board): creeps toward a rail in bounded jog segments via grblHAL TCP, learns the moving-noise baseline per segment, detects the contact jolt (~530 Hz sampling, 2-sample confirm), jog-cancels (0x85) and backs off. CSV to /tmp/bump.csv. |
|
||||
| `build-feeder.sh` | Cross-compiles `feeder.c` the same way. |
|
||||
|
||||
@@ -7,7 +7,7 @@ peak axis/vector speeds, acceleration ramp slopes, and per-move segments.
|
||||
Used to derive factory-true grblHAL settings ($110/$111 max rate, $120/$121
|
||||
accel) for the ForgeFIRM step backend (milestone 2, motion quality).
|
||||
|
||||
Accepts either a raw header-stripped stream (_RESOURCES/MOTION/*.puls) or a
|
||||
Accepts either a raw header-stripped stream (a bare .puls body) or a
|
||||
full download with the GF1 header (magic at [1:4], total header length at
|
||||
[4:8], then 8-byte key/value records). Header settings (STfr, XSmm) override
|
||||
the --rate/--mode defaults when present.
|
||||
|
||||
Reference in New Issue
Block a user