diff --git a/config/example-extras.cfg b/config/example-extras.cfg index a75f8b10..6adf0592 100644 --- a/config/example-extras.cfg +++ b/config/example-extras.cfg @@ -1249,19 +1249,46 @@ # using encoder. This parameter must be provided when using menu. #click_pin: # The pin connected to 'enter' button or encoder 'click'. This parameter -# must be provided when using menu. +# must be provided when using menu. The presence of an 'analog_range_click_pin' +# config parameter turns this parameter from digital to analog. #back_pin: # The pin connected to 'back' button. This parameter is optional, menu -# can be used without it. +# can be used without it. The presence of an 'analog_range_back_pin' +# config parameter turns this parameter from digital to analog. #up_pin: # The pin connected to 'up' button. This parameter must be provided -# when using menu without encoder. +# when using menu without encoder. The presence of an 'analog_range_up_pin' +# config parameter turns this parameter from digital to analog. #down_pin: # The pin connected to 'down' button. This parameter must be provided -# when using menu without encoder. +# when using menu without encoder. The presence of an 'analog_range_down_pin' +# config parameter turns this parameter from digital to analog. #kill_pin: -# The pin connected to 'kill' button. This button will call -# emergency stop. +# The pin connected to 'kill' button. This button will call emergency stop. +# The presence of an 'analog_range_kill_pin' config parameter turns this +# parameter from digital to analog. +#analog_pullup_resistor: 4700 +# The resistance (in ohms) of the pullup attached to the analog button. +# The default is 4700 ohms. +#analog_pin_debug: +# When enabled it will output analog (ADC) button readings to the log. +# It's useful for finding analog button resistance range values. +# The default is False (disabled) +#analog_range_click_pin: +# The resistance range for a 'enter' button. Range minimum and maximum +# comma-separated values must be provided when using analog button. +#analog_range_back_pin: +# The resistance range for a 'back' button. Range minimum and maximum +# comma-separated values must be provided when using analog button. +#analog_range_up_pin: +# The resistance range for a 'up' button. Range minimum and maximum +# comma-separated values must be provided when using analog button. +#analog_range_down_pin: +# The resistance range for a 'down' button. Range minimum and maximum +# comma-separated values must be provided when using analog button. +#analog_range_kill_pin: +# The resistance range for a 'kill' button. Range minimum and maximum +# comma-separated values must be provided when using analog button. # Custom thermistors (one may define any number of sections with a diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py index 9ebd3adc..b06f1f83 100644 --- a/klippy/extras/buttons.py +++ b/klippy/extras/buttons.py @@ -7,6 +7,10 @@ import logging QUERY_TIME = .002 RETRANSMIT_COUNT = 50 +ADC_REPORT_TIME = 0.015 +ADC_DEBOUNCE_TIME = 0.025 +ADC_SAMPLE_TIME = 0.001 +ADC_SAMPLE_COUNT = 6 # Rotary encoder handler https://github.com/brianlow/Rotary # Copyright 2011 Ben Buxton (bb@cactii.net). @@ -112,6 +116,79 @@ class MCU_buttons: self.last_button = button +class MCU_ADC_buttons: + def __init__(self, printer, pin, pullup, debug=False): + self.reactor = printer.get_reactor() + self.buttons = [] + self.last_button = None + self.last_pressed = None + self.last_debouncetime = 0 + self.pullup = pullup + self.debug = debug + self.pin = pin + self.min_value = self.max_value = None + ppins = printer.lookup_object('pins') + self.mcu_adc = ppins.setup_pin('adc', self.pin) + self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT) + self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback) + + def setup_button(self, min_value, max_value, callback): + if self.min_value is None: + self.min_value = min_value + else: + self.min_value = min(self.min_value, min_value) + + if self.max_value is None: + self.max_value = max_value + else: + self.max_value = max(self.max_value, max_value) + + self.buttons.append((min_value, max_value, callback)) + + def adc_callback(self, read_time, read_value): + adc = max(.00001, min(.99999, read_value)) + r = self.pullup * adc / (1.0 - adc) + self.reactor.register_async_callback( + (lambda e, s=self, v=r: s.handle_button(e, v))) + + def get_button(self, value): + if (self.min_value is not None and self.max_value is not None + and self.min_value <= value <= self.max_value): + for i, (min_value, max_value, cb) in enumerate(self.buttons): + if min_value < value < max_value: + return i + return None + + def handle_button(self, eventtime, value): + btn = self.get_button(int(value)) + + # If the button changed, due to noise or pressing: + if btn != self.last_button: + # reset the debouncing timer + self.last_debouncetime = eventtime + + # button debounce check & new button pressed + if ((eventtime - self.last_debouncetime) >= ADC_DEBOUNCE_TIME + and self.last_button == btn and self.last_pressed != btn): + # release last_pressed + if self.last_pressed is not None: + self.call_button(eventtime, self.last_pressed, False) + self.last_pressed = None + if btn is not None: + self.call_button(eventtime, btn, True) + self.last_pressed = btn + + self.last_button = btn + if self.debug is True: + logging.info( + "analog pin: %s value: %d" % (self.pin, int(value))) + + def call_button(self, eventtime, button, state): + if button < len(self.buttons): + minval, maxval, callback = self.buttons[button] + callback(eventtime, state) + + ###################################################################### # Rotary Encoders ###################################################################### @@ -138,6 +215,20 @@ class PrinterButtons: def __init__(self, config): self.printer = config.get_printer() self.mcu_buttons = {} + self.adc_buttons = {} + def register_adc_button( + self, pin, min_val, max_val, pullup, callback, debug=False): + adc_buttons = self.adc_buttons.get(pin) + if adc_buttons is None: + self.adc_buttons[pin] = adc_buttons = MCU_ADC_buttons( + self.printer, pin, pullup, debug) + adc_buttons.setup_button(min_val, max_val, callback) + def register_adc_button_push( + self, pin, min_val, max_val, pullup, callback, debug=False): + def helper(eventtime, state, callback=callback): + if state: + callback(eventtime) + self.register_adc_button(pin, min_val, max_val, pullup, helper, debug) def register_buttons(self, pins, callback): # Parse pins ppins = self.printer.lookup_object('pins') diff --git a/klippy/extras/display/menu.py b/klippy/extras/display/menu.py index b3c0ea16..b3a24154 100644 --- a/klippy/extras/display/menu.py +++ b/klippy/extras/display/menu.py @@ -1000,7 +1000,21 @@ class MenuManager: self.up_pin = config.get('up_pin', None) self.down_pin = config.get('down_pin', None) self.kill_pin = config.get('kill_pin', None) + # analog button ranges + self.analog_range_click_pin = config.get( + 'analog_range_click_pin', None) + self.analog_range_back_pin = config.get( + 'analog_range_back_pin', None) + self.analog_range_up_pin = config.get( + 'analog_range_up_pin', None) + self.analog_range_down_pin = config.get( + 'analog_range_down_pin', None) + self.analog_range_kill_pin = config.get( + 'analog_range_kill_pin', None) self._last_click_press = 0 + self.analog_pullup = config.getfloat( + 'analog_pullup_resistor', 4700., above=0.) + self.analog_pin_debug = config.getboolean('analog_pin_debug', False) self._encoder_fast_rate = config.getfloat( 'encoder_fast_rate', .03, above=0.) self._last_encoder_cw_eventtime = 0 @@ -1012,6 +1026,7 @@ class MenuManager: self.printer.register_event_handler("klippy:ready", self.handle_ready) # register buttons & encoder if self.buttons: + # digital buttons if self.encoder_pins: try: pin1, pin2 = self.encoder_pins.split(',') @@ -1021,20 +1036,75 @@ class MenuManager: pin1.strip(), pin2.strip(), self.encoder_cw_callback, self.encoder_ccw_callback) if self.click_pin: - self.buttons.register_buttons( - [self.click_pin], self.click_callback) + if self.analog_range_click_pin is not None: + try: + p_min, p_max = map( + float, self.analog_range_click_pin.split(',')) + except Exception: + raise config.error( + "Unable to parse analog_range_click_pin") + self.buttons.register_adc_button( + self.click_pin, p_min, p_max, self.analog_pullup, + self.click_callback, self.analog_pin_debug) + else: + self.buttons.register_buttons( + [self.click_pin], self.click_callback) if self.back_pin: - self.buttons.register_button_push( - self.back_pin, self.back_callback) + if self.analog_range_back_pin is not None: + try: + p_min, p_max = map( + float, self.analog_range_back_pin.split(',')) + except Exception: + raise config.error( + "Unable to parse analog_range_back_pin") + self.buttons.register_adc_button_push( + self.back_pin, p_min, p_max, self.analog_pullup, + self.back_callback, self.analog_pin_debug) + else: + self.buttons.register_button_push( + self.back_pin, self.back_callback) if self.up_pin: - self.buttons.register_button_push( - self.up_pin, self.up_callback) + if self.analog_range_up_pin is not None: + try: + p_min, p_max = map( + float, self.analog_range_up_pin.split(',')) + except Exception: + raise config.error( + "Unable to parse analog_range_up_pin") + self.buttons.register_adc_button_push( + self.up_pin, p_min, p_max, self.analog_pullup, + self.up_callback, self.analog_pin_debug) + else: + self.buttons.register_button_push( + self.up_pin, self.up_callback) if self.down_pin: - self.buttons.register_button_push( - self.down_pin, self.down_callback) + if self.analog_range_down_pin is not None: + try: + p_min, p_max = map( + float, self.analog_range_down_pin.split(',')) + except Exception: + raise config.error( + "Unable to parse analog_range_down_pin") + self.buttons.register_adc_button_push( + self.down_pin, p_min, p_max, self.analog_pullup, + self.down_callback, self.analog_pin_debug) + else: + self.buttons.register_button_push( + self.down_pin, self.down_callback) if self.kill_pin: - self.buttons.register_button_push( - self.kill_pin, self.kill_callback) + if self.analog_range_kill_pin is not None: + try: + p_min, p_max = map( + float, self.analog_range_kill_pin.split(',')) + except Exception: + raise config.error( + "Unable to parse analog_range_kill_pin") + self.buttons.register_adc_button_push( + self.kill_pin, p_min, p_max, self.analog_pullup, + self.kill_callback, self.analog_pin_debug) + else: + self.buttons.register_button_push( + self.kill_pin, self.kill_callback) # Add MENU commands self.gcode.register_mux_command("MENU", "DO", 'dump', self.cmd_DO_DUMP,