Change the button gesture recognizer state machine

- Use a synchronized event queue rather than overwriting "current" button state
    - Use aggressive assertions to ensure state machine is coherent
This commit is contained in:
Aaron Culliney 2016-04-16 14:16:34 -07:00
parent 9c9ba6e7ef
commit 13184ec4be
2 changed files with 152 additions and 83 deletions

View File

@ -22,6 +22,13 @@
# define TOUCH_JOY_LOG(...) # define TOUCH_JOY_LOG(...)
#endif #endif
#define DEBUG_TOUCH_JOY_GESTURE 0
#if DEBUG_TOUCH_JOY_GESTURE
# define TOUCH_JOY_GESTURE_LOG(...) LOG(__VA_ARGS__)
#else
# define TOUCH_JOY_GESTURE_LOG(...)
#endif
#define ROSETTE_ROWS 3 #define ROSETTE_ROWS 3
#define ROSETTE_COLS 3 #define ROSETTE_COLS 3

View File

@ -17,16 +17,21 @@
#define BUTTON_TAP_DELAY_NANOS_DEFAULT 50000000 #define BUTTON_TAP_DELAY_NANOS_DEFAULT 50000000
static GLTouchJoyVariant happyHappyJoyJoy = { 0 }; typedef struct touch_event_s {
struct touch_event_s *next;
static struct { interface_touch_event_t event;
uint8_t currJoyButtonValue0; uint8_t currJoyButtonValue0;
uint8_t currJoyButtonValue1; uint8_t currJoyButtonValue1;
uint8_t currButtonDisplayChar; uint8_t currButtonDisplayChar;
} touch_event_s;
static touch_event_s *touchEventQ = NULL;
static GLTouchJoyVariant happyHappyJoyJoy = { 0 };
static struct {
void (*buttonDrawCallback)(char newChar); void (*buttonDrawCallback)(char newChar);
bool trackingButton;
bool trackingButtonMove;
pthread_t tapDelayThreadId; pthread_t tapDelayThreadId;
pthread_mutex_t tapDelayMutex; pthread_mutex_t tapDelayMutex;
pthread_cond_t tapDelayCond; pthread_cond_t tapDelayCond;
@ -51,9 +56,6 @@ static inline void _reset_axis_state(void) {
static inline void _reset_buttons_state(void) { static inline void _reset_buttons_state(void) {
joy_button0 = 0x0; joy_button0 = 0x0;
joy_button1 = 0x0; joy_button1 = 0x0;
joys.currJoyButtonValue0 = 0;
joys.currJoyButtonValue1 = 0;
joys.currButtonDisplayChar = ' ';
} }
static void touchjoy_resetState(void) { static void touchjoy_resetState(void) {
@ -63,12 +65,19 @@ static void touchjoy_resetState(void) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Tap Delay Thread : delays processing of touch-down so that a different joystick button can be fired // Tap Delay Thread : implements a gesture recognizer that differentiates between a "long touch", "tap", and "swipe
// down/up" gestures. Necessarily delays processing of initial touch down event to make a proper determination. Also
// delays resetting joystick button state after touch up event to avoid resetting joystick button state too soon.
//
// * long touch and tap are interpreted as one (configurable) joystick button fire event
// * swipe up and swipe down are the other two (configurable) joystick button fire events
//
static inline void _signal_tap_delay(void) { static struct timespec *_tap_wait(void) {
pthread_mutex_lock(&joys.tapDelayMutex); static struct timespec wait = { 0 };
pthread_cond_signal(&joys.tapDelayCond); clock_gettime(CLOCK_REALTIME, &wait);
pthread_mutex_unlock(&joys.tapDelayMutex); wait = timespec_add(wait, joys.tapDelayNanos);
return &wait;
} }
static void *_button_tap_delayed_thread(void *dummyptr) { static void *_button_tap_delayed_thread(void *dummyptr) {
@ -76,65 +85,98 @@ static void *_button_tap_delayed_thread(void *dummyptr) {
pthread_mutex_lock(&joys.tapDelayMutex); pthread_mutex_lock(&joys.tapDelayMutex);
bool deepSleep = false; int timedOut = ETIMEDOUT;
uint8_t currJoyButtonValue0 = 0x0;
uint8_t currJoyButtonValue1 = 0x0;
uint8_t currButtonDisplayChar = ' ';
for (;;) { for (;;) {
if (UNLIKELY(!joyglobals.isAvailable)) { if (UNLIKELY(!joyglobals.isAvailable)) {
break; break;
} }
struct timespec wait; if (timedOut) {
clock_gettime(CLOCK_REALTIME, &wait); // reset state and deep sleep waiting for touch down
wait = timespec_add(wait, joys.tapDelayNanos); _reset_buttons_state();
int timedOut = pthread_cond_timedwait(&joys.tapDelayCond, &joys.tapDelayMutex, &wait); // wait and possibly consume event TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] deep sleep ...");
assert((!timedOut || timedOut == ETIMEDOUT) && "should not fail any other way"); pthread_cond_wait(&joys.tapDelayCond, &joys.tapDelayMutex);
} else {
// delays reset of button state while remaining ready to process a touch down
TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] event looping ...");
timedOut = pthread_cond_timedwait(&joys.tapDelayCond, &joys.tapDelayMutex, _tap_wait()); // wait and possibly consume event
assert((!timedOut || timedOut == ETIMEDOUT) && "should not fail any other way");
if (timedOut) {
// reset state and go into deep sleep
continue;
}
_reset_buttons_state();
}
if (!timedOut) { if (UNLIKELY(!joyglobals.isAvailable)) {
if (!deepSleep) { break;
if (joys.trackingButtonMove) { }
// dragging
currJoyButtonValue0 = 0x0; TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] touch down ...");
currJoyButtonValue1 = 0x0;
currButtonDisplayChar = joys.currButtonDisplayChar; touch_event_s *touchCurrEvent = NULL;
} else if (joys.trackingButton) { touch_event_s *touchPrevEvent = touchEventQ;
// touch down -- delay consumption to determine if tap or drag assert(touchPrevEvent && "should be a touch event ready to consume");
currJoyButtonValue0 = joys.currJoyButtonValue0; touchEventQ = touchEventQ->next;
currJoyButtonValue1 = joys.currJoyButtonValue1; assert(touchPrevEvent->event == TOUCH_DOWN && "event queue head should be a touch down event");
currButtonDisplayChar = joys.currButtonDisplayChar;
joys.buttonDrawCallback(currButtonDisplayChar); for (;;) {
// zero the buttons before delay // delay processing of touch down to perform simple gesture recognition
_reset_buttons_state(); timedOut = pthread_cond_timedwait(&joys.tapDelayCond, &joys.tapDelayMutex, _tap_wait());
continue; assert((!timedOut || timedOut == ETIMEDOUT) && "should not fail any other way");
} else {
// touch up becomes tap if (UNLIKELY(!joyglobals.isAvailable)) {
joys.currJoyButtonValue0 = currJoyButtonValue0; break;
joys.currJoyButtonValue1 = currJoyButtonValue1; }
joys.currButtonDisplayChar = currButtonDisplayChar;
} touchCurrEvent = touchEventQ;
if (!touchCurrEvent) {
assert(timedOut);
// touch-down-and-hold
TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] long touch ...");
joy_button0 = touchPrevEvent->currJoyButtonValue0;
joy_button1 = touchPrevEvent->currJoyButtonValue1;
joys.buttonDrawCallback(touchPrevEvent->currButtonDisplayChar);
continue;
}
touchEventQ = touchEventQ->next;
if (touchCurrEvent->event == TOUCH_MOVE) {
// dragging ...
TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] move ...");
joy_button0 = touchCurrEvent->currJoyButtonValue0;
joy_button1 = touchCurrEvent->currJoyButtonValue1;
joys.buttonDrawCallback(touchCurrEvent->currButtonDisplayChar);
FREE(touchPrevEvent);
touchPrevEvent = touchCurrEvent;
} else if (touchCurrEvent->event == TOUCH_UP) {
// tap
TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] touch up ...");
joy_button0 = touchPrevEvent->currJoyButtonValue0;
joy_button1 = touchPrevEvent->currJoyButtonValue1;
joys.buttonDrawCallback(touchPrevEvent->currButtonDisplayChar);
timedOut = 0;
break;
} else {
LOG("OOPS : unexpected touch event : %d", touchCurrEvent->event);
assert(false);
} }
} }
joy_button0 = joys.currJoyButtonValue0; FREE(touchPrevEvent);
joy_button1 = joys.currJoyButtonValue1; FREE(touchCurrEvent);
joys.buttonDrawCallback(joys.currButtonDisplayChar); }
deepSleep = false; // clear out event queue
if (timedOut && !joys.trackingButton) { touch_event_s *p = touchEventQ;
deepSleep = true; while (p) {
_reset_buttons_state(); touch_event_s *dead = p;
TOUCH_JOY_LOG(">>> [DELAYEDTAP] end ..."); p = p->next;
pthread_cond_wait(&joys.tapDelayCond, &joys.tapDelayMutex); // consume initial touch down FREE(dead);
TOUCH_JOY_LOG(">>> [DELAYEDTAP] begin ...");
} else {
TOUCH_JOY_LOG(">>> [DELAYEDTAP] event looping ...");
}
} }
pthread_mutex_unlock(&joys.tapDelayMutex); pthread_mutex_unlock(&joys.tapDelayMutex);
joys.tapDelayThreadId = 0;
joys.tapDelayMutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; joys.tapDelayMutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
joys.tapDelayCond = (pthread_cond_t)PTHREAD_COND_INITIALIZER; joys.tapDelayCond = (pthread_cond_t)PTHREAD_COND_INITIALIZER;
@ -145,15 +187,23 @@ static void *_button_tap_delayed_thread(void *dummyptr) {
static void touchjoy_setup(void (*buttonDrawCallback)(char newChar)) { static void touchjoy_setup(void (*buttonDrawCallback)(char newChar)) {
assert((joys.tapDelayThreadId == 0) && "setup called multiple times!"); assert((joys.tapDelayThreadId == 0) && "setup called multiple times!");
joys.buttonDrawCallback = buttonDrawCallback;
pthread_create(&joys.tapDelayThreadId, NULL, (void *)&_button_tap_delayed_thread, (void *)NULL); long lVal = 0;
const interface_device_t screenOwner = prefs_parseLongValue(PREF_DOMAIN_TOUCHSCREEN, PREF_SCREEN_OWNER, &lVal, /*base:*/10) ? lVal : TOUCH_DEVICE_NONE;
if (screenOwner == TOUCH_DEVICE_JOYSTICK) {
joys.buttonDrawCallback = buttonDrawCallback;
pthread_create(&joys.tapDelayThreadId, NULL, (void *)&_button_tap_delayed_thread, (void *)NULL);
}
} }
static void touchjoy_shutdown(void) { static void touchjoy_shutdown(void) {
pthread_mutex_lock(&joys.tapDelayMutex);
pthread_cond_signal(&joys.tapDelayCond); pthread_cond_signal(&joys.tapDelayCond);
if (pthread_join(joys.tapDelayThreadId, NULL)) { pthread_mutex_unlock(&joys.tapDelayMutex);
if (joys.tapDelayThreadId && pthread_join(joys.tapDelayThreadId, NULL)) {
ERRLOG("OOPS: pthread_join tap delay thread ..."); ERRLOG("OOPS: pthread_join tap delay thread ...");
} }
joys.tapDelayThreadId = 0;
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -196,30 +246,46 @@ static void touchjoy_axisUp(int x, int y) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// button state // button state
static void _set_current_button_state(touchjoy_button_type_t theButtonChar) { static void _signal_tap_delay_event(interface_touch_event_t type, touchjoy_button_type_t theButtonChar) {
touch_event_s *touchEvent = MALLOC(sizeof(*touchEvent));
touchEvent->next = NULL;
touchEvent->event = type;
if (theButtonChar == TOUCH_BUTTON1) { if (theButtonChar == TOUCH_BUTTON1) {
joys.currJoyButtonValue0 = 0x80; touchEvent->currJoyButtonValue0 = 0x80;
joys.currJoyButtonValue1 = 0; touchEvent->currJoyButtonValue1 = 0;
joys.currButtonDisplayChar = MOUSETEXT_OPENAPPLE; touchEvent->currButtonDisplayChar = MOUSETEXT_OPENAPPLE;
} else if (theButtonChar == TOUCH_BUTTON2) { } else if (theButtonChar == TOUCH_BUTTON2) {
joys.currJoyButtonValue0 = 0; touchEvent->currJoyButtonValue0 = 0;
joys.currJoyButtonValue1 = 0x80; touchEvent->currJoyButtonValue1 = 0x80;
joys.currButtonDisplayChar = MOUSETEXT_CLOSEDAPPLE; touchEvent->currButtonDisplayChar = MOUSETEXT_CLOSEDAPPLE;
} else if (theButtonChar == TOUCH_BOTH) { } else if (theButtonChar == TOUCH_BOTH) {
joys.currJoyButtonValue0 = 0x80; touchEvent->currJoyButtonValue0 = 0x80;
joys.currJoyButtonValue1 = 0x80; touchEvent->currJoyButtonValue1 = 0x80;
joys.currButtonDisplayChar = '+'; touchEvent->currButtonDisplayChar = '+';
} else { } else {
joys.currJoyButtonValue0 = 0; touchEvent->currJoyButtonValue0 = 0;
joys.currJoyButtonValue1 = 0; touchEvent->currJoyButtonValue1 = 0;
joys.currButtonDisplayChar = ' '; touchEvent->currButtonDisplayChar = ' ';
} }
pthread_mutex_lock(&joys.tapDelayMutex);
touch_event_s *p0 = NULL;
touch_event_s *p = touchEventQ;
while (p) {
p0 = p;
p = p->next;
}
if (p0) {
p0->next = touchEvent;
} else {
touchEventQ = touchEvent;
}
pthread_cond_signal(&joys.tapDelayCond);
pthread_mutex_unlock(&joys.tapDelayMutex);
} }
static void touchjoy_buttonDown(void) { static void touchjoy_buttonDown(void) {
_set_current_button_state(joys.touchDownChar); _signal_tap_delay_event(TOUCH_DOWN, joys.touchDownChar);
joys.trackingButton = true;
_signal_tap_delay();
} }
static void touchjoy_buttonMove(int dx, int dy) { static void touchjoy_buttonMove(int dx, int dy) {
@ -232,16 +298,12 @@ static void touchjoy_buttonMove(int dx, int dy) {
theButtonChar = joys.southChar; theButtonChar = joys.southChar;
} }
_set_current_button_state(theButtonChar); _signal_tap_delay_event(TOUCH_MOVE, theButtonChar);
_signal_tap_delay();
} }
joys.trackingButtonMove = true;
} }
static void touchjoy_buttonUp(int dx, int dy) { static void touchjoy_buttonUp(int dx, int dy) {
joys.trackingButton = false; _signal_tap_delay_event(TOUCH_UP, ' ');
joys.trackingButtonMove = false;
_signal_tap_delay();
} }
static void touchjoy_prefsChanged(const char *domain) { static void touchjoy_prefsChanged(const char *domain) {