How we got adaptive bed mesh working on the Creality K2 — the reverse-engineering story behind KAMP-K2
The behind-the-scenes story: prtouch_v3_wrapper.so, the master-server daemon, and how KAMP-K2 sidesteps both without patching any binary or core Klipper file.
KAMP-K2 didn't come from nowhere. Before we packaged it as the installer-driven KAMP fork it is now, we spent a few months working out why upstream KAMP didn't just work on the Creality K2 family — and the answer turns out to be a fairly involved reverse-engineering story. This is what we found.
If you're just looking to install adaptive meshing, the KAMP-K2 announcement has the one-liner. This article is the why.
The starting symptom
A 30 mm calibration cube on a stock K2 takes around 108 seconds of bed probing — a 7×7 grid (49 points) covering the entire bed. You don't need 49 measured points to print a 30 mm cube. You need 4×4 across the patch the cube actually sits on. The same applies for any small or off-centre part: the K2 always probes the whole bed.
That's exactly the problem KAMP solves on most other Klipper printers. So why doesn't it on the K2?
Discovery 1: prtouch_v3_wrapper.so
Inside the K2's stock Klipper install, at klippy/extras/prtouch_v3_wrapper.cpython-39.so, Creality ship a compiled Python binary that hijacks the BED_MESH_CALIBRATE gcode command. Decompiling it turns up three things:
1. The wrapper's own BED_MESH_CALIBRATE handler ignores MESH_MIN, MESH_MAX and PROBE_COUNT runtime parameters. It runs a hardcoded full-bed mesh regardless of what you pass it. 2. On certain firmware revisions, passing those parameters anyway crashes with IndexError: list index out of range at prtouch_v3_wrapper.py:1922. 3. It doesn't honour GCODE_FILE either — so even though Creality's own master-server daemon passes the active gcode filename, the wrapper falls back to a default full mesh.
KAMP relies on Klipper's upstream BED_MESH_CALIBRATE handler being present and accepting those parameters. With the wrapper sitting in the way, KAMP's adaptive bounds get silently discarded.
Discovery 2: master-server independently triggers meshes
Even if you somehow got BED_MESH_CALIBRATE to honour adaptive bounds, you'd still get a full mesh on every print. Why? Because there's a second component — master-server, a C++ daemon running on the printer at /usr/bin/master-server — that independently fires G29 BED_TEMP=NN and BED_MESH_CALIBRATE GCODE_FILE='...' during print prep. It runs before any slicer start-gcode, regardless of whether you upload via Moonraker, Fluidd, USB stick or Creality's cloud.
So the call path looks like this:
``` [upload] │ ▼ master-server ──► fires G29 + BED_MESH_CALIBRATE │ ▼ prtouch_v3_wrapper.so hijacks the call │ ▼ full 7×7 mesh runs (parameters ignored) │ ▼ [slicer's start-gcode finally runs — but mesh already done] ```
You can write the world's most carefully tuned Orca start-gcode and it has no effect, because master-server already triggered the mesh before your code got to run.
The fix — without modifying core Klipper or the binary
Patching bed_mesh.py or prtouch_v3_wrapper.so directly would work but is fragile: any Creality firmware update would clobber the change and brick adaptive meshing again. Instead, we wrote a small Klipper extras module — extras/restore_bed_mesh.py — that does two things:
1. Re-registers BED_MESH_CALIBRATE to Klipper's upstream handler at printer.lookup_object('bed_mesh').bmc.cmd_BED_MESH_CALIBRATE. The strain-gauge probe (the actual reason the wrapper exists) keeps working because we're only swapping the high-level gcode command — not the probe object underneath. 2. Wraps the upstream handler with a guard that requires MESH_MIN/MESH_MAX bounds to actually run a mesh. Bare calls (from master-server) become no-ops; calls with adaptive bounds run as intended.
Then a set of macro replacements deal with the master-server side:
G29andBED_MESH_CALIBRATE_START_PRINTare replaced with no-op macros that emit a fake[G29_TIME]Execution time: 0.0line somaster-server's response parser doesn't time out waiting.START_PRINTgets a realBED_MESH_CALIBRATEcall inserted at the point where slicer metadata is available — which is where KAMP's adaptive bounds wrap the call.
The whole thing is additive and reversible. No core Klipper files are modified, no .so binary is patched, no firmware is flashed. Three config changes plus a drop-in extras module, and you can revert to factory by deleting them.
From prototype to KAMP-K2
The first version of this — released as k2-adaptive-bedmesh — was a minimal proof-of-concept that read adaptive bounds from slicer start-gcode placeholders directly. It worked, but required custom slicer setup that didn't share knowledge with the wider KAMP community.
The second version, KAMP-K2, wraps the same hijack mechanism inside a fork of upstream KAMP. That gives you:
- KAMP's
exclude_objectmetadata approach, so no slicer placeholders are needed. - Adaptive line purging — the prime line follows the perimeter of your print rather than always running across the front of the bed.
- A one-line installer that handles SSH, backups, dependencies, and revert.
The original k2-adaptive-bedmesh repo is kept online as historical reference; for practical use, start with KAMP-K2.
Want to read deeper?
The full reverse-engineering work — Creality binary mapping, the RS-485 motor protocol, the 226-parameter motor controller map — lives at k2-reverse-engineering. It's the upstream research that this whole bed-mesh story rests on, and there'll be more articles drawing from it in the coming weeks.