PA calibration reads Spoolman first; guard resets a sensor left in PA mode

- LANE_CALIBRATE_PA: when a calibration will run, assign the lane's spool
  again (SET_SPOOL_ID) so AFC reads the Spoolman temperature now; AFC
  only reads it at spool assignment and Klipper start (TPU calibrated at
  210C after Spoolman was set to 230C). The calibration moved to
  _LANE_CALIBRATE_PA_RUN, rendered after the refresh.
- bdp_guard: an emergency stop during PA_CALIBRATE leaves the sensor in
  PA mode (FIRMWARE_RESTART does not reset it). When arming, read the
  mode register and switch the sensor back to probe mode with a fresh
  baseline before watching it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-19 16:04:40 +02:00
1 parent d49e65c0bb
commit 93ae8795b8
3 files changed
+54 -3

No files matched your search

+2 -2
View File
@@ -42,7 +42,7 @@ everything below, grouped by topic. Comment out what you do not use there.
| `purge_until_empty.cfg` | `PURGE_UNTIL_EMPTY`: purges a snapped filament out until there is no more nozzle pressure | same as `purge_check.cfg` | | `purge_until_empty.cfg` | `PURGE_UNTIL_EMPTY`: purges a snapped filament out until there is no more nozzle pressure | same as `purge_check.cfg` |
| **pressure_advance/** | | | | **pressure_advance/** | | |
| `bd_pressure.cfg` | `[bdpressure bd_pa]` (I2C on the Nitehawk) and the vendor macros `PA_CALIBRATE`, `PA_E`, `PA_RESET`, `PA_STATE`; `PA_CALIBRATE` no longer sets Z back to a value read before the test (wrong after a Z hop in `G28 XY`) | `bdpressure.py` from the bd_pressure repo | | `bd_pressure.cfg` | `[bdpressure bd_pa]` (I2C on the Nitehawk) and the vendor macros `PA_CALIBRATE`, `PA_E`, `PA_RESET`, `PA_STATE`; `PA_CALIBRATE` no longer sets Z back to a value read before the test (wrong after a Z hop in `G28 XY`) | `bdpressure.py` from the bd_pressure repo |
| `bd_pa_spoolman.cfg` + `bd_pa_*.sh` | `LANE_CALIBRATE_PA`, `PA_CALIBRATE_ALL_LANES`, `LANE_RECAL_PA`, `SET_SPOOL_ID`, `PA_STATUS`: pressure advance per lane/spool, cached locally and in Spoolman; a calibration with no result, a pause during the test or too few/stuck sensor readings is rejected and not saved | bd_pressure, AFC, Spoolman, `gcode_shell_command` | | `bd_pa_spoolman.cfg` + `bd_pa_*.sh` | `LANE_CALIBRATE_PA`, `PA_CALIBRATE_ALL_LANES`, `LANE_RECAL_PA`, `SET_SPOOL_ID`, `PA_STATUS`: pressure advance per lane/spool, cached locally and in Spoolman; before a calibration the lane's spool is assigned again so a temperature just changed in Spoolman is used; a calibration with no result, a pause during the test or too few/stuck sensor readings is rejected and not saved | bd_pressure, AFC, Spoolman, `gcode_shell_command` |
| **spoolman/** | | | | **spoolman/** | | |
| `active_spool.cfg` | `SET_ACTIVE_SPOOL`, `CLEAR_ACTIVE_SPOOL` | Spoolman in Moonraker | | `active_spool.cfg` | `SET_ACTIVE_SPOOL`, `CLEAR_ACTIVE_SPOOL` | Spoolman in Moonraker |
| **leds/** | | | | **leds/** | | |
@@ -57,7 +57,7 @@ everything below, grouped by topic. Comment out what you do not use there.
| `xy_drift.py` | `AFC_XY_SAFETY_CHECK`: measures lost XY steps from the stepper step counts at the endstop triggers (CoreXY). The toolhead position after homing cannot show lost steps. | | `xy_drift.py` | `AFC_XY_SAFETY_CHECK`: measures lost XY steps from the stepper step counts at the endstop triggers (CoreXY). The toolhead position after homing cannot show lost steps. |
| `bdwidth_status.py` | exposes bdwidth's motion counter and width to macros (`present: False` with neutral values when no bdwidth is configured, so the macros also work without it), and keeps a USB dropout of bdwidth (port: usb) from shutting down Klipper: the read is retried every 10 s and the port reopened | | `bdwidth_status.py` | exposes bdwidth's motion counter and width to macros (`present: False` with neutral values when no bdwidth is configured, so the macros also work without it), and keeps a USB dropout of bdwidth (port: usb) from shutting down Klipper: the read is retried every 10 s and the port reopened |
| `bdpressure_check.py` | bd_pressure load check (`BDP_CHECK_START/STOP/QUERY`). Needs the load-check firmware from the bd_pressure `load-check-extension` branch; on stock firmware it reports "not available" and does nothing. Also reports how many readings the last `PA_CALIBRATE` got (`pa_points`, `pa_distinct`), used to reject runs with a silent or stuck sensor. | | `bdpressure_check.py` | bd_pressure load check (`BDP_CHECK_START/STOP/QUERY`). Needs the load-check firmware from the bd_pressure `load-check-extension` branch; on stock firmware it reports "not available" and does nothing. Also reports how many readings the last `PA_CALIBRATE` got (`pa_points`, `pa_distinct`), used to reject runs with a silent or stuck sensor. |
| `bdp_guard.py` | nozzle collision guard: while armed (`BDP_GUARD_ARM/DISARM/QUERY`) reads the bd_pressure force every 20 ms over I2C and, in stop mode, shuts Klipper down when it stays above the threshold. Reports reads, peak and errors per phase in the console and klippy.log. Puts a lock on the sensor's I2C connection that `bdpressure.py` and `bdpressure_check.py` also go through (Klipper allows one I2C request at a time per device). Needs the load-check firmware. | | `bdp_guard.py` | nozzle collision guard: while armed (`BDP_GUARD_ARM/DISARM/QUERY`) reads the bd_pressure force every 20 ms over I2C and, in stop mode, shuts Klipper down when it stays above the threshold. Reports reads, peak and errors per phase in the console and klippy.log. When it arms and finds the sensor still in PA mode (an emergency stop during `PA_CALIBRATE`; `FIRMWARE_RESTART` does not reset the sensor), it switches it to probe mode and takes a new zero first. Puts a lock on the sensor's I2C connection that `bdpressure.py` and `bdpressure_check.py` also go through (Klipper allows one I2C request at a time per device). Needs the load-check firmware. |
### Scripts (`scripts/`) ### Scripts (`scripts/`)
+29
View File
@@ -40,6 +40,9 @@
import logging import logging
REG_MODE = 48 # status_clk: 7 = PA mode, 2 = probe (endstop) mode
REG_RESET_PROBE = 52 # set_normal: write 1 = take the current reading as zero
MODE_PA = 7
REG_EXT = 54 # extension marker 0xBD + version REG_EXT = 54 # extension marker 0xBD + version
REG_DELTA = 64 # chk_delta, int16 little-endian REG_DELTA = 64 # chk_delta, int16 little-endian
EXT_MAGIC = 0xBD EXT_MAGIC = 0xBD
@@ -134,6 +137,31 @@ class BDPGuard:
return False return False
return len(data) == 2 and data[0] == EXT_MAGIC and data[1] >= 1 return len(data) == 2 and data[0] == EXT_MAGIC and data[1] >= 1
def _ensure_probe_mode(self, gcmd):
# An emergency stop during PA_CALIBRATE leaves the sensor in PA mode
# (FIRMWARE_RESTART does not reset it); bdpressure.py only switches
# it back at the next homing move, after the guard is armed. In PA
# mode the delta means nothing and could trip the guard. Register 48
# is the mode: 7 = PA, 2 = probe.
if getattr(self.sensor, 'last_state', 0):
return # a calibration runs, leave the sensor alone
try:
mode = bytearray(self.i2c.i2c_read([REG_MODE], 1)['response'])
if len(mode) != 1 or mode[0] != MODE_PA:
return
self.sensor.set_probe_mode()
# new samples at the probe sample rate, then a fresh baseline
self.reactor.pause(self.reactor.monotonic() + 0.5)
self.i2c.i2c_write([REG_RESET_PROBE, 1])
self.reactor.pause(self.reactor.monotonic() + 0.3)
except Exception:
logging.exception("bdp_guard: switching the sensor to probe mode failed")
return
msg = ("BDP_GUARD: bd_pressure was still in PA mode (stopped "
"calibration?), switched to probe mode")
logging.info(msg)
gcmd.respond_info(msg)
def _stop_timer(self): def _stop_timer(self):
if self.timer is not None: if self.timer is not None:
self.reactor.unregister_timer(self.timer) self.reactor.unregister_timer(self.timer)
@@ -200,6 +228,7 @@ class BDPGuard:
gcmd.respond_info("BDP_GUARD_ARM: bd_pressure load-check " gcmd.respond_info("BDP_GUARD_ARM: bd_pressure load-check "
"extension not found, guard not armed") "extension not found, guard not armed")
return return
self._ensure_probe_mode(gcmd)
self.phase_mode = mode self.phase_mode = mode
self._reset_phase(phase) self._reset_phase(phase)
self.armed_until = self.reactor.monotonic() + self.timeout self.armed_until = self.reactor.monotonic() + self.timeout
+23 -1
View File
@@ -395,8 +395,30 @@ gcode:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Core: calibrate (or restore from cache) PA for one lane # Core: calibrate (or restore from cache) PA for one lane
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# The lane's temperature (and material) come from Spoolman through AFC,
# but AFC only reads them when a spool is assigned or Klipper starts: a
# nozzle temperature changed in Spoolman afterwards was not used (TPU
# calibrated at 210C after Spoolman was set to 230C). When a calibration
# will run, assign the lane's spool again first so AFC reads Spoolman now.
# The calibration is a separate macro, rendered after that refresh.
[gcode_macro LANE_CALIBRATE_PA] [gcode_macro LANE_CALIBRATE_PA]
description: Calibrate PA for one AFC lane using bd_pressure, with caching description: Calibrate PA for one AFC lane using bd_pressure, with caching (reads the lane's Spoolman data again first)
# LANE=<lane_name> [FORCE=1]
gcode:
{% set lane = params.LANE|default("")|string %}
{% set force = params.FORCE|default(0)|int %}
{% set vars = printer.save_variables.variables %}
{% set spool_id = vars["pa_lane_" ~ lane ~ "_spool_id"]|default(0)|int %}
{% set cached = ("pa_value_spool_" ~ spool_id) in vars %}
{% set key = "AFC_stepper " ~ lane %}
{% set afc_spool = printer[key].spool_id|default(none, true) if key in printer else none %}
{% if lane and spool_id > 0 and (force or not cached) and afc_spool %}
SET_SPOOL_ID LANE={lane} SPOOL_ID={afc_spool}
{% endif %}
_LANE_CALIBRATE_PA_RUN {rawparams}
[gcode_macro _LANE_CALIBRATE_PA_RUN]
description: Helper for LANE_CALIBRATE_PA: calibrate PA for one AFC lane using bd_pressure, with caching
# LANE=<lane_name> [FORCE=1] # LANE=<lane_name> [FORCE=1]
gcode: gcode:
{% set lane = params.LANE|default("")|string %} {% set lane = params.LANE|default("")|string %}