| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  | # Support for button detection and callbacks | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Copyright (C) 2018  Kevin O'Connor <kevin@koconnor.net> | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # This file may be distributed under the terms of the GNU GPLv3 license. | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # Button state tracking | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-26 00:18:48 -04:00
										 |  |  | QUERY_TIME = .002 | 
					
						
							|  |  |  | RETRANSMIT_COUNT = 50 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  | class MCU_buttons: | 
					
						
							|  |  |  |     def __init__(self, printer, mcu): | 
					
						
							|  |  |  |         self.reactor = printer.get_reactor() | 
					
						
							|  |  |  |         self.mcu = mcu | 
					
						
							| 
									
										
										
										
											2018-09-03 11:48:22 -04:00
										 |  |  |         self.mcu.register_config_callback(self.build_config) | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  |         self.pin_list = [] | 
					
						
							|  |  |  |         self.callbacks = [] | 
					
						
							|  |  |  |         self.invert = self.last_button = 0 | 
					
						
							|  |  |  |         self.ack_cmd = None | 
					
						
							|  |  |  |         self.ack_count = 0 | 
					
						
							|  |  |  |     def setup_buttons(self, pins, callback): | 
					
						
							|  |  |  |         mask = 0 | 
					
						
							|  |  |  |         shift = len(self.pin_list) | 
					
						
							|  |  |  |         for pin_params in pins: | 
					
						
							|  |  |  |             if pin_params['invert']: | 
					
						
							|  |  |  |                 self.invert |= 1 << len(self.pin_list) | 
					
						
							|  |  |  |             mask |= 1 << len(self.pin_list) | 
					
						
							|  |  |  |             self.pin_list.append((pin_params['pin'], pin_params['pullup'])) | 
					
						
							|  |  |  |         self.callbacks.append((mask, shift, callback)) | 
					
						
							|  |  |  |     def build_config(self): | 
					
						
							|  |  |  |         if not self.pin_list: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self.oid = self.mcu.create_oid() | 
					
						
							|  |  |  |         self.mcu.add_config_cmd("config_buttons oid=%d button_count=%d" % ( | 
					
						
							|  |  |  |             self.oid, len(self.pin_list))) | 
					
						
							|  |  |  |         for i, (pin, pull_up) in enumerate(self.pin_list): | 
					
						
							|  |  |  |             self.mcu.add_config_cmd( | 
					
						
							|  |  |  |                 "buttons_add oid=%d pos=%d pin=%s pull_up=%d" % ( | 
					
						
							|  |  |  |                     self.oid, i, pin, pull_up), is_init=True) | 
					
						
							|  |  |  |         cmd_queue = self.mcu.alloc_command_queue() | 
					
						
							|  |  |  |         self.ack_cmd = self.mcu.lookup_command( | 
					
						
							|  |  |  |             "buttons_ack oid=%c count=%c", cq=cmd_queue) | 
					
						
							|  |  |  |         clock = self.mcu.get_query_slot(self.oid) | 
					
						
							|  |  |  |         rest_ticks = self.mcu.seconds_to_clock(QUERY_TIME) | 
					
						
							|  |  |  |         self.mcu.add_config_cmd( | 
					
						
							| 
									
										
										
										
											2019-02-27 13:34:22 -05:00
										 |  |  |             "buttons_query oid=%d clock=%d" | 
					
						
							| 
									
										
										
										
											2019-07-05 19:30:30 -04:00
										 |  |  |             " rest_ticks=%d retransmit_count=%d invert=%d" % ( | 
					
						
							|  |  |  |                 self.oid, clock, rest_ticks, RETRANSMIT_COUNT, | 
					
						
							|  |  |  |                 self.invert), is_init=True) | 
					
						
							| 
									
										
										
										
											2019-06-20 17:36:46 -04:00
										 |  |  |         self.mcu.register_response(self.handle_buttons_state, | 
					
						
							|  |  |  |                                    "buttons_state", self.oid) | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  |     def handle_buttons_state(self, params): | 
					
						
							|  |  |  |         # Expand the message ack_count from 8-bit | 
					
						
							|  |  |  |         ack_count = self.ack_count | 
					
						
							|  |  |  |         ack_diff = (ack_count - params['ack_count']) & 0xff | 
					
						
							|  |  |  |         if ack_diff & 0x80: | 
					
						
							|  |  |  |             ack_diff -= 0x100 | 
					
						
							|  |  |  |         msg_ack_count = ack_count - ack_diff | 
					
						
							|  |  |  |         # Determine new buttons | 
					
						
							|  |  |  |         buttons = params['state'] | 
					
						
							|  |  |  |         new_count = msg_ack_count + len(buttons) - self.ack_count | 
					
						
							|  |  |  |         if new_count <= 0: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         new_buttons = buttons[-new_count:] | 
					
						
							|  |  |  |         # Send ack to MCU | 
					
						
							|  |  |  |         self.ack_cmd.send([self.oid, new_count]) | 
					
						
							|  |  |  |         self.ack_count += new_count | 
					
						
							|  |  |  |         # Call self.handle_button() with this event in main thread | 
					
						
							|  |  |  |         for b in new_buttons: | 
					
						
							|  |  |  |             self.reactor.register_async_callback( | 
					
						
							|  |  |  |                 (lambda e, s=self, b=ord(b): s.handle_button(e, b))) | 
					
						
							|  |  |  |     def handle_button(self, eventtime, button): | 
					
						
							|  |  |  |         button ^= self.invert | 
					
						
							|  |  |  |         changed = button ^ self.last_button | 
					
						
							|  |  |  |         for mask, shift, callback in self.callbacks: | 
					
						
							|  |  |  |             if changed & mask: | 
					
						
							|  |  |  |                 callback(eventtime, (button & mask) >> shift) | 
					
						
							|  |  |  |         self.last_button = button | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-26 00:18:48 -04:00
										 |  |  | ###################################################################### | 
					
						
							|  |  |  | # ADC button tracking | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ADC_REPORT_TIME = 0.015 | 
					
						
							|  |  |  | ADC_DEBOUNCE_TIME = 0.025 | 
					
						
							|  |  |  | ADC_SAMPLE_TIME = 0.001 | 
					
						
							|  |  |  | ADC_SAMPLE_COUNT = 6 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-14 18:18:52 +03:00
										 |  |  | class MCU_ADC_buttons: | 
					
						
							| 
									
										
										
										
											2019-11-07 18:32:26 -05:00
										 |  |  |     def __init__(self, printer, pin, pullup): | 
					
						
							| 
									
										
										
										
											2019-04-14 18:18:52 +03:00
										 |  |  |         self.reactor = printer.get_reactor() | 
					
						
							|  |  |  |         self.buttons = [] | 
					
						
							|  |  |  |         self.last_button = None | 
					
						
							|  |  |  |         self.last_pressed = None | 
					
						
							|  |  |  |         self.last_debouncetime = 0 | 
					
						
							|  |  |  |         self.pullup = pullup | 
					
						
							|  |  |  |         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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def call_button(self, eventtime, button, state): | 
					
						
							|  |  |  |         if button < len(self.buttons): | 
					
						
							|  |  |  |             minval, maxval, callback = self.buttons[button] | 
					
						
							|  |  |  |             callback(eventtime, state) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  | ###################################################################### | 
					
						
							|  |  |  | # Rotary Encoders | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-26 00:18:48 -04:00
										 |  |  | # Rotary encoder handler https://github.com/brianlow/Rotary | 
					
						
							|  |  |  | # Copyright 2011 Ben Buxton (bb@cactii.net). | 
					
						
							|  |  |  | # Licenced under the GNU GPL Version 3. | 
					
						
							|  |  |  | R_START     = 0x0 | 
					
						
							|  |  |  | R_CW_FINAL  = 0x1 | 
					
						
							|  |  |  | R_CW_BEGIN  = 0x2 | 
					
						
							|  |  |  | R_CW_NEXT   = 0x3 | 
					
						
							|  |  |  | R_CCW_BEGIN = 0x4 | 
					
						
							|  |  |  | R_CCW_FINAL = 0x5 | 
					
						
							|  |  |  | R_CCW_NEXT  = 0x6 | 
					
						
							|  |  |  | R_DIR_CW    = 0x10 | 
					
						
							|  |  |  | R_DIR_CCW   = 0x20 | 
					
						
							|  |  |  | R_DIR_MSK   = 0x30 | 
					
						
							|  |  |  | # Use the full-step state table (emits a code at 00 only) | 
					
						
							|  |  |  | ENCODER_STATES = ( | 
					
						
							|  |  |  |   # R_START | 
					
						
							|  |  |  |   (R_START,    R_CW_BEGIN,  R_CCW_BEGIN, R_START), | 
					
						
							|  |  |  |   # R_CW_FINAL | 
					
						
							|  |  |  |   (R_CW_NEXT,  R_START,     R_CW_FINAL,  R_START | R_DIR_CW), | 
					
						
							|  |  |  |   # R_CW_BEGIN | 
					
						
							|  |  |  |   (R_CW_NEXT,  R_CW_BEGIN,  R_START,     R_START), | 
					
						
							|  |  |  |   # R_CW_NEXT | 
					
						
							|  |  |  |   (R_CW_NEXT,  R_CW_BEGIN,  R_CW_FINAL,  R_START), | 
					
						
							|  |  |  |   # R_CCW_BEGIN | 
					
						
							|  |  |  |   (R_CCW_NEXT, R_START,     R_CCW_BEGIN, R_START), | 
					
						
							|  |  |  |   # R_CCW_FINAL | 
					
						
							|  |  |  |   (R_CCW_NEXT, R_CCW_FINAL, R_START,     R_START | R_DIR_CCW), | 
					
						
							|  |  |  |   # R_CCW_NEXT | 
					
						
							|  |  |  |   (R_CCW_NEXT, R_CCW_FINAL, R_CCW_BEGIN, R_START) | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  | class RotaryEncoder: | 
					
						
							|  |  |  |     def __init__(self, cw_callback, ccw_callback): | 
					
						
							|  |  |  |         self.cw_callback = cw_callback | 
					
						
							|  |  |  |         self.ccw_callback = ccw_callback | 
					
						
							| 
									
										
										
										
											2018-07-01 15:41:14 +03:00
										 |  |  |         self.encoder_state = R_START | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  |     def encoder_callback(self, eventtime, state): | 
					
						
							| 
									
										
										
										
											2019-02-27 13:34:22 -05:00
										 |  |  |         es = ENCODER_STATES[self.encoder_state & 0xf][state & 0x3] | 
					
						
							|  |  |  |         self.encoder_state = es | 
					
						
							|  |  |  |         if es & R_DIR_MSK == R_DIR_CW: | 
					
						
							| 
									
										
										
										
											2018-07-01 15:41:14 +03:00
										 |  |  |             self.cw_callback(eventtime) | 
					
						
							| 
									
										
										
										
											2019-02-27 13:34:22 -05:00
										 |  |  |         elif es & R_DIR_MSK == R_DIR_CCW: | 
					
						
							| 
									
										
										
										
											2018-07-01 15:41:14 +03:00
										 |  |  |             self.ccw_callback(eventtime) | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # Button registration code | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class PrinterButtons: | 
					
						
							|  |  |  |     def __init__(self, config): | 
					
						
							|  |  |  |         self.printer = config.get_printer() | 
					
						
							|  |  |  |         self.mcu_buttons = {} | 
					
						
							| 
									
										
										
										
											2019-04-14 18:18:52 +03:00
										 |  |  |         self.adc_buttons = {} | 
					
						
							| 
									
										
										
										
											2019-11-07 18:32:26 -05:00
										 |  |  |     def register_adc_button(self, pin, min_val, max_val, pullup, callback): | 
					
						
							| 
									
										
										
										
											2019-04-14 18:18:52 +03:00
										 |  |  |         adc_buttons = self.adc_buttons.get(pin) | 
					
						
							|  |  |  |         if adc_buttons is None: | 
					
						
							|  |  |  |             self.adc_buttons[pin] = adc_buttons = MCU_ADC_buttons( | 
					
						
							| 
									
										
										
										
											2019-11-07 18:32:26 -05:00
										 |  |  |                 self.printer, pin, pullup) | 
					
						
							| 
									
										
										
										
											2019-04-14 18:18:52 +03:00
										 |  |  |         adc_buttons.setup_button(min_val, max_val, callback) | 
					
						
							| 
									
										
										
										
											2019-11-07 18:32:26 -05:00
										 |  |  |     def register_adc_button_push(self, pin, min_val, max_val, pullup, callback): | 
					
						
							| 
									
										
										
										
											2019-04-14 18:18:52 +03:00
										 |  |  |         def helper(eventtime, state, callback=callback): | 
					
						
							|  |  |  |             if state: | 
					
						
							|  |  |  |                 callback(eventtime) | 
					
						
							| 
									
										
										
										
											2019-11-07 18:32:26 -05:00
										 |  |  |         self.register_adc_button(pin, min_val, max_val, pullup, helper) | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  |     def register_buttons(self, pins, callback): | 
					
						
							|  |  |  |         # Parse pins | 
					
						
							|  |  |  |         ppins = self.printer.lookup_object('pins') | 
					
						
							|  |  |  |         mcu = mcu_name = None | 
					
						
							|  |  |  |         pin_params_list = [] | 
					
						
							|  |  |  |         for pin in pins: | 
					
						
							| 
									
										
										
										
											2018-07-26 09:44:45 -04:00
										 |  |  |             pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True) | 
					
						
							| 
									
										
										
										
											2018-01-26 14:27:55 -05:00
										 |  |  |             if mcu is not None and pin_params['chip'] != mcu: | 
					
						
							|  |  |  |                 raise ppins.error("button pins must be on same mcu") | 
					
						
							|  |  |  |             mcu = pin_params['chip'] | 
					
						
							|  |  |  |             mcu_name = pin_params['chip_name'] | 
					
						
							|  |  |  |             pin_params_list.append(pin_params) | 
					
						
							|  |  |  |         # Register pins and callback with the appropriate MCU | 
					
						
							|  |  |  |         mcu_buttons = self.mcu_buttons.get(mcu_name) | 
					
						
							|  |  |  |         if (mcu_buttons is None | 
					
						
							|  |  |  |             or len(mcu_buttons.pin_list) + len(pin_params_list) > 8): | 
					
						
							|  |  |  |             self.mcu_buttons[mcu_name] = mcu_buttons = MCU_buttons( | 
					
						
							|  |  |  |                 self.printer, mcu) | 
					
						
							|  |  |  |         mcu_buttons.setup_buttons(pin_params_list, callback) | 
					
						
							|  |  |  |     def register_rotary_encoder(self, pin1, pin2, cw_callback, ccw_callback): | 
					
						
							|  |  |  |         re = RotaryEncoder(cw_callback, ccw_callback) | 
					
						
							|  |  |  |         self.register_buttons([pin1, pin2], re.encoder_callback) | 
					
						
							|  |  |  |     def register_button_push(self, pin, callback): | 
					
						
							|  |  |  |         def helper(eventtime, state, callback=callback): | 
					
						
							|  |  |  |             if state: | 
					
						
							|  |  |  |                 callback(eventtime) | 
					
						
							|  |  |  |         self.register_buttons([pin], helper) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def load_config(config): | 
					
						
							|  |  |  |     return PrinterButtons(config) |