//
// This is a single-, double-, triple-, and quadruple-click detector
// component for LinuxCNC.
//
// Copyright 2012 Sebastian Kuzminsky <seb@highlab.com>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
component multiclick "Single-, double-, triple-, and quadruple-click detector";
license "GPL";
pin in bit in "The input line, this is where we look for clicks.";
pin out bit single_click
"Goes high briefly when a single-click is detected on the 'in' pin.";
pin out bit single_click_only
"""Goes high briefly when a single-click is detected on the 'in' pin
and no second click followed it.""";
pin out bit double_click
"Goes high briefly when a double-click is detected on the 'in' pin.";
pin out bit double_click_only
"""Goes high briefly when a double-click is detected on the 'in' pin
and no third click followed it.""";
pin out bit triple_click
"Goes high briefly when a triple-click is detected on the 'in' pin.";
pin out bit triple_click_only
"""Goes high briefly when a triple-click is detected on the 'in' pin
and no fourth click followed it.""";
pin out bit quadruple_click
"Goes high briefly when a quadruple-click is detected on the 'in' pin.";
pin out bit quadruple_click_only
"""Goes high briefly when a quadruple-click is detected on the 'in'
pin and no fifth click followed it.""";
// for debugging
pin out s32 state;
param rw bit invert_input = FALSE
"""If FALSE (the default), clicks start with rising edges. If TRUE,
clicks start with falling edges.""";
param rw u32 max_hold_ns = 250000000
"""If the input is held down longer than this, it's not part of a
multi-click. (Default 250,000,000 ns, 250 ms.)""";
param rw u32 max_space_ns = 250000000
"""If the input is released longer than this, it's not part of a
multi-click. (Default 250,000,000 ns, 250 ms.)""";
param rw u32 output_hold_ns = 100000000
"""Positive pulses on the output pins last this long. (Default
100,000,000 ns, 100 ms.)""";
variable int click_state = 0; // wish i could have an enum type here
//
// When entering a new non-IDLE state in the state machine, timer is set to
// 0 and timeout is set to the timeout for the new state.
//
// Each time the component's function runs and the state machine is not
// IDLE, the timer is incremented by the thread period. When the timer
// exceeds the timeout, the state machine resets.
//
variable unsigned timer;
variable unsigned timeout;
//
// When an "X"-click (for X={single,double,triple,quadruple}) is detected,
// the X-click pin goes high and the X_click_hold_timer is set to the
// output_hold_ns param. Each invocation of the function, the
// X_click_hold_timer is decremented by the thread period, and when the
// timer falls below 0, the X-click output pin goes back to low again.
//
// Similarly for the X_click_only_hold_timer, but it doesnt start until
// max_space_ns after the click is detected (to ensure that this is not an
// X+1 click).
//
variable unsigned single_click_hold_timer;
variable unsigned single_click_only_hold_timer;
variable unsigned double_click_hold_timer;
variable unsigned double_click_only_hold_timer;
variable unsigned triple_click_hold_timer;
variable unsigned triple_click_only_hold_timer;
variable unsigned quadruple_click_hold_timer;
variable unsigned quadruple_click_only_hold_timer;
description """A click is defined as a rising edge on the 'in' pin,
followed by the 'in' pin being True for at most 'max-hold-ns' nanoseconds,
followed by a falling edge.
A double-click is defined as two clicks, separated by at
most 'max-space-ns' nanoseconds with the 'in' pin in the False state.
I bet you can guess the definition of triple- and quadruple-click.
You probably want to run the input signal through a debounce component
before feeding it to the multiclick detector, if the input is at all
noisy.
The '*-click' pins go high as soon as the input detects the correct
number of clicks.
The '*-click-only' pins go high a short while after the click, after
the click separator space timeout has expired to show that no further
click is coming. This is useful for triggering halui MDI commands.""";
function _ nofp "Detect single-, double-, triple-, and quadruple-clicks";
;;
typedef enum {
IDLE = 0,
SAW_FIRST_RISING_EDGE,
SAW_FIRST_CLICK,
SAW_SECOND_RISING_EDGE,
SAW_SECOND_CLICK,
SAW_THIRD_RISING_EDGE,
SAW_THIRD_CLICK,
SAW_FOURTH_RISING_EDGE,
SAW_FOURTH_CLICK,
HELD_TOO_LONG
} state_t;
FUNCTION(_) {
int new_in = in;
if (1 == invert_input) {
new_in = !new_in;
}
if (click_state != IDLE) {
timer += period;
}
//
// update the output pins
//
if (single_click_hold_timer > 0) {
if (single_click_hold_timer > period) {
single_click_hold_timer -= period;
} else {
single_click_hold_timer = 0;
single_click = 0;
}
}
if (single_click_only_hold_timer > 0) {
if (single_click_only_hold_timer > period) {
single_click_only_hold_timer -= period;
} else {
single_click_only_hold_timer = 0;
single_click_only = 0;
}
}
if (double_click_hold_timer > 0) {
if (double_click_hold_timer > period) {
double_click_hold_timer -= period;
} else {
double_click_hold_timer = 0;
double_click = 0;
}
}
if (double_click_only_hold_timer > 0) {
if (double_click_only_hold_timer > period) {
double_click_only_hold_timer -= period;
} else {
double_click_only_hold_timer = 0;
double_click_only = 0;
}
}
if (triple_click_hold_timer > 0) {
if (triple_click_hold_timer > period) {
triple_click_hold_timer -= period;
} else {
triple_click_hold_timer = 0;
triple_click = 0;
}
}
if (triple_click_only_hold_timer > 0) {
if (triple_click_only_hold_timer > period) {
triple_click_only_hold_timer -= period;
} else {
triple_click_only_hold_timer = 0;
triple_click_only = 0;
}
}
if (quadruple_click_hold_timer > 0) {
if (quadruple_click_hold_timer > period) {
quadruple_click_hold_timer -= period;
} else {
quadruple_click_hold_timer = 0;
quadruple_click = 0;
}
}
if (quadruple_click_only_hold_timer > 0) {
if (quadruple_click_only_hold_timer > period) {
quadruple_click_only_hold_timer -= period;
} else {
quadruple_click_only_hold_timer = 0;
quadruple_click_only = 0;
}
}
//
// update state, if needed
//
switch (click_state) {
case IDLE: {
if (1 == new_in) {
click_state = SAW_FIRST_RISING_EDGE;
timer = 0;
timeout = max_hold_ns;
}
break;
}
case SAW_FIRST_RISING_EDGE: {
if (0 == new_in) {
click_state = SAW_FIRST_CLICK;
timer = 0;
timeout = max_space_ns;
single_click = 1;
single_click_hold_timer = output_hold_ns;
}
break;
}
case SAW_FIRST_CLICK: {
if (1 == new_in) {
click_state = SAW_SECOND_RISING_EDGE;
timer = 0;
timeout = max_hold_ns;
}
break;
}
case SAW_SECOND_RISING_EDGE: {
if (0 == new_in) {
click_state = SAW_SECOND_CLICK;
timer = 0;
timeout = max_space_ns;
double_click = 1;
double_click_hold_timer = output_hold_ns;
}
break;
}
case SAW_SECOND_CLICK: {
if (1 == new_in) {
click_state = SAW_THIRD_RISING_EDGE;
timer = 0;
timeout = max_hold_ns;
}
break;
}
case SAW_THIRD_RISING_EDGE: {
if (0 == new_in) {
click_state = SAW_THIRD_CLICK;
timer = 0;
timeout = max_space_ns;
triple_click = 1;
triple_click_hold_timer = output_hold_ns;
}
break;
}
case SAW_THIRD_CLICK: {
if (1 == new_in) {
click_state = SAW_FOURTH_RISING_EDGE;
timer = 0;
timeout = max_hold_ns;
}
break;
}
case SAW_FOURTH_RISING_EDGE: {
if (0 == new_in) {
// four clicks is the most we look for, and we just saw it,
// so we're done now
click_state = SAW_FOURTH_CLICK;
timer = 0;
timeout = max_space_ns;
quadruple_click = 1;
quadruple_click_hold_timer = output_hold_ns;
}
break;
}
case SAW_FOURTH_CLICK: {
if (1 == new_in) {
click_state = HELD_TOO_LONG;
}
break;
}
case HELD_TOO_LONG: {
if (0 == new_in) {
click_state = IDLE;
}
break;
}
default: {
// invalid click_state!
click_state = IDLE;
}
}
if ((click_state != IDLE) && (click_state != HELD_TOO_LONG) && (timer > timeout)) {
if (1 == new_in) {
click_state = HELD_TOO_LONG;
} else {
// timeout after some activity on the input line, trigger one
// of the "only" outputs if appropriate
switch (click_state) {
case SAW_FIRST_CLICK: {
single_click_only = 1;
single_click_only_hold_timer = output_hold_ns;
break;
}
case SAW_SECOND_CLICK: {
double_click_only = 1;
double_click_only_hold_timer = output_hold_ns;
break;
}
case SAW_THIRD_CLICK: {
triple_click_only = 1;
triple_click_only_hold_timer = output_hold_ns;
break;
}
case SAW_FOURTH_CLICK: {
quadruple_click_only = 1;
quadruple_click_only_hold_timer = output_hold_ns;
break;
}
}
click_state = IDLE;
}
}
state = click_state;
}