Makes the Creator 5 Pro's 4-tool toolchanger and print lifecycle work for
prints started from Mainsail/Moonraker (e.g. sliced in OrcaSlicer), without
the stock touchscreen app in the loop. Based on reverse-engineering the
stock firmwareExe binary (addresses referenced in the source comments),
with some behaviour improved along the way — deliberate divergences are
documented in the file headers.
| File | Goes to (on the printer) | What it does |
|---|---|---|
klippy-extras/ff_toolchange.py |
/usr/prog/klipper/klippy/extras/ |
The toolchanger: T0..T3, dock/grab state machine with sensor polling and retries, per-tool G-code offsets, TOOLCHANGE_SET_PRINT_OFFSET (the absolute print-start Z offset), TOOLCHANGE_STATUS, TOOLCHANGE_RELOAD, TOOLCHANGE_PARK |
config/ff-toolchange.cfg |
/usr/data/config/ |
Configuration + SDCARD_PRINT_FILE wrapper. Carries no per-unit numbers — calibration is read live from firmwareExe's own JSON |
config/ff-print-macros.cfg |
/usr/data/config/ |
START_PRINT / END_PRINT / PAUSE / RESUME / CANCEL_PRINT, reconstructed from the app's sequences |
orca/ |
OrcaSlicer printer profile | Machine start/end G-code, change-filament G-code, example project |
docs/notes/ |
(reference only) | Condensed reverse-engineering notes: what the stock app actually does, with binary addresses |
This is unofficial, reverse-engineered firmware modification. It is not endorsed by FlashForge, it voids whatever warranty you had, and you alone are responsible for what happens to your printer. Mistakes here are not hypothetical — a wrong Z offset drives the nozzle through the build plate, and a failed grab or missing check can end like this:
Read the warnings below, run the Verify section before the first print, and stay next to the machine until you trust it.
- The eddy probe's Z home is ~3.2 mm below the real bed plane. The stock
app compensates with an absolute
SET_GCODE_OFFSET Z=+3.2xxat print start;START_PRINTcallsTOOLCHANGE_SET_PRINT_OFFSETto do the same. Printing without these macros (or with a stale copy of them) drives the nozzle into the plate. - Never edit the JSON under
/usr/data/firmwareRes/config/. It holds per-unit factory/touchscreen calibration; the app rewrites those files wholesale. This module only reads them. - Klipper lives on the firmware partition. A FlashForge OTA update will
overwrite
/usr/prog/klipper/, deletingff_toolchange.py. Keep this repo and re-run step 1 after every firmware update.
On the printer (ssh as pwned — this assumes a jailbroken printer; how to
get root/ssh access is covered in the community
Discord):
# 1. the klippy extra (firmware partition — may need remount rw)
scp klippy-extras/ff_toolchange.py pwned@PRINTER:/usr/prog/klipper/klippy/extras/
# 2. the config files (data partition — survives OTA)
scp config/ff-toolchange.cfg config/ff-print-macros.cfg pwned@PRINTER:/usr/data/config/- Append to
/usr/data/config/printer.cfg(order matters — must come AFTER[virtual_sdcard]is defined; do not put these inprinter.override.cfg, which is included first):
[include ff-toolchange.cfg]
[include /usr/data/config/ff-print-macros.cfg]- Reboot the printer.
Before moving anything:
-
TOOLCHANGE_STATUS— every dock coordinate, feedrate and offset with its provenance; flags disagreement with the app'snow_extruder. -
Physical Z check (clean bed, nothing on it):
G28 T0 TOOLCHANGE_SET_PRINT_OFFSET NOZZLE=220 BED=80 LAYER=0.25 TOOL=0 G1 X150 Y150 F6000 G1 Z0.1 F600
The offset command reports a Z around +3.2 with a term-by-term breakdown, and the nozzle should end up a paper-thickness above the bed at the center. If it presses into the plate instead, STOP — the offset is not being applied; do not print until
TOOLCHANGE_STATUSis clean. Afterwards restore the idle state:G1 Z10 F1200 TOOLCHANGE_PARK SET_GCODE_OFFSET X=0 Y=0 Z=0 MOVE=1
After a touchscreen recalibration, run TOOLCHANGE_RELOAD (or reboot) to
pick up the new JSON.
In the printer profile:
- Machine start G-code: contents of
orca/machine-start-gcode.txt - Machine end G-code: contents of
orca/machine-end-gcode.txt - Change filament G-code: contents of
orca/change-filament-gcode.txt—T[next_extruder] ; ff-toolchange, where the trailing comment is load-bearing (see below)
Or skip the copy-pasting: open
orca/creator-mainsail.3mf in OrcaSlicer —
an example project with the "Mainsail - Flashforge Creator 5 Pro 0.4
nozzle" printer profile already carrying all three G-code blocks above.
Re-slice anything sliced with older start G-code — old files won't call
TOOLCHANGE_SET_PRINT_OFFSET and will print ~3.2 mm low.
START_PRINT options: LEVEL=1 probes a fresh mesh (recommended for the
first print), SOAK=<seconds> overrides the bed heat soak (default 30,
SOAK=0 skips; progress is reported in the console).
The comment satisfies two different parsers at once:
- OrcaSlicer reads the change-filament block with a real G-code parser.
It sees an actual
T<n>command in there, decides the custom G-code already changes the tool (custom_gcode_changes_tool()), and stops emitting its own bareTnafter the block — so the tool change happens exactly once. - FlashForge's Klipper fork traps toolchange lines before they reach
the G-code interpreter:
virtual_sdcard.py(line 566) checksline.startswith("T") and line in VALID_GCODE_T, wherelinecomes straight fromdata.split('\n')and is never stripped or normalized. A bareT2matches the exact string inVALID_GCODE_T, gets swallowed by the fork, and is handed to the touchscreen app, which then blocks the print on its owndoingChangeEx/refuellingstate — state that only the touchscreen UI advances.T2 ; ff-toolchangeis not the exact stringT2, so the trap does not fire; the line falls through togcode.run_script(), whereff_toolchange.py'sT2handler services it.
This is also why touchscreen-started prints are untouched: FF-sliced files
carry bare Tn lines, which the fork still intercepts and the app still
services exactly as before. Only lines with the marker comment reach this
module.
- Per-tool offsets are differences vs a base tool (T0), applied on each
grab the way
CommMgr::setGrabGcodeOffsetMgrcomputes them; the once-per-print absolute Z base (BuildPage::startPrint) is set byTOOLCHANGE_SET_PRINT_OFFSETafter the first grab and carried through toolchanges. The base-tool term cancels algebraically, so a print may start on any tool. Babysteps made mid-print are carried across toolchanges, as in the app. Note this split (absolute base at print start + differences on grab) is a rewrite of how the originalfirmwareExestructures it, kept for fidelity — it would arguably be cleaner to apply the absolute offset on the first toolchange rather than as aSTART_PRINTstep, and that change is being considered (see Roadmap). - Everything is a Python extra (not gcode_macro) because the grab sequence polls the grab sensor and retries up to 3 times — a macro renders its whole template before executing and cannot poll.
Fool-proofing, in rough priority order:
- Forbid
G28while a tool is mounted. Homing Z runs on the eddy probe's frame; with a tool in the head the nozzle rams into the plate. The module should refuse (or auto-park first) instead of trusting the operator. - Z-height / clearance check after picking a tool, before leaving the dock area — catch a bad grab early instead of dragging or breaking the tool on the way out.
- Move the absolute print Z offset from
START_PRINTto the first toolchange of a job, removing the ordering dependency between the macro and the slicer's start G-code. - Block the stock touchscreen UI during Mainsail prints. The app doesn't know a print is running (it only tracks jobs it started itself), so the screen stays fully live — a stray tap can home, move the carriage, start a filament load, or fire its own toolchange into the middle of a running job. The module should lock the UI out (or at least its motion commands) while a Mainsail-started print is active.
Condensed notes from the firmwareExe analysis — recovered sequences, binary
addresses, JSON semantics — live in docs/notes/:
architecture overview, the Klipper-fork delta (including the Tn interception),
the verified grab/release sequences, the offset model, and the full print
lifecycle with the deliberate divergences listed.
If this saved your build plate (or your sanity), you can
buy me a coffee new hotend.
