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(...)
#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_COLS 3

View File

@ -17,16 +17,21 @@
#define BUTTON_TAP_DELAY_NANOS_DEFAULT 50000000
static GLTouchJoyVariant happyHappyJoyJoy = { 0 };
static struct {
typedef struct touch_event_s {
struct touch_event_s *next;
interface_touch_event_t event;
uint8_t currJoyButtonValue0;
uint8_t currJoyButtonValue1;
uint8_t currButtonDisplayChar;
} touch_event_s;
static touch_event_s *touchEventQ = NULL;
static GLTouchJoyVariant happyHappyJoyJoy = { 0 };
static struct {
void (*buttonDrawCallback)(char newChar);
bool trackingButton;
bool trackingButtonMove;
pthread_t tapDelayThreadId;
pthread_mutex_t tapDelayMutex;
pthread_cond_t tapDelayCond;
@ -51,9 +56,6 @@ static inline void _reset_axis_state(void) {
static inline void _reset_buttons_state(void) {
joy_button0 = 0x0;
joy_button1 = 0x0;
joys.currJoyButtonValue0 = 0;
joys.currJoyButtonValue1 = 0;
joys.currButtonDisplayChar = ' ';
}
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) {
pthread_mutex_lock(&joys.tapDelayMutex);
pthread_cond_signal(&joys.tapDelayCond);
pthread_mutex_unlock(&joys.tapDelayMutex);
static struct timespec *_tap_wait(void) {
static struct timespec wait = { 0 };
clock_gettime(CLOCK_REALTIME, &wait);
wait = timespec_add(wait, joys.tapDelayNanos);
return &wait;
}
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);
bool deepSleep = false;
uint8_t currJoyButtonValue0 = 0x0;
uint8_t currJoyButtonValue1 = 0x0;
uint8_t currButtonDisplayChar = ' ';
int timedOut = ETIMEDOUT;
for (;;) {
if (UNLIKELY(!joyglobals.isAvailable)) {
break;
}
struct timespec wait;
clock_gettime(CLOCK_REALTIME, &wait);
wait = timespec_add(wait, joys.tapDelayNanos);
int timedOut = pthread_cond_timedwait(&joys.tapDelayCond, &joys.tapDelayMutex, &wait); // wait and possibly consume event
assert((!timedOut || timedOut == ETIMEDOUT) && "should not fail any other way");
if (timedOut) {
// reset state and deep sleep waiting for touch down
_reset_buttons_state();
TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] deep sleep ...");
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 (!deepSleep) {
if (joys.trackingButtonMove) {
// dragging
currJoyButtonValue0 = 0x0;
currJoyButtonValue1 = 0x0;
currButtonDisplayChar = joys.currButtonDisplayChar;
} else if (joys.trackingButton) {
// touch down -- delay consumption to determine if tap or drag
currJoyButtonValue0 = joys.currJoyButtonValue0;
currJoyButtonValue1 = joys.currJoyButtonValue1;
currButtonDisplayChar = joys.currButtonDisplayChar;
joys.buttonDrawCallback(currButtonDisplayChar);
// zero the buttons before delay
_reset_buttons_state();
continue;
} else {
// touch up becomes tap
joys.currJoyButtonValue0 = currJoyButtonValue0;
joys.currJoyButtonValue1 = currJoyButtonValue1;
joys.currButtonDisplayChar = currButtonDisplayChar;
}
if (UNLIKELY(!joyglobals.isAvailable)) {
break;
}
TOUCH_JOY_GESTURE_LOG(">>> [DELAYEDTAP] touch down ...");
touch_event_s *touchCurrEvent = NULL;
touch_event_s *touchPrevEvent = touchEventQ;
assert(touchPrevEvent && "should be a touch event ready to consume");
touchEventQ = touchEventQ->next;
assert(touchPrevEvent->event == TOUCH_DOWN && "event queue head should be a touch down event");
for (;;) {
// delay processing of touch down to perform simple gesture recognition
timedOut = pthread_cond_timedwait(&joys.tapDelayCond, &joys.tapDelayMutex, _tap_wait());
assert((!timedOut || timedOut == ETIMEDOUT) && "should not fail any other way");
if (UNLIKELY(!joyglobals.isAvailable)) {
break;
}
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;
joy_button1 = joys.currJoyButtonValue1;
joys.buttonDrawCallback(joys.currButtonDisplayChar);
FREE(touchPrevEvent);
FREE(touchCurrEvent);
}
deepSleep = false;
if (timedOut && !joys.trackingButton) {
deepSleep = true;
_reset_buttons_state();
TOUCH_JOY_LOG(">>> [DELAYEDTAP] end ...");
pthread_cond_wait(&joys.tapDelayCond, &joys.tapDelayMutex); // consume initial touch down
TOUCH_JOY_LOG(">>> [DELAYEDTAP] begin ...");
} else {
TOUCH_JOY_LOG(">>> [DELAYEDTAP] event looping ...");
}
// clear out event queue
touch_event_s *p = touchEventQ;
while (p) {
touch_event_s *dead = p;
p = p->next;
FREE(dead);
}
pthread_mutex_unlock(&joys.tapDelayMutex);
joys.tapDelayThreadId = 0;
joys.tapDelayMutex = (pthread_mutex_t)PTHREAD_MUTEX_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)) {
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) {
pthread_mutex_lock(&joys.tapDelayMutex);
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 ...");
}
joys.tapDelayThreadId = 0;
}
// ----------------------------------------------------------------------------
@ -196,30 +246,46 @@ static void touchjoy_axisUp(int x, int y) {
// ----------------------------------------------------------------------------
// 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) {
joys.currJoyButtonValue0 = 0x80;
joys.currJoyButtonValue1 = 0;
joys.currButtonDisplayChar = MOUSETEXT_OPENAPPLE;
touchEvent->currJoyButtonValue0 = 0x80;
touchEvent->currJoyButtonValue1 = 0;
touchEvent->currButtonDisplayChar = MOUSETEXT_OPENAPPLE;
} else if (theButtonChar == TOUCH_BUTTON2) {
joys.currJoyButtonValue0 = 0;
joys.currJoyButtonValue1 = 0x80;
joys.currButtonDisplayChar = MOUSETEXT_CLOSEDAPPLE;
touchEvent->currJoyButtonValue0 = 0;
touchEvent->currJoyButtonValue1 = 0x80;
touchEvent->currButtonDisplayChar = MOUSETEXT_CLOSEDAPPLE;
} else if (theButtonChar == TOUCH_BOTH) {
joys.currJoyButtonValue0 = 0x80;
joys.currJoyButtonValue1 = 0x80;
joys.currButtonDisplayChar = '+';
touchEvent->currJoyButtonValue0 = 0x80;
touchEvent->currJoyButtonValue1 = 0x80;
touchEvent->currButtonDisplayChar = '+';
} else {
joys.currJoyButtonValue0 = 0;
joys.currJoyButtonValue1 = 0;
joys.currButtonDisplayChar = ' ';
touchEvent->currJoyButtonValue0 = 0;
touchEvent->currJoyButtonValue1 = 0;
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) {
_set_current_button_state(joys.touchDownChar);
joys.trackingButton = true;
_signal_tap_delay();
_signal_tap_delay_event(TOUCH_DOWN, joys.touchDownChar);
}
static void touchjoy_buttonMove(int dx, int dy) {
@ -232,16 +298,12 @@ static void touchjoy_buttonMove(int dx, int dy) {
theButtonChar = joys.southChar;
}
_set_current_button_state(theButtonChar);
_signal_tap_delay();
_signal_tap_delay_event(TOUCH_MOVE, theButtonChar);
}
joys.trackingButtonMove = true;
}
static void touchjoy_buttonUp(int dx, int dy) {
joys.trackingButton = false;
joys.trackingButtonMove = false;
_signal_tap_delay();
_signal_tap_delay_event(TOUCH_UP, ' ');
}
static void touchjoy_prefsChanged(const char *domain) {