bdwidth_status: keep a bdwidth USB dropout from shutting down Klipper

bdwidth reads its CH340 from a reactor timer without error handling, so
"write failed: [Errno 5] Input/output error" shut down Klipper mid print.
bdwidth_status now wraps the sensor's Read_bdwidth (bdwidth.py itself
unchanged): on a USB error it closes the port, reports once and returns
False, so bdwidth retries every 10 s; the port is reopened when the
device is back. New status fields usb_ok and usb_errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-12 18:03:18 +02:00
1 parent eb7e0d4482
commit e4d2230a57
3 files changed
+57 -1

No files matched your search

+1 -1
View File
@@ -50,7 +50,7 @@ everything below, grouped by topic. Comment out what you do not use there.
| File | For | | File | For |
|---|---| |---|---|
| `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 | | `bdwidth_status.py` | exposes bdwidth's motion counter and width to macros, 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. | | `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. |
### Scripts (`scripts/`) ### Scripts (`scripts/`)
Binary file not shown.
+56
View File
@@ -26,6 +26,19 @@
# only starts reading on the first SET_BDWIDTH ... ENABLE # only starts reading on the first SET_BDWIDTH ... ENABLE
# linear_motion - counts per mm (motion_linear_coefficient) # linear_motion - counts per mm (motion_linear_coefficient)
# sample_time - seconds between sensor reads # sample_time - seconds between sensor reads
# usb_ok - False while the USB connection is down (port: usb)
# usb_errors - USB read errors since Klipper start
#
# USB guard (port: usb): bdwidth reads the sensor from a reactor timer without
# any error handling, so one USB dropout of its CH340 (write failed: [Errno 5]
# Input/output error) shut down Klipper mid print. This module wraps the
# sensor's Read_bdwidth at connect: on a USB error it closes the port, reports
# it and returns False, after which bdwidth tries again 10 s later; the next
# reads reopen the port (the /dev/serial/by-id path returns when the device
# reconnects). bdwidth.py itself is not changed.
import logging
class BDWidthStatus: class BDWidthStatus:
@@ -34,11 +47,52 @@ class BDWidthStatus:
self.name = config.get('sensor', 'fila_width_0') self.name = config.get('sensor', 'fila_width_0')
self.sensor_name = 'bdwidth ' + self.name self.sensor_name = 'bdwidth ' + self.name
self.sensor = None self.sensor = None
self.usb_ok = True
self.usb_errors = 0
self._read = None
self.printer.register_event_handler('klippy:connect', self.printer.register_event_handler('klippy:connect',
self._handle_connect) self._handle_connect)
def _handle_connect(self): def _handle_connect(self):
self.sensor = self.printer.lookup_object(self.sensor_name) self.sensor = self.printer.lookup_object(self.sensor_name)
if (getattr(self.sensor, 'port', None) == 'usb'
and callable(getattr(self.sensor, 'Read_bdwidth', None))
and getattr(self.sensor, 'usb', None) is not None):
self._read = self.sensor.Read_bdwidth
self.sensor.Read_bdwidth = self._guarded_read
logging.info("bdwidth_status: USB guard active for %s", self.name)
def _respond(self, msg):
logging.warning(msg)
try:
self.printer.lookup_object('gcode').respond_info(msg)
except Exception:
pass
def _guarded_read(self):
# Called from bdwidth's reactor timer: must never raise
usb = self.sensor.usb
try:
if not usb.is_open:
usb.open()
result = self._read()
except Exception as e:
self.usb_errors += 1
try:
usb.close()
except Exception:
pass
if self.usb_ok:
self.usb_ok = False
self._respond("bdwidth %s: USB error (%s), print continues "
"without width readings, retrying"
% (self.name, e))
return False
if not self.usb_ok:
self.usb_ok = True
self._respond("bdwidth %s: USB connection restored after %d "
"errors" % (self.name, self.usb_errors))
return result
def get_status(self, eventtime): def get_status(self, eventtime):
s = self.sensor s = self.sensor
@@ -64,6 +118,8 @@ class BDWidthStatus:
'active': str(getattr(s, 'is_active', 'disable')), 'active': str(getattr(s, 'is_active', 'disable')),
'linear_motion': getattr(s, 'linear_motion', 42.8), 'linear_motion': getattr(s, 'linear_motion', 42.8),
'sample_time': getattr(s, 'sample_time', 1.), 'sample_time': getattr(s, 'sample_time', 1.),
'usb_ok': self.usb_ok,
'usb_errors': self.usb_errors,
} }