From 38d7b9ada0d61971fdaf3e853892e7fb4051b36a Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Fri, 26 Jan 2018 14:27:55 -0500 Subject: [PATCH] buttons: Add initial support for detecting button presses Add mcu support for periodically polling for a button press. Add host code support for registering buttons and invoking callbacks for them. Signed-off-by: Kevin O'Connor --- klippy/extras/buttons.py | 143 ++++++++++++++++++++++++++++++++++ klippy/pins.py | 5 +- src/Makefile | 2 +- src/buttons.c | 160 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 klippy/extras/buttons.py create mode 100644 src/buttons.c diff --git a/klippy/extras/buttons.py b/klippy/extras/buttons.py new file mode 100644 index 00000000..fe683f8d --- /dev/null +++ b/klippy/extras/buttons.py @@ -0,0 +1,143 @@ +# Support for button detection and callbacks +# +# Copyright (C) 2018 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging + +QUERY_TIME = .002 +RETRANSMIT_COUNT = 50 + + +###################################################################### +# Button state tracking +###################################################################### + +class MCU_buttons: + def __init__(self, printer, mcu): + self.reactor = printer.get_reactor() + self.mcu = mcu + mcu.add_config_object(self) + 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( + "buttons_query oid=%d clock=%d rest_ticks=%d retransmit_count=%d" % ( + self.oid, clock, rest_ticks, RETRANSMIT_COUNT), is_init=True) + self.mcu.register_msg( + self.handle_buttons_state, "buttons_state", self.oid) + 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 + + +###################################################################### +# Rotary Encoders +###################################################################### + +class RotaryEncoder: + def __init__(self, cw_callback, ccw_callback): + self.cw_callback = cw_callback + self.ccw_callback = ccw_callback + self.next_callback = None + def encoder_callback(self, eventtime, state): + # XXX - do full encoder state tracking + if state == 3: + self.next_callback = None + elif state == 2: + self.next_callback = self.ccw_callback + elif state == 1: + self.next_callback = self.cw_callback + elif self.next_callback is not None: + self.next_callback(eventtime) + self.next_callback = None + + +###################################################################### +# Button registration code +###################################################################### + +class PrinterButtons: + def __init__(self, config): + self.printer = config.get_printer() + self.mcu_buttons = {} + 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: + pin_params = ppins.lookup_pin('digital_in', pin) + 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) diff --git a/klippy/pins.py b/klippy/pins.py index eec0df84..ac8fd8bd 100644 --- a/klippy/pins.py +++ b/klippy/pins.py @@ -199,8 +199,9 @@ class PrinterPins: self.chips = {} self.active_pins = {} def lookup_pin(self, pin_type, pin_desc, share_type=None): - can_invert = pin_type in ['stepper', 'endstop', 'digital_out', 'pwm'] - can_pullup = pin_type == 'endstop' + can_invert = pin_type in ['stepper', 'endstop', 'digital_in', + 'digital_out', 'pwm'] + can_pullup = pin_type in ['endstop', 'digital_in'] desc = pin_desc.strip() pullup = invert = 0 if can_pullup and desc.startswith('^'): diff --git a/src/Makefile b/src/Makefile index 92667b2a..22db35ea 100644 --- a/src/Makefile +++ b/src/Makefile @@ -5,4 +5,4 @@ src-$(CONFIG_HAVE_GPIO) += gpiocmds.c stepper.c endstop.c src-$(CONFIG_HAVE_GPIO_ADC) += adccmds.c src-$(CONFIG_HAVE_GPIO_SPI) += spicmds.c src-$(CONFIG_HAVE_GPIO_HARD_PWM) += pwmcmds.c -src-$(CONFIG_HAVE_USER_INTERFACE) += lcd_st7920.c lcd_hd44780.c +src-$(CONFIG_HAVE_USER_INTERFACE) += lcd_st7920.c lcd_hd44780.c buttons.c diff --git a/src/buttons.c b/src/buttons.c new file mode 100644 index 00000000..7f6e1ee7 --- /dev/null +++ b/src/buttons.c @@ -0,0 +1,160 @@ +// Report on user interface buttons +// +// Copyright (C) 2018 Kevin O'Connor +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "basecmd.h" // oid_alloc +#include "board/gpio.h" // struct gpio_in +#include "board/irq.h" // irq_disable +#include "command.h" // DECL_COMMAND +#include "sched.h" // struct timer + +struct buttons { + struct timer time; + uint32_t rest_ticks; + uint8_t pressed, last_pressed; + uint8_t report_count, reports[8]; + uint8_t ack_count, retransmit_state, retransmit_count; + uint8_t button_count; + struct gpio_in pins[0]; +}; + +enum { BF_NO_RETRANSMIT = 0x80, BF_PENDING = 0xff, BF_ACKED = 0xfe }; + +static struct task_wake buttons_wake; + +static uint_fast8_t +buttons_event(struct timer *t) +{ + struct buttons *b = container_of(t, struct buttons, time); + + // Read pins + uint8_t i, bit, status = 0; + for (i = 0, bit = 1; i < b->button_count; i++, bit <<= 1) { + uint8_t val = gpio_in_read(b->pins[i]); + if (val) + status |= bit; + } + + // Check if any pins have changed since last time + uint8_t diff = status ^ b->pressed; + if (diff) { + // At least one pin has changed - do button debouncing + uint8_t debounced = ~(status ^ b->last_pressed); + if (diff & debounced) { + // Pin has been consistently different - report it + b->pressed = (b->pressed & ~debounced) | (status & debounced); + if (b->report_count < sizeof(b->reports)) { + b->reports[b->report_count++] = b->pressed; + sched_wake_task(&buttons_wake); + b->retransmit_state = BF_PENDING; + } + } + } + b->last_pressed = status; + + // Check if a retransmit is needed + uint8_t retransmit_state = b->retransmit_state; + if (!(retransmit_state & BF_NO_RETRANSMIT)) { + retransmit_state--; + if (retransmit_state & BF_NO_RETRANSMIT) + // timeout - do retransmit + sched_wake_task(&buttons_wake); + b->retransmit_state = retransmit_state; + } + + // Reschedule timer + b->time.waketime += b->rest_ticks; + return SF_RESCHEDULE; +} + +void +command_config_buttons(uint32_t *args) +{ + uint8_t button_count = args[1]; + if (button_count > 8) + shutdown("Max of 8 buttons"); + struct buttons *b = oid_alloc( + args[0], command_config_buttons + , sizeof(*b) + sizeof(b->pins[0]) * button_count); + b->button_count = button_count; + b->time.func = buttons_event; +} +DECL_COMMAND(command_config_buttons, "config_buttons oid=%c button_count=%c"); + +void +command_buttons_add(uint32_t *args) +{ + struct buttons *b = oid_lookup(args[0], command_config_buttons); + uint8_t pos = args[1]; + if (pos >= b->button_count) + shutdown("Set button past maximum button count"); + b->pins[pos] = gpio_in_setup(args[2], args[3]); +} +DECL_COMMAND(command_buttons_add, "buttons_add oid=%c pos=%c pin=%u pull_up=%c"); + +void +command_buttons_query(uint32_t *args) +{ + struct buttons *b = oid_lookup(args[0], command_config_buttons); + sched_del_timer(&b->time); + b->time.waketime = args[1]; + b->rest_ticks = args[2]; + b->ack_count = b->report_count = 0; + b->retransmit_state = BF_ACKED; + b->retransmit_count = args[3]; + if (b->retransmit_count >= BF_NO_RETRANSMIT) + shutdown("Invalid buttons retransmit count"); + if (! b->rest_ticks) + return; + sched_add_timer(&b->time); +} +DECL_COMMAND(command_buttons_query, + "buttons_query oid=%c clock=%u rest_ticks=%u retransmit_count=%c"); + +void +command_buttons_ack(uint32_t *args) +{ + struct buttons *b = oid_lookup(args[0], command_config_buttons); + uint8_t count = args[1]; + b->ack_count += count; + irq_disable(); + if (count >= b->report_count) { + b->report_count = 0; + b->retransmit_state = BF_ACKED; + } else { + uint8_t pending = b->report_count - count, i; + for (i=0; ireports[i] = b->reports[i+count]; + b->report_count = pending; + } + irq_enable(); +} +DECL_COMMAND(command_buttons_ack, "buttons_ack oid=%c count=%c"); + +void +buttons_task(void) +{ + if (!sched_check_wake(&buttons_wake)) + return; + uint8_t oid; + struct buttons *b; + foreach_oid(oid, b, command_config_buttons) { + // See if need to transmit buttons_state + if (b->retransmit_state != BF_PENDING) + continue; + // Generate message + irq_disable(); + uint8_t report_count = b->report_count; + if (!report_count) { + irq_enable(); + continue; + } + b->retransmit_state = b->retransmit_count; + irq_enable(); + sendf("buttons_state oid=%c ack_count=%c state=%*s" + , oid, b->ack_count, report_count, b->reports); + } +} +DECL_TASK(buttons_task);