CAMPAIGN-LOG gains the 2026-08-24 drill: five minutes of openssl speed on
the one core over the live camera stream, 30 C chassis, plateau 70.8 C
with no throttle and every cooling device at state 0. BRINGUP's facts bank
drops its open question (whether ForgeFIRM's load wants the heatsink the
factory never fitted) for the measured fact: 14 C of headroom to the
passive trip under full load, a hot-chassis case above roughly 44 C.
Docs only, outside every layer; no acceptance consequence.
The attended nine passed after the unattended 36, so campaign
c-20260824231028-b7ca closed 45 of 45 from nothing and authorizes the
image. BRINGUP now describes the present: the 45-test catalog with the
bench actuator, the latest campaign, the board-only kernel's shape in the
facts bank (the performance governor, the SDMA clock ownership,
pstore/ramoops, MemTotal), the GPU stream path as the proven default.
Items 20 and 21 close; item 12 keeps only its leftovers and item 13 the
first release; item 16 records the hog case passing and the stream-live
drill still owed; a new item 20 carries the image trims the kernel review
left on the table.
CAMPAIGN-LOG gains the dated record of the attended run and, under the
superseded notes, the closed items as they stood plus the acceptance
burden plan and the kernel configuration review verbatim; both tree-root
working files are deleted.
No catalog consequence: documentation only.
The dual-stack listener reports every peer as a sockaddr_in6; ulfius
2.7.15 copied sixteen bytes of it, so forgectrl's loopback-only cooling
channel refused the controller's every report (403 loopback only) and
the engine never saw a run or an armed window. The recipe carries the
patch: a sockaddr_storage allocation and a copy of the family's length,
in the dispatcher and in ulfius_copy_request.
forgetest: forgectrl.auth asserts POST /cool/state from loopback -> 200
beside the LAN 403, and covers src/peer.*. BRINGUP item 21 and the
campaign log record how the campaign on dev 20260824215906 found it.
The check reads /sys/kernel/debug/clk/sdma/clk_enable_count and fails
below 1, naming the fault directly instead of the free-space mismatch it
surfaced as after the 150 s settle. BRINGUP item 21 describes the fault
and the fix; CAMPAIGN-LOG carries the record of how it was found.
Covers: image.health already maps kernel-module-glowforge/** and
linux-fslc/**.
With the access point's RA and DHCPv6 off, the board took the firewall's
lease; every service answered on the global address from another VLAN
and the board reached the IPv6 WAN gateway.
Docs only; no catalog consequence.
The missing GUA was not the firewall: its DHCPv6 server advertises an
address, but an access point on the bench VLAN still runs RA and DHCPv6
in server mode, its Advertise with nothing to give arrives first, and
busybox udhcpc6 keeps the first Advertise it sees. Dev 20260824201945
carries patch 0015 and the last stray dmesg line is gone.
Docs only; no catalog consequence.
The 2026-08-24 second-round record on dev 20260824200726: the dmesg
lines gone, the UP kernel on the performance governor, the two-file
WL18xx firmware set, every port answering over IPv6 on the ULA, the
export, and the two leftovers (no GUA from the DHCPv6 server, the NVS
loader line that patch 0015 removes). Item 21 says what remains.
Docs only; no catalog consequence.
The distro keeps its ipv6 feature (busybox networking and ifupdown
inet6, openssh, ntp and rsyslog follow); forgetest binds "::" by
default, one dual-stack socket that still serves IPv4. The release
image drops nano (with it file and the libmagic database, 8.7 MB; the
dev image keeps nano), the udev hardware database (7.7 MB of USB and
PCI identities for buses the board lacks, via BAD_RECOMMENDATIONS) and
urllib3's pyOpenSSL/cryptography recommendation (6 MB; nothing imports
them, TLS goes through the standard library). BRINGUP item 21 records
the round and the bench checks it owes; item 16 gets its re-measure
plan on the UP kernel with the performance governor.
Coverage: image.* covers the rootfs composition and the distro conf
(platform); forgetest's own suite covers the server.
kas/README.md keeps the build, lockfile, pin and release procedure and a
short build-time facts list; its push/release checklist and Scarthgap
migration backlog are archived verbatim in CAMPAIGN-LOG ("Superseded
status notes"). The real-time argument (PREEMPT, why PREEMPT_RT is not
selectable or needed) joins the BRINGUP hardware facts bank next to the
ring measurements it rests on; the camera pointers go to the 0011-0013
patch headers; the image recipe's gfui-client comment drops its README
pointer.
Docs and a recipe comment only; no catalog consequence.
BRINGUP gains Next-work item 21, the kernel trim and what it still owes
(a GRBL job and the acceptance campaign); CAMPAIGN-LOG records the
2026-08-24 review, the trim, and the bench drill on dev 20260824164619
through the forced-panic read-back from pstore.
Docs only; no catalog consequence.
The pin moves to forgectrl d97cb35: zero-based fragment timestamps and
the panel's clamped live-edge seek, the two faults MSE playback in a
real browser surfaced. CAMPAIGN-LOG carries the session record,
including the coexistence drill (a jog at commanded feed under both
live streams, underruns unmoved, planner buffer full). BRINGUP item 20
is down to one line: the full acceptance campaign on an image carrying
this pin.
The pin moves to forgectrl deee6a1: the render overlaps the previous
frame' copies and encodes behind an EGL fence, measured 13.8 fps
single-viewer at ~14 percent CPU and 9.8 fps with both stream types
served at once, luma bit-clean throughout. CAMPAIGN-LOG carries the
dated record; BRINGUP item 20 is down to MSE playback, the
coexistence drill, and the campaign.
The pin moves to forgectrl 2d59d78: the chroma passes point-sample
instead of box-averaging, taking the GPU render from 140 ms to 64 and
the stream to ~9 fps at ~7 percent CPU, with luma measured bit-clean
against the CPU path (which also retired the bottom-row artifact).
The CSI hardware frame skip is live-proven with the GPU path and is
the recommended low-CPU configuration. CAMPAIGN-LOG carries the dated
second-session record; BRINGUP item 20 now lists the render-encode
overlap as the path to 15 fps, then MSE playback, the CPU measure,
coexistence, and the campaign.
The pin moves to forgectrl 6614833: the five hardware corrections from
the GPU demosaic's first bench session (surfaceless EGL config, ARGB
render targets, GR88 raw import, the ipu_copy stride-fix crop between
the GPU and the CODA, the chroma mirror width). CAMPAIGN-LOG carries
the dated record of how each was found; BRINGUP item 20 now separates
what that session proved (the full GPU -> IPU -> VPU path serving
correct frames, H.264 as valid fragmented MP4 on hardware, texture
limits, slot fit) from what remains (the 140 ms render, the bottom
row, MSE playback, the CPU measurement, frame skip, coexistence, the
campaign).
The stream conversion now runs on the GC880 when the image carries the
GL stack, and /cam/h264 serves the same picture as fragmented MP4 in a
fraction of MJPEG's bytes (forgectrl 6573abd). This side supplies what
that needs and holds it to account:
- Distro: opengl stays a feature, with mesa trimmed to exactly the
path used (gallium etnaviv, GLES/EGL/GBM, no GLX, no display
platforms). Image: libegl-mesa, libgles2-mesa, libgbm,
mesa-megadriver. forgectrl dlopens them, so an image without them
still streams on NEON. Platform change: the next campaign is full.
- forgetest: camera.h264-stream exercises the endpoint end to end
(codec header, init segment, media fragments, /cam/status encoder
state) and accepts a clean 503 on a machine without the stack;
the camera covers name the new forgectrl files.
- VIDEO.md 5.6 now describes both streams and why H.264 earns its
place (the WiFi transmit path measures about 7 percent of the core
per MB/s sent); BRINGUP records the bench-validation checklist as
Next work item 20, including the release-size watch: mesa must fit
the 200 MiB slot gate.
fixture.py: the bench's /data/forgetest/fixture.json (hostname, key,
optional ip, the channels wired, arm_press), a resolver for
<hostname>.local asked of the network directly (the image has no mDNS
resolver), and the client. The runner probes it before every run and
at most every 30 s otherwise; ctx.act asks it for a channel it covers
and still waits for the machine's own reading, falling back to the
operator's notice when the box fails. A test declares with hands=(...)
what it asks of a person beyond its typed actions; an operator test
with none, whose actions the fixture covers, is routed into the
unattended queue, its Ready gates pass, and a prompt it raises anyway
is a FAIL naming the undeclared step. Live tests never move; their arm
press stays a person's unless the bench opted in, in which case the
fixture presses when the button lights. Whatever the box still holds
after a run is released before the baseline's post pass and recorded.
The page shows what the fixture covers. Contract in ACCEPTANCE.md; the
wiring facts, with the interlock connector left to the bench to settle
(SAFETY.md and the sister map differ), in BRINGUP.
Catalog unchanged in its definitions; the cloud and laser suites'
shared code moved, so their implementation hashes move with it.
Dev image 20260823161333, campaign c-20260823161923-0dd7, 44 of 44,
release authorized. The cloud split of the acceptance plan is complete:
the protocol by the emulator with only the app to drive, the machine's
print behavior by the offline service, the two together by one real
print, and the service's hunt paid only where it is the subject.
The service-protocol half of the cloud catalog on its own test: the cloud
client restarted as gfutilities' emulator under the /run/gfcloud-emulate
marker signs in, passes the firmware check, opens the WebSocket, answers
the connect-time hunt and the image requests with the dev image's canned
frames, and runs a print from the app through the real download path to
':completed' - nothing moves, nothing arms, and only the app has to be
driven, by a person or an agent through the prompt API. The real client
is restarted afterward and its hunt waited out. session_live now knows
the emulator's session is not the machine's, so enter_cloud restarts it
rather than reusing it; restart_client is the one restart the offline
and emulator entries share.
The dev image adds python3-gfutilities-emulator (the fixtures, packaged
on their own in meta-openglow); forgefirm-app moves to 12ad3b1 (gfcloud
--emulate). Catalog: 44 tests, 27 auto / 9 operator / 8 live; the new
test covers the gfutilities service layer and examples/, which step 4
will take off the other cloud tests. Replays over the prompt script;
contract and BRINGUP updated. A layer change (the dev image recipe):
everything re-requires on the next image.
Every offline test now removes the jobs it wrote under /tmp/forgetest
on its way out (the bench rule: nothing left behind in the session that
put it there). CAMPAIGN-LOG gets the 2026-08-22 entry for dev image
20260822232347: the offline service dry-checked, then campaign
c-20260822233344-08de, 13 run and 43 of 43 with the four offline tests in
5.5 minutes, and what the machine said under it. BRINGUP: the offline
service is done and bench-validated.
Four cloud tests no longer need the app, an account, a network, or
anything on the bed: cloud.lid-interlock-abort, lid-during-button-wait,
paused-lid-cancel and oversize-stream run under the offline service
(enter_offline restarts the cloud client with the /run marker for that
one start; Offline is the socket; offline_job writes the job). The jobs
come from forgetest/puls.py: the header of a factory print of this
machine type (134 tags, MCsn 0, so the client's limits and settings come
from where a service job's do) over a square traced at a steady feed
with a leading power byte of zero and no LASER bit anywhere - the arm
unlocks the latch, the beam is never commanded, so the tests stay live
and need no scrap. A job longer than the ring (33 MiB of ticks, an hour
of squares) is an 87 kB gzip written in a tenth of a second, in place of
a full-bed raster designed in the app.
session_live reads the offline mark as "no web session"; enter_cloud
restarts an offline client with the service, so cloud.pause-resume (the
one real print left, with cloud.mode-switch the service-protocol half of
the catalog) follows the offline tests without the operator's hand.
Replays over a fake socket; the contract and BRINGUP say how the cloud
catalog splits. Catalog consequence: the four re-ported tests move;
nothing else is invalidated.
CAMPAIGN-LOG: the 2026-08-22 entry for dev image 20260822204234 - the
three harness findings (the jog witness, the fading button LED, the
duplicated app cancel), the implementation-hash change they exposed,
and campaign c-20260822220701-a1c0: 43 of 43 from nothing, the 16
attended tests in 19 minutes, with what each new witness read on the
machine. BRINGUP: bench-validated, the latest campaign, the hash rule.
A campaign asked a person for about eighty things: lid, button and
interlock actions, app jobs, and sixteen confirmations by eye, most of
them as popups to read and answer while the head was already moving.
This is the forgetest-only step of cutting that down.
The operator channel. A test asks for its operator's part in four ways:
ctx.ready() pre-announces a timed step and waits for the click that
starts it; ctx.notice() is a standing instruction with no button, the
test watching the machine for the result; ctx.act(channel, state) is a
machine action by name (lid, interlock, button) - a notice for the
operator today, proven done by the switch reading or an `until`
condition, recorded in evidence.actions with who performed it, and the
seam a bench actuator plugs into through runner.fixture; ctx.confirm()
stays for the yes/no the evidence cannot answer. Tests declare
`actions`; a `precheck` refuses a start the machine cannot honor (a
reason, no result, a queue skips it and carries on).
The page shows what you will do before it is asked: the running test's
steps, a queue's attended tests still waiting, or the test whose title
you clicked while idle; notices and prompts sit under it. The campaign
card no longer carries baseline, queue and leftover notes: those go to
the runner journal (daemon.log, syslog as `forgetest`, and the run in
progress), with a Runner journal button in the footer.
The catalog, 43 tests (27 auto, 8 operator, 8 live; was 45: 25/12/8):
cloud.mode-switch absorbs cloud.hunt-lid-open and cloud.gfhome-homing
(the connect made with the lid open, the hunt judged lid-open with its
Z cycle, the re-hunt waited out after the close, the switch back, then
$H judged by gfhome's own "homing complete" line with its motion
windows; precheck homing_mode = gfcloud). kernel.fire-line is auto with
the HV-not-good precheck, camera.snapshot is auto (a second frame with
the lid lamp off differs and is smaller), motion.jog-roundtrip is auto
(the head accelerometer per leg, the supervisor's own witness). The
remaining attended tests use Ready gates and act(); the head's beam
detector and the button LEDs replace the eye, leaving two confirms: the
emission witness's mark and the app's display in cloud.pause-resume.
Proof: tests/test_operator.py (the channel, the precheck, the journal),
the cloud replays re-targeted to the merged round trip over a fake grbl
and the bench excerpts, 196 host tests green, coverage lint 0 uncovered.
Every re-ported attended test is owed one bench run on the next dev
image (BRINGUP). Catalog consequence: the merged and reclassified
tests' implementation hashes move; nothing else is invalidated.
forgectrl pin b27398a: the SoC die joins the watched board temperatures
(/status temps soc_c and the kernel's CPU cooling state soc_throttle,
the Status tab, the per-job range line naming a throttle), a throttle
starting or ending is logged, and the supply sensor stays a raw count by
decision (its heatsink cannot be reached with a thermometer while the
machine runs). forgefirm-app pin e65cfc2 (0.1.16+git): every pulse-header
key without an applier is declared with its reason or counted as
undecided in the job log, and CLOUD.md carries one disposition table for
the whole header.
BRINGUP: the pulse-header envelope item is closed. Its durable content is
in the facts bank ("The factory's envelope, decoded": the mandatory tags,
the empty tach windows, the factory's pause and fail tiers, the
per-sensor units, the unarmed flow controller, and ForgeFIRM's answer
with its catalog proofs) and in the "Deliberately not gated" paragraph,
which names every declared family. Item 19 is now the bench-measured
head crash and rail-contact detector; the fire-watch item holds the
header's lid IR thresholds as its prior. The facts bank also records the
SoC's own thermal guard (85 C passive, 90 C critical, no heatsink on the
factory board) and the board temperatures at idle.
Catalog: cooling.gate-off checks the die field, the unthrottled state at
idle and the widened run-end line (the unit fake mirrors it). COOLING
section 9 and the SERVICES verification status describe the present.
forgectrl pin 76115fd: the chassis LM75 and the supply sensor ride /status
as temps (degrees and a raw count), the engine ranges them over every run
session into one run-end line, and a critical fault that clears with its
session yields the reason to the standing hold.
Bench: critical_tier_drill.py (a bench tool now, registered as
critical-tier) sets the ceiling, the resume gate and the critical line a
few tenths above the live upstream reading and lets the engine's own
flow-check heater warm the loop through them inside one M8 session;
temp_calibrate.py gains supply-watch, supply-point and supply-fit for the
supply sensor against a thermometer on its heatsink, the fit shown beside
UAPI.md's unverified guess.
Catalog: cooling.gate-off checks the /status temps fields and the run-end
board-temperature line (the unit fake mirrors both, one new failure
case); cooling.critical-tier checks the reason after a faulted session.
Docs: CAMPAIGN-LOG entries for cooling.critical-tier on dev image
20260822154257 and the warm-loop drill (OVERTEMP at 10 s, CRITICAL at
14 s, the fault ending with the session); BRINGUP item 19, the facts bank
(board temperatures at idle), COOLING section 9, the bench README.
forgectrl pin a1875a8: cool_temp_critical_c (default 38 C) is the fail
tier above the coolant ceiling's pause: at or over it in a run session the
verdict is CRITICAL (fire blocked, hold, no resume this job), the fault
ends with the session, the settings API keeps it above the ceiling, and
its top turns the gate off.
Catalog: cooling.critical-tier (auto, 45 tests), driven through the
settings API like cooling.gate-off: a critical line at the ceiling is
refused (and undone should it ever be accepted); with the ceiling, the
resume gate and the critical line all under the coolant's temperature a
session reads CRITICAL rather than OVERTEMP with no resume and the reason
naming the tier and the coolant; after the session the ceiling alone
holds (OVERTEMP); with the critical line at its top the gate is off
(gates_off, the run-start log line) and the ceiling alone pauses;
restored, OK with nothing off. Four unit cases against a scripted engine.
_after_session takes the condition to wait for.
Docs: COOLING (verdict table, section 5, the settings table, the quick
reference), BRINGUP item 19 and the catalog count, the CAMPAIGN-LOG
paragraph for the fan-fault session rule on dev image 20260822145201.
The dated record of dev image 20260822135848: the campaign of every
non-operator, non-live test at 18 of 18 PASS with the measured floors and
the operating-point rule (cooling.fan-gate-trips and the hunt leg of
cloud.mode-switch as recorded), and the unplugged-exhaust-fan drill:
AIRFLOW at the grace plus three ticks with the exhaust dead, the other
fans held, the reason relayed on the Grbl port, the replugged fan ok
inside the next session's grace.
The drill showed the fault riding into idle, where the hold canceled
jogs and would have refused the cloud print that re-proves the fan.
forgectrl pin d51dbdb: the fault ends with its run session, and the next
session judges every fan afresh. cooling.fan-gate-trips checks the
verdict is OK with no hold once the tripped session is over (the unit
fake mirrors it); its covers, and the cooling tests' shared covers, gain
src/coolfmt.* (the tree manifest carries the new files at the bumped pin,
and the lint was right to ask). cloud.mode-switch samples the hunt's gate
rows twice a second: a hunt's run phase is a few seconds long.
Docs: COOLING 3a, BRINGUP item 19, CAMPAIGN-LOG.
forgectrl pin 47e4256: the airflow floors set from the bench measurement
(exhaust 6400, intake 2290, air assist 6000 rpm, purge current 300, grace
15 s) and the operating-point rule: a fan is judged while the laser is
armed, when a job's profile may raise it but never lower it below the run
duty, or whenever it is commanded at the run duty; the service's hunts,
sent with the extraction fans off, are measured and published unjudged.
Catalog: cloud.mode-switch now waits out the connect-time hunt sampling
/cool/status and requires no AIRFLOW, the exhaust row unjudged and the
exhaust actually off; its covers gain forgectrl src/cool.* and
src/airflow.*. cooling.fan-gate-trips uses an 8 s test grace (the
intakes take 7 s to 90 percent and tripped under the old 2 s at the new
floor) and its off leg waits for the row state as well as gates_off.
fan_floor_measure.py names a reply that is not JSON and calls the purge
readings idle and run (the pump is always on).
Docs: COOLING 3a and the settings table, BRINGUP item 19 and a facts-bank
entry with the measured fan speeds, the bench README, and a CAMPAIGN-LOG
entry for the measurement, the two status-document finds, the hunt find
and the hot-deployed bench runs of both tests.
The acceptance catalog gains the test behind forgectrl 5a31d66: an
exhaust floor at the legal maximum must trip AIRFLOW after the grace and
three ticks (hold, fire blocked, no resume, the exhaust gate TRIPPED and
named in the reason); a purge current floor at the ADC rail must trip
the same way; an exhaust floor of zero must read off in gates_off and
trip nothing; restored, the next session must run OK with every fan at
or above its floor. Three host cases against a scripted engine that
models the grace and the readings. The covers map names src/airflow.*.
COOLING.md gains 3a (the airflow gates) and the five settings with their
ranges and bands; SERVICES.md the gate rule, the rows, the AIRFLOW
verdict and fan_gates; SAFETY.md names the floors among the verdict's
inputs; BRINGUP item 19 records the gates as in with provisional floors
and the catalog at 44. Pins: forgectrl 5a31d66, forgefirm-app 81027ff
(0.1.15+git); fetch-verified.
cloud.pause-resume passed on dev image 20260821220926 with the service's
hunt windows (10 to 50 C) ignored as looser and the print's window (33 C,
5 C floor, 116 rpm air-assist floor) matched. The test now quotes the
print's job-limits line rather than the session's first (a hunt's), and
keeps the engine line that carries the header beside the last one.
forgectrl pin moves to e0b41b3 (the "not stricter" notice once per value).
The pulse header's envelope now reaches the cooling engine: the cloud
client (python3-gfhardware c34faa1) derives the coolant window and the
fans' minimum speeds from the header and rides them on every report,
and the engine (forgectrl 57f6064) resolves each as the stricter of its
setting and the job's, never looser and never overruling an off gate.
cloud.pause-resume, which runs a real print, now also reads the client's
"job limits from the header:" line and the engine's "effective limits:"
line from the two logs; two host cases cover the failure paths. The
needle guard lists the engine's phrases as not the app's.
COOLING.md section 2 explains what a cloud job brings with it; BRINGUP
item 19 records the pass-through as landed. Pins: forgectrl 57f6064,
forgefirm-app c34faa1 (0.1.14+git); fetch-verified.
First bench run of the gate-settings pattern: PASS on dev image
20260821210903 after the harness fix in f274eb1; the entry records the
failed first attempt and why it was the test's fault. BRINGUP item 19
carries the bench result.
The acceptance catalog gains the test behind forgectrl 9e44fdc: the
coolant ceiling set just over its legal minimum must trip OVERTEMP with
a hold and fire blocked at the next run start; set to its top the engine
must skip the gate (verdict OK), report it in gates_off on /status and
/cool/status, and log the run-start line; the original values are
restored, on failure too, and proven restored. Five host cases against a
scripted engine. The cooling covers map now names the files that carry
gate state (gates, settings, status, the panel) and corrects a glob that
matched nothing: the GRBL cooling client is src/glowforge_cooling.c, not
src/gfcool*. The fake forgectrl serves /logs/tail and keeps blank form
values as "clear", which is what the daemon does with them.
Docs: COOLING.md section 8 carries each setting's legal range and
recommended band and a new 8a on turning a gate off; SAFETY.md names
what no setting can reach; ACCEPTANCE.md records that gates are
exercised through the settings API, never GFCOOL_* env overrides;
BRINGUP item 19 records the pattern as landed and the catalog is 43.
forgectrl pin moves to 9e44fdc; fetch-verified.
The full acceptance campaign on dev image 20260821181036, the first built on
the <recipe>-pin.inc layout, satisfied 42 of 42 and its export authorizes a
release, so the status header, the acceptance section and item 12 describe
that rather than a campaign still owed; the catalog count is 42, not 35.
CAMPAIGN-LOG records the completion. Item 7 stops listing the cloud work the
campaign closed (the oversize live print, the progress bar watched from the
app, packaged-path cloud boot, the lid flash), records the operator's
declines (SPKI pinning, emulator parity, the ten-event pause machine) and the
reasoned memory guards, and item 19 no longer lists MCsn and PDfm as
unchecked: the client refuses both, so seventeen mandatory tags are dropped,
not nineteen.
forgefirm-app pin moves to b82d379 (python3-gfhardware): CLOUD.md and three
present-tense comments, no behavior change. No catalog consequence beyond the
pin bump itself, which the manifest scopes to the component.
They were open because the service has never sent them and cannot be asked
to. That is not a reason to leave them open: the factory application says
what each one does, and it says all four are hand-offs to programs this
machine does not have. BRINGUP's cloud item now records the disposition
rather than the question.
Two tests already run a print end to end, and progress is a property of a
running print, so the checks go there rather than into a test of their own
that would cost the operator another job.
cloud.pause-resume takes the job that fits the ring: the client names the
length it is reporting against, and the operator is asked the question only a
person can answer, whether the bar actually moved.
cloud.oversize-stream takes the job that does not fit, which is where a moving
denominator would show: the kernel's program total grows all run long under a
live feed, and the test already samples it growing, so the check is that the
figure progress divides by is larger than that - the job, not the count the
ring had swallowed when the run started.
The forgetest replay plays a captured log from a build that predates the line,
so it carries the line where the current build emits it, as it already does
for the warm-up and the rest.
BRINGUP's cloud item now says a print reports itself again, and what is left
on it is a print watched from the app.
The campaign log gains the F1 entry: the question was which of three carriers
the factory uses for the progress bar, and the answer is that two of them
collapse into one. Progress rides an outbound WSS type:"progress" frame that is
itself the periodic settings report, every 30 s. The write-up lives in CLOUD.md;
the log records how it was gotten and what else the capture proved, including a
factory progress total that grew 256 KiB per interval because the factory
live-appends to its own ring.
BRINGUP's cloud item now lists F2 among the open work with the carrier settled,
so what is owed is emitting the frame against the feeder's job total rather
than the kernel byte counter.
Documentation only, no behavior change, so no acceptance-catalog consequence.
cloud.pause-resume runs a print end to end, which is exactly what the job
lifecycle needs to be seen: a non-zero hold before the first fire, a non-zero
rest after the park, and neither on the connect-time hunt in the same
session. Folding the checks in there costs the operator nothing, where a
test of its own would cost another print.
The forgetest replay noticed first: it plays a real captured log from a build
that predates those lines. Rather than editing what the machine said that
day, the replay carries the two lines where the current build emits them.
BRINGUP's cloud item now names what is actually open on the header keys, the
park and the two periods, and records that the pause constants were looked
for in the wrong place.
BRINGUP's cloud item said a print is capped by the ring and listed the two
gaps that closed when it stopped being. It now says what bounds a print
(memory, through pulse_reject_threshold_bytes) and what catches a feed that
wedges (progress against room, not ring depth), and it names the two new
cases that cannot be induced from the bench: a body past the memory guard,
because the service has no such job to send, and a wedged feed, because a
healthy machine will not stall on request.
forgectrl.settings-bounds gains a probe at the far end of the new byte
range, so the validator behind those keys is exercised rather than assumed.
kernel.backtrack-bounds plays a program, stops it, and holds the readback
to the bytes it played less the deceleration tail: a step past the boundary
has to be refused rather than quietly shortened, and the run at the boundary
has to play out and come back idle. Motors locked, latch locked, duty zero,
so nothing moves and nothing fires.
cloud.oversize-stream pauses and resumes the live-fed print it already has
running. That is the pause the kernel change makes possible, and it costs a
minute of a job that is on the bed either way.
BRINGUP's pause bullet, its ring facts and item 18 all said a ring under a
live feed has nothing left to back into. The gap the writer keeps clear says
otherwise; what is still open for GRBL mode is the bookkeeping above the
ring, not the kernel below it.
The ring and the device-tree pool that backs it are now the size the
factory firmware runs. Docs follow, and the hardware facts bank gains the
reserved-memory map: 511 MiB usable, 96 MiB reserved for DMA, ~454 MiB
left to Linux.
image.health reads the pool size and ring_mb back and fails if they
disagree, so a pool the parameter does not use cannot ship unnoticed, and
checks that free never exceeds the ring less its gap.
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.
MOTION said jogs, homing and hunts were all ungated. Jogs are:
gfsw_visible withholds the door signal while the core is idle, jogging or
homing, so a jog both starts and runs with the lid open. Homing is not.
With homing_mode = gfcloud - the only method that works today - $H hands
the cycle to a cloud homing session, and its move to the home corner is
an ordinary motion action taking the default lid_gated=True: refused with
the lid open, stopped on a lid edge mid-run. Only the lens/Z hunt inside
that session passes lid_gated=False, and that session is also the only
place a hunt happens in GRBL mode - there is no hunt outside one.
BRINGUP gains item 18: GRBL pause and resume should leave no gap in the
cut, the way the factory's does. The beam stops at the start of the hold
(disable_laser_during_hold, on by default), so the head travels the whole
deceleration dark and the resume restarts from a standstill where the
decel ended - an unburned length, then a dwell through the accel that M3
shows as a deeper spot. The kernel waypoint backtrack cloud mode uses is
refused with EPERM on a live-streamed ring, so the equivalent has to be
built above the ring, where grblHAL still holds the planned path the
kernel has already overwritten.
Two wording fixes: the cooling verdict is described as a report rather
than a file, and gfcloud homing as using the machine's builtin
credentials.
Documentation only - no behavior change, so no acceptance-catalog
consequence.
grblHAL-glowforge 03d70e0, pinned here, makes density the shipped model
and $35 the density floor at 10. Every analog session in the stream
harness now selects its model rather than inheriting it: without that the
flip would have quietly turned them into density runs and taken the
analog fallback's coverage with them. laser.power-floor carries the new
floor and its PWMSAR minimum, and its description no longer describes
$35 as a duty floor.
All ten stream sessions pass on the new defaults, with the analog duties
shifting exactly as the floor predicts - min_value 12, gradient 0.115 -
along with both C harnesses and the lifecycle harness.
Recorded in BRINGUP and the campaign log, including what the defaults
rest on and what they do not: the seven ladders ran at F300 and F100 at
constant power, so production feeds, M4 into corners and the raster path
remain unproven on hardware.
$35 = 10 under the density model is a density floor, not a duty floor. It
maps S onto 9.4-100 percent density, so a commanded 1 percent lands at
10.2 percent, just above the marking floor the earlier ladders measured.
A ladder reweighted to the bottom of the user scale - 1, 2, 5, 10, 20,
40, 70, 100 percent of S - marked on all eight rungs, with eight current
segments over a 42.0 s window against exactly 8 x 5.25, and segment means
climbing 136 to 968.
That meets the goal the ladders started from: a user's 1 percent is a
real visible mark rather than silence, and 100 percent is full power. It
took all three pieces - density so every level is real pulses, the
minimum pulse so they stay strikeable, the floor so the user's range sits
on the band that works.
dladder no longer tells the operator to re-run at other base periods to
choose one. The period cancels out of the low end, and what a failing
rung now indicates is a floor set too low.
min_ticks 6 broke 5 percent striking - seven current segments where 3 gave
eight, on a 36.5 s fire window against 41.4 s for eight rungs. Below the
minimum the model emits min ticks every min/on periods, so the interval
between pulses is min_ticks x tick / density and the base period cancels,
which is also why periods 10, 20 and 40 gave identical results earlier.
At 5 percent that is 2.26 ms at min 3, which struck, against 4.51 ms at 6,
which did not: doubling the minimum doubles the gap as well as the pulse,
and the discharge is re-struck each pulse.
min 3 sits at the factory's own operating point - its 6.5 percent engrave
jobs place 100 us pulses 1.54 ms apart against 1.64 ms for min 3 at that
density - and 6 is outside anything the factory does. The bench is back
at 3.
Measured band for this tube: strikes from ~5 percent density, marks from
~10 percent at F300. That closes the pulse-structure route to a usable
1 percent, since the interval grows as 1/density and 1 percent implies an
11 ms gap. The low end is a scaling problem, and $35 is the control.
The fifth ladder, the first with a minimum pulse, moved the floor down a
full rung: only 5 percent failed to mark, and 5 percent now strikes. The
trace carries eight current segments where the run before it had seven,
with fire beginning at 6.2 s exactly at rung 1 and the usual flat
saturated final segment anchoring the count from the other end.
That retracts what the previous entry concluded. Pulse length is not
irrelevant: 10 percent moved from no mark at F100, with three times the
dose per millimeter, to a mark at F300 at the same density, the only
change being its pulses growing from 36-71 us stubs to 106 us. The
matched-pairs argument was sound but drawn entirely from comparisons at
or above 20 percent density, where every pulse length in play was already
long enough - it generalized from the one regime where pulse length does
not bite. Above ~100 us dose governs; below it pulse length does; below
~36 us the supply does not strike. The factory's 100 us quantum sits on
that boundary.
dladder now reads laser_pulse_min_ticks and prints what is actually
emitted. Without that its table reports the pulse density alone would
give, which is wrong wherever the minimum applies - at min 6 the bottom
four rungs all emit 213 us and vary their rate instead, and the operator
reads that table to interpret the material.
Rule 15 in the stream harness holds both halves of the minimum
(grblHAL-glowforge f7e8c17, pinned here): no emitted burst falls below
laser_pulse_min_ticks, excepting one clipped by fire going off mid-burst,
and the levels too faint to fill a window still render their exact
average density. Checked against a run at minimum 1 so it cannot pass
vacuously - level 2 goes from 444 bursts of one tick to 147 of three at
the same density, and levels already above the minimum are unchanged.
Four bench ladders settle the base period at 20. The same six rungs
marked in all of them, and the matched pairs across periods separate the
variables: at identical pulse length, halving the density killed the
mark; at identical density, varying the pulse 3x changed nothing. Feed
does not move it either - 10 percent at F100 carries 44 percent more
energy per mm than 20 percent at F300, which marks, and still left
nothing. The low-end marking limit is average power, not dose per length
and not pulse length.
The F100 trace separates two failures that look alike on the material:
seven current segments for eight rungs, anchored by a flat saturated
final segment that can only be full density, put 5 percent at no
discharge at all and 10 percent at a full 15 seconds of current with no
mark. Only the first is ours, and the minimum pulse is the answer to it.
Also recorded: the factory's Precision Power 1 runs a 19.53 percent FIRE
duty cycle at full PWM duty, and its 1-100 scale maps onto density
18.9-79.5 percent, so its 1 percent is the bottom of the useful band
rather than 1 percent of the range. Under the density model $35 and $36
are that same control - a density floor and ceiling - which makes the
user-facing scale a settings choice rather than new code.