| /* Copyright 2014 The Chromium OS Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| /* Board specific gesture recognition */ |
| |
| #include "accelgyro.h" |
| #include "common.h" |
| #include "console.h" |
| #include "hooks.h" |
| #include "gesture.h" |
| #include "lid_switch.h" |
| #include "lightbar.h" |
| #include "motion_sense.h" |
| #include "task.h" |
| #include "timer.h" |
| #include "util.h" |
| |
| /* Console output macros */ |
| #define CPUTS(outstr) cputs(CC_GESTURE, outstr) |
| #define CPRINTS(format, args...) cprints(CC_GESTURE, format, ## args) |
| #define CPRINTF(format, args...) cprintf(CC_GESTURE, format, ## args) |
| |
| |
| /* |
| * Double tap detection parameters |
| * Double tap works by looking for two isolated Z-axis accelerometer impulses |
| * preceded and followed by relatively calm periods of accelerometer motion. |
| * |
| * Define an outer and inner window. The inner window specifies how |
| * long the tap impulse is expected to last. The outer window specifies the |
| * period before the initial tap impluse and after the final tap impulse for |
| * which to check for relatively calm periods. In between the two impulses |
| * there is a minimum and maximum interstice time allowed. |
| */ |
| #define OUTER_WINDOW \ |
| (CONFIG_GESTURE_TAP_OUTER_WINDOW_T / \ |
| CONFIG_GESTURE_SAMPLING_INTERVAL_MS) |
| #define INNER_WINDOW \ |
| (CONFIG_GESTURE_TAP_INNER_WINDOW_T / \ |
| CONFIG_GESTURE_SAMPLING_INTERVAL_MS) |
| #define MIN_INTERSTICE \ |
| (CONFIG_GESTURE_TAP_MIN_INTERSTICE_T / \ |
| CONFIG_GESTURE_SAMPLING_INTERVAL_MS) |
| #define MAX_INTERSTICE \ |
| (CONFIG_GESTURE_TAP_MAX_INTERSTICE_T / \ |
| CONFIG_GESTURE_SAMPLING_INTERVAL_MS) |
| #define MAX_WINDOW OUTER_WINDOW |
| |
| /* State machine states for detecting double tap */ |
| enum tap_states { |
| /* Look for calm before the storm */ |
| TAP_IDLE, |
| /* Record first Z impulse */ |
| TAP_IMPULSE_1, |
| |
| /* Eye of the storm, expect Z motion to drop and then suddenly spike */ |
| TAP_INTERSTICE_DROP, |
| TAP_INTERSTICE_RISE, |
| |
| /* Record second Z impulse */ |
| TAP_IMPULSE_2, |
| /* Should be quiet after the storm */ |
| TAP_AFTER_EVENT |
| }; |
| |
| /* Tap sensor to use */ |
| static struct motion_sensor_t *sensor = |
| &motion_sensors[CONFIG_GESTURE_SENSOR_BATTERY_TAP]; |
| |
| /* Tap state information */ |
| static int history_z[MAX_WINDOW]; /* Changes in Z */ |
| static int history_xy[MAX_WINDOW]; /* Changes in X and Y */ |
| static int state, history_idx; |
| static int history_initialized, history_init_index; |
| static int tap_debug; |
| |
| /* Tap detection flag */ |
| static int tap_detection; |
| |
| /* |
| * TODO(crosbug.com/p/33102): Cleanup this function: break into multiple |
| * functions and generalize so it can be used for other boards. |
| */ |
| static int gesture_tap_for_battery(void) |
| { |
| /* Current and previous accel x,y,z */ |
| int x, y, z; |
| static int x_p, y_p, z_p; |
| |
| /* Number of iterations in this state */ |
| static int state_cnt; |
| |
| /* |
| * Running sums of data diffs for inner and outer windows. |
| * Z data kept separate from X and Y data |
| */ |
| static int sum_z_inner, sum_z_outer, sum_xy_inner, sum_xy_outer; |
| |
| /* Total variation in each signal, normalized for window size */ |
| int delta_z_outer, delta_z_inner, delta_xy_outer, delta_xy_inner; |
| |
| /* Max variation seen during tap event and state cnts since max */ |
| static int delta_z_inner_max; |
| static int cnts_since_max; |
| |
| /* Interstice Z motion thresholds */ |
| static int z_drop_thresh, z_rise_thresh; |
| |
| int history_idx_inner, state_p; |
| int ret = 0; |
| |
| /* Get data */ |
| x = sensor->xyz[0]; |
| y = sensor->xyz[1]; |
| z = sensor->xyz[2]; |
| |
| /* |
| * Calculate history of change in Z sensor and keeping |
| * running sums for the past. |
| */ |
| history_idx_inner = history_idx - INNER_WINDOW; |
| if (history_idx_inner < 0) |
| history_idx_inner += MAX_WINDOW; |
| sum_z_inner -= history_z[history_idx_inner]; |
| sum_z_outer -= history_z[history_idx]; |
| history_z[history_idx] = ABS(z - z_p); |
| sum_z_inner += history_z[history_idx]; |
| sum_z_outer += history_z[history_idx]; |
| |
| /* |
| * Calculate history of change in X and Y sensors combined |
| * and keep a running sum of the change over the past. |
| */ |
| sum_xy_inner -= history_xy[history_idx_inner]; |
| sum_xy_outer -= history_xy[history_idx]; |
| history_xy[history_idx] = ABS(x - x_p) + ABS(y - y_p); |
| sum_xy_inner += history_xy[history_idx]; |
| sum_xy_outer += history_xy[history_idx]; |
| |
| /* Increment history index */ |
| history_idx = (history_idx == MAX_WINDOW - 1) ? 0 : (history_idx + 1); |
| |
| /* Store previous X, Y, Z data */ |
| x_p = x; |
| y_p = y; |
| z_p = z; |
| |
| /* |
| * Ignore data until we fill history buffer and wrap around. If |
| * detection is paused, history_init_index will store the index |
| * when paused, so that when re-started, we will wait until we |
| * wrap around again. |
| */ |
| if (history_idx == history_init_index) |
| history_initialized = 1; |
| if (!history_initialized) |
| return 0; |
| |
| /* |
| * Normalize data based on window size and isolate outer and inner |
| * window data. |
| */ |
| delta_z_outer = (sum_z_outer - sum_z_inner) * 1000 / |
| (OUTER_WINDOW - INNER_WINDOW); |
| delta_z_inner = sum_z_inner * 1000 / INNER_WINDOW; |
| delta_xy_outer = (sum_xy_outer - sum_xy_inner) * 1000 / |
| (OUTER_WINDOW - INNER_WINDOW); |
| delta_xy_inner = sum_xy_inner * 1000 / INNER_WINDOW; |
| |
| state_cnt++; |
| state_p = state; |
| |
| switch (state) { |
| case TAP_IDLE: |
| /* Look for a sudden increase in Z movement */ |
| if (delta_z_inner > 30000 && |
| delta_z_inner > 13 * delta_z_outer && |
| delta_z_inner > 1 * delta_xy_inner) { |
| delta_z_inner_max = delta_z_inner; |
| state_cnt = 0; |
| state = TAP_IMPULSE_1; |
| } |
| break; |
| |
| case TAP_IMPULSE_1: |
| /* Find the peak inner window of Z movement */ |
| if (delta_z_inner > delta_z_inner_max) { |
| delta_z_inner_max = delta_z_inner; |
| cnts_since_max = state_cnt; |
| } |
| |
| /* After inner window has passed, move to next state */ |
| if (state_cnt >= INNER_WINDOW) { |
| state = TAP_INTERSTICE_DROP; |
| z_drop_thresh = delta_z_inner_max / 12; |
| z_rise_thresh = delta_z_inner_max / 3; |
| state_cnt += INNER_WINDOW - cnts_since_max; |
| } |
| break; |
| |
| case TAP_INTERSTICE_DROP: |
| /* Check for z motion to go back down first */ |
| if (delta_z_inner < z_drop_thresh) |
| state = TAP_INTERSTICE_RISE; |
| |
| if (state_cnt > MAX_INTERSTICE) |
| state = TAP_IDLE; |
| |
| break; |
| |
| case TAP_INTERSTICE_RISE: |
| /* Then, check for z motion to go back up */ |
| if (delta_z_inner > z_rise_thresh) { |
| if (state_cnt < MIN_INTERSTICE) { |
| state = TAP_IDLE; |
| } else { |
| delta_z_inner_max = delta_z_inner; |
| state_cnt = 0; |
| state = TAP_IMPULSE_2; |
| } |
| } |
| |
| if (state_cnt > MAX_INTERSTICE) |
| state = TAP_IDLE; |
| break; |
| |
| case TAP_IMPULSE_2: |
| /* Find the peak inner window of Z movement */ |
| if (delta_z_inner > delta_z_inner_max) { |
| delta_z_inner_max = delta_z_inner; |
| cnts_since_max = state_cnt; |
| } |
| |
| /* After inner window has passed, move to next state */ |
| if (state_cnt >= INNER_WINDOW) { |
| state = TAP_AFTER_EVENT; |
| state_cnt += INNER_WINDOW - cnts_since_max; |
| } |
| |
| case TAP_AFTER_EVENT: |
| /* Check for small Z movement after the event */ |
| if (state_cnt < OUTER_WINDOW) |
| break; |
| |
| if (2 * delta_z_inner_max > 3 * delta_z_outer && |
| delta_z_outer > 1 * delta_xy_outer) |
| ret = 1; |
| |
| state = TAP_IDLE; |
| break; |
| } |
| |
| /* On state transitions, print debug info */ |
| if (tap_debug && |
| (state != state_p || |
| (state_cnt % 10000 == 9999))) { |
| /* make sure we don't divide by 0 */ |
| if (delta_z_outer == 0 || delta_xy_inner == 0) |
| CPRINTS("tap st %d->%d, error div by 0", |
| state_p, state); |
| else |
| CPRINTF("[%T tap st %d->%d, st_cnt %-3d ", |
| state_p, state, state_cnt); |
| CPRINTF("Z_in:Z_out %-3d, Z_in:XY_in %-3d ", |
| delta_z_inner / delta_z_outer, |
| delta_z_inner / delta_xy_inner); |
| CPRINTF("dZ_in %-8.3d, dZ_in_max %-8.3d, " |
| "dZ_out %-8.3d]\n", |
| delta_z_inner, delta_z_inner_max, |
| delta_z_outer); |
| } |
| |
| return ret; |
| } |
| |
| static void gesture_chipset_resume(void) |
| { |
| /* disable tap detection */ |
| tap_detection = 0; |
| } |
| DECLARE_HOOK(HOOK_CHIPSET_RESUME, gesture_chipset_resume, |
| GESTURE_HOOK_PRIO); |
| |
| static void gesture_chipset_suspend(void) |
| { |
| /* |
| * Clear tap init and history initialized so that we have to |
| * record a whole new set of data, and enable tap detection |
| */ |
| history_initialized = 0; |
| history_init_index = history_idx; |
| state = TAP_IDLE; |
| tap_detection = 1; |
| } |
| DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, gesture_chipset_suspend, |
| GESTURE_HOOK_PRIO); |
| |
| void gesture_calc(uint32_t *event) |
| { |
| /* Only check for gesture if lid is closed and tap detection is on */ |
| if (!tap_detection || lid_is_open()) |
| return; |
| |
| if (gesture_tap_for_battery()) |
| *event |= TASK_EVENT_MOTION_ACTIVITY_INTERRUPT( |
| MOTIONSENSE_ACTIVITY_DOUBLE_TAP); |
| } |
| |
| /*****************************************************************************/ |
| /* Console commands */ |
| static int command_tap_info(int argc, char **argv) |
| { |
| int val; |
| |
| ccprintf("tap: %s\n", (tap_detection && !lid_is_open()) ? |
| "on" : "off"); |
| |
| if (argc > 1) { |
| if (!parse_bool(argv[1], &val)) |
| return EC_ERROR_PARAM1; |
| tap_debug = val; |
| } |
| |
| ccprintf("debug: %s\n", tap_debug ? "on" : "off"); |
| ccprintf("odr: %d\n", sensor->drv->get_data_rate(sensor)); |
| |
| return EC_SUCCESS; |
| } |
| DECLARE_CONSOLE_COMMAND(tapinfo, command_tap_info, |
| "debug on/off", |
| "Print tap information"); |
| |