E -- values and counts. The START_PRINT prologue has no M400 and an idle timeout of 864000, not 1800000. The nozzle-clean temperature comes from the material table FIRST, so NOZZLE=240 with no per-tool temp cleans at 220 -- documented backwards before. TOOLS is the paired 0:220,2:240 form. The wipe Z is the raw eddy frame, not the homed one. The runout M117 strings are "T<n> clog" / "T<n> out of filament", not E-codes. Grab-verify raises E0051+tool, not E0143. Docks are at X~297; 250/280 are staging waypoints. The grab sequence is documented in the order _grab actually emits it. ff_print exposes seven status fields, not nine, and never emits TOOLS=. Package sizes reconciled against the real artifacts (62MB built, ~93MB stock, ~370MB of CI assets, ~60s not 90s). testing.md's sample output block matched no header the code prints, so it is replaced with the real ones; its pytest row listed two of six suites. F -- stale claims. _FF_PREFLIGHT is not "missing". SDCARD_SET_CHANNEL is not still issued (764af2a). The app and Moonraker do not coexist; the app is gone. TOOLCHANGE_STATUS has nothing to compare against since the JSON reader was removed. The JSON is re-imported every boot until a nozzle is saved, not once. The 23-check render harness was deleted in91604c2. PURGE now wipes by default. 50-print-lifecycle.md gained the section on how the sequences are actually entered now -- [ff_print], not the slicer. The dead "profiles" concept is gone from three files, one of which printed it on every replica run. G -- overclaims. The README no longer says a stock reflash puts the printer back "exactly as it was": it says what is restored and what survives inert, which is what test-recovery actually asserts. The random ssh password is conditional on ROOT_PW_HASH being unset. verify.sh no longer warns "ssh will not start" on every build for a dropbear we deliberately do not ship -- the check now reads the way round it means. case-install.sh's read-only block says it asserts nothing. An uncalibrated tool contributes z_adjust, not zero, in three operator-facing places. offset_base does not cancel out in X/Y. The safe-moves file's Z50 promise is scoped to its own lines, with the implicit prepare's G1 Z10 spelled out. 48 passed, 5 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.2 KiB
Toolchange: sequences, sensors, mounted-tool state
Address-verified against the live binary (CommMgr methods). Ported in
payload/klipper/extras/ff_toolchange.py; divergences are deliberate and listed at the end.
Physical model
- 4 docks on the right, X ≈ 297 (250 and 280 are the staging and approach
waypoints,
x_safe/x_approach), per-dock X/Y fromextruder.json(x/y_check_pos..3), station basex/y/z_station_pos. - Lock motor on the carriage grabs/releases the head. Its endstop is registered on
manual_stepper gear_stepper;MOTOR_GRAB/MOTOR_RELEASEare STOP_ON_ENDSTOP-style moves that respond "endstop triggered / not triggered". - Sensors (
gcode_button):extruder_pos1..4= head present in dock N;extruder_grab(1..4)= something locked on the carriage;servo_min/max= lock mechanism end positions. - Invariant checked constantly: a head is EITHER in its dock OR on the carriage (E0127-134, E0139-146 cover every disagreement).
Grab — doGrabExtruderLatest @0x7a8190
- Home gate (abort if not homed, E0160).
- Dock-must-be-OCCUPIED precheck: 20 × 50 ms polls of
checkInLocation(tool); failure → E0127+tool, no motion. SET_VELOCITY_LIMIT ACCEL=8000;SET_GCODE_OFFSET X=0 Y=0 MOVE=1 MOVE_SPEED=100.- Approach, once, before the retry loop:
G1 X250 F<fast>(x_safe),G1 Y<dockY>,G1 X280(x_approach). - Then up to 3 attempts. Each begins by re-engaging the dock —
G1 X<dockX> F<slow>(+grabOffsetcorrection from test.json) — which is what undoes the previous attempt's back-off. Poll the grab sensor for up to 1 s; on successMOTOR_GRAB,G1 X<dockX-20> F4800(the app's literal pullback feed, not the calibrated one),MOTOR_GRAB2, and leave the loop. On failure back off toG1 X280and wait.G1 X250 F1500is the retreat after the loop, not part of an attempt. - Verify: dock sensor released AND grab sensor pressed → else E0051+tool
(
ERR_GRAB_VERIFY_BASE; the app's own code here was not recovered). - Apply per-tool G-code offsets (see
40-offsets.md). SET_VELOCITY_LIMIT ACCEL=20000.
Feedrates: test.json grabSpeed/grabSpeedSlow × 60, fallbacks 24000/6000.
Release — doReleaseExtruderLatest @0x7aa394
Not symmetric with grab; the details matter:
- Home gate.
- Dock-must-be-EMPTY precheck, 20 × 50 ms; failure → E0131+tool, no motion.
ACCEL=8000; clear X/Y offsets. No ACTIVATE_EXTRUDER anywhere on release.- Approach once:
G1 X250 F<fast>,G1 Y<dockY>. No X280 stage (grab-only). - Up to 3 attempts, each:
G1 X<dockX-10>(no F, modal feed) →G1 X<dockX> F<slow>(slow fallback here is 5400, not 6000) → wait up to 20 × 50 ms for the dock sensor to read "seated" → only thenMOTOR_RELEASE→ supervise the lock endstop; "ended untriggered" → re-issue MOTOR_RELEASE, max 3 sends per attempt. - Success only: retreat
G1 X250 F4800. On failure the carriage stays at the dock. - Verify: in dock AND grab sensor clear → else E0144; all-attempts-exhausted → E0135+tool.
- Unconditional tail (success AND failure):
MOTOR_STOP,ACCEL=20000(hence try/finally in the port).
In-process advantage: the app greps response text for "endstop (not) triggered" and
polls 40 × 50 ms; a klippy extra just catches printer.command_error from
MOTOR_RELEASE synchronously — a strictly stronger signal.
Mid-print change (changeExtruderChannel)
Save state (SAVE_GCODE_STATE NAME=PAUSE_state), manage temps, release old → grab
new, prime G1 E15 F240 / retract with part-fan ooze control, RESTORE_GCODE_STATE MOVE=1, restore M220.
Which tool is mounted
The app persists an imperative index (now_extruder in extruder.json), reset to −1 by
homing, and never derives it from sensors — because its grab-sensor read
(getGrabSensorStatus @0x76f294) is a plain OR of four bytes: it knows only that
something is held, never which.
ff_toolchange.py derives identity instead: extruder_pos1..4 are per-tool dock
switches, so the mounted tool is the one whose dock is empty (while the grab
sensor is pressed). Exactly-one-empty + held → that tool; all-occupied + held, or
several-empty + held → abort (faulty switch / ambiguous — refusing beats guessing,
since guessing wrong would drop the carried tool into another tool's dock).
Nothing is persisted; the value cannot go stale. now_extruder is never written
(see 40-offsets.md on why writing the JSON is forbidden), and it is no longer
read either: the JSON reader was removed, so TOOLCHANGE_STATUS reports only
sensors and Klipper config and has nothing to compare against.
Deliberate divergences in the port
- No 4th unsupervised
MOTOR_RELEASE(the app fires one before checking its own failure counter — energising the lock with nobody watching). - No
M119refresh polls — Klipper'sgcode_buttonstate is event-driven; the 20 × 50 ms gating semantics are kept as wall-clock deadlines. now_extrudernot written; identity is sensor-derived.- Modal-state restore added: the app leaks G90 + last feedrate + accel into whatever runs next; the port snapshots and restores absolute/relative mode, feedrate, and the pre-sequence accel limit.