<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: suzukesan</title>
    <description>The latest articles on DEV Community by suzukesan (@suzukesan).</description>
    <link>https://dev.to/suzukesan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4021788%2Fb8721943-8a6c-4478-8c28-8aa7abbe236c.png</url>
      <title>DEV Community: suzukesan</title>
      <link>https://dev.to/suzukesan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/suzukesan"/>
    <language>en</language>
    <item>
      <title>Fixing the CL2529 Error on a Creality Ender-3 V3 Plus (Klipper Z-Probe Timeout) — Traced Through Source Code</title>
      <dc:creator>suzukesan</dc:creator>
      <pubDate>Wed, 08 Jul 2026 18:36:09 +0000</pubDate>
      <link>https://dev.to/suzukesan/fixing-the-cl2529-error-on-a-creality-ender-3-v3-plus-klipper-z-probe-timeout-traced-through-198p</link>
      <guid>https://dev.to/suzukesan/fixing-the-cl2529-error-on-a-creality-ender-3-v3-plus-klipper-z-probe-timeout-traced-through-198p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published (in Japanese) on my blog: &lt;a href="https://suzuke.page/posts/ender3-v3-troubleshooting" rel="noopener noreferrer"&gt;suzuke.page/posts/ender3-v3-troubleshooting&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Symptom
&lt;/h2&gt;

&lt;p&gt;My Creality Ender-3 V3 Plus kept throwing a &lt;strong&gt;CL2529&lt;/strong&gt; error during the first Z leveling (&lt;code&gt;G28 Z&lt;/code&gt;) at the start of a print.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A warning screen pops up during the initial leveling pass&lt;/li&gt;
&lt;li&gt;Error code: &lt;strong&gt;CL2529&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Pressing "Continue" re-runs leveling, and the print starts fine the second time&lt;/li&gt;
&lt;li&gt;It happened often enough to be genuinely annoying&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Investigation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Reading the Klipper log
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /usr/data/printer_data/logs/klippy.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"error&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;529&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;probe&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;tri_"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turned up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error message: &lt;code&gt;PR_ERR_CODE_G28_Z_DETECTION_TIMEOUT: G28 Z try probe out of times.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Internal code: &lt;code&gt;key529&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The bed uses 4 strain-gauge sensors (CH0–CH3) to measure probe pressure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CH3's sensor readings had abnormal spikes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[PRES_CH3] [..., 324110.000, -980617.000, ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the healthy channels ramped up gradually, CH3 jumped from &lt;code&gt;324110&lt;/code&gt; to &lt;code&gt;-980617&lt;/code&gt; — a clear noise/glitch, not a real reading.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inconsistent probe results
&lt;/h3&gt;

&lt;p&gt;Comparing three probe attempts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[G28_FIRST_MMS] [2.763, 3.248, 4.132]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spread between &lt;code&gt;2.763mm&lt;/code&gt; and &lt;code&gt;4.132mm&lt;/code&gt; is about 1.4mm — way too inconsistent. That inconsistency is the direct cause of the timeout.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Disassembling the binary
&lt;/h3&gt;

&lt;p&gt;The core probing logic lives in a compiled binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/share/klipper/klippy/extras/prtouch_v1_wrapper.cpython-38-mipsel-linux-gnu.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;strings&lt;/code&gt; on it surfaced the relevant config parameter names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;strings prtouch_v1_wrapper.cpython-38-mipsel-linux-gnu.so | &lt;span class="nb"&gt;grep &lt;/span&gt;tri_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tri_try_max_times
tri_min_hold
tri_max_hold
tri_hftr_cut
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These turned out to be parameters read straight out of &lt;code&gt;printer.cfg&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Finding the actual source
&lt;/h3&gt;

&lt;p&gt;The breakthrough: Creality published the Python source for &lt;code&gt;prtouch_v1_wrapper.py&lt;/code&gt; under the GPL in December 2025.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/CrealityOfficial/K1_Series_Klipper" rel="noopener noreferrer"&gt;CrealityOfficial/K1_Series_Klipper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Commit: &lt;a href="https://github.com/CrealityOfficial/K1_Series_Klipper/commit/e09f36e6ada60e5467b0bef731a96263b5d8095b" rel="noopener noreferrer"&gt;e09f36e&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That source makes the error logic completely clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error definition:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PR_ERR_CODE_G28_Z_DETECTION_TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;key529&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PR_ERR_CODE_G28_Z_DETECTION_TIMEOUT: G28 Z try probe out of times.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Retry limit:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tri_try_max_times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tri_try_max_times&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it isn't set in &lt;code&gt;printer.cfg&lt;/code&gt;, the default is 10 retries. &lt;strong&gt;Setting it to 0 disables the check entirely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error-raising logic:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tri_try_max_times&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;use_tri_times&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tri_try_max_times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print_msg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WHY ERROR&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FUN:G28_Z Coarse Probe Out of max {} times:{}...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(...))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ck_and_raise_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PR_ERR_CODE_G28_Z_DETECTION_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error is only ever raised when &lt;code&gt;tri_try_max_times != 0&lt;/code&gt;. So setting it to &lt;code&gt;0&lt;/code&gt; skips the check completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Continue" actually does
&lt;/h2&gt;

&lt;p&gt;I also traced what happens when you hit "Continue" on the printer's screen after the error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CL2529 error fires
  ↓
ck_and_raise_error() runs
  ├─ Nozzle retracts 5mm (safety measure)
  └─ raise self.printer.command_error(...)  ← shows the warning
  ↓
User presses "Continue"
  ↓
SET_KINEMATIC_POSITION → resets current position
  ↓
G28 Z restarts from scratch
  ↓
All channels read clean this time → probe succeeds → print starts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, this is a &lt;code&gt;command_error&lt;/code&gt; (a warning-level condition), not a Klipper shutdown. Pressing "Continue" just restarts leveling from zero, and the second attempt usually succeeds normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;tri_try_max_times: 0&lt;/code&gt; to the &lt;code&gt;[prtouch_v2]&lt;/code&gt; section of &lt;code&gt;printer.cfg&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@192.168.0.230
vi /usr/data/printer_data/config/printer.cfg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[prtouch_v2]&lt;/span&gt;
&lt;span class="c"&gt;# ... existing settings ...
&lt;/span&gt;&lt;span class="err"&gt;tri_try_max_times:&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also clear the error history so it stops showing on screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"list":[]}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /usr/data/creality/userdata/fault_code/fault_code_info.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart Klipper to apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/etc/init.d/S55klipper_service restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Making it survive firmware updates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem: firmware updates wipe the setting
&lt;/h3&gt;

&lt;p&gt;Klipper's startup script (&lt;code&gt;/etc/init.d/S55klipper_service&lt;/code&gt;) compares the ROM firmware version against the version recorded in &lt;code&gt;printer.cfg&lt;/code&gt; on every boot. When a firmware update changes that version, &lt;strong&gt;the body of &lt;code&gt;printer.cfg&lt;/code&gt; gets overwritten with the ROM defaults&lt;/strong&gt; (only the &lt;code&gt;SAVE_CONFIG&lt;/code&gt; section survives).&lt;/p&gt;

&lt;p&gt;The ROM's default &lt;code&gt;[prtouch_v2]&lt;/code&gt; section doesn't include &lt;code&gt;tri_try_max_times&lt;/code&gt;, so the CL2529 error comes right back after an update.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: an init script that re-applies it automatically
&lt;/h3&gt;

&lt;p&gt;I added &lt;code&gt;/etc/init.d/S56klipper_custom&lt;/code&gt;, which runs right after Klipper starts and re-injects the setting if it's missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/init.d/S56klipper_custom &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/sh
#
# Ensure custom Klipper settings persist across firmware updates.
#

CFG=/usr/data/printer_data/config/printer.cfg

apply_patch() {
    if [ -f "&lt;/span&gt;&lt;span class="nv"&gt;$CFG&lt;/span&gt;&lt;span class="sh"&gt;" ] &amp;amp;&amp;amp; ! grep -q "tri_try_max_times" "&lt;/span&gt;&lt;span class="nv"&gt;$CFG&lt;/span&gt;&lt;span class="sh"&gt;"; then
        sed -i "/^&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="sh"&gt;prtouch_v2&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sh"&gt;/a tri_try_max_times: 0" "&lt;/span&gt;&lt;span class="nv"&gt;$CFG&lt;/span&gt;&lt;span class="sh"&gt;"
        return 0
    fi
    return 1
}

start() {
    if apply_patch; then
        echo "Custom settings applied, restarting Klipper..."
        /etc/init.d/S55klipper_service restart
    else
        echo "Custom settings already present, no action needed."
    fi
}

case "&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;" in
  start)
        start
        ;;
  stop)
        ;;
  restart|reload)
        start
        ;;
  *)
        echo "Usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="sh"&gt; {start|stop|restart}"
        exit 1
esac
exit &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /etc/init.d/S56klipper_custom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Printer boots → S55 starts Klipper (a firmware update may have just overwritten &lt;code&gt;printer.cfg&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;S56 runs immediately after → checks whether &lt;code&gt;tri_try_max_times&lt;/code&gt; is present in &lt;code&gt;printer.cfg&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If present&lt;/strong&gt; → does nothing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If missing&lt;/strong&gt; → adds &lt;code&gt;tri_try_max_times: 0&lt;/code&gt; back into &lt;code&gt;[prtouch_v2]&lt;/code&gt; and restarts Klipper&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have other settings you want to survive firmware updates, you can add the same pattern inside &lt;code&gt;apply_patch&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this actually safe?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tri_try_max_times: 0&lt;/code&gt; only disables the error &lt;strong&gt;check&lt;/strong&gt; — the probing itself still runs exactly as before. In the source, setting it to &lt;code&gt;0&lt;/code&gt; just skips the &lt;code&gt;if&lt;/code&gt; block that raises the error; it doesn't touch probe accuracy or leveling quality in any way.&lt;/p&gt;

&lt;p&gt;The fact that pressing "Continue" almost always succeeds on the retry is further evidence this is transient sensor noise, and that the probe's own retry logic already compensates for it just fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hardware-level improvement worth considering
&lt;/h2&gt;

&lt;p&gt;This fix addresses it in software, but the root cause is that one of the four strain-gauge sensors under the bed — specifically CH3 — is producing abnormal noise.&lt;/p&gt;

&lt;p&gt;The config change just prevents that noise from tripping the error; it doesn't fix the sensor itself. Replacing the strain-gauge sensor would likely improve actual probe accuracy and give more consistent leveling, on top of making this error go away for good.&lt;/p&gt;

</description>
      <category>3dprinting</category>
      <category>klipper</category>
      <category>linux</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
