Printer calibration
Print Handy can run a printer's calibrations from chat — but the pattern is the product, and it is the same for every driven calibration:
- Before values first. Print Handy reads the current values from the printer and shows them before anything runs.
- Confirm, with the warnings. The confirmation names exactly what will be sent and states the plain warnings for that calibration (heat, motion, noise, staying nearby).
- Run — idle only. A calibration runs only while the printer is idle.
print_statsprinting, paused, unrecognised or unreadable refuses before anything is sent, and the state is checked again immediately before the run command goes out. - New values AND differences. When the run finishes (Print Handy polls the printer for the result — never one long blocking call), it presents the new values and the numeric differences: old → new with signed deltas (mesh range, PID terms, shaper frequency/type, screw errors). Moonraker holds the run command's HTTP call open until the gcode finishes, so a run that outlasts the client timeout is treated as sent and its result is still collected by polling; a connection that fails before the command goes out is a failure. Console results are matched by timestamp, so a full Moonraker console history (it keeps the newest 1000 lines) does not hide them.
- Accept / Discard. Nothing is saved before this step. Accept sends exactly the documented save, once (and warns first when the save restarts Klipper). SAVE_CONFIG restarts Klipper mid-request, so Moonraker usually answers 503 or drops the connection; that is the expected restart, never resent. Accept reports success once Klipper is ready again, and says the save is unconfirmed if it does not come back. Discard sends the documented revert, or nothing when nothing was applied — and never a save. Both re-check that the printer is still idle first. No answer before the prompt times out is not a Discard: nothing is sent, and the reply says the new values stay in memory until Klipper restarts.
- A record either way. Every finished run (accepted, discarded or failed) and every report or guide writes a calibration record;
/print-calibration recordslists the recent ones.
The primitives and save semantics come from the Stage 0 evidence on issue #565 (ZMod wiki, Klipper documentation and source, Moonraker), recorded as a comment on the issue before the code was written. This page matches print_handy/calibration.py; if the two ever disagree, one of them is a bug. The chat commands on this page are not live-hardware verified yet; Stage 2 of #565 is a live session on a real AD5X. (A full mesh run on an AD5X was verified on 2026-10-28 through the mesh-run script path, not through /print-calibration; see DEVICE-AD5X.md.)
The commands
/print-calibration— the menu: the calibrations this printer actually offers, from live discovery (below), and how each is served./print-calibration mesh— full bed mesh./print-calibration screws— bed screws./print-calibration pid extruder [temp]/pid bed [temp]— heater PID. Without a temperature, the flow asks (extruder: 240 °C default, 200/255 offered; bed: 60 °C default, 80/110 offered — calibrate at the temperature you print at)./print-calibration shaper— input shaper./print-calibration belts— belt check (ZMod; report-only)./print-calibration zoffset— the Z-offset report and the mode-correct adjustment guide./print-calibration records— recent calibration records for the printer.
Who is eligible, and what is offered
Eligibility mirrors firmware updates (#561): the printer must connect over Moonraker and its firmware family must be zmod or vanilla klipper. Vendor Klipper forks (Elegoo, Creality, QIDI, Anycubic) are refused with a plain explanation — their calibration macros and save behaviour are the vendor's own. Non-Moonraker printers have no calibration path.
Eligibility is only the gate. A calibration is then offered only when live discovery shows its primitive exists on that printer — the macro in Moonraker's objects list, or the section in the configfile settings. Nothing is assumed from the model name: no AUTO_FULL_BED_LEVEL, no mesh menu entry.
Per-calibration notes
Full bed mesh
- ZMod, screenless: ZMod's
AUTO_FULL_BED_LEVEL(load-cell aware, cleans the nozzle first; ZMod's docs say the built-in web mesh tools are not suitable for these printers). It is run asAUTO_FULL_BED_LEVEL PROFILE=print_handy SAVE=0 EXTRUDER_TEMP=220 BED_TEMP=60(the temperatures of the screenless mesh contract,config/mesh-interface.json), so it measures into the scratch profileprint_handyin memory only. The before value is the savedautoprofile — the one ZMod'sSTART_PRINT MESH=autoloads. Accept loads the measurement, saves it asauto, removes the scratch profile and sendsSAVE_CONFIG; Discard removes the scratch profile and reloads the previously active profile (or clears the mesh). It never replaces the native screen's mesh. The run also re-zeros the Z offset's start point, as ZMod's leveling always does. - ZMod, native screen: the screen owns the mesh — it loads its mesh for every print — so Print Handy reports the stored mesh's range and guides a rebuild from the stock screen instead of measuring over the owner.
- Generic Klipper:
BED_MESH_CALIBRATE PROFILE=print_handy, same accept/discard shape; Accept saves it asdefault, the profile Klipper loads at startup.
Right after the new mesh reads back, Print Handy sends TURN_OFF_HEATERS and checks that both heater targets read 0, so the printer does not sit hot while the Accept/Discard question waits. The result says whether that was confirmed; if it was not, it tells you to turn the heaters off yourself. The printer is re-checked first: if it is no longer idle (a print started from the screen or another client meanwhile), the heaters are left alone and the result says so.
The result presentation is the mesh range (lowest → highest probed point) before and after, with the signed range difference.
Bed screws
- ZMod: guide only.
BED_LEVEL_SCREWS_TUNEmeasures with the load cells and then waits for its own confirmation at the printer, which a chat prompt cannot answer. The guide carries ZMod's own warnings: clean the nozzle first, the printer does not reset the heater temperatures afterwards (zero them by hand), and the bed mesh and Z offset are invalid after a screw tune. - Generic Klipper with
[screws_tilt_adjust]: driven.SCREWS_TILT_CALCULATEis one non-interactive command whose console output names each screw's probed height and the exact turn (adjust CW 01:15— a clock face). The flow is measure → you turn the screws → measure again, up to three rounds, showing each screw's error and its change between rounds. Nothing is ever saved — the adjustment is physical. - Generic Klipper with only
[bed_screws]: guide only —BED_SCREWS_ADJUSTis an interactiveACCEPT/ADJUSTEDdialogue at the nozzle.
Z offset — report + guide in every mode
This is the Stage 0 verdict, and it is deliberate. On ZMod, the native screen's offset and the screenless offset are two separate values, each living its own life (ZMod wiki). There is no pre-print Z-offset value the bot can responsibly set in either mode:
- Native screen: the screen owns its offset (
zProbeOffset). It is adjusted only during printing, on the screen, and the screen saves it and loads it at every print start. Adjusting through the web interface while the screen is in use is not advisable (the screen overwrites it),Z_OFFSET_APPLY_ENDSTOPmust not be used, and[probe] z_offsetmust not be edited. Print Handy reports the screen's value (and the screenless store's, as a separate value with the difference) and gives the screen steps. Creator 5 / 5 Pro are the exception — their two stores are synchronised — but the adjustment is still an in-print, at-the-printer action. - Screenless: the offset is the saved
gcode_offsets.z, loaded at print start whenSAVE_ZMOD_DATA LOAD_ZOFFSET=1has been armed once. Adjustments happen during a print from the web interface and ZMod saves them itself — noZ_OFFSET_APPLY_*commands are needed or wanted. Print Handy reports the saved value and whetherLOAD_ZOFFSET=1is armed, and explainsLOAD_ZOFFSET_NATIVEfor the one-time copy from the screen's store: with its defaultSTART=0it copies the native-screen offset into screenless mode and saves it (START=1, used at boot, only reports). There is no reverse copy. - Generic Klipper:
PROBE_CALIBRATEis an interactiveTESTZpaper-test dialogue at the bed — guide only. Print Handy reports the configuredprobe.z_offsetand walks through the steps.
Extruder / bed PID — the accept gate is the point
ZMod's own PID_TUNE_EXTRUDER / PID_TUNE_BED macros automatically SAVE_CONFIG when they finish, which would destroy the accept step — Print Handy never sends them. The flows use raw Klipper:
- Run:
PID_CALIBRATE HEATER=extruder TARGET=<t>(orheater_bed). It applies the tuned values in the heater's memory, stores them for SAVE_CONFIG, and reports them on the console (PID parameters: pid_Kp=… pid_Ki=… pid_Kd=…), which is where Print Handy reads the result. - Before values come from the configfile settings (
pid_Kp/pid_Ki/ pid_Kdfor the heater). - Accept = one
SAVE_CONFIG(restarts Klipper — warned at the accept step). - Discard = one
FIRMWARE_RESTART, which drops the in-memory values and restores the saved ones. The restart is named in the result text before Discard can be chosen. Never a save. - ZMod also recommends
[heater_bed] max_power: 0.6on the AD5X so the bed heats fast enough to tune correctly; the guide text notes it and the bot never edits the config for you.
Input shaper
- ZMod:
ZSHAPER; generic Klipper:SHAPER_CALIBRATE(only offered where a resonance tester / accelerometer is configured). - Both only recommend values on the console; nothing is applied or saved by the calibration itself. Accept = one
SAVE_CONFIG(restarts Klipper); Discard sends nothing. One honesty note the flow states: the recommendation stays pending inside Klipper until the next restart, so an unrelated SAVE_CONFIG before a restart would include it. - Graphs and CSVs land in the printer's Configuration →
mod_data. SAVE_CONFIG does not updatemax_accel; if the report suggests a lower acceleration, that is a separate manual edit. Klipper advises against running shaper calibration often — the resonant vibration wears parts — and the confirmation says so.
Belts (ZMod, report-only)
BELTS_SHAPER_CALIBRATION analyses CoreXY belt frequencies (half-axis test, spectrogram in the web interface under Configuration → mod_data). It applies nothing and saves nothing, so there is no accept step: the flow confirms (it is loud, and the printer shakes), runs, and reports what the printer's console answered.
Records
One JSON file per printer, calibration_records.json under <state dir>/printers/<printer id>/, newest last, capped at 100. Each record holds the printer, the calibration name, the timestamp, the before values, the after values, the numeric differences, the outcome (accepted, discarded, failed, or reported for reports, guides and the belt check) and a short detail (for example the PID target). /print-calibration records lists the ten most recent, newest first.
What is not covered
- Vendor-fork and non-Moonraker printers (above).
- Any calibration whose primitive live discovery does not find.
- Setting a Z offset from chat, in any mode (see the verdict above).
- Editing
printer.cfgvalues directly (for examplemax_powerormax_accel): noted in guides where relevant, never written by the bot.
