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 <kevin@koconnor.net>
This commit is contained in:
parent
4061026c25
commit
38d7b9ada0
|
@ -0,0 +1,143 @@
|
|||
# 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
|
||||
|
||||
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)
|
|
@ -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('^'):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
// Report on user interface buttons
|
||||
//
|
||||
// Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
//
|
||||
// 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; i<pending; i++)
|
||||
b->reports[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);
|
Loading…
Reference in New Issue