mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
Drill the backtrack boundary, and pause a streamed print
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.
This commit is contained in:
+21
-12
@@ -297,9 +297,11 @@ factory 2.6.0-2228 session; measured numbers in the facts bank).
|
||||
- **Ignored:** a lid open during a hunt, homing, a jog, or at idle.
|
||||
- **The button pauses and resumes a job.** Cloud mode uses the factory's
|
||||
laser-off backtrack and resume lead (`cloud_pause_backtrack_ticks` 2000 /
|
||||
`cloud_resume_lead_ticks` 1950); GRBL mode uses feed hold / cycle start — the
|
||||
kernel refuses a backtrack on a live-streamed ring, so a resumed GRBL cut
|
||||
picks up where the deceleration ended. A pause is not a cancel: the latch
|
||||
`cloud_resume_lead_ticks` 1950), on a preloaded job and a live-fed one
|
||||
alike: the retrace is sized to `cnc/max_backtrack` and the lead follows it,
|
||||
so a pause with little history behind it shortens both rather than failing.
|
||||
GRBL mode uses feed hold / cycle start, so a resumed GRBL cut picks up where
|
||||
the deceleration ended (item 18). A pause is not a cancel: the latch
|
||||
stays unlocked and the window open across it. There is no resume dwell: the
|
||||
safing chain re-arms ~216 ms before the first step (facts bank).
|
||||
- **`lid_policy = hold`** selects stock grblHAL door behavior instead (park in
|
||||
@@ -662,9 +664,12 @@ not a release.
|
||||
Bench-verified on the 16 MiB ring the earlier images shipped, and the
|
||||
mechanism is size-independent: 20 MB streamed at 100 kHz through the wrapping
|
||||
ring, 0 ENOMEM, 0.4 ms max write latency, starve → `underrun` per protocol.
|
||||
The ring caps legacy cloud-mode job length (whole-file preload: ~1 MiB per
|
||||
100 s of 10 kHz stream, so ~56 min); the grblHAL live feed keeps only a few
|
||||
KB in flight.
|
||||
The ring holds ~1 MiB per 100 s of 10 kHz stream, so ~56 min of a cloud
|
||||
print at a time; a longer job is fed live as it plays, and the grblHAL feed
|
||||
keeps only a few KB in flight. The 32 KiB gap is retained history: the
|
||||
writer stops that far short of the play head, so any fill leaves 3.2 s of
|
||||
played program (at the print tick) to back a pause into, which is what
|
||||
`cnc/max_backtrack` reports less the deceleration tail.
|
||||
- **Reserved memory**: 511 MiB usable DRAM (`0x10000000`–`0x2fefffff`), of which
|
||||
96 MiB is reserved for DMA: the 32 MiB `cnc-pulsebuf` no-map pool (dynamically
|
||||
placed, `alignment = size`, so it lands at `0x2c000000`) plus 64 MiB of
|
||||
@@ -1248,13 +1253,17 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
laser-off backtrack (`cloud_pause_backtrack_ticks` 2000), then a laser-off
|
||||
lead back up to speed on the next press (`cloud_resume_lead_ticks` 1950),
|
||||
so the beam returns only once the head is retracing ground it already cut
|
||||
and is back at feed. That mechanism cannot be borrowed here: the kernel
|
||||
refuses a negative `resume` with `EPERM` once the ring has been
|
||||
live-streamed (`UAPI.md`), because the bytes to back into have already been
|
||||
overwritten.
|
||||
and is back at feed. The kernel offers that mechanism to a live feed as
|
||||
well: what bounds a backward run is the ring's retained history, not how
|
||||
the ring was filled, and the 32 KiB the writer must leave clear is 3.2 s of
|
||||
history at the print tick (`cnc/max_backtrack`, `UAPI.md`). What is not
|
||||
settled is the bookkeeping above it: a backward run moves the head and the
|
||||
kernel's counters while grblHAL's planner still holds a partly executed
|
||||
block, so borrowing the mechanism means reconciling the two, and a GRBL
|
||||
cut runs a much shorter queue than a cloud print does.
|
||||
|
||||
So the equivalent belongs above the ring, where grblHAL still holds what
|
||||
the kernel does not — the planned path. Shape to evaluate: capture the
|
||||
So the equivalent likely belongs above the ring, where grblHAL still holds
|
||||
what the kernel does not: the planned path. Shape to evaluate: capture the
|
||||
point where the beam went off at the hold; on the resume plan a laser-off
|
||||
retrace back along the path and a laser-off accelerate-in, and unmask FIRE
|
||||
only once the head is at feed and has passed the captured point. Open: how
|
||||
|
||||
@@ -764,20 +764,22 @@ def pause_resume(ctx):
|
||||
|
||||
|
||||
@test("cloud.oversize-stream", title="A print longer than the ring is fed while it plays",
|
||||
subsystem="cloud", kind="live", est_min=10,
|
||||
subsystem="cloud", kind="live", est_min=12,
|
||||
covers=_CLOUD_COVERS,
|
||||
requires=["cloud.lid-interlock-abort"],
|
||||
requires=["cloud.lid-interlock-abort", "cloud.pause-resume"],
|
||||
steps=[CLOUD_STEP,
|
||||
"Scrap on the bed and a LONG job ready in the app - one whose run time is longer "
|
||||
"than the ring holds (over an hour at the usual print tick). A full-bed raster "
|
||||
"engrave is the easy way to get one.",
|
||||
"Print from the app and press the button when it lights. Let it cut for about two "
|
||||
"minutes, then cancel the print from the app."],
|
||||
"minutes, pause and resume it with the button, then cancel the print from the app."],
|
||||
description="The service sends one pulse file for a print however long it is, and a long one "
|
||||
"is several times the ring: the machine holds the job in memory, fills the ring, "
|
||||
"starts, and tops the ring up as it drains. This checks the signature of that - "
|
||||
"the device in live-feed mode, the kernel's program total growing during the run, "
|
||||
"and no underrun - and that the job still cancels cleanly.")
|
||||
"and no underrun - that a live-fed print pauses and resumes the factory way, the "
|
||||
"ring having kept the history to back into, and that the job still cancels "
|
||||
"cleanly.")
|
||||
def oversize_stream(ctx):
|
||||
ev = ctx.evidence
|
||||
offset = enter_cloud(ctx)
|
||||
@@ -811,6 +813,27 @@ def oversize_stream(ctx):
|
||||
ev["underruns_during"] = after
|
||||
ctx.check(after == before, "the ring ran dry during the run (underruns %s -> %s)", before, after)
|
||||
|
||||
# The pause on a live feed: the ring keeps the history to back into, so a
|
||||
# streamed print retraces and leads back on exactly like a preloaded one.
|
||||
ev["max_backtrack"] = hw.sysfs_int("cnc/max_backtrack", 0)
|
||||
ctx.log("max_backtrack while the feed runs: %s steps", ev["max_backtrack"])
|
||||
ctx.instruct("Press the button once (pause), watch the head stop and back up a few "
|
||||
"millimeters, wait about 3 seconds, press it again (resume), then click Done.")
|
||||
got = wait_log(ctx, offset, ["button pressed mid-run; pausing", "paused at",
|
||||
"button pressed while paused; resuming"], 90)
|
||||
ev["pause_log"] = {k: bool(v) for k, v in got.items()}
|
||||
ctx.check(got["button pressed mid-run; pausing"], "the press did not pause the live-fed run")
|
||||
ctx.check(got["paused at"], "the pause did not settle (no 'paused at')")
|
||||
ctx.check(got["button pressed while paused; resuming"], "the second press did not resume")
|
||||
backtracked = [ln for ln in log_lines_since(GFCLOUD_LOG, offset)
|
||||
if "backtrack refused" in ln]
|
||||
ev["backtrack_refused_lines"] = backtracked[:2]
|
||||
ctx.check(not backtracked, "the live-fed pause could not back up (%s)", backtracked[:1])
|
||||
ctx.check(hw.sysfs_int("cnc/underruns", 0) == before,
|
||||
"the pause or the resume starved the ring")
|
||||
ctx.confirm("Did the head back up a few millimeters with the laser off on the first press, "
|
||||
"and pick the cut back up on the second?")
|
||||
|
||||
ctx.instruct("Now cancel the print from the app.")
|
||||
fin = wait_action_finished(ctx, offset, "print", 300)
|
||||
ev["print_finished"] = message(fin)
|
||||
|
||||
@@ -292,6 +292,89 @@ def k1_k2(ctx):
|
||||
"laser_enable/laser_on at 0 throughout")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- backtrack
|
||||
|
||||
@test("kernel.backtrack-bounds", title="A backward run is bounded by the history the ring still holds",
|
||||
subsystem="kernel", kind="auto", hardware="takeover", always=True, est_min=2,
|
||||
covers=_KERNEL_COVERS,
|
||||
requires=["kernel.k1-k2"],
|
||||
description="A pause walks the program backward to put the beam back over ground the job "
|
||||
"already cut, and the ring is what remembers that ground. cnc/max_backtrack is "
|
||||
"the distance still available: what has played, less the tail the deceleration "
|
||||
"spends. This drills the boundary on real hardware - the readback matches the "
|
||||
"bytes played, a step beyond it is refused rather than quietly shortened, and "
|
||||
"the run at the boundary plays out and returns to idle. Motors locked, latch "
|
||||
"locked, duty zero: nothing moves and nothing fires.")
|
||||
def backtrack_bounds(ctx):
|
||||
ev = ctx.evidence
|
||||
tail = TICK_HZ * TICK_HZ // (2 * 125000) # v^2/2a: 400 steps at the print tick
|
||||
with ctx.takeover():
|
||||
stream = POWER0 + PAD * (6 * TICK_HZ)
|
||||
ctx.log("%d bytes = %.1f s of pads at %d Hz; decel tail %d steps",
|
||||
len(stream), len(stream) / TICK_HZ, TICK_HZ, tail)
|
||||
snap(ctx, "pre")
|
||||
wr("cnc/motor_lock", 15)
|
||||
wr("cnc/laser_latch", 1)
|
||||
wr("cnc/ramp_rate", 125000)
|
||||
wr("cnc/step_freq", TICK_HZ)
|
||||
with PulseDevice(ctx) as dev:
|
||||
dev.rewind()
|
||||
wr("cnc/enable", 1)
|
||||
ctx.sleep(0.5)
|
||||
dev.write(stream)
|
||||
wr("cnc/run", 1)
|
||||
ctx.sleep(1.5) # well past the accel ramp
|
||||
wr("cnc/stop", 1)
|
||||
state = wait_state(ctx, "idle", 5, poll=0.01)
|
||||
ctx.check(state == "idle", "the controlled stop did not reach idle (state=%s)", state)
|
||||
|
||||
played = rd_pos()[3]
|
||||
budget = int(rd("cnc/max_backtrack"))
|
||||
ev["played"] = played
|
||||
ev["max_backtrack"] = budget
|
||||
ctx.log("played %d bytes; max_backtrack %d", played, budget)
|
||||
ctx.check(budget > 0, "max_backtrack is %d after %d bytes played", budget, played)
|
||||
# The two numbers come from different SDMA registers (the byte
|
||||
# counter and the ring head), so allow a few bytes of skew - but
|
||||
# not the whole gap, and not the whole played span.
|
||||
ctx.check(abs(budget - (played - tail)) <= 8,
|
||||
"max_backtrack %d is not the %d bytes played less the %d-step decel tail",
|
||||
budget, played, tail)
|
||||
|
||||
# One step past the boundary: refused, and the device stays idle.
|
||||
refused = None
|
||||
try:
|
||||
wr("cnc/resume", -(budget + 1))
|
||||
except OSError as e:
|
||||
refused = e.errno
|
||||
ev["over_long_errno"] = refused
|
||||
ctx.check(refused == errno.EPERM,
|
||||
"a backtrack one step past the boundary was not refused with EPERM (%s)",
|
||||
refused)
|
||||
state = rd("cnc/state")
|
||||
ctx.check(state == "idle", "the refused backtrack left the device %s", state)
|
||||
|
||||
# At the boundary: runs, decelerates inside genuine data, ends idle.
|
||||
wr("cnc/resume", -budget)
|
||||
wait_state(ctx, "running", 2, poll=0.005)
|
||||
hits, state = watch_laser_until_idle(ctx, 30)
|
||||
after = int(rd("cnc/max_backtrack"))
|
||||
ev["after"] = {"state": state, "max_backtrack": after,
|
||||
"faults": rd("cnc/faults"), "underruns": rd("cnc/underruns")}
|
||||
ctx.log("backtrack of %d done: state=%s max_backtrack now %d", budget, state, after)
|
||||
# Leave the program drained so the device ends where the other
|
||||
# drills expect it.
|
||||
wr("cnc/resume", 0)
|
||||
wait_state(ctx, "running", 2, poll=0.005)
|
||||
wait_state(ctx, "idle", 30)
|
||||
ctx.check(not hits, "the laser asserted during the backward run: %s", hits[:10])
|
||||
ctx.check(state == "idle", "the backward run ended in %s", state)
|
||||
ctx.check(ev["after"]["faults"] == "0", "faults=%s after the backward run",
|
||||
ev["after"]["faults"])
|
||||
ctx.log("PASS: max_backtrack tracks the played history, the boundary is enforced, "
|
||||
"and the run at it plays out clean")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- K3
|
||||
|
||||
def _k3_phase(ctx):
|
||||
|
||||
Reference in New Issue
Block a user