diff --git a/README.md b/README.md index b754fb2..22432ca 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,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. | | `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. | -| `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. 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. 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/`) diff --git a/extras/bdp_guard.py b/extras/bdp_guard.py index e7ab24a..777e867 100644 --- a/extras/bdp_guard.py +++ b/extras/bdp_guard.py @@ -46,6 +46,38 @@ EXT_MAGIC = 0xBD MODES = ('log', 'stop') +# Klipper allows only one I2C request at a time per device: a second one +# while the first waits for its answer raises KeyError ('i2c_response', oid) +# and shuts Klipper down. The guard reads from a timer while G-code runs, +# and bdpressure.py writes the probe mode at every homing move (seen: G28 +# during an armed guard), so all transfers of the sensor take this lock. +# The guard puts it on the sensor object at connect; bdpressure and +# bdpressure_check use sensor.i2c and so get it too. +class LockedI2C: + def __init__(self, i2c, mutex): + self._i2c = i2c + self._mutex = mutex + + def i2c_read(self, *args, **kwargs): + with self._mutex: + return self._i2c.i2c_read(*args, **kwargs) + + def i2c_write(self, *args, **kwargs): + with self._mutex: + return self._i2c.i2c_write(*args, **kwargs) + + def i2c_write_wait_ack(self, *args, **kwargs): + with self._mutex: + return self._i2c.i2c_write_wait_ack(*args, **kwargs) + + def i2c_transfer(self, *args, **kwargs): + with self._mutex: + return self._i2c.i2c_transfer(*args, **kwargs) + + def __getattr__(self, name): + return getattr(self._i2c, name) + + class BDPGuard: def __init__(self, config): self.printer = config.get_printer() @@ -76,7 +108,13 @@ class BDPGuard: def _handle_connect(self): self.sensor = self.printer.lookup_object(self.sensor_name, None) - self.i2c = getattr(self.sensor, 'i2c', None) + i2c = getattr(self.sensor, 'i2c', None) + if i2c is not None and not isinstance(i2c, LockedI2C): + # Every user of the sensor (bdpressure, bdpressure_check, this + # guard) now goes through one lock, see LockedI2C + i2c = LockedI2C(i2c, self.reactor.mutex()) + self.sensor.i2c = i2c + self.i2c = i2c def _handle_shutdown(self): self._stop_timer() diff --git a/extras/bdpressure_check.py b/extras/bdpressure_check.py index 146d125..e2d38cc 100644 --- a/extras/bdpressure_check.py +++ b/extras/bdpressure_check.py @@ -35,7 +35,6 @@ class BDPressureCheck: def __init__(self, config): self.printer = config.get_printer() self.sensor_name = 'bdpressure ' + config.get('sensor', 'bd_pa') - self.i2c = None self.sensor = None self.status = {'available': False, 'running': False, 'peak': 0, 'above_ms': 0, 'longest_ms': 0, 'delta': 0} @@ -50,9 +49,13 @@ class BDPressureCheck: desc="Read the bd_pressure load check values") def _handle_connect(self): - sensor = self.printer.lookup_object(self.sensor_name) - self.sensor = sensor - self.i2c = getattr(sensor, 'i2c', None) + self.sensor = self.printer.lookup_object(self.sensor_name) + + @property + def i2c(self): + # Looked up at every use: bdp_guard replaces the sensor's i2c with a + # locked wrapper at connect, possibly after this module's connect. + return getattr(self.sensor, 'i2c', None) def _read(self): if self.i2c is None: