From 272e815522b0bc8e0806e052b73a5cc1af979cd7 Mon Sep 17 00:00:00 2001 From: Gareth Farrington Date: Thu, 20 Mar 2025 16:55:33 -0700 Subject: [PATCH] buttons: Debounce gcode_button and filament_switch_sensor (#6848) Add `debounce_delay` config option which sets the debounce time, defaults to 0 Signed-off-by: Gareth Farrington --- docs/Config_Reference.md | 9 ++++++ klippy/extras/buttons.py | 35 +++++++++++++++++++++ klippy/extras/filament_motion_sensor.py | 4 +-- klippy/extras/filament_switch_sensor.py | 19 ++++++----- klippy/extras/gcode_button.py | 8 +++-- klippy/extras/hall_filament_width_sensor.py | 2 +- 6 files changed, 63 insertions(+), 14 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 44f6431f..86067475 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3278,6 +3278,10 @@ pin: # A list of G-Code commands to execute when the button is released. # G-Code templates are supported. The default is to not run any # commands on a button release. +#debounce_delay: +# A period of time in seconds to debounce events prior to running the +# button gcode. If the button is pressed and released during this +# delay, the entire button press is ignored. Default is 0. ``` ### [output_pin] @@ -4641,6 +4645,11 @@ more information. # dispatch and execution of the runout_gcode. It may be useful to # increase this delay if OctoPrint exhibits strange pause behavior. # Default is 0.5 seconds. +#debounce_delay: +# A period of time in seconds to debounce events prior to running the +# switch gcode. The switch must he held in a single state for at least +# this long to activate. If the switch is toggled on/off during this delay, +# the event is ignored. Default is 0. #switch_pin: # The pin on which the switch is connected. This parameter must be # provided. diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index daa998a9..4f012f51 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -244,6 +244,33 @@ class HalfStepRotaryEncoder(BaseRotaryEncoder): BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CCW), ) +class DebounceButton: + def __init__(self, config, button_action): + self.printer = config.get_printer() + self.reactor = self.printer.get_reactor() + self.button_action = button_action + self.debounce_delay = config.getfloat('debounce_delay', 0., minval=0.) + self.logical_state = None + self.physical_state = None + self.latest_eventtime = None + def button_handler(self, eventtime, state): + self.physical_state = state + self.latest_eventtime = eventtime + # if there would be no state transition, ignore the event: + if self.logical_state == self.physical_state: + return + trigger_time = eventtime + self.debounce_delay + self.reactor.register_callback(self._debounce_event, trigger_time) + def _debounce_event(self, eventtime): + # if there would be no state transition, ignore the event: + if self.logical_state == self.physical_state: + return + # if there were more recent events, they supersede this one: + if (eventtime - self.debounce_delay) < self.latest_eventtime: + return + # enact state transition and trigger action + self.logical_state = self.physical_state + self.button_action(self.latest_eventtime, self.logical_state) ###################################################################### # Button registration code @@ -261,6 +288,14 @@ class PrinterButtons: self.adc_buttons[pin] = adc_buttons = MCU_ADC_buttons( self.printer, pin, pullup) adc_buttons.setup_button(min_val, max_val, callback) + def register_debounce_button(self, pin, callback, config): + debounce = DebounceButton(config, callback) + return self.register_buttons([pin], debounce.button_handler) + def register_debounce_adc_button(self, pin, min_val, max_val, pullup + , callback, config): + debounce = DebounceButton(config, callback) + return self.register_adc_button(pin, min_val. min_val, max_val, pullup + , debounce.button_handler) def register_adc_button_push(self, pin, min_val, max_val, pullup, callback): def helper(eventtime, state, callback=callback): if state: diff --git a/klippy/extras/filament_motion_sensor.py b/klippy/extras/filament_motion_sensor.py index fb886aa5..dd476d28 100644 --- a/klippy/extras/filament_motion_sensor.py +++ b/klippy/extras/filament_motion_sensor.py @@ -63,7 +63,7 @@ class EncoderSensor: def _extruder_pos_update_event(self, eventtime): extruder_pos = self._get_extruder_pos(eventtime) # Check for filament runout - self.runout_helper.note_filament_present( + self.runout_helper.note_filament_present(eventtime, extruder_pos < self.filament_runout_pos) return eventtime + CHECK_RUNOUT_TIMEOUT def encoder_event(self, eventtime, state): @@ -71,7 +71,7 @@ class EncoderSensor: self._update_filament_runout_pos(eventtime) # Check for filament insertion # Filament is always assumed to be present on an encoder event - self.runout_helper.note_filament_present(True) + self.runout_helper.note_filament_present(eventtime, True) def load_config_prefix(config): return EncoderSensor(config) diff --git a/klippy/extras/filament_switch_sensor.py b/klippy/extras/filament_switch_sensor.py index 51d8ba4b..d1acadd5 100644 --- a/klippy/extras/filament_switch_sensor.py +++ b/klippy/extras/filament_switch_sensor.py @@ -5,6 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging + class RunoutHelper: def __init__(self, config): self.name = config.get_name().split()[-1] @@ -24,7 +25,7 @@ class RunoutHelper: self.insert_gcode = gcode_macro.load_template( config, 'insert_gcode') self.pause_delay = config.getfloat('pause_delay', .5, above=.0) - self.event_delay = config.getfloat('event_delay', 3., above=0.) + self.event_delay = config.getfloat('event_delay', 3., minval=.0) # Internal state self.min_event_systime = self.reactor.NEVER self.filament_present = False @@ -59,19 +60,20 @@ class RunoutHelper: except Exception: logging.exception("Script running error") self.min_event_systime = self.reactor.monotonic() + self.event_delay - def note_filament_present(self, is_filament_present): + def note_filament_present(self, eventtime, is_filament_present): if is_filament_present == self.filament_present: return self.filament_present = is_filament_present - eventtime = self.reactor.monotonic() + if eventtime < self.min_event_systime or not self.sensor_enabled: # do not process during the initialization time, duplicates, # during the event delay time, while an event is running, or # when the sensor is disabled return # Determine "printing" status + now = self.reactor.monotonic() idle_timeout = self.printer.lookup_object("idle_timeout") - is_printing = idle_timeout.get_status(eventtime)["state"] == "Printing" + is_printing = idle_timeout.get_status(now)["state"] == "Printing" # Perform filament action associated with status change (if any) if is_filament_present: if not is_printing and self.insert_gcode is not None: @@ -79,14 +81,14 @@ class RunoutHelper: self.min_event_systime = self.reactor.NEVER logging.info( "Filament Sensor %s: insert event detected, Time %.2f" % - (self.name, eventtime)) + (self.name, now)) self.reactor.register_callback(self._insert_event_handler) elif is_printing and self.runout_gcode is not None: # runout detected self.min_event_systime = self.reactor.NEVER logging.info( "Filament Sensor %s: runout event detected, Time %.2f" % - (self.name, eventtime)) + (self.name, now)) self.reactor.register_callback(self._runout_event_handler) def get_status(self, eventtime): return { @@ -108,11 +110,12 @@ class SwitchSensor: printer = config.get_printer() buttons = printer.load_object(config, 'buttons') switch_pin = config.get('switch_pin') - buttons.register_buttons([switch_pin], self._button_handler) + buttons.register_debounce_button(switch_pin, self._button_handler + , config) self.runout_helper = RunoutHelper(config) self.get_status = self.runout_helper.get_status def _button_handler(self, eventtime, state): - self.runout_helper.note_filament_present(state) + self.runout_helper.note_filament_present(eventtime, state) def load_config_prefix(config): return SwitchSensor(config) diff --git a/klippy/extras/gcode_button.py b/klippy/extras/gcode_button.py index 669edfb4..de280c98 100644 --- a/klippy/extras/gcode_button.py +++ b/klippy/extras/gcode_button.py @@ -5,6 +5,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging + class GCodeButton: def __init__(self, config): self.printer = config.get_printer() @@ -13,12 +14,13 @@ class GCodeButton: self.last_state = 0 buttons = self.printer.load_object(config, "buttons") if config.get('analog_range', None) is None: - buttons.register_buttons([self.pin], self.button_callback) + buttons.register_debounce_button(self.pin, self.button_callback + , config) else: amin, amax = config.getfloatlist('analog_range', count=2) pullup = config.getfloat('analog_pullup_resistor', 4700., above=0.) - buttons.register_adc_button(self.pin, amin, amax, pullup, - self.button_callback) + buttons.register_debounce_adc_button(self.pin, amin, amax, pullup, + self.button_callback, config) gcode_macro = self.printer.load_object(config, 'gcode_macro') self.press_template = gcode_macro.load_template(config, 'press_gcode') self.release_template = gcode_macro.load_template(config, diff --git a/klippy/extras/hall_filament_width_sensor.py b/klippy/extras/hall_filament_width_sensor.py index 8dab3522..c71b2b2e 100644 --- a/klippy/extras/hall_filament_width_sensor.py +++ b/klippy/extras/hall_filament_width_sensor.py @@ -125,7 +125,7 @@ class HallFilamentWidthSensor: # Update filament array for lastFilamentWidthReading self.update_filament_array(last_epos) # Check runout - self.runout_helper.note_filament_present( + self.runout_helper.note_filament_present(eventtime, self.runout_dia_min <= self.diameter <= self.runout_dia_max) # Does filament exists if self.diameter > 0.5: