public/javascripts/handler.js

/**
 * @module Handler
 * @author Eric Udlis, Michael Handler
 * @description Handle all updates and interfacing between the front-end and back-end
 */
const CLIENT = require('./public/javascripts/communication');
const DATA_INTERFACING = require('./public/javascripts/datainterfacing');
const COMMUNICATIONS_EMITTER = require('./public/javascripts/communication').recievedEmitter;
const CONSTANTS = require('./constants');
const DYNAMIC_LOADING = require('./public/javascripts/dynamicloading');
const RENDERER = require('./public/javascripts/renderer');
const TIMER = require('./public/javascripts/Timer');
const CACHE = require('./cache');
const DATA_RECORDING = require('./dataRecording');

// New OOP stuff
const State = require('./public/javascripts/State');
const ControlPanelButton = require('./public/assets/ControlPanelButton');

const D = document;
const TIMEOUT = 5000;
const CONNECTION_CHECK_INTERVAL = 1000;
const AUTOSAVE_INTERVAL = 30000;
const STATE_BUTTONS = [['Power Off', '#C10000'], ['Idle', '#3C9159'],
  ['Pumpdown', '#C66553', true], ['Propulsion', '#C6A153', true], ['Braking', '#34495E'],
  ['Stopped', '#34495E'], ['Crawl Precharge', '#C6A153'], ['Crawl', '#A84671', true],
  ['Post Run', '#34495E'], ['Safe to Approach', '#3C9159'],
  ['Run Fault', 'red', false, true], ['Non-Run Fault', 'red', false, true]];
  // [Display Name, btn color, ishazardous, isFault]
ControlPanelButton.setParent(document.getElementById('controlpanelBox'));
const CONFIRMATION_MODAL = D.querySelector('.confirmationModal');
ControlPanelButton.setModalTemplate(CONFIRMATION_MODAL);

const LV_INDICATOR = D.getElementById('connectionDot1');
const HV_INDICATOR = D.getElementById('connectionDot2');
const RECIEVE_INDICATOR_1 = D.getElementById('link1');
const RECIEVE_INDICATOR_2 = D.getElementById('link2');
const DATA_RECORD_BUTTON = new ControlPanelButton('dataRecord', 'Start Data Recording', '#AEA8D3', false);
const ARCHIVE_BUTTON = new ControlPanelButton('archiveData', 'Save Data Recording', '#AEA8D3', false);
ARCHIVE_BUTTON.greyOut();
const COMMAND_TORQUE = new ControlPanelButton('cmdTorque', 'Cmd Torque', '#F4D76F', true);
const PRIMARY_BRAKE_ON = new ControlPanelButton('primBrakeOn', 'Prim. Brake Act', '#34495E', false);
const PRIMARY_BRAKE_OFF = new ControlPanelButton('primBrakeOff', 'Prim. Brake Retr', '#34495E', false);
const PRECHARGE_ENABLE = new ControlPanelButton('precharge', 'Precharge', '#C6A153', true);
const SECONDARY_BRAKE_ON = new ControlPanelButton('secBrakeOff', 'Sec. Brake Act', '#5C97BF', false);
const SECONDARY_BRAKE_OFF = new ControlPanelButton('secBrakeOn', 'Sec. Brake Retr', '#5C97BF', false);
const LATCH_ON = new ControlPanelButton('latchOn', 'Latch On', '#554188', true);
const HV_ENABLE = new ControlPanelButton('hvEnable', 'HV Enable', '#F4D76F', true);
const HV_DISABLE = new ControlPanelButton('hvDisable', 'HV Disable', '#F4D76F', false);
const LATCH_OFF = new ControlPanelButton('latchOff', 'Latch off', '#554188', true);

const EMERGENCY_STOP_BTN = D.getElementById('estop');
const TABLES_RENDERER = new RENDERER();
const GLOBAL_TIMER = new TIMER();
const { stateTimer: STATE_TIMER } = DYNAMIC_LOADING;

let activeTimer = GLOBAL_TIMER;
let { DEBUG } = Boolean(process.env) || false;
let boneStatus = [false, false]; // [LV, HV]
let packetCounts = [0, 0]; // [LV, HV]
// eslint-disable-next-line no-unused-vars
let oldCounts = [0, 0];


/**
 * @param  {String} group Group the sensor belongs to
 * @param  {String} sensor Sensor to modify
 */
// Renders latest entry in cace to the tables
function renderData(group, sensor) {
  // Get numbers
  const t = D.getElementById(String(sensor));
  const stored = CACHE[group][sensor];
  // Set number
  if (stored[stored.length - 1] == null) {
    t.innerHTML = 'Not Available';
  } else {
    t.innerHTML = String(stored[stored.length - 1]);
  }
}

// State Machine Control Panel Event Listeners
/**
 * Creates a JSON save labled "Autosave"
 */
function autosave() {
  DATA_INTERFACING.archiveData('autosave');
}

/**
 * Toggles the primary braking indicators and calls the
 * communication call if noted by call
 * @param {Boolean} state // True for on, false for off
 * @param {Boolean} call
 */
function togglePrimBrake(state, call) {
  if (state) {
    PRIMARY_BRAKE_ON.activate();
    PRIMARY_BRAKE_OFF.deactivate();
    if (call) CLIENT.primBrakeOn();
  }
  if (!state) {
    PRIMARY_BRAKE_OFF.activate();
    PRIMARY_BRAKE_ON.deactivate();
    if (call) CLIENT.primBrakeOff();
  }
}
/**
 * Toggles the secondary braking indicators and calls the
 * communication call if noted by call
 * @param {Boolean} state // True for on, false for off
 * @param {Boolean} call
 */
function toggleSecBrake(state, call) {
  if (state) {
    SECONDARY_BRAKE_ON.activate();
    SECONDARY_BRAKE_OFF.deactivate();
    if (call) CLIENT.secBrakeOn();
  }
  if (!state) {
    SECONDARY_BRAKE_ON.deactivate();
    SECONDARY_BRAKE_OFF.activate();
    if (call) CLIENT.secBrakeOff();
  }
}
/**
 * Checks packet for primary and seconday braking status
 * changes indicators and deletes flags
 * @param {Object} basePacket The original packet
 * @returns {Object} fixedPacket The modified packet
 */
function checkBraking(basePacket) {
  let fixedPacket = basePacket;
  if (basePacket.braking !== undefined) {
    if (basePacket.braking.primBrake === 1) togglePrimBrake(false, false);
    else togglePrimBrake(true, false);
    if (basePacket.braking.secBrake === 1) toggleSecBrake(false, false);
    else toggleSecBrake(true, false);
    delete fixedPacket.braking.primBrake;
    delete fixedPacket.braking.secBrake;
  }
  return fixedPacket;
}

// Connection Indicators
/**
 * Sets recieve indicators and starts/stops timer based on status
 * @param {Boolean} state true for ok false for bad
 */
function setRecieve(state) {
  if (state) {
    RECIEVE_INDICATOR_1.className = 'statusGood';
    RECIEVE_INDICATOR_2.className = 'statusGood';
    D.getElementById('ageDisplay').className = 'statusGood';
    if (!GLOBAL_TIMER.process) {
      GLOBAL_TIMER.start();
    }
  }
  if (!state) {
    RECIEVE_INDICATOR_1.className = 'statusBad';
    RECIEVE_INDICATOR_2.className = 'statusBad';
    D.getElementById('ageDisplay').innerHTML = 'N/A';
    D.getElementById('ageDisplay').className = 'statusBad';
    GLOBAL_TIMER.reset();
  }
}
/**
 * Displays the given timer in defined display location
 * @param {Timer} timer Displays the timers current time
 */
function displayTimer(timer) {
  if (`${timer.getSeconds()}`.length === 1) D.getElementById('ageDisplay').innerHTML = `${timer.getMinutes()}:0${timer.getSeconds()}`;
  else D.getElementById('ageDisplay').innerHTML = `${timer.getMinutes()}:${timer.getSeconds()}`;
}
/**
 * Sets the LV indicator
 * @param {Boolean} state true for ok false for bad
 */
function setLVIndicator(state) {
  if (state) LV_INDICATOR.className = 'statusGood';
  if (!state) LV_INDICATOR.className = 'statusBad';
}
/**
 *Sets the HV indicator
 * @param {Boolean} state true for ok false for bad
 */
function setHVIndicator(state) {
  if (state) HV_INDICATOR.className = 'statusGood';
  if (!state) HV_INDICATOR.className = 'statusBad';
  if (!state && !DEBUG) State.setActiveState(0, CONFIRMATION_MODAL);
}
/**
 * Checks if dashboard has recieved packets within timeout period
 * If so, sets recieve indicator good and renders to tables
 * If bad,sets recieve indicator bad and stops rendering to tables
 */
function checkRecieve() {
  let now = new Date().getTime();
  let difference = now - TABLES_RENDERER.lastRecievedTime;
  displayTimer(activeTimer);
  // console.log(`now: ${now} difference ${difference} timeout ${TIMEOUT}`);
  if ((difference > TIMEOUT) || TABLES_RENDERER.lastRecievedTime === 0) {
    setRecieve(false);
    TABLES_RENDERER.stopRenderer();
  } else {
    setRecieve(true);
    TABLES_RENDERER.startRenderer();
  }
}
/**
 * Sends TCP heartbeats to Pod
 */
function sendHeartbeats() {
  CLIENT.sendLVPing();
  CLIENT.sendHVPing();
}

/**
 * Sets LV and HV indicators based on boneStatus
 */
function checkTransmit() {
  setLVIndicator(boneStatus[0]);
  setHVIndicator(boneStatus[1]);
}

/**
 * Updates lables of tables based on recieving packets or not
 */
function updateLabels() {
  if (oldCounts[0] < packetCounts[0] + 5) {
    // Still Recieving packets
    D.getElementById('motionDisconnected').style.display = 'none';
    D.getElementById('brakingDisconnected').style.display = 'none';
  } else {
    // Haven't recieved packets
    D.getElementById('motionDisconnected').style.display = 'inline';
    D.getElementById('brakingDisconnected').style.display = 'inline';
  }
  if (oldCounts[1] < packetCounts[1] + 5) {
    // Still recieving packets
    D.getElementById('motorDisconnected').style.display = 'none';
    D.getElementById('batteryDisconnected').style.display = 'none';
  } else {
    // Haven't recieved packets
    D.getElementById('motorDisconnected').style.display = 'inline';
    D.getElementById('batteryDisconnected').style.display = 'inline';
  }
  oldCounts[0] = packetCounts[0];
  oldCounts[1] = packetCounts[1];
  /** ***************************
  // return null;
   ***************************** */
}
/**
 * Checks if recieving packets, sends heartbeats to pod, checks if we get call back from pod
 * Updates Table Lables
 */
function podConnectionCheck() {
  checkRecieve();
  sendHeartbeats();
  checkTransmit();
  updateLabels();
}
/**
 * Checks weather given packet is a HV or LV packet and increments counter accordingly
 * @param {Object} input Packet to check
 */
function checkPackets(input) {
  if (input.braking && input.motion) packetCounts[0]++;
  if (input.motor && input.battery) packetCounts[1]++;
}


// Event Listeners
// Changes timer based on user input
document.getElementById('ageDisplay').addEventListener('click', () => {
  if (activeTimer === GLOBAL_TIMER) {
    document.getElementById('ageLabel').innerHTML = 'State Timer';
    activeTimer = STATE_TIMER;
    return;
  }
  if (activeTimer === STATE_TIMER || propTimer) {
    document.getElementById('ageLabel').innerHTML = 'Global Timer';
    activeTimer = GLOBAL_TIMER;
  }
});

// Sends Estop on user click
EMERGENCY_STOP_BTN.addEventListener('click', () => {
  CLIENT.sendEBrake();
});


// Initilization
/**
 * Creates the states and state machine buttons
 */

function setControlPanelListeners() {
  PRIMARY_BRAKE_ON.onClick(() => {
    togglePrimBrake(true, true);
  });

  PRIMARY_BRAKE_OFF.onClick(() => {
    togglePrimBrake(false, true);
  });

  SECONDARY_BRAKE_ON.onClick(() => {
    toggleSecBrake(true, true);
  });

  SECONDARY_BRAKE_OFF.onClick(() => {
    toggleSecBrake(false, true);
  });

  HV_ENABLE.onClick(() => {
    CLIENT.enableHV();
    HV_DISABLE.deactivate();
    HV_ENABLE.activate();
  });

  HV_DISABLE.onClick(() => {
    CLIENT.disableHV();
    HV_DISABLE.activate();
    HV_ENABLE.deactivate();
  });

  COMMAND_TORQUE.onClick(() => {
    CLIENT.commandTorque();
  });

  LATCH_ON.onClick(() => {
    LATCH_ON.activate();
    LATCH_OFF.deactivate();
    CLIENT.toggleLatch(true);
  });

  LATCH_OFF.onClick(() => {
    LATCH_OFF.activate();
    LATCH_ON.deactivate();
    CLIENT.toggleLatch(false);
  });

  PRECHARGE_ENABLE.onClick(() => {
    CLIENT.enPrecharge();
  });

  // Starts the recording of data to dataRecording.js
  DATA_RECORD_BUTTON.onClick(() => {
    if (!DATA_INTERFACING.isDataRecording) {
      DATA_INTERFACING.recordingEvent.emit('on'); // Tell DI to run start recording data
      console.log('recording data');
      DATA_RECORD_BUTTON.greyOut();
      ARCHIVE_BUTTON.colorize();
    } else {
      console.log('data is already being recorded');
    }
  });

  // Archives the data from dataRecording.js if data is being recorded
  ARCHIVE_BUTTON.onClick(() => {
    if (DATA_INTERFACING.isDataRecording) {
      DATA_INTERFACING.recordingEvent.emit('off'); // Tells DI to stop recording data
      DATA_INTERFACING.archiveData();
      console.log('archiving data');
      DATA_RECORD_BUTTON.colorize();
      ARCHIVE_BUTTON.greyOut();
    } else {
      console.log('data was not being recorded');
    }
  });
}
function createStateMachineButtons() {
  return new Promise((resolve, reject) => {
    let parent = document.getElementById('statemachineBox');
    if (!parent) reject(new Error('Parent not found'));
    STATE_BUTTONS.forEach((state) => {
      let formattedText = state[0].replace(/ /g, '').toLowerCase();
      let newState = new State(formattedText, state[0], null, state[1], state[2], state[3]);
      newState.btn.setParent(parent);
      newState.btn.onClick(() => {
        State.setActiveState(newState, CONFIRMATION_MODAL);
      });
    });
    State.setActiveState(0);
    resolve(State.getActiveState());
  });
}

/**
 * Function to create the caches and tables and dropdowns of the dash
 */
function createDashboard() {
  return new Promise((resolve, reject) => {
    try {
      DATA_INTERFACING.createCache();
      DATA_INTERFACING.createCache(DATA_RECORDING);
      DYNAMIC_LOADING.fillAllItems();
      DYNAMIC_LOADING.fillAllTables();
    } catch (e) {
      reject(new Error(e));
    }
    resolve();
  });
}

/**
 * Function to run at start of dashboard
 */
function init() {
  createDashboard().then(() => { // First create the dashboard
    setControlPanelListeners();
    createStateMachineButtons().then(() => { // Then create all the state objects
      setInterval(podConnectionCheck, CONNECTION_CHECK_INTERVAL); // Finally set intervals
      // Autosaves on interval
      setInterval(autosave, AUTOSAVE_INTERVAL);
    }).catch((err) => {
      throw err;
    });
  });
  displayTimer(GLOBAL_TIMER);
  console.log(DEBUG);
}
// Run at init
init();

// Events

// Data in recieved
COMMUNICATIONS_EMITTER.on('dataIn', (input) => {
  if (DEBUG) console.log(input);
  checkPackets(input);
  let fixedPacket = checkBraking(input);
  DATA_INTERFACING.normalizePacket(fixedPacket);
  TABLES_RENDERER.lastRecievedTime = new Date().getTime();
});

COMMUNICATIONS_EMITTER.on('Lost', (ip) => {
  if (ip === CONSTANTS.lvBone.ip) {
    if (boneStatus[0]) console.error('lost LV bone');
    boneStatus[0] = false;
  }
  if (ip === CONSTANTS.hvBone.ip) {
    if (boneStatus[1]) console.error('lost LV bone');
    boneStatus[1] = false;
  }
});

COMMUNICATIONS_EMITTER.on('ok', (ip) => {
  if (ip === CONSTANTS.lvBone.ip) { boneStatus[0] = true; }
  if (ip === CONSTANTS.hvBone.ip) { boneStatus[1] = true; }
});

DATA_INTERFACING.packetHandler.on('renderData', () => {
  const renderable = DATA_INTERFACING.findRenderable();
  const groups = Object.keys(renderable);
  groups.forEach((group) => {
    const sensors = Object.keys(renderable[group]);
    sensors.forEach((sensor) => {
      // Check to see if that particular sensor is being rendered at the time
      try {
        renderData(group, sensor);
      } catch (error) {
        // If not, alert the user and move on
        console.error(`Error: Sensor ${sensor} in ${group} not rendered`);
      }
    });
  });
});