lgaticaq/meitrack-parser

View on GitHub
src/index.js

Summary

Maintainability
D
1 day
Test Coverage
C
76%
'use strict'

const crc = require('crc')
const pad = require('pad')

const patterns = {
  mvt380: /^\$\$([\x41-\x7A])(\d{1,3}),(\d{15}),([0-9A-F]{3}),(\d{1,3}),([-]?\d+\.\d+),([-]?\d+\.\d+),(\d{12}),([AV]),(\d{1,3}),(\d{1,2}),(\d+(\.\d+)?),(\d+(\.\d+)?),(\d+(\.\d+)?),(\d+(\.\d+)?),(\d+(\.\d+)?),(\d+),(\d{3})\|(\d{1,3})\|([0-9A-F]{4})\|([0-9A-F]{4}),([0-9A-F]{4}),([0-9A-F]{1,4})?\|([0-9A-F]{1,4})?\|([0-9A-F]{1,4})?\|([0-9A-F]{1,4})\|([0-9A-F]{1,4}),([0-9A-F]{8})?,?([0-9A-F]+)?,?(\d{1,2})?,?([0-9A-F]{4})?,?([0-9A-F]{6})?\|?([0-9A-F]{6})?\|?([0-9A-F]{6})?\|?\*([0-9A-F]{2})\r\n$/,
  ok: /^\$\$([\x41-\x7A])(\d{1,3}),(\d{15}),([0-9A-F]{3}),OK\*([0-9A-F]{2})\r\n$/
}

const parseAlrm = event => {
  const alarms = {
    '1': { type: 'SOS_Button' },
    '2': { type: 'DI', number: 2, status: true },
    '3': { type: 'DI', number: 3, status: true },
    '4': { type: 'DI', number: 4, status: true },
    '5': { type: 'DI', number: 5, status: true },
    '9': { type: 'DI', number: 1, status: false },
    '10': { type: 'DI', number: 2, status: false },
    '11': { type: 'DI', number: 3, status: false },
    '12': { type: 'DI', number: 4, status: false },
    '13': { type: 'DI', number: 5, status: false },
    '17': { type: 'DI', number: 2, status: false },
    '18': { type: 'lowExternalBattery' },
    '19': { type: 'Over_Speed', status: true },
    '20': { type: 'Geo_Fence', status: true },
    '21': { type: 'Geo_Fence', status: false },
    '22': { type: 'Charge', status: true },
    '23': { type: 'Charge', status: false },
    '24': { type: 'gpsSignal', status: false },
    '25': { type: 'gpsSignal', status: true },
    '26': { type: 'Sleep', status: true },
    '27': { type: 'Sleep', status: false },
    '28': { type: 'gpsAntennaCut' },
    '29': { type: 'deviceReboot' },
    '31': { type: 'Heartbeat' },
    '32': { type: 'Angle' },
    '33': { type: 'distanceIntervalTracking' },
    '34': { type: 'replyCurrent' },
    '35': { type: 'Gps' },
    '36': { type: 'tow' },
    '37': { type: 'Rfid' },
    '39': { type: 'picture' },
    '40': { type: 'powerOff' },
    '41': { type: 'Moving', status: false },
    '42': { type: 'Moving', status: true },
    '44': { type: 'jamming', status: true },
    '50': { type: 'temperature', status: true },
    '51': { type: 'temperature', status: false },
    '52': { type: 'fuelFulled' },
    '53': { type: 'fuelEmpty' },
    '54': { type: 'fuelStolen' },
    '56': { type: 'armed' },
    '57': { type: 'disarmed' },
    '58': { type: 'stealing' },
    '63': { type: 'jamming', status: false },
    '65': { type: 'pressInput1ToCall' },
    '66': { type: 'pressInput2ToCall' },
    '67': { type: 'pressInput3ToCall' },
    '68': { type: 'pressInput4ToCall' },
    '69': { type: 'pressInput5ToCall' },
    '70': { type: 'rejectIncomingCall' },
    '71': { type: 'getLocationByCall' },
    '72': { type: 'autoAnswerIncomingCall' },
    '73': { type: 'listenIn' },
    '79': { type: 'fall' },
    '80': { type: 'install' },
    '81': { type: 'dropOff' },
    '139': { type: 'maintenance' }
  }
  return event in alarms ? alarms[event] : null
}

const getMvt380 = raw => {
  const match = patterns.mvt380.exec(raw)
  const status = match[27]
    .split('')
    .map(x => pad(4, parseInt(x, 10).toString(2), '0'))
    .join('')
  const data = {
    raw: match[0],
    type: 'data',
    manufacturer: 'meitrack',
    device: 'MVT380',
    imei: parseInt(match[3], 10),
    command: match[4],
    alarm: parseAlrm(match[5]),
    loc: {
      type: 'Point',
      coordinates: [parseFloat(match[7]), parseFloat(match[6])]
    },
    datetime: new Date(
      match[8].replace(
        /(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/,
        '20$1-$2-$3T$4:$5:$6.000Z'
      )
    ),
    gpsStatus: match[9] === 'A',
    satellites: parseInt(match[10], 10),
    gsmSignal: parseInt(match[11], 10),
    speed: parseFloat(match[12]),
    direction: parseFloat(match[14]),
    hdop: parseFloat(match[16]),
    altitude: parseFloat(match[18]),
    odometer: parseFloat(match[20]),
    runtime: parseInt(match[22], 10),
    mcc: match[23],
    mnc: match[24],
    lac: parseInt(match[25], 16),
    cid: parseInt(match[26], 16),
    status: {
      output: {
        '1': status[15] === '1',
        '2': status[14] === '1',
        '3': status[13] === '1',
        '4': status[12] === '1',
        '5': status[11] === '1',
        '6': status[10] === '1',
        '7': status[9] === '1',
        '8': status[8] === '1'
      },
      input: {
        '1': status[7] === '1',
        '2': status[6] === '1',
        '3': status[5] === '1',
        '4': status[4] === '1',
        '5': status[3] === '1',
        '6': status[2] === '1',
        '7': status[1] === '1',
        '8': status[0] === '1'
      }
    },
    voltage: {
      ad1: match[28] ? parseInt(match[28], 16) * 6 / 1024 : null,
      ad2: match[29] ? parseInt(match[29], 16) * 6 / 1024 : null,
      ad3: match[30] ? parseInt(match[30], 16) * 6 / 1024 : null,
      battery: match[31] ? parseInt(match[31], 16) * 3 * 2 / 1024 : null,
      inputCharge: match[32] ? parseInt(match[32], 16) * 3 * 16 / 1024 : null
    }
  }
  return data
}

const parseCode = raw => {
  const match = patterns.ok.exec(raw)
  const code = match[4]
  const codes = {
    C01: 'SETIOSWITCH',
    F09: 'CLEARBUF',
    A12: 'SETGPRSINTERVAL',
    B07: 'SETOVERSPEEDALARM',
    F01: 'RBOOT',
    F02: 'RBOOT'
  }
  const data = {
    manufacturer: 'meitrack',
    device: 'MEITRACK-COMMAND-OK',
    type: 'ok',
    code: code
  }
  if (Object.keys(codes).indexOf(code) > -1) data.command = codes[code]
  return data
}

const parse = raw => {
  let result = { type: 'UNKNOWN', raw: raw.toString() }
  if (patterns.mvt380.test(raw)) {
    result = getMvt380(raw)
  } else if (patterns.ok.test(raw)) {
    result = parseCode(raw)
  }
  return result
}

const isMeitrack = raw => {
  let result = false
  if (patterns.mvt380.test(raw)) {
    result = true
  } else if (patterns.ok.test(raw)) {
    result = true
  }
  return result
}

// Random integer from 65 to 122 (41 to 7a in hex)
const getRandomDataIdentifier = () => {
  const int = Math.floor(Math.random() * (122 - 65 + 1) + 65)
  return String.fromCharCode(int)
}

const getCommand = (imei, command) => {
  const raw1 = `,${imei},${command}*`
  const raw2 = `@@${getRandomDataIdentifier()}${raw1.length + 4}${raw1}`
  return `${raw2}${pad(
    2,
    crc
      .crc1(raw2)
      .toString(16)
      .toUpperCase(),
    '0'
  )}\r\n`
}

const parseCommand = data => {
  let raw, state, port, speed, interval
  if (/^[1-5]{1}_(on|off|status)$/.test(data.instruction)) {
    let _data = data.instruction.split('_')
    port = _data[0]
    state = _data[1]
    let initial = [2, 2, 2, 2, 2]
    const states = { off: 0, on: 1, status: 2 }
    initial[port - 1] = states[state]
    speed = data.speed || 0
    raw = `C01,${speed},${initial.join('')}`
  } else if (data.instruction === 'clear_mem') {
    raw = 'F09,3'
  } else if (data.instruction === 'set_interval_gprs') {
    interval = data.interval || 6 * 10
    if (interval < 12) interval = 12
    let mod = interval % 6
    if (mod > 0) interval -= mod
    raw = `A12,${interval}`
  } else if (/^set_speed_(on|off)(E)?$/.test(data.instruction)) {
    speed = data.speed || 0
    state = data.instruction.split('_')[2]
    if (state === 'off') speed = 0
    raw = `B07,${speed}`
  } else if (data.instruction === 'Custom') {
    raw = data.command
  } else if (/^reboot_gsm$/.test(data.instruction)) {
    raw = 'F01'
  } else if (/^reboot_gps$/.test(data.instruction)) {
    raw = 'F02'
  }
  return getCommand(data.imei, raw)
}

const getRebootCommand = imei => {
  return getCommand(imei, 'F02')
}

const getImei = raw => {
  let imei = null
  const data = raw.toString()
  if (patterns.mvt380.test(data)) {
    imei = patterns.mvt380.exec(data)[3]
  }
  return imei
}

module.exports = {
  parse: parse,
  patterns: patterns,
  getMvt380: getMvt380,
  isMeitrack: isMeitrack,
  parseCommand: parseCommand,
  getRebootCommand: getRebootCommand,
  getImei: getImei
}