text: var IS_TEST_MODE = !!process.env.IS_TEST_MODE;
  var Board = require("./board");
  var Expander = require("./expander");
  var Emitter = require("events").EventEmitter;
  var util = require("util");
  var Fn = require("./fn");
  
  var constrain = Fn.constrain;
  var fma = Fn.fma;
  var int16 = Fn.int16;
  var sum = Fn.sum;
  var toFixed = Fn.toFixed;
  
  var priv = new Map();
  var calibrationSize = 10;
  
  var aX = "x";
  var aY = "y";
  var aZ = "z";
  var axes = [aX, aY, aZ];
  
  function analogInitialize(opts, dataHandler) {
    var state = priv.get(this);
    var dataPoints = {};
  
    state.zeroV = opts.zeroV || this.DEFAULTS.zeroV;
    state.sensitivity = opts.sensitivity || this.DEFAULTS.sensitivity;
  
    this.pins.forEach(function(pin, index) {
      this.io.pinMode(pin, this.io.MODES.ANALOG);
      this.io.analogRead(pin, function(data) {
        var axis = axes[index];
        dataPoints[axis] = data;
        dataHandler(dataPoints);
      }.bind(this));
    }, this);
  }
  
  function analogToGravity(raw, axis) {
    var state = priv.get(this);
    var zeroV = state.zeroV;
  
    if (Array.isArray(zeroV) && zeroV.length > 0) {
      var axisIndex = axes.indexOf(axis);
      zeroV = zeroV[axisIndex || 0];
    }
  
    return (raw - zeroV) / state.sensitivity;
  }
  
  var Controllers = {
    ANALOG: {
      DEFAULTS: {
        value: {
          zeroV: 478,
          sensitivity: 96
        }
      },
      initialize: {
        value: analogInitialize
      },
      toGravity: {
        value: analogToGravity
      }
    },
    // http://www.invensense.com/mems/gyro/mpu6050.html
    MPU6050: {
      initialize: {
        value: function(opts, dataHandler) {
          var IMU = require("./imu");
          var driver = IMU.Drivers.get(this.board, "MPU6050", opts);
          var state = priv.get(this);
  
          state.sensitivity = opts.sensitivity || 16384;
  
          driver.on("data", function(data) {
            dataHandler(data.accelerometer);
          });
        }
      },
      toGravity: {
        value: function(raw) {
          var state = priv.get(this);
          return raw / state.sensitivity;
        }
      }
    },
    BNO055: {
      initialize: {
        value: function(opts, dataHandler) {
          var IMU = require("./imu");
          var driver = IMU.Drivers.get(this.board, "BNO055", opts);
          var state = priv.get(this);
  
          // AF p.31, Table 3-17: Accelerometer Unit settings
          state.sensitivity = 100;
  
          driver.on("data", function(data) {
            dataHandler(data.accelerometer);
          });
        }
      },
      toGravity: {
        value: function(raw) {
          var state = priv.get(this);
          return raw / state.sensitivity;
        }
      }
    },
  
    ADXL335: {
      DEFAULTS: {
        value: {
          zeroV: 330,
          sensitivity: 66.5
        }
      },
      initialize: {
        value: analogInitialize
      },
      toGravity: {
        value: analogToGravity
      }
    },
  
    // http://www.analog.com/media/en/technical-documentation/data-sheets/ADXL345.pdf
    ADXL345: {
      ADDRESSES: {
        value: [0x53]
      },
      REGISTER: {
        value: {
          // Page 23
          // REGISTER MAP
          //
          POWER: 0x2D,
          // 0x31 49 DATA_FORMAT R/W 00000000 Data format control
          DATA_FORMAT: 0x31,
          // 0x32 50 DATAX0 R 00000000 X-Axis Data 0
          DATAX0: 0x32
        }
      },
      initialize: {
        value: function(opts, dataHandler) {
          var READLENGTH = 6;
          var address = opts.address || this.ADDRESSES[0];
          var state = priv.get(this);
  
          opts.address = address;
  
          this.io.i2cConfig(opts);
  
          // Standby mode
          this.io.i2cWrite(address, this.REGISTER.POWER, 0);
  
          // Enable measurements
          this.io.i2cWrite(address, this.REGISTER.POWER, 8);
  
          /*
  
            Page 26
  
            Register 0x31—DATA_FORMAT (Read/Write)
  
            | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
            | - | - | - | - | - | - | - | - |
            | T | S | I | - | F | J | R     |
  
            T: SELF_TEST
            S: SPI
            I: INT_INVERT
            -:-
            F: FULL_RES
            J: JUSTIFY
            R: RANGE
  
            Range notes: https://github.com/rwaldron/johnny-five/issues/1135#issuecomment-219541346
  
            +/- 16g  0b11
            +/- 8g   0b10
            +/- 4g   0b01
            +/- 2g   0b00
  
  
            Start with FULL_RES bit on
  
            0b00001000 = 0x08 = 8
          */
          var format = 0x08;
  
          /*
            Determine range
  
            0b00000000 = 0 = ±2g
            0b00000001 = 1 = ±4g
            0b00000010 = 2 = ±8g
            0b00000011 = 3 = ±16g
          */
          var range = ({ 2: 0, 4: 1, 8: 2, 16: 3 })[opts.range || 2];
  
          /*
            Page 4
            Sensitivity at             XOUT  YOUT ZOUT
            ±2 g, 10-bit resolution     230   256  282
            ±4 g, 10-bit resolution     115   128  141
            ±8 g, 10-bit resolution      57    64   71
            ±16 g, 10-bit resolution     29    32   35
          */
          state.sensitivity = [{
            x: 230,
            y: 256,
            z: 282
          }, {
            x: 115,
            y: 128,
            z: 141
          }, {
            x: 57,
            y: 64,
            z: 71
          }, {
            x: 29,
            y: 32,
            z: 35
          }][range];
  
  
          // Merge the format and range bits to set the DATA_FORMAT
          this.io.i2cWrite(address, this.REGISTER.DATA_FORMAT, format | range);
  
          this.io.i2cRead(address, this.REGISTER.DATAX0, READLENGTH, function(data) {
            dataHandler({
              x: int16(data[1], data[0]),
              y: int16(data[3], data[2]),
              z: int16(data[5], data[4])
            });
          });
        },
      },
      toGravity: {
        value: function(raw, axis) {
          var state = priv.get(this);
          return raw / state.sensitivity[axis];
        }
      }
    },
    MMA7361: {
      DEFAULTS: {
        value: {
          zeroV: [336, 372, 287],
          sensitivity: 170
        }
      },
      initialize: {
        value: function(opts, dataHandler) {
          var state = priv.get(this);
  
          /* istanbul ignore else */
          if (opts.sleepPin !== undefined) {
            state.sleepPin = opts.sleepPin;
            this.io.pinMode(state.sleepPin, 1);
            this.io.digitalWrite(state.sleepPin, 1);
          }
  
          analogInitialize.call(this, opts, dataHandler);
        }
      },
      toGravity: {
        value: analogToGravity
      },
      enabledChanged: {
        value: function(value) {
          var state = priv.get(this);
  
          /* istanbul ignore else */
          if (state.sleepPin !== undefined) {
            this.io.digitalWrite(state.sleepPin, value ? 1 : 0);
          }
        }
      }
    },
  
    // https://cdn.sparkfun.com/datasheets/Sensors/Accelerometers/MMA8452Q-rev8.1.pdf
    // http://cache.freescale.com/files/sensors/doc/app_note/AN4076.pdf?fpsp=1
    // http://cache.freescale.com/files/sensors/doc/app_note/AN4072.pdf
    MMA8452: {
      ADDRESSES: {
        value: [0x1D]
      },
      REGISTER: {
        value: {
          // Page 18
          // 6. Register Descriptions
          STATUS: 0x00,
          OUT_X_MSB: 0x01,
          XYZ_DATA_CFG: 0x0E,
          PULSE_CFG: 0x21,
          PULSE_SRC: 0x22,
          PULSE_THSX: 0x23,
          PULSE_THSY: 0x24,
          PULSE_THSZ: 0x25,
          PULSE_TMLT: 0x26,
          PULSE_LTCY: 0x27,
          PULSE_WIND: 0x28,
          CTRL_REG1: 0x2A,
          CTRL_REG4: 0x2E,
          CTRL_REG5: 0x2F,
        }
      },
      initialize: {
        value: function(opts, dataHandler) {
          var state = priv.get(this);
          var address = opts.address || this.ADDRESSES[0];
  
          opts.address = address;
  
          // TODO: make user definable.
          // 0b000 800Hz
          // 0b001 400Hz
          // 0b010 200Hz
          // 0b011 100Hz
          // 0b100 50Hz
          // 0b101 12Hz
          // 0b110 6Hz
  
          var rates = [ 800,  400,  200,  100,  50,  12,  6, ];
          var odr = rates.indexOf(opts.odr || 800);
          var scale = opts.range || 2;
          var fsr = ({ 2: 0, 4: 1, 8: 2 })[scale];
  
          opts.taps = opts.taps || {
            x: false,
            y: false,
            z: true,
          };
  
          var taps = {
            x: opts.taps.x ? 0x08 : 0x80,
            y: opts.taps.y ? 0x08 : 0x80,
            z: opts.taps.z ? 0x08 : 0x80,
          };
  
          state.scale = scale;
  
          var computed = {
            x: null,
            y: null,
            z: null,
          };
  
          this.io.i2cConfig(
            Object.assign(opts, {
              settings: {
                stopTX: false
              }
            })
          );
  
          if (odr === -1) {
            throw new RangeError("Invalid odr. Expected one of: 800,  400,  200,  100,  50,  12,  6");
          }
  
          /*
            Initial CTRL_REG1 State
  
            11000010 = 194 = 0xC2 -> ?
            00000010 = 8 = 0x08
            ^--------- ASLP_RATE1
             ^-------- ASLP_RATE0
              ^------- DR2
               ^------ DR1
                ^----- DR0
                 ^---- Noise
                  ^--- Fast Read
                   ^-- Standby Mode
          */
  
          var config = 0x08;
  
          /*
            Page 5 (AN4076)
            4.0 Setting the Data Rate
  
            Set ODR
  
            Shift the odr bits into place.
  
            Default: 800Hz
  
            11000010 = 194 = 0xC2 -> ?
            00000010 = 8 = 0x08
              ^^^----- DR2, DR1, DR0: 000
          */
          config |= odr << 3;
  
          /*
            Enter Standby Mode
  
            11000010 = 194 = 0xC2 -> ?
                  ^--- Fast Read
                   ^-- Standby Mode
  
            00000010 = 8 = 0x08
                  ^--- Fast Read
                   ^-- Standby Mode
  
          */
  
          this.io.i2cWriteReg(address, this.REGISTER.CTRL_REG1, config);
  
          /*
            Set FSR
  
            Default: ±2g
  
            00000000 = 0 = 0x00 ()
                  ^^----- FS1, FS2
          */
          this.io.i2cWriteReg(address, this.REGISTER.XYZ_DATA_CFG, fsr);
  
          var temp = 0;
  
          /*
            Page 10 (AN4072)
            4.2 Registers 0x23 - 0x25 PULSE_THSX, Y, Z
                Pulse Threshold for X, Y and Z Registers
  
            0x80 = B7 is HIGH
            10000000
            If B7 is HIGH, do not enable
          */
          if (!(taps.x & 0x80)) {
            // 00000011
            temp |= 0x03;
            this.io.i2cWriteReg(address, this.REGISTER.PULSE_THSX, taps.x);
          }
  
          if (!(taps.y & 0x80)) {
            // 00001100
            temp |= 0x0C;
            this.io.i2cWriteReg(address, this.REGISTER.PULSE_THSY, taps.y);
          }
  
          if (!(taps.z & 0x80)) {
            // 00110000
            temp |= 0x30;
            this.io.i2cWriteReg(address, this.REGISTER.PULSE_THSZ, taps.z);
          }
  
          /*
            Page 11, 12, 13 (AN4072)
  
            Configure Tap Axis'
  
            Table 1. Register 0x21 PULSE_CFG Register (Read/Write) and Description
  
            | Tap Enable |  7  |  6  |  5  |  4  |  3  |  2  |  1  |  0  |
            | ---------- | --- | --- | --- | --- | --- | --- | --- | --- |
            |            | DPA | ELE | ZD  | ZS  | YD  | YS  | XD  | XS  |
            | Single     |  0  |  1  |  0  |  1  |  0  |  1  |  0  |  1  |
            | Double     |  0  |  1  |  1  |  0  |  1  |  0  |  1  |  0  |
            | Both       |  0  |  1  |  1  |  1  |  1  |  1  |  1  |  1  |
  
  
            In the default case, `temp | 0x40` will be:
  
            01110000 = 112 = 0x70
  
            Latch On
            ZD On
            ZS On
  
          */
  
          this.io.i2cWriteReg(address, this.REGISTER.PULSE_CFG, temp | 0x40);
  
          /*
            Set TIME LIMIT for tap detection
  
            60ms / 800Hz = 60ms / 1.25ms = 48 (counts) = 0x30
            80ms / 800Hz = 80ms / 1.25ms = 64 (counts) = 0x40
          */
          this.io.i2cWriteReg(address, this.REGISTER.PULSE_TMLT, 60 / (1000 / rates[odr]));
          /*
            Set the PULSE LATENCY.
  
            This is the time that must pass after the first
            tap, but within the PULSE WINDOW for a double tap to register.
  
            200ms / 800Hz = 200ms / 1.25ms = 160 (counts) = 0xA0
          */
          this.io.i2cWriteReg(address, this.REGISTER.PULSE_LTCY, 200 / (1000 / rates[odr]));
  
          /*
            Set the PULSE WINDOW.
  
            This is the maximum interval of time to elapse after the latency
            interval, for which the second pulse must occur for double taps.
  
            The maximum allowed time:
  
            800Hz * 255 = 1.25ms * 255 = 318ms
          */
          this.io.i2cWriteReg(address, this.REGISTER.PULSE_WIND, 0xFF);
  
          /*
            Leave Standby Mode
  
            11000011 = 195 = 0xC3
            00000011 = 3 = 0x03
                  ^--- Fast Read
                   ^-- Standby Mode
          */
  
          config |= 0x01;
  
          this.io.i2cWriteReg(address, this.REGISTER.CTRL_REG1, config);
  
  
          this.io.i2cRead(address, this.REGISTER.STATUS, 7, function(data) {
            var status = (data.shift() & 0x08) >>> 3;
  
            /* istanbul ignore else */
            if (status) {
              // Page 9 (AN4076)
              //
              // 7.0 14-bit, 12-bit or 10-bit Data Streaming and Data Conversions
              computed.x = int16(data[0], data[1]) >> 4;
              computed.y = int16(data[2], data[3]) >> 4;
              computed.z = int16(data[4], data[5]) >> 4;
  
              dataHandler(computed);
            }
          }.bind(this));
  
          this.io.i2cRead(address, this.REGISTER.PULSE_SRC, 1, function(data) {
            var status = data[0];
            var tap = status & 0x7F;
  
            /* istanbul ignore else */
            if (status & 0x80) {
              this.emit("tap");
  
              // Single Tap
              /* istanbul ignore else */
              if ((tap >> 2) & 0x01) {
                this.emit("tap:single");
  
                // Double Tap (must be both S and D bits)
                /* istanbul ignore else */
                if ((tap >> 3) & 0x01) {
                  this.emit("tap:double");
                }
              }
            }
          }.bind(this));
        },
      },
      toGravity: {
        value: function(raw) {
          var state = priv.get(this);
          // return raw / ((1 << 11) * state.scale);
          return raw / ((1 << 11) * state.scale);
        }
      }
    },
    MMA7660: {
      ADDRESSES: {
        value: [0x4C]
      },
      REGISTER: {
        value: {
          READREGISTER: 0x00,
          RATE: 0x08,
          MODE: 0x07,
        }
      },
      initialize: {
        value: function(opts, dataHandler) {
          var READLENGTH = 3;
          var address = opts.address || this.ADDRESSES[0];
          var state = priv.get(this);
  
          state.sensitivity = 21.33;
  
          opts.address = address;
  
          this.io.i2cConfig(opts);
  
          // http://www.freescale.com.cn/files/sensors/doc/data_sheet/MMA7660FC.pdf?fpsp=1
          //
          // Standby mode
          this.io.i2cWrite(address, this.REGISTER.MODE, 0x00);
  
          // Sample Rate ()
          this.io.i2cWrite(address, this.REGISTER.RATE, 0x07);
  
          // Active Mode
          this.io.i2cWrite(address, this.REGISTER.MODE, 0x01);
  
          this.io.i2cRead(address, this.REGISTER.READREGISTER, READLENGTH, function(data) {
            dataHandler({
              // Shift off the sign bits
              // This solution is used in
              // https://github.com/intel-iot-devkit/upm/blob/master/src/mma7660/mma7660.cxx
              x: (data[0] << 2) / 4,
              y: (data[1] << 2) / 4,
              z: (data[2] << 2) / 4,
            });
          });
        },
      },
      toGravity: {
        value: function(raw) {
          var state = priv.get(this);
          return raw / state.sensitivity;
        }
      }
    },
  
    ESPLORA: {
      DEFAULTS: {
        value: {
          zeroV: [320, 330, 310],
          sensitivity: 170
        }
      },
      initialize: {
        value: function(opts, dataHandler) {
          this.pins = [5, 11, 6];
          analogInitialize.call(this, opts, dataHandler);
        }
      },
      toGravity: {
        value: analogToGravity
      }
    },
  
    // https://cdn-shop.adafruit.com/datasheets/LIS3DHappnote.pdf
    LIS3DH: {
      ADDRESSES: {
        value: [0x18]
      },
      REGISTER: {
        value: {
          OUT_X_L: 0x28,
          CTRL_REG1: 0x20,
          CTRL_REG2: 0x21,
          CTRL_REG3: 0x22,
          CTRL_REG4: 0x23,
          CTRL_REG5: 0x24,
  
          TEMP_CFG_REG: 0x1F,
  
          CLICK_CFG: 0x38,
          CLICK_SRC: 0x39,
          CLICK_THS: 0x3A,
          TIME_LIMIT: 0x3B,
          TIME_LATENCY: 0x3C,
          TIME_WINDOW: 0x3D,
        }
      },
      initialize: {
        value: function(opts, dataHandler) {
          var state = priv.get(this);
          var address = opts.address || 0x18;
  
          // 2G  = 0b00
          // 4G  = 0b01
          // 8G  = 0b10
          // 16G = 0b11
          var range = ({
            2: 0,
            4: 1,
            8: 2,
            16: 3
          })[opts.range || 4];
  
          /* istanbul ignore if */
          if (range === undefined) {
            range = 1;
          }
  
          var divider = [
            16380,
            8190,
            4096,
            1365,
          ][range];
  
          /* istanbul ignore if */
          if (divider === undefined) {
            divider = 1;
          }
  
          var threshold = [
            80,
            40,
            20,
            10,
          ][range];
  
          /* istanbul ignore if */
          if (threshold === undefined) {
            threshold = 10;
          }
  
  
          state.divider = divider;
          state.expander = Expander.get({
            address: address,
            controller: this.controller,
            bus: this.bus,
          });
  
          // TODO: this should come from the expander
          var ctrl4 = 0x88 | (range << 4);
  
          state.expander.i2cWrite(address, this.REGISTER.CTRL_REG4, ctrl4);
  
          // Acceleration
          state.expander.i2cReadOnce(address, this.REGISTER.CTRL_REG1, 1, function(data) {
            var ctrl1 = data[0];
  
            // Set to 200Hz
            ctrl1 &= ~0xF0;
            ctrl1 |= 6 << 4;
  
            state.expander.i2cWrite(address, this.REGISTER.CTRL_REG1, ctrl1);
  
            // Page 21
            // 6.1.1 I2C operation
            // Autoincrement bit set on register (0x80)
            state.expander.i2cRead(address, this.REGISTER.OUT_X_L | 0x80, 6, function(data) {
              dataHandler({
                x: Fn.int16(data[1], data[0]),
                y: Fn.int16(data[3], data[2]),
                z: Fn.int16(data[5], data[4]),
              });
            });
  
  
            // Tap
            // TODO: make this optional (use "newListener"?)
            //
            // See MMA8452 driver for strategy
            //
            // state.expander.i2cReadOnce(address, this.REGISTER.CTRL_REG3, 1, function(data) {
            //   var ctrl3 = data[0];
  
            //   // Shut off Int 1 Click
            //   ctrl3 &= ~0x80;
            //   ctrl3 |= 6 << 4;
  
            //   state.expander.i2cWrite(address, this.REGISTER.CTRL_REG1, ctrl3);
  
            //   // Page 21
            //   // 6.1.1 I2C operation
            //   // Autoincrement bit set on register (0x80)
            //   state.expander.i2cRead(address, this.REGISTER.OUT_X_L | 0x80, 6, function(data) {
            //     dataHandler({
            //       x: Fn.int16(data[1], data[0]),
            //       y: Fn.int16(data[3], data[2]),
            //       z: Fn.int16(data[5], data[4]),
            //     });
            //   });
            // }.bind(this));
  
  
  
            // Page 35
            // 8.3.7 CTRL_REG3 [Interrupt CTRL register] (22h)
            state.expander.i2cWrite(address, this.REGISTER.CTRL_REG3, 0x80);
  
            // Page 40
            // 9.2.1 Control register 5 (0x24)
            state.expander.i2cWrite(address, this.REGISTER.CTRL_REG5, 0x08);
  
            // Page 32
            // 8.3.1 TAP_CFG
            //
            // This register is called both CLICK_CFG and TAP_CFG
            //
            // 0b00101010  = 0x2A = 42
            state.expander.i2cWrite(address, this.REGISTER.CLICK_CFG, 0x2A);
  
            // Page 36
            // 8.4.1 Playing with TAP_TimeLimit
            //
            // ...Offers some guidance. Ultimately I opted to take inspiration
            // from Adafruit's driver and example:
            //
            // Times in ms
            // var timelimit = 10;
            // var timelatency = 20;
            // var timewindow = 255;
            var timelimit = 10;
            var timelatency = 20;
            var timewindow = 255;
  
            state.expander.i2cWrite(address, this.REGISTER.CLICK_THS, threshold);
            state.expander.i2cWrite(address, this.REGISTER.TIME_LIMIT, timelimit);
            state.expander.i2cWrite(address, this.REGISTER.TIME_LATENCY, timelatency);
            state.expander.i2cWrite(address, this.REGISTER.TIME_WINDOW, timewindow);
  
            // Page 33
            // 8.3.2 TAP_SRC (39h)
            //
            // This register is called both CLICK_SRC and TAP_SRC
            //
  
            var lastEmitTime = null;
  
            state.expander.i2cRead(address, this.REGISTER.CLICK_SRC, 1, function(data) {
              var status = data[0];
              var thisEmitTime = Date.now();
              // var tap = status & 0x7F;
  
              if (lastEmitTime === null) {
                lastEmitTime = thisEmitTime - 101;
              }
  
              /* istanbul ignore if */
              if (thisEmitTime < (lastEmitTime + 100)) {
                return;
              }
  
              if (status === 0x00) {
                return;
              }
  
              /* istanbul ignore if */
              if (!(status & 0x30)) {
                return;
              }
  
              lastEmitTime = thisEmitTime;
  
              this.emit("tap");
  
              if (status & 0x10) {
                this.emit("tap:single");
              }
  
              if (status & 0x20) {
                // TODO: Figure out if we can determine a
                // combined single + double tap
                this.emit("tap:single");
                this.emit("tap:double");
              }
            }.bind(this));
          }.bind(this));
        },
      },
      toGravity: {
        value: function(raw) {
          var state = priv.get(this);
          return raw / state.divider;
        },
      },
    },
  };
  
  // Otherwise known as...
  Controllers.TINKERKIT = Controllers.ANALOG;
  Controllers.MMA8452Q = Controllers.MMA8452;
  
  function magnitude(x, y, z) {
    var a;
  
    a = x * x;
    a = fma(y, y, a);
    a = fma(z, z, a);
  
    return Math.sqrt(a);
  }
  
  /**
   * Accelerometer
   * @constructor
   *
   * five.Accelerometer([ x, y[, z] ]);
   *
   * five.Accelerometer({
   *   pins: [ x, y[, z] ]
   *   zeroV: ...
   *   sensitivity: ...
   * });
   *
   *
   * @param {Object} opts [description]
   *
   */
  
  function Accelerometer(opts) {
    if (!(this instanceof Accelerometer)) {
      return new Accelerometer(opts);
    }
  
    var controller = null;
  
    var state = {
      enabled: true,
      x: {
        value: 0,
        previous: 0,
        stash: [],
        orientation: null,
        inclination: null,
        acceleration: null,
        calibration: []
      },
      y: {
        value: 0,
        previous: 0,
        stash: [],
        orientation: null,
        inclination: null,
        acceleration: null,
        calibration: []
      },
      z: {
        value: 0,
        previous: 0,
        stash: [],
        orientation: null,
        inclination: null,
        acceleration: null,
        calibration: []
      }
    };
  
    Board.Component.call(
      this, opts = Board.Options(opts)
    );
  
    if (opts.controller && typeof opts.controller === "string") {
      controller = Controllers[opts.controller.toUpperCase()];
    } else {
      controller = opts.controller;
    }
  
    if (controller == null) {
      controller = Controllers.ANALOG;
    }
  
    Board.Controller.call(this, controller, opts);
  
    if (!this.toGravity) {
      this.toGravity = opts.toGravity || function(raw) {
        return raw;
      };
    }
  
    if (!this.enabledChanged) {
      this.enabledChanged = function() {};
    }
  
    priv.set(this, state);
  
    /* istanbul ignore else */
    if (typeof this.initialize === "function") {
      this.initialize(opts, function(data) {
        var isChange = false;
  
        if (!state.enabled) {
          return;
        }
  
        Object.keys(data).forEach(function(axis) {
          var value = data[axis];
          var sensor = state[axis];
  
          if (opts.autoCalibrate && sensor.calibration.length < calibrationSize) {
            var axisIndex = axes.indexOf(axis);
            sensor.calibration.push(value);
  
            if (!Array.isArray(state.zeroV)) {
              state.zeroV = [];
            }
  
            state.zeroV[axisIndex] = sum(sensor.calibration) / sensor.calibration.length;
            if (axis === aZ) {
              state.zeroV[axisIndex] -= state.sensitivity;
            }
          }
  
          // The first run needs to prime the "stash"
          // of data values.
          if (sensor.stash.length === 0) {
            for (var i = 0; i < 5; i++) {
              sensor.stash[i] = value;
            }
          }
  
          sensor.previous = sensor.value;
          sensor.stash.shift();
          sensor.stash.push(value);
  
          sensor.value = (sum(sensor.stash) / 5) | 0;
  
          if (this.acceleration !== sensor.acceleration) {
            sensor.acceleration = this.acceleration;
            isChange = true;
            this.emit("acceleration", sensor.acceleration);
          }
  
          if (this.orientation !== sensor.orientation) {
            sensor.orientation = this.orientation;
            isChange = true;
            this.emit("orientation", sensor.orientation);
          }
  
          if (this.inclination !== sensor.inclination) {
            sensor.inclination = this.inclination;
            isChange = true;
            this.emit("inclination", sensor.inclination);
          }
        }, this);
  
        this.emit("data", {
          x: state.x.value,
          y: state.y.value,
          z: state.z.value
        });
  
        if (isChange) {
          this.emit("change", {
            x: this.x,
            y: this.y,
            z: this.z
          });
        }
      }.bind(this));
    }
  
    Object.defineProperties(this, {
      hasAxis: {
        writable: IS_TEST_MODE ? true : /* istanbul ignore next */ false,
        value: function(axis) {
          /* istanbul ignore next */
          return state[axis] ? state[axis].stash.length > 0 : false;
        }
      },
      enable: {
        value: function() {
          state.enabled = true;
          this.enabledChanged(true);
          return this;
        }
      },
      disable: {
        value: function() {
          state.enabled = false;
          this.enabledChanged(false);
          return this;
        }
      },
      zeroV: {
        get: function() {
          return state.zeroV;
        }
      },
      /**
       * [read-only] Calculated pitch value
       * @property pitch
       * @type Number
       */
      pitch: {
        get: function() {
          var x = this.x;
          var y = this.y;
          var z = this.z;
          var rads = this.hasAxis(aZ) ?
            Math.atan2(x, Math.hypot(y, z)) :
            Math.asin(constrain(x, -1, 1));
  
          return toFixed(rads * Fn.RAD_TO_DEG, 2);
        }
      },
      /**
       * [read-only] Calculated roll value
       * @property roll
       * @type Number
       */
      roll: {
        get: function() {
          var x = this.x;
          var y = this.y;
          var z = this.z;
          var rads = this.hasAxis(aZ) ?
            Math.atan2(y, Math.hypot(x, z)) :
            Math.asin(constrain(y, -1, 1));
  
          return toFixed(rads * Fn.RAD_TO_DEG, 2);
        }
      },
      x: {
        get: function() {
          return toFixed(this.toGravity(state.x.value, aX), 2);
        }
      },
      y: {
        get: function() {
          return toFixed(this.toGravity(state.y.value, aY), 2);
        }
      },
      z: {
        get: function() {
          return this.hasAxis(aZ) ?
            toFixed(this.toGravity(state.z.value, aZ), 2) : 0;
        }
      },
      acceleration: {
        get: function() {
          return magnitude(
            this.x,
            this.y,
            this.z
          );
        }
      },
      inclination: {
        get: function() {
          return Math.atan2(this.y, this.x) * Fn.RAD_TO_DEG;
        }
      },
      orientation: {
        get: function() {
          var abs = Math.abs;
          var x = this.x;
          var y = this.y;
          var z = this.hasAxis(aZ) ? this.z : 1;
          var absX = abs(x);
          var absY = abs(y);
          var absZ = abs(z);
  
          if (absX < absY && absX < absZ) {
            if (x > 0) {
              return 1;
            }
            return -1;
          }
          if (absY < absX && absY < absZ) {
            if (y > 0) {
              return 2;
            }
            return -2;
          }
          if (absZ < absX && absZ < absY) {
            // TODO: figure out how to test this
            /* istanbul ignore else */
            if (z > 0) {
              return 3;
            }
            /* istanbul ignore next */
            return -3;
          }
          return 0;
        }
      }
    });
  }
  
  util.inherits(Accelerometer, Emitter);
  
  /* istanbul ignore else */
  if (IS_TEST_MODE) {
    Accelerometer.purge = function() {
      priv.clear();
    };
  }
  
  
  module.exports = Accelerometer;
  