Show:

File: src/io/SoftPot.js

/**
 * Copyright (c) 2011-2012 Jeff Hoefs <soundanalogous@gmail.com>
 * Released under the MIT license. See LICENSE file for details.
 */

JSUTILS.namespace('BO.io.SoftPot');

BO.io.SoftPot = (function() {

  var SoftPot;

  // private static constants:
  var TAP_TIMEOUT = 200,
    FLICK_TIMEOUT = 200,
    PRESS_TIMER_INTERVAL = 10,
    MIN_VALUE = 0.01,
    DEBOUNCE_TIMEOUT = 20;

  // dependencies
  var PhysicalInputBase = BO.PhysicalInputBase,
    Pin = BO.Pin,
    PinEvent = BO.PinEvent,
    Scaler = BO.filters.Scaler,
    Timer = JSUTILS.Timer,
    TimerEvent = JSUTILS.TimerEvent,
    SoftPotEvent = BO.io.SoftPotEvent;

  /**
   * Creates an interface to a SoftPot sensor. A softpot is a type of
   * analog resistive sensor that acts as a type of slider input. There are
   * straight and curved variations. This object provides a number of useful
   * events such as Press, Release, Drag, Tap and capturing Flick gestures.
   * See [Breakout/examples/sensors/softpot.html](https://github.com/soundanalogous/Breakout/blob/master/examples/sensors/softpot.html) for an example application.
   *
   * @class SoftPot
   * @constructor
   * @extends BO.PhysicalInputBase
   * @param {IOBoard} board A reference to the IOBoard instance
   * @param {Pin} pin A reference to the Pin the softpot is connected to.
   * @param {Number} softPotLength The length of the softpot in mm.
   * Default = 100.
   */
  SoftPot = function(board, pin, softPotLength) {
    "use strict";

    PhysicalInputBase.call(this);

    this.name = "SoftPot";
    this._pin = pin;

    softPotLength = softPotLength || 100;

    this._isDrag = false;
    this._flickDistance = 0;
    this._touchPoint = 0;
    this._lastMovePoint = 0;
    this._isTouched = false;
    this._distanceFromPressed = 0;
    this._minFlickMovement = 1.0 / softPotLength * 2.5;
    this._minDragMovement = 1.0 / softPotLength * 1.0;
    this._tapTimeout = TAP_TIMEOUT;
    this._minValue = MIN_VALUE;

    this._board = board;
    board.enableAnalogPin(this._pin.analogNumber);

    this._debugMode = BO.enableDebugging;

    this._pin.addEventListener(PinEvent.CHANGE, this.onPinChange.bind(this));

    this._pressTimer = new Timer(PRESS_TIMER_INTERVAL, 0);
    this._flickTimer = new Timer(FLICK_TIMEOUT, 1);
  };


  SoftPot.prototype = JSUTILS.inherit(PhysicalInputBase.prototype);
  SoftPot.prototype.constructor = SoftPot;

  /**
   * @private
   * @method onPinChange
   * @param {Event} evt PinEvent.CHANGE
   */
  SoftPot.prototype.onPinChange = function(evt) {
    var val = evt.target.value;

    // _minValue is the minimum value required to set the release state
    // should be as close to zero as possible
    if (this._isTouched && val < this._minValue) {
      this.onRelease();
    } else if (val >= this._minValue) {
      if (!this._isTouched) {
        this.startTouch(val);
        this._lastMovePoint = val;
      } else {
        this.onMove(val);
      }
    }
  };

  /**
   * @private
   * @method setMinFlickMovement
   * @param {Number} touchPoint The value where the touch is occuring on the
   * strip
   */
  SoftPot.prototype.setMinFlickMovement = function(num) {
    this._minFlickMovement = num;
  };

  /**
   * @private
   * @method startTouch
   */
  SoftPot.prototype.startTouch = function(touchPoint) {

    this._pressTimer.reset();
    this._pressTimer.start();

    // where we pressed
    this._touchPoint = touchPoint;
    this.dispatch(SoftPotEvent.PRESS);

    this._isTouched = true;
    this._isDrag = false;
  };

  /**
   * @private
   * @method onRelease
   */
  SoftPot.prototype.onRelease = function() {

    var dispatchedFlick = false;

    // discard unintentional touch / noise
    if (this._pressTimer.currentCount > DEBOUNCE_TIMEOUT / PRESS_TIMER_INTERVAL) {
      // must meet minimum time requirement for flick
      if (this._flickTimer.running) {
        if (this._flickDir > 0) {
          this.dispatch(SoftPotEvent.FLICK_DOWN);
        } else {
          this.dispatch(SoftPotEvent.FLICK_UP);
        }
        dispatchedFlick = true;

      }

      if (!dispatchedFlick) {
        // Check for presses
        if (this._pressTimer.running) {

          // If less than tap timeout, then it is a tap
          if (!this._isDrag && this._pressTimer.currentCount <= this._tapTimeout / PRESS_TIMER_INTERVAL) {
            this.dispatch(SoftPotEvent.TAP);
          }
        }
      }
    }

    this.dispatch(SoftPotEvent.RELEASE);

    this.resetForNext();
  };

  /**
   * @private
   * @method onMove
   * @param {Number} touchPoint The value where the touch is occuring on the
   * strip
   */
  SoftPot.prototype.onMove = function(touchPoint) {

    this._touchPoint = touchPoint;
    // Save current point
    var curMovePoint = touchPoint;

    // Flick handeling
    this._flickDistance = Math.abs(curMovePoint - this._lastMovePoint);

    if (!this._isDrag && this._flickDistance > this._minFlickMovement) {
      this._flickTimer.reset();
      this._flickTimer.start();

      if (curMovePoint - this._lastMovePoint > 0) {
        this._flickDir = 1;
      } else {
        this._flickDir = -1;
      }

      this._isDrag = false;
    }

    var dragDistance = Math.abs(curMovePoint - this._lastMovePoint);

    // Dragging handler
    // Don't check when flick timer is running
    //console.log("min drag = " + this._minDragMovement);
    if ((dragDistance > this._minDragMovement) && (this._flickTimer.running === false)) {
      this._isDrag = true;
    }

    if (this._isDrag) {
      this.dispatch(SoftPotEvent.DRAG);
      this._distanceFromPressed = curMovePoint - this._lastMovePoint;
    }

    this.debug("SoftPot: distance traveled flick is " + this._flickDistance);
    this.debug("SoftPot: distance traveled drag is " + dragDistance);

    // Reuse for next
    this._lastMovePoint = curMovePoint;
  };

  /**
   * Scale from the minimum and maximum input values to 0.0 -> 1.0.
   *
   * @method setRange
   * @param {Number} minimum The minimum value
   * @param {Number} maximum The maximum value
   */
  SoftPot.prototype.setRange = function(minimum, maximum) {
    this._pin.removeAllFilters();
    this._pin.addFilter(new Scaler(minimum, maximum, 0, 1, Scaler.LINEAR));
  };

  /**
   * @private
   * @method dispatch
   * @type {Event} type The event type
   */
  SoftPot.prototype.dispatch = function(type) {
    this.debug("SoftPot dispatch " + type);
    this.dispatchEvent(new SoftPotEvent(type, this._touchPoint));
  };

  /**
   * Reset whenever you need the next Touch point.
   * @private
   * @method resetForNext
   */
  SoftPot.prototype.resetForNext = function() {
    this._flickTimer.stop();
    this._pressTimer.stop();
    this._isTouched = false;
    this._isDrag = false;
  };

  /**
   * For debugging.
   *
   * @private
   */
  SoftPot.prototype.debug = function(str) {
    if (this._debugMode) {
      console.log(str);
    }
  };

  Object.defineProperties(SoftPot.prototype, {
    /**
     * The current value.
     * @property value
     * @type Number
     */
    value: {
      get: function() {
        return this._touchPoint;
      }
    },

    /**
     * The current distance from the press point.
     * @property distanceFromPressed
     * @type Number
     */
    distanceFromPressed: {
      get: function() {
        return this._distanceFromPressed;
      }
    },

    /**
     * The minimum distance required to trigger a flick event. Change this
     * value to fine tune the flick gesture.
     * @property minFlickMovement
     * @type Number
     */
    minFlickMovement: {
      get: function() {
        return this._minFlickMovement;
      },
      set: function(min) {
        this._minFlickMovement = min;
      }
    },

    /**
     * The minimum distance required to trigger a drag event. Change this
     * value to fine tune the drag response.
     * @property minDragMovement
     * @type Number
     */
    minDragMovement: {
      get: function() {
        return this._minDragMovement;
      },
      set: function(min) {
        this._minDragMovement = min;
      }
    },

    /**
     * The maximum time (in milliseconds) between a press and release in
     * order to trigger a TAP event.
     * @property tapTimeout
     * @type Number
     */
    tapTimeout: {
      get: function() {
        return this._tapTimeout;
      },
      set: function(t) {
        this._tapTimeout = t;
      }
    },

    /**
     * The minimum value required to set the Release state. This number should
     * be as close to zero as possible. Increase this value if you are noticing
     * fluttering between the Pressed and Released states. Default value = 0.01;
     * @property minValue
     * @type Number
     */
    minValue: {
      get: function() {
        return this._minValue;
      },
      set: function(val) {
        this._minValue = val;
      }
    }
  });

  // Document events

  /**
   * The softPotPressed event is dispatched when pressure is applied to
   * the softpot surface.
   * @type BO.io.SoftPotEvent.PRESS
   * @event softPotPressed
   * @param {BO.io.SoftPot} target A reference to the SoftPot object
   */

  /**
   * The softPotReleased event is dispatched when pressure is released from
   * the softpot surface.
   * @type BO.io.SoftPotEvent.RELEASE
   * @event softPotReleased
   * @param {BO.io.SoftPot} target A reference to the SoftPot object
   */

  /**
   * The softPotDrag event is dispatched when a drag is detected along
   * the length of the softpot sensor.
   * @type BO.io.SoftPotEvent.DRAG
   * @event softPotDrag
   * @param {BO.io.SoftPot} target A reference to the SoftPot object
   */

  /**
   * The softPotFlickUp event is dispatched when a flick gesture is detected
   * in the direction of the sensor pins.
   * @type BO.io.SoftPotEvent.FLICK_UP
   * @event softPotFlickUp
   * @param {BO.io.SoftPot} target A reference to the SoftPot object
   */

  /**
   * The softPotFlickDown event is dispatched when a flick gesture is
   * detected in the direction away from the sensor pins.
   * @type BO.io.SoftPotEvent.FLICK_DOWN
   * @event softPotFlickDown
   * @param {BO.io.SoftPot} target A reference to the SoftPot object
   */

  /**
   * The softPotTap event is dispatched when a press and release occurs
   * in in less than the duration specified by the tapTimeout property.
   * @type BO.io.SoftPotEvent.TAP
   * @event softPotTap
   * @param {BO.io.SoftPot} target A reference to the SoftPot object
   */

  return SoftPot;

}());