button-events
Version:
Button event module used to debounce a digital input signal and produce button action events.
246 lines (215 loc) • 8.6 kB
JavaScript
const EventEmitter = require('events').EventEmitter;
const STATE_DISABLED = 0;
const STATE_IDLE = 1;
const STATE_PRESSED = 2;
const STATE_CLICKED = 3;
const STATE_CLICKED_PRESSED = 4;
const STATE_DOUBLE_CLICKED = 5;
const STATE_DOUBLE_CLICKED_PRESSED = 6;
const STATE_TRIPLE_CLICKED = 7;
const STATE_TRIPLE_CLICKED_PRESSED = 8;
const STATE_QUADRUPLE_CLICKED = 9;
const STATE_RELEASE_WAIT = 10;
// low level button state events
const EVENT_BUTTON_CHANGED = 'button_changed';
const EVENT_BUTTON_PRESS = 'button_press';
const EVENT_BUTTON_RELEASE = 'button_release';
// button event types
const EVENT_BUTTON_PRESSED = 'pressed';
const EVENT_BUTTON_CLICKED = 'clicked';
const EVENT_BUTTON_CLICKED_PRESSED = 'clicked_pressed';
const EVENT_BUTTON_DOUBLE_CLICKED = 'double_clicked';
const EVENT_BUTTON_DOUBLE_CLICKED_PRESSED = 'double_clicked_pressed';
const EVENT_BUTTON_TRIPLE_CLICKED = 'triple_clicked';
const EVENT_BUTTON_TRIPLE_CLICKED_PRESSED = 'triple_clicked_pressed';
const EVENT_BUTTON_QUADRUPLE_CLICKED = 'quadruple_clicked';
const EVENT_BUTTON_RELEASED = 'released';
// unified button event type that passes the button event status
const EVENT_BUTTON_STATUS = 'button_event';
const Defaults = {
usePullUp: true,
timing: {
debounce: 30, // milliseconds to debounce input
pressed: 200, // milliseconds to wait until we assume the user intended a button press event
clicked: 200 // milliseconds to wait until we assume the user intended a button clicked event
}
};
class ButtonEvents extends EventEmitter {
constructor (Config) {
super();
this.Config = Object.assign({}, Defaults, Config);
this.Config.timing = Object.assign({}, Defaults.timing, this.Config.timing); // deep copy of timing defaults
this.debounce = false;
this.debounceTimer = null;
this.emitTimer = null;
if (this.Config.hasOwnProperty('preread')) {
// use preread value to initialize state
this.buttonState = ((this.Config.usePullUp ? !this.Config.preread : this.Config.preread) ? STATE_RELEASE_WAIT : STATE_IDLE);
this.lastValue = ((this.Config.usePullUp ? !this.Config.preread : this.Config.preread) ? 1 : 0);
}
else {
// no preread, assum button starts not pressed
this.buttonState = STATE_IDLE;
this.lastValue = (this.Config.usePullUp ? 1 : 0);
}
}
// called by parent when general purpose input value changes
gpioChange (value) {
if (this.buttonState === STATE_DISABLED) return "disabled";
value = (this.Config.usePullUp ? !value : value); // invert if pull up
this.lastValue = value;
if (this.debounce) return "debounced";
if (!this.Config.timing.debounce) {
this.processLastValue();
return "final";
}
this.debounceStart();
return "accepted";
}
// call before destruction to cleanup resources
cleanup () {
this.buttonState = STATE_DISABLED;
this.removeAllListeners();
clearTimeout(this.debounceTimer);
clearTimeout(this.emitTimer);
}
// start the input signal debounce
debounceStart () {
this.debounce = true;
this.debounceTimer = setTimeout(this.debounceComplete.bind(this), this.Config.timing.debounce);
}
// debounce timed out, complete button change
debounceComplete () {
this.processLastValue();
// debounce complete
this.debounce = false;
}
// process the last button input value
processLastValue () {
this.emit(EVENT_BUTTON_CHANGED);
if (this.lastValue) {
clearTimeout(this.emitTimer);
// debounced button press
this.emit(EVENT_BUTTON_PRESS);
// determine how to handle button press based on current button state
switch (this.buttonState) {
case STATE_CLICKED:
// transition from a clicked state to clicked and pressed
this.buttonState = STATE_CLICKED_PRESSED;
break;
case STATE_DOUBLE_CLICKED:
this.buttonState = STATE_DOUBLE_CLICKED_PRESSED;
break;
case STATE_TRIPLE_CLICKED:
this.buttonState = STATE_TRIPLE_CLICKED_PRESSED;
break;
default:
// begin the pressed state
this.buttonState = STATE_PRESSED;
}
// delay event to allow for further state transition
this.emitTimer = setTimeout(this.emitState.bind(this), this.Config.timing.pressed);
}
else {
// debounced button release
this.emit(EVENT_BUTTON_RELEASE);
// determine how to handle button release based on current button state
switch (this.buttonState) {
case STATE_PRESSED:
// transition from pressed to clicked
clearTimeout(this.emitTimer);
this.buttonState = STATE_CLICKED;
// delay event to allow for further state transition
this.emitTimer = setTimeout(this.emitState.bind(this), this.Config.timing.clicked);
break;
case STATE_CLICKED_PRESSED:
// transition from clicked and pressed to double clicked
clearTimeout(this.emitTimer);
this.buttonState = STATE_DOUBLE_CLICKED;
// delay event to allow for further state transition
this.emitTimer = setTimeout(this.emitState.bind(this), this.Config.timing.clicked);
break;
case STATE_DOUBLE_CLICKED_PRESSED:
// transition from double clicked and pressed to triple clicked
clearTimeout(this.emitTimer);
this.buttonState = STATE_TRIPLE_CLICKED;
// delay event to allow for further state transition
this.emitTimer = setTimeout(this.emitState.bind(this), this.Config.timing.clicked);
break;
case STATE_TRIPLE_CLICKED_PRESSED:
// transition from triple clicked and pressed to quadruple clicked
clearTimeout(this.emitTimer);
this.buttonState = STATE_QUADRUPLE_CLICKED;
// no further transitions
this.emitState();
break;
case STATE_RELEASE_WAIT:
// transition from release wait to idle by emitting event
clearTimeout(this.emitTimer);
this.emitState();
break;
}
}
}
// emit event for the current button state
emitState() {
switch (this.buttonState) {
case STATE_PRESSED:
// emit event and transition to release wait
this.emit(EVENT_BUTTON_PRESSED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_PRESSED);
this.buttonState = STATE_RELEASE_WAIT;
break;
case STATE_CLICKED:
// emit event and transition to idle
this.emit(EVENT_BUTTON_CLICKED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_CLICKED);
this.buttonState = STATE_IDLE;
break;
case STATE_CLICKED_PRESSED:
// emit event and transition to release wait
this.emit(EVENT_BUTTON_CLICKED_PRESSED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_CLICKED_PRESSED);
this.buttonState = STATE_RELEASE_WAIT;
break;
case STATE_DOUBLE_CLICKED:
// emit event and transition to idle
this.emit(EVENT_BUTTON_DOUBLE_CLICKED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_DOUBLE_CLICKED);
this.buttonState = STATE_IDLE;
break;
case STATE_DOUBLE_CLICKED_PRESSED:
// emit event and transition to release wait
this.emit(EVENT_BUTTON_DOUBLE_CLICKED_PRESSED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_DOUBLE_CLICKED_PRESSED);
this.buttonState = STATE_RELEASE_WAIT;
break;
case STATE_TRIPLE_CLICKED:
// emit event and transition to idle
this.emit(EVENT_BUTTON_TRIPLE_CLICKED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_TRIPLE_CLICKED);
this.buttonState = STATE_IDLE;
break;
case STATE_TRIPLE_CLICKED_PRESSED:
// emit event and transition to release wait
this.emit(EVENT_BUTTON_TRIPLE_CLICKED_PRESSED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_TRIPLE_CLICKED_PRESSED);
this.buttonState = STATE_RELEASE_WAIT;
break;
case STATE_QUADRUPLE_CLICKED:
// emit event and transition to idle
this.emit(EVENT_BUTTON_QUADRUPLE_CLICKED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_QUADRUPLE_CLICKED);
this.buttonState = STATE_IDLE;
break;
case STATE_RELEASE_WAIT:
// emit event and transition to idle
this.emit(EVENT_BUTTON_RELEASED);
this.emit(EVENT_BUTTON_STATUS, EVENT_BUTTON_RELEASED);
this.buttonState = STATE_IDLE;
break;
}
this.emitTimer = null;
}
}
module.exports = ButtonEvents;