From 63c1fdf5ebb88df2baf096fcbd06f65ccc420249 Mon Sep 17 00:00:00 2001
From: Titus Meyer
Date: Sun, 31 May 2026 10:54:36 +0200
Subject: [PATCH 1/6] mondernize z-calibration, compatibility and release
workflow
---
.github/workflows/ci.yml | 37 ++
.github/workflows/compat.yml | 29 ++
.github/workflows/release.yml | 86 +++++
.gitignore | 24 ++
AGENTS.md | 140 ++++++++
CONTRIBUTING.md | 109 ++++++
README.md | 164 +++++++--
docs/maintainer-release.md | 337 ++++++++++++++++++
install.sh | 239 ++++++++-----
klipper_compat.py | 266 +++++++++++++++
pictures/banner-dark.png | Bin 0 -> 463080 bytes
pictures/banner-light.png | Bin 0 -> 586382 bytes
pictures/banner.png | Bin 741726 -> 0 bytes
pictures/social-preview.png | Bin 0 -> 1020818 bytes
scripts/check_all.py | 64 ++++
scripts/check_firmware_compat.py | 137 ++++++++
scripts/check_klipper_contract.py | 252 ++++++++++++++
scripts/check_release.py | 83 +++++
scripts/check_whitespace.py | 87 +++++
scripts/update_moonraker.py | 90 +++++
tests/fakes.py | 399 ++++++++++++++++++++++
tests/test_check_all.py | 77 +++++
tests/test_firmware_compat.py | 141 ++++++++
tests/test_import_bootstrap.py | 63 ++++
tests/test_install_script.py | 126 +++++++
tests/test_klipper_compat.py | 48 +++
tests/test_klipper_contract.py | 180 ++++++++++
tests/test_release_helpers.py | 91 +++++
tests/test_z_calibration.py | 545 ++++++++++++++++++++++++++++++
z_calibration.py | 426 +++++++++++------------
30 files changed, 3903 insertions(+), 337 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 .github/workflows/compat.yml
create mode 100644 .github/workflows/release.yml
create mode 100644 .gitignore
create mode 100644 AGENTS.md
create mode 100644 CONTRIBUTING.md
create mode 100644 docs/maintainer-release.md
create mode 100644 klipper_compat.py
create mode 100644 pictures/banner-dark.png
create mode 100644 pictures/banner-light.png
delete mode 100644 pictures/banner.png
create mode 100644 pictures/social-preview.png
create mode 100644 scripts/check_all.py
create mode 100644 scripts/check_firmware_compat.py
create mode 100644 scripts/check_klipper_contract.py
create mode 100644 scripts/check_release.py
create mode 100644 scripts/check_whitespace.py
create mode 100644 scripts/update_moonraker.py
create mode 100644 tests/fakes.py
create mode 100644 tests/test_check_all.py
create mode 100644 tests/test_firmware_compat.py
create mode 100644 tests/test_import_bootstrap.py
create mode 100644 tests/test_install_script.py
create mode 100644 tests/test_klipper_compat.py
create mode 100644 tests/test_klipper_contract.py
create mode 100644 tests/test_release_helpers.py
create mode 100644 tests/test_z_calibration.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..d36a242
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,37 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Python ${{ matrix.python-version }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version:
+ - "3.9"
+ - "3.13"
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Run local validation
+ run: python scripts/check_all.py
+
+ - name: Check pull request diff whitespace
+ if: github.event_name == 'pull_request'
+ run: git diff --check "origin/${{ github.base_ref }}...HEAD"
diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml
new file mode 100644
index 0000000..b64dead
--- /dev/null
+++ b/.github/workflows/compat.yml
@@ -0,0 +1,29 @@
+name: Klipper And Kalico Compatibility
+
+on:
+ schedule:
+ - cron: "17 4 * * 1"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ firmware-contract:
+ name: Firmware contracts
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Check repository
+ run: python scripts/check_all.py
+
+ - name: Check firmware contracts
+ run: python scripts/check_firmware_compat.py
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..aef971e
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,86 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: "Existing tag to draft, for example v1.2.0"
+ required: true
+ type: string
+ channel:
+ description: "Expected release channel"
+ required: true
+ default: "stable"
+ type: choice
+ options:
+ - stable
+ - beta
+
+permissions:
+ contents: write
+
+jobs:
+ draft-release:
+ name: Draft release
+ runs-on: ubuntu-latest
+ environment: release
+ env:
+ RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
+ RELEASE_CHANNEL: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || '' }}
+
+ steps:
+ - name: Check out release tag
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ ref: ${{ env.RELEASE_TAG }}
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Validate release tag
+ id: release
+ run: |
+ args=(--tag "$RELEASE_TAG" --github-output "$GITHUB_OUTPUT")
+ if [ -n "$RELEASE_CHANNEL" ]; then
+ args+=(--channel "$RELEASE_CHANNEL")
+ fi
+ python scripts/check_release.py "${args[@]}"
+
+ - name: Run local validation
+ run: python scripts/check_all.py
+
+ - name: Build source archive
+ run: |
+ mkdir -p dist
+ archive="dist/klipper_z_calibration-${{ steps.release.outputs.version }}.tar.gz"
+ git archive \
+ --format=tar.gz \
+ --prefix="klipper_z_calibration-${{ steps.release.outputs.version }}/" \
+ --output="$archive" \
+ HEAD
+
+ - name: Create draft GitHub Release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PRERELEASE: ${{ steps.release.outputs.prerelease }}
+ TITLE: ${{ steps.release.outputs.title }}
+ TAG: ${{ steps.release.outputs.tag }}
+ run: |
+ args=(
+ release create "$TAG"
+ dist/*.tar.gz
+ --draft
+ --verify-tag
+ --title "$TITLE"
+ --generate-notes
+ )
+ if [ "$PRERELEASE" = "true" ]; then
+ args+=(--prerelease)
+ fi
+ gh "${args[@]}"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f8b63b9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+__pycache__/
+*.py[cod]
+*$py.class
+
+.pytest_cache/
+.coverage
+htmlcov/
+
+.mypy_cache/
+.ruff_cache/
+
+.venv/
+venv/
+env/
+
+.idea/
+.vscode/
+*.swp
+*~
+.codex/
+.agents/
+.compat_repos/
+
+.DS_Store
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..47228f1
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,140 @@
+# Agent Instructions
+
+This file is for automated coding agents working in this repository.
+Human contributor guidance is in `CONTRIBUTING.md`. Maintainer release steps
+are in `docs/maintainer-release.md`.
+
+## Project Rules
+
+`klipper_z_calibration` is a standalone Klipper/Kalico plugin for dockable
+contact probes.
+
+Keep these boundaries intact:
+
+- `z_calibration.py` is the Klipper plugin entrypoint.
+- `klipper_compat.py` isolates Klipper/Kalico API assumptions.
+- Only `z_calibration.py` is linked into Klipper/Kalico by `install.sh`.
+- Helper modules load from the repository checkout through the symlink target.
+- Do not add runtime Python modules unless the install model is intentionally
+ changed.
+- Preserve support for the old Kalico external plugin mechanism.
+
+Unsupported probe families are out of scope unless project policy changes:
+
+- BLTouch-style probes
+- Beacon-style probes
+- non-dockable or virtual probe implementations
+- virtual Z endstops for the calibration endstop
+
+The Wiki is the source of truth for full user configuration. Do not copy large
+Wiki sections into repository docs.
+
+## Compatibility Rules
+
+Put direct Klipper/Kalico implementation assumptions in `klipper_compat.py`.
+Examples include:
+
+- event and object lookup assumptions
+- homing/probing APIs
+- probe session APIs
+- bed mesh internals
+- toolhead status access
+- gcode offset APIs
+
+When adding or changing compatibility-sensitive behavior, add focused tests for
+the affected wrapper and update `scripts/check_klipper_contract.py` if a new
+upstream Klipper source contract is required.
+
+## Formatting
+
+This repository follows Klipper-style formatting via:
+
+```bash
+python3 scripts/check_whitespace.py
+```
+
+Requirements include:
+
+- UTF-8 encoded files
+- no trailing whitespace
+- no tabs, except where explicitly allowed
+- maximum line length of 80 characters for Python source
+- newline at end of file
+- no extra blank lines at end of file
+- no invalid control characters
+
+Keep diffs focused. Do not perform unrelated formatting-only changes.
+
+## Testing Expectations
+
+The goal is behavioral coverage, not just line coverage.
+
+Add or update tests for:
+
+- new behavior
+- bug fixes
+- compatibility changes
+- config parsing and validation
+- event and object lifecycle behavior
+- G-Code command behavior
+- probe session behavior
+- Moonraker updater config migration
+- release helper behavior
+
+Compatibility-sensitive paths should have explicit tests for feature detection,
+old/new Klipper behavior, Kalico-specific behavior, or Moonraker behavior as
+applicable.
+
+## Required Validation
+
+Before considering a task complete, run:
+
+```bash
+python3 scripts/check_all.py
+```
+
+This runs whitespace validation, shell syntax validation, compile checks, unit
+tests, and `git diff --check`.
+
+If release helper behavior changed, also run:
+
+```bash
+python3 scripts/check_release.py --tag v1.2.3 --channel stable
+python3 scripts/check_release.py --tag v1.2.3-beta.1 --channel beta
+```
+
+If Klipper API assumptions changed and a local Klipper checkout is available,
+run:
+
+```bash
+python3 scripts/check_klipper_contract.py --klipper-path ~/klipper
+```
+
+To clone or update ignored local Klipper/Kalico checkouts and run all firmware
+contract checks:
+
+```bash
+python3 scripts/check_firmware_compat.py
+```
+
+After the ignored checkouts exist, use the offline form when network access is
+not needed:
+
+```bash
+python3 scripts/check_firmware_compat.py --no-update
+```
+
+## Review Checklist
+
+Before finishing, review whether the change affects:
+
+- startup behavior
+- printer state transitions
+- configuration parsing or migration
+- Moonraker integration
+- Kalico compatibility
+- probe session cleanup
+- installer cleanup
+- release workflow behavior
+
+Document any remaining risks or assumptions in the final response.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..80b8e9a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,109 @@
+# Contributing
+
+Thanks for helping improve `klipper_z_calibration`. This project is a
+standalone Klipper/Kalico plugin for dockable contact probes.
+
+## Project Scope
+
+This repository supports:
+
+- stock Klipper installs through `klippy/extras`
+- Kalico installs through `klippy/plugins`
+- dockable contact probes
+- Moonraker Update Manager installs
+
+This repository does not support:
+
+- BLTouch-style probes
+- Beacon-style probes
+- non-dockable or virtual probe implementations
+- virtual Z endstops for the calibration endstop
+
+The Wiki remains the source of truth for complete user configuration. Avoid
+copying large Wiki sections into this repository.
+
+## Runtime Layout
+
+Keep the runtime layout simple:
+
+- `z_calibration.py` is the Klipper plugin entrypoint.
+- `klipper_compat.py` isolates compatibility-sensitive Klipper/Kalico API
+ assumptions.
+- Only `z_calibration.py` is linked into Klipper/Kalico by the installer.
+ Helper modules load from the repository checkout through the symlink target.
+
+Put direct Klipper/Kalico implementation assumptions in `klipper_compat.py`.
+Keep calibration behavior, config parsing, G-Code command handling, and runtime
+flow in `z_calibration.py`.
+
+## Code Changes
+
+- Keep behavior changes minimal and explicit.
+- Match the existing Klipper-style formatting.
+- Do not add new runtime modules unless the install model is intentionally
+ changed.
+- Do not add unsupported probe workarounds without first defining the support
+ policy and tests.
+- Preserve compatibility with the currently supported old Kalico plugin
+ mechanism.
+
+## Tests
+
+New behavior, bug fixes, and compatibility changes should include tests.
+
+Prioritize tests for:
+
+- config parsing and validation
+- event and object lifecycle behavior
+- G-Code command behavior
+- probe session behavior
+- Klipper/Kalico compatibility wrappers
+- Moonraker updater config migration
+- release helper behavior
+
+Compatibility-sensitive paths should be covered explicitly. Avoid relying only
+on broad happy-path tests.
+
+## Validation
+
+Run these checks before submitting a pull request:
+
+```bash
+python3 scripts/check_all.py
+```
+
+The check runner performs whitespace validation, shell syntax validation,
+compile checks, unit tests, and `git diff --check`.
+
+If you touch release helper behavior, also run:
+
+```bash
+python3 scripts/check_release.py --tag v1.2.3 --channel stable
+python3 scripts/check_release.py --tag v1.2.3-beta.1 --channel beta
+```
+
+If you touch Klipper API assumptions and have a local Klipper checkout, run:
+
+```bash
+python3 scripts/check_klipper_contract.py --klipper-path ~/klipper
+```
+
+To clone or update local Klipper/Kalico compatibility checkouts under the
+ignored `.compat_repos/` directory and run all firmware contract checks:
+
+```bash
+python3 scripts/check_firmware_compat.py
+```
+
+After those checkouts exist, rerun the same contract checks without network
+access:
+
+```bash
+python3 scripts/check_firmware_compat.py --no-update
+```
+
+## Releases
+
+Release publishing is maintainer-owned. Maintainers should follow:
+
+[docs/maintainer-release.md](docs/maintainer-release.md)
diff --git a/README.md b/README.md
index 855de23..c4a8f34 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,176 @@
-
+
+
+
+
+
Automatic Z-Calibration
-It's like automatically baby-stepping on your 3D printer before every print, and the first layer will
-always be perfect - no matter which nozzle or new flex-plate is being tested.
+ Automatic Z offset calibration for Klipper and Kalico printers using
+ dockable contact probes.
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-## Documentation
+## Overview
-Visit the [Wiki](https://github.com/protoloft/klipper_z_calibration/wiki) to view the full documentation for this Klipper plugin.
+`klipper_z_calibration` is a standalone Klipper/Kalico plugin that measures the
+relationship between a fixed Z endstop, a dockable probe, and the print surface,
+then applies the resulting Z offset. The primary runtime command is
+`CALIBRATE_Z`.
-The latest release notes are [here](https://github.com/protoloft/klipper_z_calibration/wiki/Changelog).
+The full user documentation remains in the
+[Wiki](https://github.com/protoloft/klipper_z_calibration/wiki). This README is
+only the quick project overview, installation entry point, and compatibility
+summary.
-:pushpin: **And remember:** The smaller the switch-offset, the further the
- nozzle is away from the bed! :wink:
+## Supported Setup
-## Further Resources
+Supported:
+
+- Klipper installations using `klippy/extras`
+- Kalico installations using the external plugin mechanism in `klippy/plugins`
+- Dockable contact probes such as Klicky-style probes
+- Moonraker Update Manager installs
+
+Not supported:
+
+- BLTouch-style probes
+- Beacon-style probes
+- Other non-dockable or virtual probe implementations
+- Virtual Z endstops for the Z calibration endstop
+
+Kalico users must enable plugin overrides:
+
+```ini
+[danger_options]
+allow_plugin_override: True
+```
+
+## Quick Install
+
+Clone the repository and run the installer as the printer user, not as root:
+
+```bash
+git clone https://github.com/protoloft/klipper_z_calibration.git
+cd klipper_z_calibration
+./install.sh
+```
+
+Useful installer options:
+
+```bash
+./install.sh -k ~/klipper
+./install.sh -m ~/printer_data/config/moonraker.conf
+./install.sh -n 2
+./install.sh -u
+```
+
+The installer links only `z_calibration.py` into Klipper or Kalico. Supporting
+Python modules are loaded from this repository checkout through the symlink
+target, so the checkout must remain in place.
+
+## Moonraker Updates
+
+The installer manages the `[update_manager z_calibration]` section in
+`moonraker.conf`.
+
+- New installations use `channel: stable`.
+- Existing sections without a `channel` are migrated to `channel: stable`.
+- Existing explicit `stable`, `beta`, or `dev` channels are left unchanged.
+- `managed_services: klipper` is configured so updates restart Klipper.
-A great how-to video by Kapman: [https://youtu.be/oQYHFecsTto](https://youtu.be/oQYHFecsTto)
+Moonraker's modern default config path is
+`~/printer_data/config/moonraker.conf`. Use `install.sh -m ` for custom
+layouts.
+
+## Configuration Notes
+
+Use the Wiki for the full configuration reference:
+
+https://github.com/protoloft/klipper_z_calibration/wiki
+
+Current releases expect the modern option names:
+
+- `offset_margins`
+- `safe_z_height`
+
+The older `max_deviation` and `clearance` options are no longer supported.
+
+The smaller the configured `switch_offset`, the farther the nozzle is from the
+bed.
+
+## Commands
+
+The plugin registers these G-Code commands:
+
+- `CALIBRATE_Z`: measure and apply the current Z offset.
+- `PROBE_Z_ACCURACY`: probe the fixed Z endstop repeatedly for repeatability
+ checks.
+- `CALCULATE_SWITCH_OFFSET`: calculate a switch offset from the current Z
+ position after calibration.
+
+Command parameters and complete configuration examples are documented in the
+Wiki.
+
+## Development And Releases
+
+Contributor guide:
+[CONTRIBUTING.md](CONTRIBUTING.md)
+
+Maintainer release process:
+[docs/maintainer-release.md](docs/maintainer-release.md)
+
+This repository includes unit tests, release validation helpers, GitHub Actions
+CI, and a scheduled Klipper/Kalico compatibility workflow. Critical Klipper and
+Kalico API assumptions are isolated in `klipper_compat.py`.
+
+Run the standard local validation suite with:
+
+```bash
+python3 scripts/check_all.py
+```
+
+## Further Resources
-### And if you are looking for an RRF version of this automatic z-offset calibration
+Kapman's how-to video:
+[https://youtu.be/oQYHFecsTto](https://youtu.be/oQYHFecsTto)
-You can find one [here](https://github.com/pRINTERnOODLE/Auto-Z-calibration-for-RRF-3.3-or-later-and-Klicky-Probe) from pRINTERnOODLE - This is really fantastic to see :tada:
+RRF version of automatic Z offset calibration:
+[Auto-Z-calibration-for-RRF-3.3-or-later-and-Klicky-Probe](https://github.com/pRINTERnOODLE/Auto-Z-calibration-for-RRF-3.3-or-later-and-Klicky-Probe)
-## Thanks for all your feedback and support!
+## Support
-And if you like my work and want to support me, you can do so here:
+If this project is useful to you, support is welcome:
[](https://ko-fi.com/X8X1C0DTD)
## Disclaimer
-You use it at your onw risk! I'm not responsible for any damage that might result. Although,
-this extension works rock solid for me and many others for years now. Always be careful
-and double check everything when configuring or working with your printer. And as always,
-never leave unattended while printing!
+Use this plugin at your own risk. You are responsible for validating your
+printer configuration and checking all motion paths before use. Never leave a
+printer unattended while printing.
diff --git a/docs/maintainer-release.md b/docs/maintainer-release.md
new file mode 100644
index 0000000..b3a3943
--- /dev/null
+++ b/docs/maintainer-release.md
@@ -0,0 +1,337 @@
+# Maintainer Release Process
+
+This document is the release checklist for maintainers of
+`klipper_z_calibration`. It is intentionally procedural so a release can be
+prepared, verified, and published without making process decisions during the
+release.
+
+## Release Model
+
+1. Keep `master` releasable at all times.
+2. Use semantic version tags for all new releases.
+3. Use stable tags for production releases:
+
+ ```text
+ v1.2.0
+ v1.2.1
+ ```
+
+4. Use beta tags for prereleases:
+
+ ```text
+ v1.2.0-beta.1
+ v1.2.0-beta.2
+ ```
+
+5. Treat GitHub Releases as the public release record.
+6. Mark beta tags as GitHub prereleases.
+7. Mark stable tags as normal GitHub releases.
+8. Let the `Release` GitHub Actions workflow create draft releases.
+9. Publish draft releases manually after reviewing the generated notes.
+
+Moonraker supports `stable`, `beta`, and `dev` channels for `git_repo`
+extensions. If `channel` is omitted, Moonraker treats the extension as `dev`.
+This project should guide normal users to `stable`.
+
+## Moonraker Channel Policy
+
+New installer-created updater sections should use `channel: stable`:
+
+```ini
+[update_manager z_calibration]
+type: git_repo
+channel: stable
+path: /home/pi/klipper_z_calibration
+origin: https://github.com/protoloft/klipper_z_calibration.git
+managed_services: klipper
+```
+
+Existing updater sections should be handled as follows:
+
+1. If `[update_manager z_calibration]` has no `channel`, migrate it to
+ `channel: stable`.
+2. If the section already has `channel: stable`, leave it unchanged.
+3. If the section already has `channel: beta`, leave it unchanged.
+4. If the section already has `channel: dev`, leave it unchanged.
+
+This preserves explicit user intent while moving old implicit `dev` users to
+the safer stable release stream.
+
+## Pre-release Checklist
+
+Run this checklist before creating any beta or stable release.
+
+1. Confirm the working tree contains only intended release changes:
+
+ ```bash
+ git status --short
+ ```
+
+2. Confirm the target branch is `master`:
+
+ ```bash
+ git branch --show-current
+ ```
+
+3. Pull or fetch the latest remote state before tagging:
+
+ ```bash
+ git fetch origin --tags
+ ```
+
+4. Run the local validation suite:
+
+ ```bash
+ python3 scripts/check_all.py
+ python3 scripts/check_release.py --tag v1.2.3 --channel stable
+ python3 scripts/check_release.py --tag v1.2.3-beta.1 --channel beta
+ python3 scripts/check_klipper_contract.py --klipper-path ~/klipper
+ ```
+
+5. Confirm the GitHub Actions CI workflow passes on `master`.
+6. Confirm the Klipper Compatibility workflow is passing or review its latest
+ failure before release.
+7. Review installer behavior:
+ - stock Klipper installs link `z_calibration.py` to `klippy/extras`
+ - Kalico installs link `z_calibration.py` to `klippy/plugins`
+ - `klipper_compat.py` loads from the repository checkout
+ - Moonraker updater config uses the agreed channel policy
+ - `managed_services: klipper` is present
+ - custom Moonraker paths use `install.sh -m `
+8. Review compatibility notes:
+ - standalone Klipper/Kalico plugin
+ - dockable contact probes only
+ - BLTouch, Beacon, and non-dockable virtual probes are unsupported
+ - `offset_margins` replaces the removed `max_deviation` option
+ - `safe_z_height` replaces the removed `clearance` option
+9. Update release notes with:
+ - compatibility changes
+ - migration notes
+ - installer behavior changes
+ - known limitations
+
+## Klipper Compatibility Monitoring
+
+Critical Klipper implementation assumptions are isolated in `klipper_compat.py`.
+The compatibility workflow validates the upstream source contracts that module
+uses.
+
+The `Klipper And Kalico Compatibility` workflow runs weekly and can be started
+manually.
+It checks:
+
+1. This repository's normal validation suite.
+2. The latest Klipper release tag.
+3. Klipper `master` as an early warning for upcoming breakage.
+4. Kalico `main` as an early warning for fork-specific breakage.
+
+Run the contract check manually against a local Klipper checkout:
+
+```bash
+python3 scripts/check_klipper_contract.py --klipper-path ~/klipper
+```
+
+To clone or update local Klipper/Kalico compatibility checkouts under the
+ignored `.compat_repos/` directory and run the same contract checks locally:
+
+```bash
+python3 scripts/check_firmware_compat.py
+```
+
+After the checkouts exist, rerun without fetching:
+
+```bash
+python3 scripts/check_firmware_compat.py --no-update
+```
+
+If the latest-release lane fails, treat it as a release blocker. If the
+`master` lane fails but the latest-release lane passes, open an issue and fix
+the compatibility layer before the upstream change reaches a Klipper release.
+
+## Beta Release Steps
+
+Use a beta release when a change needs testing on real printers before being
+promoted to stable.
+
+1. Choose the next beta version.
+
+ Example:
+
+ ```text
+ v1.2.0-beta.1
+ ```
+
+2. Run the pre-release checklist.
+3. Create and push the beta tag:
+
+ ```bash
+ git tag -a v1.2.0-beta.1 -m "v1.2.0-beta.1"
+ git push origin v1.2.0-beta.1
+ ```
+
+4. Wait for the `Release` workflow to pass on the tag.
+5. Open the draft GitHub Release created by the workflow.
+6. Confirm the draft is marked as a prerelease.
+7. Review and edit the generated release notes.
+8. Include tester guidance:
+ - configure Moonraker with `channel: beta`
+ - restart Moonraker after changing `moonraker.conf`
+ - update through Moonraker Update Manager
+ - report printer model, Klipper/Kalico commit, and probe type with issues
+9. Include rollback guidance:
+ - use Moonraker Update Manager rollback if available
+ - or switch back to `channel: stable`
+10. Publish the GitHub Release manually.
+11. Verify from a beta-channel install that Moonraker detects the prerelease.
+
+## Stable Release Steps
+
+Use a stable release for production-ready changes.
+
+1. Choose the next stable version.
+
+ Examples:
+
+ ```text
+ v1.2.0
+ v1.2.1
+ ```
+
+2. Run the pre-release checklist.
+3. If promoting a beta, confirm the stable tag points at the validated commit.
+4. Create and push the stable tag:
+
+ ```bash
+ git tag -a v1.2.0 -m "v1.2.0"
+ git push origin v1.2.0
+ ```
+
+5. Wait for the `Release` workflow to pass on the tag.
+6. Open the draft GitHub Release created by the workflow.
+7. Confirm the draft is not marked as a prerelease.
+8. Review and edit the generated release notes.
+9. Include release notes:
+ - user-visible changes
+ - compatibility changes
+ - Moonraker channel notes
+ - installer migration notes
+ - manual verification performed
+10. Publish the GitHub Release manually.
+11. Verify from a stable-channel install that Moonraker detects the release.
+
+## Post-release Verification
+
+After publishing, verify the release from user-like environments.
+
+1. Fresh stock Klipper install:
+ - run `install.sh`
+ - confirm `z_calibration.py` links into `klippy/extras`
+ - confirm no new `klipper_compat.py` link is created in `klippy/extras`
+ - confirm Moonraker updater section exists
+ - confirm Moonraker Update Manager shows the new version
+ - confirm custom Moonraker config paths work with `install.sh -m `
+2. Fresh Kalico install:
+ - run `install.sh`
+ - confirm `z_calibration.py` links into `klippy/plugins`
+ - confirm no new `klipper_compat.py` link is created in `klippy/plugins`
+ - confirm `allow_plugin_override` instructions are shown
+3. Existing no-channel Moonraker config:
+ - run installer/update path
+ - confirm the section is migrated to `channel: stable`
+4. Existing explicit-channel Moonraker config:
+ - confirm `channel: stable` is unchanged
+ - confirm `channel: beta` is unchanged
+ - confirm `channel: dev` is unchanged
+5. Runtime smoke test:
+ - Klipper starts successfully
+ - `[z_calibration]` loads
+ - `CALIBRATE_Z` is registered
+ - status object exposes `last_query` and `last_z_offset`
+
+## Hotfix Process
+
+Use a hotfix for critical compatibility or safety fixes.
+
+1. Identify the latest stable release tag.
+2. Create the fix from `master` if it is releasable.
+3. If `master` contains unrelated risky changes, branch from the latest stable
+ tag instead.
+4. Apply only the minimal fix and related tests.
+5. Run the full pre-release checklist.
+6. Publish a patch release.
+
+Example:
+
+```text
+v1.2.1
+```
+
+7. Clearly mark the release as a hotfix in the GitHub Release notes.
+
+## Release Notes Template
+
+Use this structure for GitHub Release notes:
+
+```markdown
+## Summary
+
+- Short description of the release.
+
+## Changes
+
+- User-visible change.
+- Compatibility fix.
+- Installer or Moonraker behavior change.
+
+## Compatibility
+
+- Klipper:
+- Kalico:
+- Moonraker:
+- Supported probes:
+
+## Migration Notes
+
+- Required user action, if any.
+
+## Validation
+
+- CI passed.
+- Local validation commands passed.
+- Manual install/update checks performed.
+```
+
+## Maintainer Command Reference
+
+Run all validation:
+
+```bash
+python3 scripts/check_whitespace.py
+bash -n install.sh
+python3 scripts/check_release.py --tag v1.2.3 --channel stable
+python3 scripts/check_release.py --tag v1.2.3-beta.1 --channel beta
+python3 scripts/check_klipper_contract.py --klipper-path ~/klipper
+python3 -m compileall .
+python3 -m unittest discover -s tests -v
+git diff --check
+```
+
+List recent tags:
+
+```bash
+git tag --sort=-version:refname | head -20
+```
+
+Create an annotated stable tag:
+
+```bash
+git tag -a v1.2.0 -m "v1.2.0"
+git push origin v1.2.0
+```
+
+Create an annotated beta tag:
+
+```bash
+git tag -a v1.2.0-beta.1 -m "v1.2.0-beta.1"
+git push origin v1.2.0-beta.1
+```
diff --git a/install.sh b/install.sh
index 4bd0eae..a7a2395 100755
--- a/install.sh
+++ b/install.sh
@@ -2,14 +2,47 @@
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/ && pwd )"
KLIPPER_PATH="${HOME}/klipper"
-SYSTEMDDIR="/etc/systemd/system"
MOONRAKER_CONFIG="${HOME}/printer_data/config/moonraker.conf"
MOONRAKER_FALLBACK="${HOME}/klipper_config/moonraker.conf"
+MOONRAKER_CONFIG_CUSTOM=0
NUM_INSTALLS=0
# Force script to exit if an error occurs
set -e
+set_install_paths()
+{
+ KALICO_PLUGIN_DIR="${KLIPPER_PATH}/klippy/plugins"
+ KALICO_PLUGIN_FILE="${KALICO_PLUGIN_DIR}/z_calibration.py"
+ KALICO_COMPAT_FILE="${KALICO_PLUGIN_DIR}/klipper_compat.py"
+ KLIPPER_EXTRA_FILE="${KLIPPER_PATH}/klippy/extras/z_calibration.py"
+ KLIPPER_COMPAT_FILE="${KLIPPER_PATH}/klippy/extras/klipper_compat.py"
+}
+
+is_repo_link()
+{
+ link_path="$1"
+ target_path="$2"
+ [ -L "$link_path" ] && [ "$(readlink "$link_path")" = "$target_path" ]
+}
+
+remove_repo_link()
+{
+ link_path="$1"
+ target_path="$2"
+ if is_repo_link "$link_path" "$target_path"; then
+ rm -f "$link_path"
+ fi
+}
+
+remove_file_if_present()
+{
+ file_path="$1"
+ if [ -e "$file_path" ] || [ -L "$file_path" ]; then
+ rm -f "$file_path"
+ fi
+}
+
# Step 1: Check for root user
verify_ready()
{
@@ -20,14 +53,9 @@ verify_ready()
fi
# output used number of installs
if [[ $NUM_INSTALLS == 0 ]]; then
- echo "Defaulted to one klipper install, if more than one instance, use -n"
+ echo "Defaulted to one klipper install, if more than one instance, use -n"
else
- echo "Number of Installs Selected: $NUM_INSTALLS"
- fi
- # Fall back to old config
- if [ ! -f "$MOONRAKER_CONFIG" ]; then
- echo "${MOONRAKER_CONFIG} does not exist. Falling back to ${MOONRAKER_FALLBACK}"
- MOONRAKER_CONFIG="$MOONRAKER_FALLBACK"
+ echo "Number of Installs Selected: $NUM_INSTALLS"
fi
}
@@ -42,74 +70,97 @@ check_klipper()
exit -1
fi
else
- for (( klip = 1; klip<=$NUM_INSTALLS; klip++ )); do
- if [ "$(sudo systemctl list-units --full -all -t service --no-legend | grep -F "klipper-$klip.service")" ]; then
- echo "klipper-$klip.service found!"
- else
- echo "klipper-$klip.service NOT found, please ensure you've entered the correct number of klipper instances you're running!"
- exit -1
- fi
- done
- fi
+ for (( klip = 1; klip<=$NUM_INSTALLS; klip++ )); do
+ if [ "$(sudo systemctl list-units --full -all -t service --no-legend | grep -F "klipper-$klip.service")" ]; then
+ echo "klipper-$klip.service found!"
+ else
+ echo "klipper-$klip.service NOT found, please ensure you've entered the correct number of klipper instances you're running!"
+ exit -1
+ fi
+ done
+ fi
+}
+
+resolve_moonraker_config()
+{
+ if [ -f "$MOONRAKER_CONFIG" ]; then
+ echo "Moonraker configuration found at ${MOONRAKER_CONFIG}"
+ return
+ fi
+ if [ "$MOONRAKER_CONFIG_CUSTOM" -eq 0 ] \
+ && [ -f "$MOONRAKER_FALLBACK" ]; then
+ echo "${MOONRAKER_CONFIG} does not exist. Falling back to ${MOONRAKER_FALLBACK}"
+ MOONRAKER_CONFIG="$MOONRAKER_FALLBACK"
+ echo "Moonraker configuration found at ${MOONRAKER_CONFIG}"
+ return
+ fi
+ if [ "$MOONRAKER_CONFIG_CUSTOM" -eq 0 ]; then
+ echo "Error: Moonraker configuration not found: ${MOONRAKER_CONFIG} or ${MOONRAKER_FALLBACK}. Exiting.."
+ else
+ echo "Error: Moonraker configuration not found: ${MOONRAKER_CONFIG}. Exiting.."
+ fi
+ exit -1
}
# Step 3: Check folders
-check_requirements()
+check_klipper_path()
{
if [ ! -d "${KLIPPER_PATH}/klippy/extras/" ]; then
echo "Error: Klipper not found in directory: ${KLIPPER_PATH}. Exiting.."
exit -1
fi
echo "Klipper found at ${KLIPPER_PATH}"
+}
- if [ ! -f "$MOONRAKER_CONFIG" ]; then
- echo "Error: Moonraker configuration not found: ${MOONRAKER_CONFIG}. Exiting.."
- exit -1
- fi
- echo "Moonraker configuration found at ${MOONRAKER_CONFIG}"
+check_requirements()
+{
+ check_klipper_path
+ resolve_moonraker_config
}
# Step 4: Link extension to Klipper
-link_extension()
+link_kalico_extension()
{
- echo -n "Linking extension to Klipper... "
- ln -sf "${SRCDIR}/z_calibration.py" "${KLIPPER_PATH}/klippy/extras/z_calibration.py"
+ echo -n "Linking extension to Kalico plugins... "
+ ln -sf "${SRCDIR}/z_calibration.py" "$KALICO_PLUGIN_FILE"
+ remove_repo_link "$KLIPPER_EXTRA_FILE" "${SRCDIR}/z_calibration.py"
+ remove_repo_link "$KLIPPER_COMPAT_FILE" "${SRCDIR}/klipper_compat.py"
+ remove_repo_link "$KALICO_COMPAT_FILE" "${SRCDIR}/klipper_compat.py"
+ remove_file_if_present "${KLIPPER_PATH}/klippy/extras/klipper_compat.pyc"
+ remove_file_if_present "${KALICO_PLUGIN_DIR}/klipper_compat.pyc"
echo "[OK]"
+ echo "Kalico users must enable:"
+ echo " [danger_options]"
+ echo " allow_plugin_override: True"
}
-# Step 5: Remove old dummy system service
-remove_service()
+link_klipper_extension()
{
- SERVICE_FILE="${SYSTEMDDIR}/z_calibration.service"
- if [ -f "$SERVICE_FILE" ]; then
- echo -n "Removing system service... "
- sudo service z_calibration stop
- sudo systemctl disable z_calibration.service
- sudo rm "$SERVICE_FILE"
- echo "[OK]"
- fi
- OLD_SERVICE_FILE="${SYSTEMDDIR}/klipper_z_calibration.service"
- if [ -f "$OLD_SERVICE_FILE" ]; then
- echo -n "Removing old system service... "
- sudo service klipper_z_calibration stop
- sudo systemctl disable klipper_z_calibration.service
- sudo rm "$OLD_SERVICE_FILE"
- echo "[OK]"
+ echo -n "Linking extension to Klipper extras... "
+ ln -sf "${SRCDIR}/z_calibration.py" "$KLIPPER_EXTRA_FILE"
+ remove_repo_link "$KLIPPER_COMPAT_FILE" "${SRCDIR}/klipper_compat.py"
+ remove_file_if_present "${KLIPPER_PATH}/klippy/extras/klipper_compat.pyc"
+ echo "[OK]"
+}
+
+link_extension()
+{
+ if [ -d "$KALICO_PLUGIN_DIR" ]; then
+ link_kalico_extension
+ return
fi
+ link_klipper_extension
}
-# Step 6: Add updater to moonraker.conf
+# Step 5: Add updater to moonraker.conf
add_updater()
{
echo -n "Adding update manager to moonraker.conf... "
- update_section=$(grep -c '\[update_manager[a-z ]* z_calibration\]' $MOONRAKER_CONFIG || true)
- if [ "$update_section" -eq 0 ]; then
- echo -e "\n[update_manager z_calibration]" >> "$MOONRAKER_CONFIG"
- echo "type: git_repo" >> "$MOONRAKER_CONFIG"
- echo "path: ${SRCDIR}" >> "$MOONRAKER_CONFIG"
- echo "origin: https://github.com/protoloft/klipper_z_calibration.git" >> "$MOONRAKER_CONFIG"
- echo "managed_services: klipper" >> "$MOONRAKER_CONFIG"
- echo -e "\n" >> "$MOONRAKER_CONFIG"
+ update_result=$(python3 \
+ "${SRCDIR}/scripts/update_moonraker.py" \
+ "$MOONRAKER_CONFIG" \
+ "$SRCDIR")
+ if [ "$update_result" = "changed" ]; then
echo "[OK]"
echo -n "Restarting Moonraker... "
@@ -120,7 +171,7 @@ add_updater()
fi
}
-# Step 7: Restarting Klipper
+# Step 6: Restarting Klipper
restart_klipper()
{
if [[ $NUM_INSTALLS == 0 ]]; then
@@ -128,25 +179,38 @@ restart_klipper()
sudo systemctl restart klipper
echo "[OK]"
else
- for (( klip = 1; klip<=$NUM_INSTALLS; klip++)); do
+ for (( klip = 1; klip<=$NUM_INSTALLS; klip++)); do
echo -n "Restarting Klipper-$klip... "
sudo systemctl restart klipper-$klip
echo "[OK]"
- done
+ done
fi
}
uinstall()
{
- if [ -f "${KLIPPER_PATH}/klippy/extras/z_calibration.py" ]; then
+ if is_repo_link "$KALICO_PLUGIN_FILE" "${SRCDIR}/z_calibration.py" \
+ || is_repo_link "$KALICO_COMPAT_FILE" "${SRCDIR}/klipper_compat.py" \
+ || is_repo_link "$KLIPPER_EXTRA_FILE" "${SRCDIR}/z_calibration.py" \
+ || is_repo_link "$KLIPPER_COMPAT_FILE" "${SRCDIR}/klipper_compat.py"; then
echo -n "Uninstalling z_calibration... "
- rm -f "${KLIPPER_PATH}/klippy/extras/z_calibration.py"
- rm -f "${KLIPPER_PATH}/klippy/extras/z_calibration.pyc"
+ remove_repo_link \
+ "$KALICO_PLUGIN_FILE" "${SRCDIR}/z_calibration.py"
+ remove_repo_link \
+ "$KALICO_COMPAT_FILE" "${SRCDIR}/klipper_compat.py"
+ remove_file_if_present "${KALICO_PLUGIN_DIR}/z_calibration.pyc"
+ remove_file_if_present "${KALICO_PLUGIN_DIR}/klipper_compat.pyc"
+ remove_repo_link \
+ "$KLIPPER_EXTRA_FILE" "${SRCDIR}/z_calibration.py"
+ remove_repo_link \
+ "$KLIPPER_COMPAT_FILE" "${SRCDIR}/klipper_compat.py"
+ remove_file_if_present "${KLIPPER_PATH}/klippy/extras/z_calibration.pyc"
+ remove_file_if_present "${KLIPPER_PATH}/klippy/extras/klipper_compat.pyc"
echo "[OK]"
echo "You can now remove the \"[update_manager z_calibration]\" section in your moonraker.conf and delete this directory."
echo "You also need to remove the \"[z_calibration]\" section in your Klipper configuration..."
else
- echo -n "${KLIPPER_PATH}/klippy/extras/z_calibration.py not found. Is it installed? "
+ echo -n "z_calibration.py not found. Is it installed? "
echo "[FAILED]"
fi
}
@@ -158,31 +222,36 @@ usage()
}
# Command parsing
-while getopts ":k:m:n:uh" OPTION; do
- case "$OPTION" in
- k) KLIPPER_PATH="$OPTARG" ;;
- m) MOONRAKER_CONFIG="$OPTARG" ;;
- n) NUM_INSTALLS="$OPTARG" ;;
- u) UNINSTALL=1 ;;
- h | ?) usage ;;
- esac
-done
-
-# Fall back to old config
-if [ ! -f "$MOONRAKER_CONFIG" ]; then
- echo "${MOONRAKER_CONFIG} does not exist. Falling back to ${MOONRAKER_FALLBACK}"
- MOONRAKER_CONFIG="$MOONRAKER_FALLBACK"
-fi
+main()
+{
+ OPTIND=1
+ UNINSTALL=""
+ MOONRAKER_CONFIG_CUSTOM=0
+ while getopts ":k:m:n:uh" OPTION; do
+ case "$OPTION" in
+ k) KLIPPER_PATH="$OPTARG" ;;
+ m) MOONRAKER_CONFIG="$OPTARG"
+ MOONRAKER_CONFIG_CUSTOM=1 ;;
+ n) NUM_INSTALLS="$OPTARG" ;;
+ u) UNINSTALL=1 ;;
+ h | ?) usage ;;
+ esac
+ done
+
+ set_install_paths
+ verify_ready
+ check_klipper
+ if [ -z "$UNINSTALL" ]; then
+ check_requirements
+ link_extension
+ add_updater
+ else
+ check_klipper_path
+ uinstall
+ fi
+ restart_klipper
+}
-# Run steps
-verify_ready
-check_klipper
-check_requirements
-remove_service
-if [ ! $UNINSTALL ]; then
- link_extension
- add_updater
-else
- uinstall
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ main "$@"
fi
-restart_klipper
diff --git a/klipper_compat.py b/klipper_compat.py
new file mode 100644
index 0000000..4a51c7b
--- /dev/null
+++ b/klipper_compat.py
@@ -0,0 +1,266 @@
+# Klipper compatibility helpers for z_calibration.
+#
+# Copyright (C) 2021-2025 Titus Meyer
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+from mcu import MCU_endstop
+
+
+class PrinterObjectCompat:
+ def __init__(self, printer):
+ self.printer = printer
+
+ def lookup_gcode(self):
+ return self.printer.lookup_object('gcode')
+
+ def lookup_gcode_move(self):
+ return self.printer.lookup_object('gcode_move')
+
+ def lookup_homing(self):
+ return self.printer.lookup_object('homing')
+
+ def lookup_toolhead(self):
+ return self.printer.lookup_object('toolhead')
+
+ def lookup_probe(self):
+ return self.printer.lookup_object('probe')
+
+ def lookup_optional_probe(self):
+ return self.printer.lookup_object('probe', default=None)
+
+ def lookup_safe_z_home(self):
+ return self.printer.lookup_object('safe_z_home', default=None)
+
+ def lookup_bed_mesh(self):
+ return self.printer.lookup_object('bed_mesh', default=None)
+
+ def load_gcode_macro(self, config):
+ return self.printer.load_object(config, 'gcode_macro')
+
+ def load_query_endstops(self, config):
+ return self.printer.load_object(config, 'query_endstops')
+
+
+class EndstopWrapper:
+ def __init__(self, endstop):
+ self.mcu_endstop = endstop
+ # Wrappers
+ self.get_mcu = self.mcu_endstop.get_mcu
+ self.add_stepper = self.mcu_endstop.add_stepper
+ self.get_steppers = self.mcu_endstop.get_steppers
+ self.home_start = self.mcu_endstop.home_start
+ self.home_wait = self.mcu_endstop.home_wait
+ self.query_endstop = self.mcu_endstop.query_endstop
+
+
+class HomingCompat:
+ def __init__(self, printer):
+ self.printer = printer
+ self.objects = PrinterObjectCompat(printer)
+
+ def get_z_endstop(self, query_endstops, section_name):
+ z_endstop = None
+ for endstop, name in query_endstops.endstops:
+ if name == 'stepper_z' or name == 'z':
+ if not isinstance(endstop, MCU_endstop):
+ raise self.printer.config_error(
+ "A virtual endstop for z is not supported for %s"
+ % (section_name,))
+ z_endstop = EndstopWrapper(endstop)
+ if z_endstop is None:
+ raise self.printer.config_error("No z-endstop found for %s"
+ % (section_name,))
+ return z_endstop
+
+ def get_z_rail_settings(self, rail):
+ if not rail.get_steppers()[0].is_active_axis('z'):
+ return None
+ return {
+ 'position_endstop': rail.position_endstop,
+ 'homing_speed': rail.homing_speed,
+ 'second_homing_speed': rail.second_homing_speed,
+ 'homing_retract_dist': rail.homing_retract_dist,
+ 'position_min': rail.position_min,
+ }
+
+ def probing_move(self, mcu_endstop, pos, speed):
+ homing = self.objects.lookup_homing()
+ return homing.probing_move(mcu_endstop, pos, speed)
+
+
+class ToolheadCompat:
+ def __init__(self, printer):
+ self.printer = printer
+ self.objects = PrinterObjectCompat(printer)
+
+ def _toolhead(self):
+ return self.objects.lookup_toolhead()
+
+ def get_position(self):
+ return self._toolhead().get_position()
+
+ def manual_move(self, coord, speed):
+ self._toolhead().manual_move(coord, speed)
+
+ def get_last_move_time(self):
+ return self._toolhead().get_last_move_time()
+
+ def is_axis_homed(self, axis):
+ eventtime = self.printer.get_reactor().monotonic()
+ homed_axes = self._toolhead().get_status(eventtime).get(
+ 'homed_axes', '')
+ return axis in homed_axes
+
+
+class BedMeshCompat:
+ def get_zero_reference_position(self, mesh):
+ if mesh is None:
+ return None
+ bmc = getattr(mesh, 'bmc', None)
+ if bmc is None:
+ return None
+ if (hasattr(bmc, 'probe_mgr')
+ and bmc.probe_mgr.zero_ref_pos is not None):
+ return bmc.probe_mgr.zero_ref_pos
+ if hasattr(bmc, 'zero_ref_pos') and bmc.zero_ref_pos is not None:
+ # TODO: remove - deprecated since 2024-06
+ return bmc.zero_ref_pos
+ if (hasattr(bmc, 'relative_reference_index')
+ and bmc.relative_reference_index is not None):
+ # TODO: remove: trying to read the deprecated rri
+ rri = bmc.relative_reference_index
+ return bmc.points[rri]
+ return None
+
+
+class ProbeCompat:
+ def __init__(self, helper, probe, gcmd=None):
+ self.helper = helper
+ self.probe = probe
+ self.gcmd = gcmd
+ self.gcode = helper.gcode
+ self.session = None
+
+ def get_config_defaults(self):
+ # TODO: remove: deprecated since 2024-06-10
+ if hasattr(self.probe, 'sample_count'):
+ return {
+ 'samples': self.probe.sample_count,
+ 'samples_tolerance': self.probe.samples_tolerance,
+ 'samples_tolerance_retries': self.probe.samples_retries,
+ 'lift_speed': self.probe.lift_speed,
+ 'samples_result': self.probe.samples_result,
+ 'safe_z_height': self.probe.z_offset * 2,
+ }
+ probe_params = self.probe.get_probe_params()
+ return {
+ 'samples': probe_params['samples'],
+ 'samples_tolerance': probe_params['samples_tolerance'],
+ 'samples_tolerance_retries': (
+ probe_params['samples_tolerance_retries']),
+ 'lift_speed': probe_params['lift_speed'],
+ 'samples_result': probe_params['samples_result'],
+ 'safe_z_height': self.probe.get_offsets()[2] * 2,
+ }
+
+ def get_offsets(self):
+ return self.probe.get_offsets()
+
+ def start(self):
+ if hasattr(self.probe, 'start_probe_session'):
+ self.session = self.probe.start_probe_session(self.gcmd)
+ elif hasattr(self.probe, 'multi_probe_begin'):
+ # TODO: remove: deprecated since 2024-06-10
+ self.probe.multi_probe_begin()
+ else:
+ # TODO: remove: deprecated since 2024-06-10
+ self.probe.probe_session.start_probe_session(None)
+
+ def end(self):
+ if self.session is not None:
+ self.session.end_probe_session()
+ self.session = None
+ elif hasattr(self.probe, 'multi_probe_end'):
+ # TODO: remove: deprecated since 2024-06-10
+ self.probe.multi_probe_end()
+ else:
+ # TODO: remove: deprecated since 2024-06-10
+ self.probe.probe_session.end_probe_session()
+
+ def query_endstop(self, print_time):
+ for probe_endstop in self._query_endstop_candidates():
+ query_endstop = getattr(probe_endstop, 'query_endstop', None)
+ if query_endstop is not None:
+ return query_endstop(print_time)
+ raise self.gcmd.error("%s: probe does not support endstop queries"
+ % (self.gcmd.get_command(),))
+
+ def can_probe(self):
+ return self.session is not None and hasattr(self.session, 'run_probe')
+
+ def get_legacy_probe_endstop(self):
+ probe_endstop = getattr(self.probe, 'mcu_probe', None)
+ if probe_endstop is None:
+ return None
+ if hasattr(probe_endstop, 'get_steppers'):
+ return probe_endstop
+ return getattr(probe_endstop, 'mcu_endstop', None)
+
+ def run_probe(self, speed, samples=None):
+ if not self.can_probe():
+ return None
+ pgcmd = self._create_probe_gcmd(speed, samples)
+ self.session.run_probe(pgcmd)
+ results = self.session.pull_probed_results()
+ if not results:
+ raise self.gcmd.error("%s: probe did not return a result"
+ % (self.gcmd.get_command(),))
+ return results[-1]
+
+ def get_test_position(self, probe_result):
+ if hasattr(probe_result, 'test_z'):
+ return [probe_result.test_x, probe_result.test_y,
+ probe_result.test_z]
+ if len(probe_result) >= 6:
+ return [probe_result[3], probe_result[4], probe_result[5]]
+ return probe_result[:3]
+
+ def _create_probe_gcmd(self, speed, samples):
+ params = {}
+ if hasattr(self.gcmd, 'get_command_parameters'):
+ params.update(self.gcmd.get_command_parameters())
+ samples_result = self.helper.samples_result or 'average'
+ params.update({
+ 'PROBE_SPEED': str(speed),
+ 'LIFT_SPEED': str(self.helper.lift_speed),
+ 'SAMPLES': str(samples or self.helper.samples),
+ 'SAMPLE_RETRACT_DIST': str(self.helper.retract_dist),
+ 'SAMPLES_TOLERANCE': str(self.helper.tolerance),
+ 'SAMPLES_TOLERANCE_RETRIES': str(self.helper.retries),
+ 'SAMPLES_RESULT': samples_result,
+ })
+ command = self.gcmd.get_command()
+ return self.gcode.create_gcode_command(command, command, params)
+
+ def _query_endstop_candidates(self):
+ probe_endstop = getattr(self.probe, 'mcu_probe', None)
+ candidates = [self.probe, probe_endstop]
+ if probe_endstop is not None:
+ candidates.append(getattr(probe_endstop, 'mcu_endstop', None))
+ return [candidate for candidate in candidates if candidate is not None]
+
+
+class GCodeOffsetCompat:
+ def __init__(self, gcode, gcode_move):
+ self.gcode = gcode
+ self.gcode_move = gcode_move
+
+ def set_new_offset(self, offset):
+ gcmd_offset = self.gcode.create_gcode_command("SET_GCODE_OFFSET",
+ "SET_GCODE_OFFSET",
+ {'Z': 0.0})
+ self.gcode_move.cmd_SET_GCODE_OFFSET(gcmd_offset)
+ gcmd_offset = self.gcode.create_gcode_command("SET_GCODE_OFFSET",
+ "SET_GCODE_OFFSET",
+ {'Z_ADJUST': offset})
+ self.gcode_move.cmd_SET_GCODE_OFFSET(gcmd_offset)
diff --git a/pictures/banner-dark.png b/pictures/banner-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..665fe3f1e9a51ebc95418b657dc23a09c66c3948
GIT binary patch
literal 463080
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