<commit_before>/*
 * == BSD2 LICENSE ==
 * Copyright (c) 2015, Tidepool Project
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the associated License, which is identical to the BSD 2-Clause
 * License as published by the Open Source Initiative at opensource.org.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the License for more details.
 *
 * You should have received a copy of the License along with this program; if
 * not, you can obtain one from Tidepool Project at tidepool.org.
 * == BSD2 LICENSE ==
 */

var _ = require('lodash');
var async = require('async');
var sundial = require('sundial');
var annotate = require('../../eventAnnotations');
var util = require('util');
var debugMode = require('../../../app/utils/debugMode');

var crcCalculator = require('../../crc.js');
var struct = require('../../struct.js')();
var common = require('../../commonFunctions');

var logic = require('./animasLogic');
var animasSimulator = require('./animasSimulator');

var TZOUtil = require('../../TimezoneOffsetUtil');

var debug = require('bows')('AnimasDriver');
var verbose = debugMode.isDebug;

module.exports = function (config) {
  var cfg = _.clone(config);
  var serialDevice = config.deviceComms;
  var animasDeviceId = null;
  var simulator = null;

  var BOM_BYTE = 0xC0;
  var EOM_BYTE = 0xC1;
  var ADDRESS_CONNECT = 0xFF; //used to establish connection

  var RETRIES = 10;
  var TIME_TO_WAIT = 500;
  var TIME_TO_TIMEOUT = 2000;
  var LISTEN_INTERVAL = 30; //don't set this too low to be kind to the pump and the cpu

  var CMDS = {
    CONNECT: { value: 0x93, name: 'CONNECT'},
    DISCONNECT: { value: 0x53, name: 'DISCONNECT'},
    HANDSHAKE: {value: 0xBF, name: 'HANDSHAKE'},
    UA: {value: 0x73, name: 'Unnumbered Acknowledge'},
    RI: {value: 0x5249, name: 'RI message (Read)'},
    DI: {value: [0x44,0x49], name: 'DI message (RI response)'},
    ACK: {value: 0x11, name: 'Acknowledge'}
  };

  var RECORD_TYPES = {
    SERIALNUMBER : {value: 8, name: 'SERIAL AND MODEL NUMBER'},
    BASAL_PROGRAM: {value: 11, name: 'BASAL PROGRAMS'},
    ACTIVE_BASAL_PROGRAM: {value: 12, name: 'ACTIVE BASAL PROGRAM'},
    BASAL_PROGRAM_NAMES: {value: 18, name: 'BASAL PROGRAM NAMES'},
    ADVANCED_SETTINGS: {value: 39, name: 'ADVANCED SETTINGS'},
    BG_DISPLAY_MODE: {value: 29, name: 'BG DISPLAY MODE'},
    BOLUS: {value: 21, name: 'BOLUS HISTORY', numRecords: 500, recordSize: 16},
    ALARM: {value: 23, name: 'ALARM HISTORY', numRecords: 30, recordSize: 16},
    PRIME_REWIND: {value: 24, name: 'PRIME-REWIND HISTORY', numRecords: 60, recordSize: 16},
    SUSPEND_RESUME: {value: 25, name: 'SUSPEND-RESUME HISTORY', numRecords: 30, recordSize: 16},
    BASAL: {value: 26, name: 'BASAL HISTORY', numRecords: 270, recordSize: 16},
    WIZARD: {value: 38, name: 'WIZARD HISTORY', numRecords: 500, recordSize: 16},
    BLOOD_GLUCOSE: {value: 40, name: 'BLOOD GLUCOSE HISTORY (PING)', numRecords: 1000, recordSize: 16},
    CGM_CALIBRATION: {value: 40, name: 'CGM CALIBRATION (VIBE)', numRecords: 1022, recordSize: 8},
    CGM_GLUCOSE: {value: 45, name: 'CGM GLUCOSE HISTORY', numRecords: 15872, recordSize: 8},
    CGM_TIME_ADJUST: {value: 48, name: 'CGM TIME ADJUST HISTORY'}
  };

  var PING = 'IR1285';
  var VIBE = 'IR1295';

  var MODELS = {
    15 : PING,
    16: VIBE
  };

  var BASAL_TYPES = {
    0 : 'scheduled',
    1 : 'temp'
  };

  var PRIMING_FLAGS = {
    0 : 'blank',
    1 : 'not primed',
    2 : 'tubing',
    3 : 'cannula'
  };

  var BOLUS_TYPE_NAME = {
    NORMAL: {value: 1, name: 'normal'},
    AUDIO: {value: 2, name: 'audio'},
    COMBO: {value: 3, name: 'combo'}
  };

  var BOLUS_TYPE_STATUS = {
    COMPLETED: {value: 3, name: 'completed'},
    CANCELLED: {value: 2, name: 'cancelled'}
  };

  var ALARM_TYPES = {
    ALARM_OCCLUSION1: { value: 145, name: 'Occlusion detected'},
    ALARM_OCCLUSION2: { value: 146, name: 'Occlusion detected'},
    ALARM_OCCLUSION3: { value: 147, name: 'Occlusion detected'},
    ALARM_OCCLUSION4: { value: 148, name: 'Occlusion detected'},
    ALARM_AUTO_OFF: { value: 150, name: 'Auto Off'},
    ALARM_REPLACE_BATTERY: { value: 128, name: 'Replace battery'},
    ALARM_EMPTY_CARTRIDGE: { value: 144, name: 'Empty Cartridge'},
    ALARM_LOW_CARTRIDGE: { value: 178, name: 'Low Cartridge'},
    ALARM_LOW_BATTERY: { value: 177, name: 'Low Battery'}
  };

  var ERRORS = {
    LENGTH_ERROR: { value: 0, name: 'Length error'},
    ITEM_ERROR: { value: 1, name: 'Item error'},
    READ_ERROR: { value: 3, name: 'Read error'},
    COMMAND_ERROR: { value: 4, name: 'Command error'},
    RECORD_NUMBER_ERROR: { value: 5, name: 'Record number error'},
    PUMP_NOT_SUSPENDED: { value: 6, name: 'Pump not suspended'}
  };

  var CBG_TYPE_FLAGS = [
    {value: 0x04, name: 'accumulated_adjust_time'},
    {value: 0x08, name: 'bg_calibration'},
    {value: 0x10, name: 'time_changed'},
    {value: 0x20, name: 'time_lost'},
    {value: 0x40, name: 'stop_session'},
    {value: 0x80, name: 'start_session'}
  ];

  var getFlagNames = function (list, v) {
    var flags = [];
    for (var i in list) {
      if (list[i].value & v) {
        flags.push(list[i].name);
      }
    }
    return flags;
  };

  var getCmdName = function (idx) {
    return common.getName(CMDS, idx);
  };

  var getAlarmName = function (idx,types) {
    return common.getName(types, idx);
  };

  var counters = {
    sent : 0,
    received : 0
  };

  var primaryAddress = null;
  var connectionAddress = null; // 11000000b primary devices sets bit 0 (LSB), bit 1-7 is connection address

  var modelNumber = null;
  var hasConfigInfo = false;

  var prevPayload = {};

  // builds a command in an ArrayBuffer
  // Byte 1: always 0xC0 (BOM - Beginning of Message),
  // Byte 2: address field; bit 0 is command/response bit (primary device sets this bit, secondary clears it)
  // Byte 3: control field; if bit 4 is clear sender retains transmit rights, if set receiver has transmit rights
  // Payload: 0-128 bytes
  // CRC bytes: 2 check bytes over bytes 2-3 and payload
  // EOM byte: always 0xC1 (signals end of message)

  var buildPacket = function (address, command, payload) {

    var escapedPayload = escapeCharacters(payload);
    var payloadLength = escapedPayload.length;

    // checksum only over address field, control field and payload
    var acp = [];
    acp = acp.concat(address,command,Array.prototype.slice.call(payload));
    var crc = escapeCharacters(crcCalculator.calcCheckBytes(acp));

    var datalen = payloadLength + crc.length + 4; // BOM + address field + control field + EOM = 4
    var buf = new ArrayBuffer(datalen);
    var bytes = new Uint8Array(buf);
    var ctr = struct.pack(bytes, 0, 'bbb', BOM_BYTE, address,command);

    ctr += struct.copyBytes(bytes, ctr, escapedPayload, payloadLength);
    ctr += struct.copyBytes(bytes, ctr, crc, crc.length);
    struct.pack(bytes, ctr, 'b', EOM_BYTE);

    if (verbose) {
      debug('bytes sent:', common.bytes2hex(bytes));
    }
    return buf;
  };

  var escapeCharacters = function(buf) {
    var escaped = [];
    for(var i = 0; i < buf.length; i++) {
      var byte = buf[i];
      if(byte === BOM_BYTE || byte === EOM_BYTE || byte === 0x7D) {
        if (verbose) {
          debug('Replacing special character');
        }
        escaped.push(0x7D);
        escaped.push(byte ^ 0x20);
      }
      else{
        escaped.push(byte);
      }
    }
    return escaped;
  };

  var setupConnection = function(destinationAddress) {
    var payload = [];
    var payloadlength = struct.copyBytes(payload, 0, primaryAddress, 4);
    payloadlength += struct.pack(payload,4,'ib',destinationAddress,connectionAddress);
    return {
      packet: buildPacket(
        ADDRESS_CONNECT, CMDS.CONNECT.value, payload
      ),
      parser: function (packet) {
        var data = {connected : (packet.command == CMDS.UA.value)};
        return data;
      }
    };
  };

  var handshake = function(iter) {
    return {
      packet: buildPacket(
        ADDRESS_CONNECT, CMDS.HANDSHAKE.value, primaryAddress.concat([0xFF,0xFF,0xFF,0xFF,0x02,iter])
      ),
      parser: function (packet) {
        var data = {
          destinationAddress : struct.extractInt(packet.payload, 0),
          serialNumber : struct.extractInt(packet.payload,10),
          description : struct.extractString(packet.payload,14,packet.length-1)
        };
        return data;
      }
    };
  };

  var getCounters = function() {
    // bit 5-7 : 3-bit receive counter
    // bit 4 : 1
    // bit 1-3 : 3-bit send counter
    // bit 0:  0
    var counter = (counters.received << 5) | 0x10;
    counter = (counters.sent << 1) | counter;
    return counter;
  };

  var incrementSentCounter = function() {
    counters.sent +=1;
    if(counters.sent == 7) {// counter is only 3 bits
      counters.sent = 0;
    }
  };

  var incrementReceivedCounter = function() {
    counters.received += 1;
    if(counters.received == 7) { // counter is only 3 bits
      counters.received = 0;
    }
  };

  var decrementReceivedCounter = function() {
    if(counters.received === 0) { // counter is only 3 bits
      counters.received = 7;
    }
    else {
      counters.received -= 1;
    }
  };

  var readDataPages = function (rectype, offset, numRecords) {

    var payload = new Uint8Array(8);
    var payloadlength = struct.pack(payload,0,'Ssss',CMDS.RI.value,rectype,offset,numRecords);

    return {
      packet: buildPacket(
        connectionAddress,
        getCounters(),
        payload
      ),
      parser: function (packet) {
        return packet;
      }
    };
  };

  var requestPrimeRewind = function() {
    return{
      recordType: RECORD_TYPES.PRIME_REWIND,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          deliveredAmount: (struct.extractShort(payload,8)/100.0).toFixedNumber(2), // U x 100
          primeFlags: PRIMING_FLAGS[struct.extractByte(payload,10)]
        };
      }
    };
  };

  var requestSuspendResume = function() {
    return{
      recordType: RECORD_TYPES.SUSPEND_RESUME,
      parser : function (payload) {
        var suspenddt = decodeDate(payload,4);
        var resumedt = decodeDate(payload,8);
        return {
          index : struct.extractShort(payload,2),
          suspendJsDate : suspenddt,
          suspendDeviceTime: sundial.formatDeviceTime(suspenddt),
          resumeJsDate : resumedt,
          resumeDeviceTime: sundial.formatDeviceTime(resumedt)
        };
      }
    };
  };

  var requestBolus = function() {
    return{
      recordType: RECORD_TYPES.BOLUS,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          deliveredAmount: (struct.extractInt(payload,8)/10000.0).toFixedNumber(2), // U x 10,000
          requiredAmount: struct.extractShort(payload,12)/1000.0, // U x 1,000
          duration: struct.extractShort(payload,14)*0.1*60*60*1000, // N x 0.1Hr
          bolusType: getBolusType(struct.extractByte(payload,16)),
          sync_counter: struct.extractByte(payload,17)
        };
      }
    };
  };

  var requestBasal = function() {
    return{
      recordType: RECORD_TYPES.BASAL,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          rate: struct.extractShort(payload,8)/1000.0, // U x 1,000
          basalType: BASAL_TYPES[parseInt(struct.extractByte(payload,10),10)]
        };
      }
    };
  };

  var requestWizard = function() {
    return{
      recordType: RECORD_TYPES.WIZARD,
      parser : function (payload) {
        var wizardConfig = getWizardConfig(struct.extractByte(payload,15));
        return {
          index : struct.extractShort(payload,2),
          sync_counter: struct.extractByte(payload,4),
          carb_ratio: struct.extractByte(payload,5),
          carb_amount: struct.extractShort(payload,6),
          isf: struct.extractShort(payload,8),
          bg: struct.extractShort(payload,10),
          target_bg: struct.extractShort(payload,12),
          bg_delta: struct.extractByte(payload,14), //shape
          configuration: wizardConfig,
          iob: struct.extractShort(payload,16) / 100.0
        };
      }
    };
  };

  var requestAlarm = function() {
    return{
      recordType: RECORD_TYPES.ALARM,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index: struct.extractShort(payload,2),
          jsDate: dt,
          deviceTime: sundial.formatDeviceTime(dt),
          eaw_code: struct.extractByte(payload,8), // eaw = errors, alarms and warnings
          engineering_code: struct.extractBytes(payload,9,9)
        };
      }
    };
  };

  var requestBG = function() {
    return{
      recordType: RECORD_TYPES.BLOOD_GLUCOSE,
      parser : function (payload) {
        var BASE_TIME = Date.UTC(2000, 0, 1, 0, 0, 0).valueOf();
        var bytes = struct.extractBytes(payload,8,3);
        var counter = (bytes[2] << 16) + (bytes[1] << 8) + bytes[0];
        var dt = new Date(BASE_TIME + counter * sundial.MIN_TO_MSEC);
        var pumpdt = decodeDate(payload,4);
        var bgBytes = struct.extractBytes(payload,11,3);
        // first 10 bits is glucose value, range 0-1023
        var bg = ((bgBytes[1] & 0x03) << 8) + bgBytes[0];
        var controlSolution = bgBytes[1] & 0x04 ? true : false;
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          pumpTime: sundial.formatDeviceTime(pumpdt),
          bg: bg,
          controlSolution: controlSolution
        };
      }
    };
  };

  var requestCalibration = function() {
    return{
      recordType: RECORD_TYPES.CGM_CALIBRATION,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          value: struct.extractShort(payload,8)
        };
      }
    };
  };

  var requestCBG = function() {
    return {
      recordType: RECORD_TYPES.CGM_GLUCOSE,
      parser : function (payload) {

        var bgBytes = struct.extractBytes(payload,4,2);
        // first 9 bits is glucose value, range 39-401
        var bg = ((bgBytes[1] & 0x01) << 8) + bgBytes[0];
        var flagNames = getFlagNames(CBG_TYPE_FLAGS, bgBytes[1]);

        var rawTimeStamp = null;
        if(flagNames.length === 0 || _.includes(flagNames,'bg_calibration')) {
          var BASE_TIME = Date.UTC(2008, 0, 1, 0, 0, 0).valueOf();
          var rawTimeStamp = BASE_TIME + struct.extractSignedInt(payload,6) * sundial.SEC_TO_MSEC;
          // rawTimeStamp is now "UTC" unix time without acculumated time adjust
        }
        else{
          rawTimeStamp = struct.extractSignedInt(payload,6);
          // rawTimeStamp is special time adjust record in seconds
        }

        return {
          index: struct.extractShort(payload,2),
          glucose: bg,
          cbg_types: flagNames,
          rawTimeStamp: rawTimeStamp
        };
      }
    };
  };

  // accepts a stream of bytes and tries to find a packet
  // at the beginning of it.
  // returns a packet object; if valid == true it's a valid packet
  // if packet_len is nonzero, that much should be deleted from the stream
  // if valid is false and packet_len is nonzero, the previous packet
  // should be NAKed.
  var extractPacket = function (bytes) {
    var packet = {
      bytes: bytes,
      valid: false,
      packet_len: 0,
      command: 0,
      payload: null,
      crc: 0
    };

    if (bytes[0] != BOM_BYTE) {
      return packet;
    }

    // wait until we've received the end of message
    if (bytes[bytes.length-1] !== EOM_BYTE) {
      return packet;  // we're not done yet
    }

    if (verbose) {
      debug('Raw packet: ', common.bytes2hex(bytes));
    }

    //escape characters
    var fromIndex = 0;
    var index = 0;
    while((index = _.indexOf(bytes,0x7D,fromIndex)) > 0) {
        var buf = new Uint8Array(bytes.length-1);
        var front = _.slice(bytes,0,index+1);
        var special = bytes[index+1] ^ 0x20;
        front[index] = special;
        if(special == 0x7D) {
          fromIndex = index+1; // previous escaped character was the escape character
        }
        buf.set(front,0);
        buf.set(_.slice(bytes,index+2),index+1);
        if (verbose) {
          debug('Escaped bytes:', common.bytes2hex(buf));
        }
        bytes = buf;
    }

    // calc the checksum
    packet.crc = struct.extractBytes(bytes, bytes.length - 3,2);
    var crc = crcCalculator.calcCheckBytes(_.slice(bytes,1,bytes.length-3));
    if ((crc[0] !== packet.crc[0]) || (crc[1] !== packet.crc[1])) {
      // if the crc is bad, we should discard the whole packet
      debug('Invalid CRC');
      cfg.deviceComms.flush();
      return packet;
    }

    // command is the third byte, packet is remainder of data
    packet.command = bytes[2];
    packet.packet_len = bytes.length;

    if((packet.packet_len == 6) && (packet.command & CMDS.ACK.value)) {
      // This is a Receive Ready/NOP/Ack packet
      packet.ack = true;
      var nextPacket = struct.extractByte(bytes, 2);
      counters.received = nextPacket >> 5;
      packet.valid = true;
    }
    else if ((bytes[3] == 0x45) && (bytes[4] == 0x00)) {
      // This is an error message from the pump
      packet.valid = true;
      packet.error = common.getName(ERRORS, bytes[5]);
      debug('Error message from pump:', packet.error);
      return packet;
    }
    else {
      packet.payload = new Uint8Array(packet.packet_len-6);
      for (var i = 0; i < bytes.length - 6; ++i) {
        packet.payload[i] = bytes[i + 3];
      }
      packet.valid = true;
    }

    return packet;
  };

  var animasPacketHandler = function (buffer) {
    // for efficiency reasons, we're not going to bother to ask the driver
    // to decode things that can't possibly be a packet
    // first, discard bytes that can't start a packet
    var discardCount = 0;
    while (buffer.len() > discardCount && buffer.get(0) != BOM_BYTE) {
      ++discardCount;
    }
    if (discardCount) {
      buffer.discard(discardCount);
    }

    if (buffer.len() < 6) { // all complete packets must be at least this long
      return false;       // not enough there yet
    }

    // there's enough there to try, anyway
    var packet = extractPacket(buffer.bytes());
    if (packet.packet_len !== 0) {
      // remove the now-processed packet
      buffer.discard(packet.packet_len);
    }

    if (packet.valid) {
      return packet;
    } else {
      return null;
    }
  };

  var listenForPacket = function (timeout, commandpacket, callback) {
    var timers = {
      abortTimer:null,
      listenTimer:null
    };

    if(timeout > 0) {
      timers.abortTimer = setTimeout(function () {
        clearInterval(timers.listenTimer);
        debug('TIMEOUT');
        callback(new Error('Timeout error'), null);
      }, timeout);
    }

    timers.listenTimer = setInterval(function () {
      if (serialDevice.hasAvailablePacket()) {
        var pkt = serialDevice.nextPacket();
        if (verbose) {
          debug('Received packet: ', pkt);
        }
        // we always call the callback if we get a packet back,
        // so just cancel the timers if we do
        clearTimeout(timers.abortTimer);
        clearInterval(timers.listenTimer);

        if (pkt.error) {
          return callback(new Error(pkt.error),null);
        }
        // only attempt to parse the payload if it worked
        if (pkt.payload) {
          pkt.parsed_payload = commandpacket.parser(pkt);
        }
        callback(null, pkt);
      }
    }, LISTEN_INTERVAL);

    return timers;
  };

  var sendAck = function (obj,cb) {
    var cmd = {
      packet: buildPacket(
        connectionAddress, CMDS.ACK.value | (counters.received << 5), []
      ),
      parser: function (packet) {
        var data = {
          nextPacket : struct.extractByte(packet.payload, 0)
        };
        return data;
      }
    };

    animasCommandResponse(cmd, true, function (err, result) {
      if (err) {
        cb(err, null);
      } else {
        cb(null, result);
      }
    });
  };

  var animasCommandResponse = function (commandpacket, shouldRetry, callback) {

    var retry = function (cb, results) {
      cfg.deviceComms.flush();
      var waitTimer = setTimeout(function () {
        serialDevice.writeSerial(commandpacket.packet, function () {
          // once we've sent the command, start listening for a response
          listenForPacket(TIME_TO_TIMEOUT, commandpacket, function(err, result) {
            if (err) {
              cb(err, null);
            } else {
              cb(null, result);
            }
          });
        });
      },TIME_TO_WAIT);
    };

    serialDevice.writeSerial(commandpacket.packet, function () {
      // once we've sent the command, start listening for a response
      listenForPacket(TIME_TO_TIMEOUT, commandpacket, function(err, result) {
        if(err) {
          if((err.message === 'Timeout error') && shouldRetry) {
            debug('Retrying after timeout..');
            async.retry(RETRIES, retry, function(err, result) {
              if (err) {
                callback(err, null);
              } else {
                callback(null,result);
              }
            });
          }
          else {
            callback(err, null);
          }
        }
        else{
          callback(null,result);
        }
      });
    });
  };

  var animasCommandResponseAck = function (cmd, cb) {
    // This is for Information packets, where one ACK is received after the command is sent,
    // and an ACK is sent in response. When the payload is received, another ACK is sent.

    var retry = function (cb, results) {
      if(!hasConfigInfo) {
        // Initial config request retries are handled a bit differently
        debug('Waiting and then sending NAK..');
        var waitTimer = setTimeout(function () {
          sendAck(true, function (err, ackResult){
            if(err) {
              return cb(err, null);
            }
            incrementReceivedCounter();
            sendAck(true, function (err, ack2Result){
              if(err) {
                return cb(err, null);
              }
              if(ackResult.payload && (ackResult.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
                if(!ack2Result.ack) {
                  debug('acknowledging retransmission');
                  incrementReceivedCounter();
                  sendAck(true, function (err, ack3Result){
                    return cb(null,ackResult);
                  });
                } else {
                  return cb(null,ackResult);
                }
              } else {
                return cb(new Error('We are having trouble connecting to the pump. Please try again.'), null);
              }
            });
          });
        },TIME_TO_WAIT);
      }
      else {
        debug('Sending ack and then command again..');
        incrementReceivedCounter();
        sendAck(true, function (err, result){
          if(err) {
            return cb(err, null);
          }
          if(result.ack) {
            incrementSentCounter();
            animasCommandResponse(cmd, true, function (err, result) {
              if (err) {
                cb(err, null);
              } else {
                if(result.payload && (result.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
                  sendAck(true, function (err, ackResult){
                    if(err) {
                      return cb(err, null);
                    }
                    return cb(null,result);
                  });
                }else{
                  cb(new Error('Pump not sending data. Please retry.'), null);
                }
              }
            });
          } else {
            if(result.payload && (result.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
              sendAck(true, function (err, ackResult){
                if(err) {
                  return cb(err, null);
                }
                return cb(null,result);
              });
            } else {
              return cb(new Error('Unknown packet'), null);
            }
          };
        });
      }
    };

    animasCommandResponse(cmd, true, function (err, result) {
      if (err) {
        cb(err, null);
      } else {
        if(result.ack) {
          incrementSentCounter();
          sendAck(true, function (err, result){
            if(err) {
              return cb(err, null);
            }
            incrementReceivedCounter();
            if(result.payload && (result.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
              sendAck(true, function (err, ackResult){
                if(err) {
                  return cb(err, null);
                }
                return cb(null,result);
              });
            }else {
              async.retry(RETRIES, retry, function(err, result) {
                if (err) {
                  cb(err, null);
                } else {
                  cb(null,result);
                }
              });
            }
          });
        }
        else {
          if(result.payload && (result.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
            sendAck(true, function (err, ackResult){
              if(err) {
                return cb(err, null);
              }
              return cb(null,result);
            });
          } else {
            //it's possible that we're receiving diagnostic echo messages,
            //so retry command
            async.retry(RETRIES, retry, function(err, result) {
              if (err) {
                cb(err, null);
              } else {
                cb(null,result);
              }
            });
          }
        }
      }
    });
  };

  var clearTimers = function(timers) {
    for(var i = 0; i < timers.length; i++) {
      clearTimeout(timers[i].abortTimer);
      clearInterval(timers[i].listenTimer);
    }
  };

  var discoverDevice = function(cb) {
    debug('discovering animas device');
    primaryAddress = [0x01,0x00,0x00,0x00];
    connectionAddress = 0x03; // use connection address with LSB as 1

    var i = 0;
    var found = false;
    var timers = [];

    var handshakeInterval = setInterval(function() {
      debug('Polling slot ',i);
      var cmd = handshake(i);
      i++;
      if(i === 16) {
        clearTimers(timers);
        clearInterval(handshakeInterval);
        debug('Did not find device.');
        return cb(new Error('Make sure your pump is not sleeping and aligned with the cable.'),null);
      }

      serialDevice.writeSerial(cmd.packet, function () {
        // once we've sent the command, start listening for a response
        timers.push(listenForPacket(0, cmd, function(err, result) {
          if(result && !found) {
            found = true;
            clearTimers(timers);
            clearInterval(handshakeInterval);
            cb(null, result);
          }
        }));
      });
    }, 200); // discovery timeout is 100ms, we wait a bit longer

  };

  var getConnection = function(obj, cb) {
    debug('connecting to animas');
    var i = 0;
    var connected = false;
    var timers = [];

    var connectInterval = setInterval(function() {
      debug('Attempt ',i);
      i++;
      if(i == 8) {
        clearTimers(timers);
        clearInterval(connectInterval);
        debug('Could not connect to device.');
        return cb(new Error('Make sure your pump is suspended.'),null);
      }
      if(obj.parsed_payload != null && !connected) {
        var cmd = setupConnection(obj.parsed_payload.destinationAddress);

        serialDevice.writeSerial(cmd.packet, function () {
          // once we've sent the command, start listening for a response
          timers.push(listenForPacket(0, cmd, function(err, result) {

            if(err) {
              debug(err);
            }
            else {
              clearTimers(timers);
              clearInterval(connectInterval);
              if(!connected) {
                connected = true;
                cb(null, result);
              }
            }
          }));
        });
      }
    }, TIME_TO_WAIT);
  };

  var resetConnection = function(obj, cb) {
    //cfg.deviceComms.setPaused(false);

    debug('reset connection to animas');
    counters.sent = 0;
    counters.received = 0;

    var cmd = {
      packet: buildPacket(
        connectionAddress, CMDS.CONNECT.value, []
      )
    };

    animasCommandResponse(cmd, false, function (err, result) {
      if(result) {
        result.ack = false; // this is not a regular ack
      }
      return cb(null, result);
    });
  };


  var readSerialandModelNumber = function(cb) {
    resetConnection(true, function(connectErr, obj) {
      if(connectErr) {
        return cb(connectErr, null);
      }else{
        debug('Reading serial and model numbers..');
        var cmd = readDataPages(RECORD_TYPES.SERIALNUMBER.value,0,1);
        animasCommandResponseAck(cmd, function (err, result) {
          if (err) {
            cb(err, null);<commit_msg>add empty parser to resetConnection as it may get a stray packet<commit_after>/*
 * == BSD2 LICENSE ==
 * Copyright (c) 2015, Tidepool Project
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the associated License, which is identical to the BSD 2-Clause
 * License as published by the Open Source Initiative at opensource.org.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the License for more details.
 *
 * You should have received a copy of the License along with this program; if
 * not, you can obtain one from Tidepool Project at tidepool.org.
 * == BSD2 LICENSE ==
 */

var _ = require('lodash');
var async = require('async');
var sundial = require('sundial');
var annotate = require('../../eventAnnotations');
var util = require('util');
var debugMode = require('../../../app/utils/debugMode');

var crcCalculator = require('../../crc.js');
var struct = require('../../struct.js')();
var common = require('../../commonFunctions');

var logic = require('./animasLogic');
var animasSimulator = require('./animasSimulator');

var TZOUtil = require('../../TimezoneOffsetUtil');

var debug = require('bows')('AnimasDriver');
var verbose = debugMode.isDebug;

module.exports = function (config) {
  var cfg = _.clone(config);
  var serialDevice = config.deviceComms;
  var animasDeviceId = null;
  var simulator = null;

  var BOM_BYTE = 0xC0;
  var EOM_BYTE = 0xC1;
  var ADDRESS_CONNECT = 0xFF; //used to establish connection

  var RETRIES = 10;
  var TIME_TO_WAIT = 500;
  var TIME_TO_TIMEOUT = 2000;
  var LISTEN_INTERVAL = 30; //don't set this too low to be kind to the pump and the cpu

  var CMDS = {
    CONNECT: { value: 0x93, name: 'CONNECT'},
    DISCONNECT: { value: 0x53, name: 'DISCONNECT'},
    HANDSHAKE: {value: 0xBF, name: 'HANDSHAKE'},
    UA: {value: 0x73, name: 'Unnumbered Acknowledge'},
    RI: {value: 0x5249, name: 'RI message (Read)'},
    DI: {value: [0x44,0x49], name: 'DI message (RI response)'},
    ACK: {value: 0x11, name: 'Acknowledge'}
  };

  var RECORD_TYPES = {
    SERIALNUMBER : {value: 8, name: 'SERIAL AND MODEL NUMBER'},
    BASAL_PROGRAM: {value: 11, name: 'BASAL PROGRAMS'},
    ACTIVE_BASAL_PROGRAM: {value: 12, name: 'ACTIVE BASAL PROGRAM'},
    BASAL_PROGRAM_NAMES: {value: 18, name: 'BASAL PROGRAM NAMES'},
    ADVANCED_SETTINGS: {value: 39, name: 'ADVANCED SETTINGS'},
    BG_DISPLAY_MODE: {value: 29, name: 'BG DISPLAY MODE'},
    BOLUS: {value: 21, name: 'BOLUS HISTORY', numRecords: 500, recordSize: 16},
    ALARM: {value: 23, name: 'ALARM HISTORY', numRecords: 30, recordSize: 16},
    PRIME_REWIND: {value: 24, name: 'PRIME-REWIND HISTORY', numRecords: 60, recordSize: 16},
    SUSPEND_RESUME: {value: 25, name: 'SUSPEND-RESUME HISTORY', numRecords: 30, recordSize: 16},
    BASAL: {value: 26, name: 'BASAL HISTORY', numRecords: 270, recordSize: 16},
    WIZARD: {value: 38, name: 'WIZARD HISTORY', numRecords: 500, recordSize: 16},
    BLOOD_GLUCOSE: {value: 40, name: 'BLOOD GLUCOSE HISTORY (PING)', numRecords: 1000, recordSize: 16},
    CGM_CALIBRATION: {value: 40, name: 'CGM CALIBRATION (VIBE)', numRecords: 1022, recordSize: 8},
    CGM_GLUCOSE: {value: 45, name: 'CGM GLUCOSE HISTORY', numRecords: 15872, recordSize: 8},
    CGM_TIME_ADJUST: {value: 48, name: 'CGM TIME ADJUST HISTORY'}
  };

  var PING = 'IR1285';
  var VIBE = 'IR1295';

  var MODELS = {
    15 : PING,
    16: VIBE
  };

  var BASAL_TYPES = {
    0 : 'scheduled',
    1 : 'temp'
  };

  var PRIMING_FLAGS = {
    0 : 'blank',
    1 : 'not primed',
    2 : 'tubing',
    3 : 'cannula'
  };

  var BOLUS_TYPE_NAME = {
    NORMAL: {value: 1, name: 'normal'},
    AUDIO: {value: 2, name: 'audio'},
    COMBO: {value: 3, name: 'combo'}
  };

  var BOLUS_TYPE_STATUS = {
    COMPLETED: {value: 3, name: 'completed'},
    CANCELLED: {value: 2, name: 'cancelled'}
  };

  var ALARM_TYPES = {
    ALARM_OCCLUSION1: { value: 145, name: 'Occlusion detected'},
    ALARM_OCCLUSION2: { value: 146, name: 'Occlusion detected'},
    ALARM_OCCLUSION3: { value: 147, name: 'Occlusion detected'},
    ALARM_OCCLUSION4: { value: 148, name: 'Occlusion detected'},
    ALARM_AUTO_OFF: { value: 150, name: 'Auto Off'},
    ALARM_REPLACE_BATTERY: { value: 128, name: 'Replace battery'},
    ALARM_EMPTY_CARTRIDGE: { value: 144, name: 'Empty Cartridge'},
    ALARM_LOW_CARTRIDGE: { value: 178, name: 'Low Cartridge'},
    ALARM_LOW_BATTERY: { value: 177, name: 'Low Battery'}
  };

  var ERRORS = {
    LENGTH_ERROR: { value: 0, name: 'Length error'},
    ITEM_ERROR: { value: 1, name: 'Item error'},
    READ_ERROR: { value: 3, name: 'Read error'},
    COMMAND_ERROR: { value: 4, name: 'Command error'},
    RECORD_NUMBER_ERROR: { value: 5, name: 'Record number error'},
    PUMP_NOT_SUSPENDED: { value: 6, name: 'Pump not suspended'}
  };

  var CBG_TYPE_FLAGS = [
    {value: 0x04, name: 'accumulated_adjust_time'},
    {value: 0x08, name: 'bg_calibration'},
    {value: 0x10, name: 'time_changed'},
    {value: 0x20, name: 'time_lost'},
    {value: 0x40, name: 'stop_session'},
    {value: 0x80, name: 'start_session'}
  ];

  var getFlagNames = function (list, v) {
    var flags = [];
    for (var i in list) {
      if (list[i].value & v) {
        flags.push(list[i].name);
      }
    }
    return flags;
  };

  var getCmdName = function (idx) {
    return common.getName(CMDS, idx);
  };

  var getAlarmName = function (idx,types) {
    return common.getName(types, idx);
  };

  var counters = {
    sent : 0,
    received : 0
  };

  var primaryAddress = null;
  var connectionAddress = null; // 11000000b primary devices sets bit 0 (LSB), bit 1-7 is connection address

  var modelNumber = null;
  var hasConfigInfo = false;

  var prevPayload = {};

  // builds a command in an ArrayBuffer
  // Byte 1: always 0xC0 (BOM - Beginning of Message),
  // Byte 2: address field; bit 0 is command/response bit (primary device sets this bit, secondary clears it)
  // Byte 3: control field; if bit 4 is clear sender retains transmit rights, if set receiver has transmit rights
  // Payload: 0-128 bytes
  // CRC bytes: 2 check bytes over bytes 2-3 and payload
  // EOM byte: always 0xC1 (signals end of message)

  var buildPacket = function (address, command, payload) {

    var escapedPayload = escapeCharacters(payload);
    var payloadLength = escapedPayload.length;

    // checksum only over address field, control field and payload
    var acp = [];
    acp = acp.concat(address,command,Array.prototype.slice.call(payload));
    var crc = escapeCharacters(crcCalculator.calcCheckBytes(acp));

    var datalen = payloadLength + crc.length + 4; // BOM + address field + control field + EOM = 4
    var buf = new ArrayBuffer(datalen);
    var bytes = new Uint8Array(buf);
    var ctr = struct.pack(bytes, 0, 'bbb', BOM_BYTE, address,command);

    ctr += struct.copyBytes(bytes, ctr, escapedPayload, payloadLength);
    ctr += struct.copyBytes(bytes, ctr, crc, crc.length);
    struct.pack(bytes, ctr, 'b', EOM_BYTE);

    if (verbose) {
      debug('bytes sent:', common.bytes2hex(bytes));
    }
    return buf;
  };

  var escapeCharacters = function(buf) {
    var escaped = [];
    for(var i = 0; i < buf.length; i++) {
      var byte = buf[i];
      if(byte === BOM_BYTE || byte === EOM_BYTE || byte === 0x7D) {
        if (verbose) {
          debug('Replacing special character');
        }
        escaped.push(0x7D);
        escaped.push(byte ^ 0x20);
      }
      else{
        escaped.push(byte);
      }
    }
    return escaped;
  };

  var setupConnection = function(destinationAddress) {
    var payload = [];
    var payloadlength = struct.copyBytes(payload, 0, primaryAddress, 4);
    payloadlength += struct.pack(payload,4,'ib',destinationAddress,connectionAddress);
    return {
      packet: buildPacket(
        ADDRESS_CONNECT, CMDS.CONNECT.value, payload
      ),
      parser: function (packet) {
        var data = {connected : (packet.command == CMDS.UA.value)};
        return data;
      }
    };
  };

  var handshake = function(iter) {
    return {
      packet: buildPacket(
        ADDRESS_CONNECT, CMDS.HANDSHAKE.value, primaryAddress.concat([0xFF,0xFF,0xFF,0xFF,0x02,iter])
      ),
      parser: function (packet) {
        var data = {
          destinationAddress : struct.extractInt(packet.payload, 0),
          serialNumber : struct.extractInt(packet.payload,10),
          description : struct.extractString(packet.payload,14,packet.length-1)
        };
        return data;
      }
    };
  };

  var getCounters = function() {
    // bit 5-7 : 3-bit receive counter
    // bit 4 : 1
    // bit 1-3 : 3-bit send counter
    // bit 0:  0
    var counter = (counters.received << 5) | 0x10;
    counter = (counters.sent << 1) | counter;
    return counter;
  };

  var incrementSentCounter = function() {
    counters.sent +=1;
    if(counters.sent == 7) {// counter is only 3 bits
      counters.sent = 0;
    }
  };

  var incrementReceivedCounter = function() {
    counters.received += 1;
    if(counters.received == 7) { // counter is only 3 bits
      counters.received = 0;
    }
  };

  var decrementReceivedCounter = function() {
    if(counters.received === 0) { // counter is only 3 bits
      counters.received = 7;
    }
    else {
      counters.received -= 1;
    }
  };

  var readDataPages = function (rectype, offset, numRecords) {

    var payload = new Uint8Array(8);
    var payloadlength = struct.pack(payload,0,'Ssss',CMDS.RI.value,rectype,offset,numRecords);

    return {
      packet: buildPacket(
        connectionAddress,
        getCounters(),
        payload
      ),
      parser: function (packet) {
        return packet;
      }
    };
  };

  var requestPrimeRewind = function() {
    return{
      recordType: RECORD_TYPES.PRIME_REWIND,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          deliveredAmount: (struct.extractShort(payload,8)/100.0).toFixedNumber(2), // U x 100
          primeFlags: PRIMING_FLAGS[struct.extractByte(payload,10)]
        };
      }
    };
  };

  var requestSuspendResume = function() {
    return{
      recordType: RECORD_TYPES.SUSPEND_RESUME,
      parser : function (payload) {
        var suspenddt = decodeDate(payload,4);
        var resumedt = decodeDate(payload,8);
        return {
          index : struct.extractShort(payload,2),
          suspendJsDate : suspenddt,
          suspendDeviceTime: sundial.formatDeviceTime(suspenddt),
          resumeJsDate : resumedt,
          resumeDeviceTime: sundial.formatDeviceTime(resumedt)
        };
      }
    };
  };

  var requestBolus = function() {
    return{
      recordType: RECORD_TYPES.BOLUS,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          deliveredAmount: (struct.extractInt(payload,8)/10000.0).toFixedNumber(2), // U x 10,000
          requiredAmount: struct.extractShort(payload,12)/1000.0, // U x 1,000
          duration: struct.extractShort(payload,14)*0.1*60*60*1000, // N x 0.1Hr
          bolusType: getBolusType(struct.extractByte(payload,16)),
          sync_counter: struct.extractByte(payload,17)
        };
      }
    };
  };

  var requestBasal = function() {
    return{
      recordType: RECORD_TYPES.BASAL,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          rate: struct.extractShort(payload,8)/1000.0, // U x 1,000
          basalType: BASAL_TYPES[parseInt(struct.extractByte(payload,10),10)]
        };
      }
    };
  };

  var requestWizard = function() {
    return{
      recordType: RECORD_TYPES.WIZARD,
      parser : function (payload) {
        var wizardConfig = getWizardConfig(struct.extractByte(payload,15));
        return {
          index : struct.extractShort(payload,2),
          sync_counter: struct.extractByte(payload,4),
          carb_ratio: struct.extractByte(payload,5),
          carb_amount: struct.extractShort(payload,6),
          isf: struct.extractShort(payload,8),
          bg: struct.extractShort(payload,10),
          target_bg: struct.extractShort(payload,12),
          bg_delta: struct.extractByte(payload,14), //shape
          configuration: wizardConfig,
          iob: struct.extractShort(payload,16) / 100.0
        };
      }
    };
  };

  var requestAlarm = function() {
    return{
      recordType: RECORD_TYPES.ALARM,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index: struct.extractShort(payload,2),
          jsDate: dt,
          deviceTime: sundial.formatDeviceTime(dt),
          eaw_code: struct.extractByte(payload,8), // eaw = errors, alarms and warnings
          engineering_code: struct.extractBytes(payload,9,9)
        };
      }
    };
  };

  var requestBG = function() {
    return{
      recordType: RECORD_TYPES.BLOOD_GLUCOSE,
      parser : function (payload) {
        var BASE_TIME = Date.UTC(2000, 0, 1, 0, 0, 0).valueOf();
        var bytes = struct.extractBytes(payload,8,3);
        var counter = (bytes[2] << 16) + (bytes[1] << 8) + bytes[0];
        var dt = new Date(BASE_TIME + counter * sundial.MIN_TO_MSEC);
        var pumpdt = decodeDate(payload,4);
        var bgBytes = struct.extractBytes(payload,11,3);
        // first 10 bits is glucose value, range 0-1023
        var bg = ((bgBytes[1] & 0x03) << 8) + bgBytes[0];
        var controlSolution = bgBytes[1] & 0x04 ? true : false;
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          pumpTime: sundial.formatDeviceTime(pumpdt),
          bg: bg,
          controlSolution: controlSolution
        };
      }
    };
  };

  var requestCalibration = function() {
    return{
      recordType: RECORD_TYPES.CGM_CALIBRATION,
      parser : function (payload) {
        var dt = decodeDate(payload,4);
        return {
          index : struct.extractShort(payload,2),
          jsDate : dt,
          deviceTime: sundial.formatDeviceTime(dt),
          value: struct.extractShort(payload,8)
        };
      }
    };
  };

  var requestCBG = function() {
    return {
      recordType: RECORD_TYPES.CGM_GLUCOSE,
      parser : function (payload) {

        var bgBytes = struct.extractBytes(payload,4,2);
        // first 9 bits is glucose value, range 39-401
        var bg = ((bgBytes[1] & 0x01) << 8) + bgBytes[0];
        var flagNames = getFlagNames(CBG_TYPE_FLAGS, bgBytes[1]);

        var rawTimeStamp = null;
        if(flagNames.length === 0 || _.includes(flagNames,'bg_calibration')) {
          var BASE_TIME = Date.UTC(2008, 0, 1, 0, 0, 0).valueOf();
          var rawTimeStamp = BASE_TIME + struct.extractSignedInt(payload,6) * sundial.SEC_TO_MSEC;
          // rawTimeStamp is now "UTC" unix time without acculumated time adjust
        }
        else{
          rawTimeStamp = struct.extractSignedInt(payload,6);
          // rawTimeStamp is special time adjust record in seconds
        }

        return {
          index: struct.extractShort(payload,2),
          glucose: bg,
          cbg_types: flagNames,
          rawTimeStamp: rawTimeStamp
        };
      }
    };
  };

  // accepts a stream of bytes and tries to find a packet
  // at the beginning of it.
  // returns a packet object; if valid == true it's a valid packet
  // if packet_len is nonzero, that much should be deleted from the stream
  // if valid is false and packet_len is nonzero, the previous packet
  // should be NAKed.
  var extractPacket = function (bytes) {
    var packet = {
      bytes: bytes,
      valid: false,
      packet_len: 0,
      command: 0,
      payload: null,
      crc: 0
    };

    if (bytes[0] != BOM_BYTE) {
      return packet;
    }

    // wait until we've received the end of message
    if (bytes[bytes.length-1] !== EOM_BYTE) {
      return packet;  // we're not done yet
    }

    if (verbose) {
      debug('Raw packet: ', common.bytes2hex(bytes));
    }

    //escape characters
    var fromIndex = 0;
    var index = 0;
    while((index = _.indexOf(bytes,0x7D,fromIndex)) > 0) {
        var buf = new Uint8Array(bytes.length-1);
        var front = _.slice(bytes,0,index+1);
        var special = bytes[index+1] ^ 0x20;
        front[index] = special;
        if(special == 0x7D) {
          fromIndex = index+1; // previous escaped character was the escape character
        }
        buf.set(front,0);
        buf.set(_.slice(bytes,index+2),index+1);
        if (verbose) {
          debug('Escaped bytes:', common.bytes2hex(buf));
        }
        bytes = buf;
    }

    // calc the checksum
    packet.crc = struct.extractBytes(bytes, bytes.length - 3,2);
    var crc = crcCalculator.calcCheckBytes(_.slice(bytes,1,bytes.length-3));
    if ((crc[0] !== packet.crc[0]) || (crc[1] !== packet.crc[1])) {
      // if the crc is bad, we should discard the whole packet
      debug('Invalid CRC');
      cfg.deviceComms.flush();
      return packet;
    }

    // command is the third byte, packet is remainder of data
    packet.command = bytes[2];
    packet.packet_len = bytes.length;

    if((packet.packet_len == 6) && (packet.command & CMDS.ACK.value)) {
      // This is a Receive Ready/NOP/Ack packet
      packet.ack = true;
      var nextPacket = struct.extractByte(bytes, 2);
      counters.received = nextPacket >> 5;
      packet.valid = true;
    }
    else if ((bytes[3] == 0x45) && (bytes[4] == 0x00)) {
      // This is an error message from the pump
      packet.valid = true;
      packet.error = common.getName(ERRORS, bytes[5]);
      debug('Error message from pump:', packet.error);
      return packet;
    }
    else {
      packet.payload = new Uint8Array(packet.packet_len-6);
      for (var i = 0; i < bytes.length - 6; ++i) {
        packet.payload[i] = bytes[i + 3];
      }
      packet.valid = true;
    }

    return packet;
  };

  var animasPacketHandler = function (buffer) {
    // for efficiency reasons, we're not going to bother to ask the driver
    // to decode things that can't possibly be a packet
    // first, discard bytes that can't start a packet
    var discardCount = 0;
    while (buffer.len() > discardCount && buffer.get(0) != BOM_BYTE) {
      ++discardCount;
    }
    if (discardCount) {
      buffer.discard(discardCount);
    }

    if (buffer.len() < 6) { // all complete packets must be at least this long
      return false;       // not enough there yet
    }

    // there's enough there to try, anyway
    var packet = extractPacket(buffer.bytes());
    if (packet.packet_len !== 0) {
      // remove the now-processed packet
      buffer.discard(packet.packet_len);
    }

    if (packet.valid) {
      return packet;
    } else {
      return null;
    }
  };

  var listenForPacket = function (timeout, commandpacket, callback) {
    var timers = {
      abortTimer:null,
      listenTimer:null
    };

    if(timeout > 0) {
      timers.abortTimer = setTimeout(function () {
        clearInterval(timers.listenTimer);
        debug('TIMEOUT');
        callback(new Error('Timeout error'), null);
      }, timeout);
    }

    timers.listenTimer = setInterval(function () {
      if (serialDevice.hasAvailablePacket()) {
        var pkt = serialDevice.nextPacket();
        if (verbose) {
          debug('Received packet: ', pkt);
        }
        // we always call the callback if we get a packet back,
        // so just cancel the timers if we do
        clearTimeout(timers.abortTimer);
        clearInterval(timers.listenTimer);

        if (pkt.error) {
          return callback(new Error(pkt.error),null);
        }
        // only attempt to parse the payload if it worked
        if (pkt.payload) {
          pkt.parsed_payload = commandpacket.parser(pkt);
        }
        callback(null, pkt);
      }
    }, LISTEN_INTERVAL);

    return timers;
  };

  var sendAck = function (obj,cb) {
    var cmd = {
      packet: buildPacket(
        connectionAddress, CMDS.ACK.value | (counters.received << 5), []
      ),
      parser: function (packet) {
        var data = {
          nextPacket : struct.extractByte(packet.payload, 0)
        };
        return data;
      }
    };

    animasCommandResponse(cmd, true, function (err, result) {
      if (err) {
        cb(err, null);
      } else {
        cb(null, result);
      }
    });
  };

  var animasCommandResponse = function (commandpacket, shouldRetry, callback) {

    var retry = function (cb, results) {
      cfg.deviceComms.flush();
      var waitTimer = setTimeout(function () {
        serialDevice.writeSerial(commandpacket.packet, function () {
          // once we've sent the command, start listening for a response
          listenForPacket(TIME_TO_TIMEOUT, commandpacket, function(err, result) {
            if (err) {
              cb(err, null);
            } else {
              cb(null, result);
            }
          });
        });
      },TIME_TO_WAIT);
    };

    serialDevice.writeSerial(commandpacket.packet, function () {
      // once we've sent the command, start listening for a response
      listenForPacket(TIME_TO_TIMEOUT, commandpacket, function(err, result) {
        if(err) {
          if((err.message === 'Timeout error') && shouldRetry) {
            debug('Retrying after timeout..');
            async.retry(RETRIES, retry, function(err, result) {
              if (err) {
                callback(err, null);
              } else {
                callback(null,result);
              }
            });
          }
          else {
            callback(err, null);
          }
        }
        else{
          callback(null,result);
        }
      });
    });
  };

  var animasCommandResponseAck = function (cmd, cb) {
    // This is for Information packets, where one ACK is received after the command is sent,
    // and an ACK is sent in response. When the payload is received, another ACK is sent.

    var retry = function (cb, results) {
      if(!hasConfigInfo) {
        // Initial config request retries are handled a bit differently
        debug('Waiting and then sending NAK..');
        var waitTimer = setTimeout(function () {
          sendAck(true, function (err, ackResult){
            if(err) {
              return cb(err, null);
            }
            incrementReceivedCounter();
            sendAck(true, function (err, ack2Result){
              if(err) {
                return cb(err, null);
              }
              if(ackResult.payload && (ackResult.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
                if(!ack2Result.ack) {
                  debug('acknowledging retransmission');
                  incrementReceivedCounter();
                  sendAck(true, function (err, ack3Result){
                    return cb(null,ackResult);
                  });
                } else {
                  return cb(null,ackResult);
                }
              } else {
                return cb(new Error('We are having trouble connecting to the pump. Please try again.'), null);
              }
            });
          });
        },TIME_TO_WAIT);
      }
      else {
        debug('Sending ack and then command again..');
        incrementReceivedCounter();
        sendAck(true, function (err, result){
          if(err) {
            return cb(err, null);
          }
          if(result.ack) {
            incrementSentCounter();
            animasCommandResponse(cmd, true, function (err, result) {
              if (err) {
                cb(err, null);
              } else {
                if(result.payload && (result.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
                  sendAck(true, function (err, ackResult){
                    if(err) {
                      return cb(err, null);
                    }
                    return cb(null,result);
                  });
                }else{
                  cb(new Error('Pump not sending data. Please retry.'), null);
                }
              }
            });
          } else {
            if(result.payload && (result.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
              sendAck(true, function (err, ackResult){
                if(err) {
                  return cb(err, null);
                }
                return cb(null,result);
              });
            } else {
              return cb(new Error('Unknown packet'), null);
            }
          };
        });
      }
    };

    animasCommandResponse(cmd, true, function (err, result) {
      if (err) {
        cb(err, null);
      } else {
        if(result.ack) {
          incrementSentCounter();
          sendAck(true, function (err, result){
            if(err) {
              return cb(err, null);
            }
            incrementReceivedCounter();
            if(result.payload && (result.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
              sendAck(true, function (err, ackResult){
                if(err) {
                  return cb(err, null);
                }
                return cb(null,result);
              });
            }else {
              async.retry(RETRIES, retry, function(err, result) {
                if (err) {
                  cb(err, null);
                } else {
                  cb(null,result);
                }
              });
            }
          });
        }
        else {
          if(result.payload && (result.payload.slice(0,2).toString() === CMDS.DI.value.toString())) {
            sendAck(true, function (err, ackResult){
              if(err) {
                return cb(err, null);
              }
              return cb(null,result);
            });
          } else {
            //it's possible that we're receiving diagnostic echo messages,
            //so retry command
            async.retry(RETRIES, retry, function(err, result) {
              if (err) {
                cb(err, null);
              } else {
                cb(null,result);
              }
            });
          }
        }
      }
    });
  };

  var clearTimers = function(timers) {
    for(var i = 0; i < timers.length; i++) {
      clearTimeout(timers[i].abortTimer);
      clearInterval(timers[i].listenTimer);
    }
  };

  var discoverDevice = function(cb) {
    debug('discovering animas device');
    primaryAddress = [0x01,0x00,0x00,0x00];
    connectionAddress = 0x03; // use connection address with LSB as 1

    var i = 0;
    var found = false;
    var timers = [];

    var handshakeInterval = setInterval(function() {
      debug('Polling slot ',i);
      var cmd = handshake(i);
      i++;
      if(i === 16) {
        clearTimers(timers);
        clearInterval(handshakeInterval);
        debug('Did not find device.');
        return cb(new Error('Make sure your pump is not sleeping and aligned with the cable.'),null);
      }

      serialDevice.writeSerial(cmd.packet, function () {
        // once we've sent the command, start listening for a response
        timers.push(listenForPacket(0, cmd, function(err, result) {
          if(result && !found) {
            found = true;
            clearTimers(timers);
            clearInterval(handshakeInterval);
            cb(null, result);
          }
        }));
      });
    }, 200); // discovery timeout is 100ms, we wait a bit longer

  };

  var getConnection = function(obj, cb) {
    debug('connecting to animas');
    var i = 0;
    var connected = false;
    var timers = [];

    var connectInterval = setInterval(function() {
      debug('Attempt ',i);
      i++;
      if(i == 8) {
        clearTimers(timers);
        clearInterval(connectInterval);
        debug('Could not connect to device.');
        return cb(new Error('Make sure your pump is suspended.'),null);
      }
      if(obj.parsed_payload != null && !connected) {
        var cmd = setupConnection(obj.parsed_payload.destinationAddress);

        serialDevice.writeSerial(cmd.packet, function () {
          // once we've sent the command, start listening for a response
          timers.push(listenForPacket(0, cmd, function(err, result) {

            if(err) {
              debug(err);
            }
            else {
              clearTimers(timers);
              clearInterval(connectInterval);
              if(!connected) {
                connected = true;
                cb(null, result);
              }
            }
          }));
        });
      }
    }, TIME_TO_WAIT);
  };

  var resetConnection = function(obj, cb) {
    //cfg.deviceComms.setPaused(false);

    debug('reset connection to animas');
    counters.sent = 0;
    counters.received = 0;

    var cmd = {
      packet: buildPacket(
        connectionAddress, CMDS.CONNECT.value, []
      ),
      parser: function (packet) {
        return packet;
      }
    };

    animasCommandResponse(cmd, false, function (err, result) {
      if(result) {
        result.ack = false; // this is not a regular ack
      }
      return cb(null, result);
    });
  };


  var readSerialandModelNumber = function(cb) {
    resetConnection(true, function(connectErr, obj) {
      if(connectErr) {
        return cb(connectErr, null);
      }else{
        debug('Reading serial and model numbers..');
        var cmd = readDataPages(RECORD_TYPES.SERIALNUMBER.value,0,1);
        animasCommandResponseAck(cmd, function (err, result) {
          if (err) {
            cb(err, null);
{'fraction_non_alphanumeric': 0.12501043562471822, 'fraction_numerical': 0.01883421549147618, 'mean_word_length': 2.1882885280809155, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5880685', 'n_tokens_mistral': 19235, 'n_tokens_neox': 18153, 'n_words': 4685}