const LibHelper = require("./LibHelper"); class LibDevice { /** * * @param {Buffer} buffer * @param {Object} opts * @returns {Object} me * must return at least => obj.protocol_name */ static identifyProtocolFromBuffer(buffer, opts = {}) { let ori_hex_str = ""; if (typeof opts.skip_buffer != "undefined" && opts.skip_buffer === true) { ori_hex_str = buffer; } else { ori_hex_str = buffer.toString("hex"); // 16 } const data = ori_hex_str; const me = { ori_string: data, }; // eelinkCustom if (opts.isEelinkCustom) { me.protocol_name = "eelinkCustom"; me.field0 = data.slice(0, 2); // system status flag me.field1 = data.slice(2, 18); // IMEI me.field2 = data.slice(18, 34); // IMSI me.field3 = data.slice(34, 42); // Time me.field4 = data.slice(42, 58); // Location me.field5 = data.slice(58, 62); // Altitude me.field6 = data.slice(62, 66); // Heading me.field7 = data.slice(66, 68); // Velocity me.field8 = data.slice(68, 70); // Power Input Voltage me.field9 = data.slice(70, 72); // GPIO Status me.field10 = data.slice(72, 74); // Time Seconds me.field11 = data.slice(74, 76); // RSSI me.field12 = data.slice(76, 78); // Sequence Counter me.field13 = data.slice(78, 80); // Vendor ID me.field14 = data.slice(80, 82); // GPS Satellites me.field15 = data.slice(82, 90); // Accumulators me.ori_string = data; } // gt06 else if (data.slice(0, 4) == "7878" || data.slice(0, 4) == "7979") { me.protocol_name = "gt06"; me.start = data.slice(0, 4); me.length = parseInt(data.slice(4, 6), 16); me.protocol_id = data.slice(6, 8); me.serial_number = data.slice(-12, -8); me.error_check = data.slice(-8, -4); me.finish = data.slice(-4); // data.substr(6 + me.length * 2, 4); if (me.finish != "0d0a") { throw "finish code incorrect!"; } me.ori_string = data; } // gt02a else if (data.slice(0, 4) == "6868") { me.protocol_name = "gt02a"; } // tk103 // else if (data.indexOf("B")) { // me.protocol_name = 'tk103'; // } // eelink else if (data.slice(0, 4) == "6767") { me.protocol_name = "eelink"; me.start = data.slice(0, 4); me.pid = data.slice(4, 6); me.size = parseInt(data.slice(6, 10), 16); me.sequence = parseInt(data.slice(10, 4), 16); me.ori_string = data; } // unknown else { me.protocol_name = "unknown"; } return me; } /** * * @param {Object} me * @param {String} device_id * @returns * must return at least => act.cmd,act.action_type,act.device_id */ static gt06Action(me, device_id = null) { const act = {}; // Login message if (me.protocol_id == "01") { act.action_type = "login"; act.cmd = "login_request"; act.action = "login_request"; act.device_id = me.ori_string.slice(8).slice(0, 16).padStart(16, "0"); act.buffer_resp = LibDevice.gt06AuthorizeResp(me); } // Location data else if (me.protocol_id == "12" || me.protocol_id == "a0") { act.action_type = "location"; act.cmd = "ping"; act.action = "ping"; act.device_id = device_id; // because device_id only sent when login // content data // me.ori_string.slice(8).slice(0, 52); // me.ori_string.substr(8, me.length * 2); // gps information act.gps_string = me.ori_string.slice(8).slice(0, 36); act.gps_data = LibDevice.gt06ParseLocation(me, act.gps_string); // if (!act.gps_data) { // //Something bad happened // _this.do_log('GPS Data can\'t be parsed. Discarding packet...'); // return false; // } // lbs information act.lbs_string = me.ori_string.slice(8).slice(36, 52); act.lbs_data = LibDevice.gt06ParseLbs(me, act.lbs_string); } // Status information else if (me.protocol_id == "13") { act.action_type = "heartbeat"; act.cmd = "heartbeat"; act.action = "heartbeat"; act.device_id = device_id; // because device_id only sent when login // status information act.stts_string = me.ori_string.slice(8).slice(0, 10); act.stts_data = LibDevice.gt06ParseHeartbeat(me, act.stts_string); act.buffer_resp = LibDevice.gt06HeartbeatResp(me); } // String information else if (me.protocol_id == "15") { // act.action_type = 'other'; // 'heartbeat'; // act.cmd = 'other'; // 'heartbeat'; // act.action = 'other'; // 'heartbeat'; // act.device_id = ''; // device_id; // because device_id only sent when login act.action_type = "heartbeat"; act.cmd = "heartbeat"; act.action = "heartbeat"; act.device_id = device_id; // because device_id only sent when login // status information act.stts_string = me.ori_string.slice(8).slice(0, 10); act.stts_data = LibDevice.gt06ParseHeartbeat(me, act.stts_string); act.buffer_resp = LibDevice.gt06HeartbeatResp(me); } // Alarm data else if (me.protocol_id == "16" || me.protocol_id == "18") { act.action_type = "alarm"; act.cmd = "alarm"; act.action = "alarm"; act.device_id = device_id; // because device_id only sent when login // content data // me.ori_string.slice(8).slice(0, 64); // me.ori_string.substr(8, me.length * 2); // gps information act.gps_string = me.ori_string.slice(8).slice(0, 36); act.gps_data = LibDevice.gt06ParseLocation(me, act.gps_string); // lbs information act.lbs_string = me.ori_string.slice(8).slice(36, 54); act.lbs_data = LibDevice.gt06ParseLbs(me, act.lbs_string); // status information act.stts_string = me.ori_string.slice(8).slice(54, 64); act.stts_data = LibDevice.gt06ParseStatusAlarm(me, act.stts_string); act.buffer_resp = LibDevice.gt06AlarmResp(me); } // GPS, query address information by phone number else if (me.protocol_id == "1A") { act.action_type = "other"; act.cmd = "other"; act.action = "other"; act.device_id = ""; } // Command information sent by the server to the terminal else if (me.protocol_id == "80") { act.action_type = "other"; act.cmd = "other"; act.action = "other"; act.device_id = ""; } else { act.action_type = "other"; act.cmd = "other"; act.action = "other"; act.device_id = ""; } return act; } static gt06AuthorizeResp(me) { return Buffer.from("787805010001d9dc0d0a", "hex"); } static gt06ParseLocation(me, gps_string = null) { if (!gps_string) { gps_string = me.ori_string.slice(8).slice(0, 36); } let year = (parseInt(gps_string.slice(0, 2), 16) + "").padStart(2, 0); year = "20" + year; let month = (parseInt(gps_string.slice(2, 4), 16) + "").padStart(2, 0); let day = (parseInt(gps_string.slice(4, 6), 16) + "").padStart(2, 0); let hour = (parseInt(gps_string.slice(6, 8), 16) + "").padStart(2, 0); let minute = (parseInt(gps_string.slice(8, 10), 16) + "").padStart(2, 0); let second = (parseInt(gps_string.slice(10, 12), 16) + "").padStart(2, 0); let ob1 = LibHelper.hex2bin(gps_string.slice(32, 34)); // orientation_byte1 let ob1_bit7 = ob1.slice(0, 1); let ob1_bit6 = ob1.slice(1, 2); let ob1_bit5 = ob1.slice(2, 3); // 0 (realtime GPS) or differential positioning let ob1_bit4 = ob1.slice(3, 4); // 1 (GPS has been positioned) let ob1_bit3 = ob1.slice(4, 5); // 0 (east longitude) || 1 (west longitude) let ob1_bit2 = ob1.slice(5, 6); // 1 (north latitude) || 0 (south latitude) let ob1_bit1 = ob1.slice(6, 7); let ob1_bit0 = ob1.slice(7, 8); let ob2 = LibHelper.hex2bin(gps_string.slice(34, 36)); // orientation_byte2 // let ob2_bit7 = ob2.slice(0, 1); // let ob2_bit6 = ob2.slice(1, 2); // let ob2_bit5 = ob2.slice(2, 3); // let ob2_bit4 = ob2.slice(3, 4); // let ob2_bit3 = ob2.slice(4, 5); // let ob2_bit2 = ob2.slice(5, 6); // let ob2_bit1 = ob2.slice(6, 7); // let ob2_bit0 = ob2.slice(7, 8); let lat_wind = ""; // wind direction N,S let lng_wind = ""; // wind direction W,E if (ob1_bit3 == 1) { lng_wind = "W"; } if (ob1_bit3 == 0) { lng_wind = "E"; } if (ob1_bit2 == 1) { lat_wind = "N"; } if (ob1_bit2 == 0) { lat_wind = "S"; } const data = { date_raw: gps_string.slice(0, 12), date: `${year}-${month}-${day} ${hour}:${minute}:${second}`, quantity_pos_satellites_raw: gps_string.slice(12, 14), quantity_pos_satellites_c: parseInt(gps_string.slice(12, 13), 16), // length of gps information quantity_pos_satellites_b: parseInt(gps_string.slice(13, 14), 16), // number of positioning satellites realtime_dif_gps: ob1_bit5, // 0 (realtime GPS) or differential positioning positioning_gps: ob1_bit6, // 1 (GPS has been positioned) latitude_raw: gps_string.slice(14, 22), longitude_raw: gps_string.slice(22, 30), lat_wind_direction: lat_wind, lng_wind_direction: lng_wind, latitude: LibDevice.gt06Hex2DMM(gps_string.slice(14, 22), lat_wind), longitude: LibDevice.gt06Hex2DMM(gps_string.slice(22, 30), lng_wind), speed: parseInt(gps_string.slice(30, 32), 16), // km/h orientation_raw: gps_string.slice(32, 36), orientation: parseInt(`${ob1_bit1}${ob1_bit0}${ob2}`, 2), // -360 ~ 360 derajat }; return data; } // more accurate static gt06Hex2DMM(hex, direction) { hex = parseInt(hex, 16); // convert hexadecimal to Degrees Minutes.m (DMM) let a = parseInt(hex, 10); // to decimal values let b = a / 30000.0; let degrees = b / 60; let minutes = b % 60; // convert DMM to Decimal Degrees (DD) // let d = minutes / 60 // let dd = degrees + d // add - follow wind direction if (direction == "S" || direction == "W" || direction == "s" || direction == "w") { degrees = degrees * -1; } return degrees; } // ga akurat static gt06Hex2DMM1(hex, direction) { hex = parseInt(hex, 16); // convert hexadecimal to Degrees Minutes.m (DMM) let a = parseInt(hex, 10); // to decimal values let b = a / 30000.0; let degrees = b / 60; let minutes = b % 60; // convert DMM to Decimal Degrees (DD) let d = minutes / 60; let dd = degrees + d; // add - follow wind direction if (direction == "S" || direction == "W" || direction == "s" || direction == "w") { dd = dd * -1; } return dd; } static gt06HeartbeatResp(me) { return Buffer.from("787805130001d9dc0d0a", "hex"); } static gt06ParseHeartbeat(me, stts_string) { if (!stts_string) { stts_string = me.ori_string.slice(8).slice(0, 10); } let terminal_info_raw = stts_string.slice(0, 2); let tib1 = LibHelper.hex2bin(terminal_info_raw); // terminal_info_byte1 let tib1_bit7 = tib1.slice(0, 1); let tib1_bit6 = tib1.slice(1, 2); let tib1_bit5 = tib1.slice(2, 3); let tib1_bit4 = tib1.slice(3, 4); let tib1_bit3 = tib1.slice(4, 5); let tib1_bit2 = tib1.slice(5, 6); let tib1_bit1 = tib1.slice(6, 7); let tib1_bit0 = tib1.slice(7, 8); /** * 0: No Power (shutdown) * 1: Extremely Low Battery (not enough for calling or sending text messages, etc.) * 2: Very Low Battery (Low Battery Alarm) * 3: Low Battery (can be used normally) * 4: Medium * 5: High * 6: Very High */ let voltage_level = stts_string.slice(2, 4); let gsm_signal_strength = stts_string.slice(4, 6); let alarm_stts = stts_string.slice(6, 8); // former bit: terminal alarm status (suitable for alarm packet and electronic fence project) let language = stts_string.slice(8, 10); // latter bit: the current language used in the terminal const data = { terminal_info_raw: terminal_info_raw, terminal_info_byte: tib1, terminal_info: { oil_electricity: tib1_bit7, // 1: oil and electricity disconnected, 0: gas oil and electricity connected gps_tracking: tib1_bit6, // 1: GPS tracking is on, 0: GPS tracking is off stts: `${tib1_bit5}${tib1_bit4}${tib1_bit3}`, // 100: SOS, 011: Low Battery Alarm, 010: Power Cut Alarm, 001: Shock Alarm, 000: Normal charge: tib1_bit2, // 1: Charge On, 0: Charge Off acc: tib1_bit1, // 1: ACC high, 0: ACC Low is_active: tib1_bit0, // 1: Activated, 0: Deactivated }, voltage_level, gsm_signal_strength, // 0x00: no signal; 0x01: extremely weak signal; 0x02: very weak signal; 0x03: good signal; 0x04: strong signal. alarm_stts, // 0x00: normal, 0x01: SOS, 0x02: Power Cut Alarm, 0x03: Shock Alarm, 0x04: Fence In Alarm, 0x05: Fence Out Alarm language, // 0x01: Chinese, 0x02: English }; return data; } static gt06ParseLbs(me, lbs_string) { let mcc_raw = null, mcc = null, // mobile country code mnc_raw = null, mnc = null, // mobile network code lac_raw = null, lac = null, // location area code cellID_raw = null, cellID = null, // cell tower id lbs_length_raw = null, lbs_length = null; // from location if (lbs_string.length == 16) { mcc_raw = lbs_string.slice(0, 4); mnc_raw = lbs_string.slice(4, 6); lac_raw = lbs_string.slice(6, 10); cellID_raw = lbs_string.slice(10, 16); } // from alarm else if (lbs_string.length == 18) { lbs_length_raw = lbs_string.slice(0, 2); mcc_raw = lbs_string.slice(2, 6); mnc_raw = lbs_string.slice(6, 8); lac_raw = lbs_string.slice(8, 12); cellID_raw = lbs_string.slice(12, 18); } if (lbs_length_raw) { lbs_length = parseInt(lbs_length_raw, 16); } if (mnc_raw && mcc_raw && lac_raw && cellID_raw) { mnc = parseInt(mnc_raw, 16); mcc = parseInt(mcc_raw, 16); lac = parseInt(lac_raw, 16); cellID = parseInt(cellID_raw, 16); } const data = { lbs_length_raw, lbs_length, mcc_raw, mcc, mnc_raw, mnc, lac_raw, lac, cellID_raw, cellID, }; return data; } static gt06ParseStatusAlarm(me, stts_string) { if (!stts_string) { stts_string = me.ori_string.slice(8).slice(54, 10); } let terminal_info_raw = stts_string.slice(0, 2); let tib1 = LibHelper.hex2bin(terminal_info_raw); // terminal_info_byte1 let tib1_bit7 = tib1.slice(0, 1); let tib1_bit6 = tib1.slice(1, 2); let tib1_bit5 = tib1.slice(2, 3); let tib1_bit4 = tib1.slice(3, 4); let tib1_bit3 = tib1.slice(4, 5); let tib1_bit2 = tib1.slice(5, 6); let tib1_bit1 = tib1.slice(6, 7); let tib1_bit0 = tib1.slice(7, 8); /** * 0: No Power (shutdown) * 1: Extremely Low Battery (not enough for calling or sending text messages, etc.) * 2: Very Low Battery (Low Battery Alarm) * 3: Low Battery (can be used normally) * 4: Medium * 5: High * 6: Very High */ let voltage_level = stts_string.slice(2, 4); let gsm_signal_strength = stts_string.slice(4, 6); let alarm_stts = stts_string.slice(6, 8); // former bit: terminal alarm status (suitable for alarm packet and electronic fence project) let language = stts_string.slice(8, 10); // latter bit: the current language used in the terminal const data = { terminal_info_raw: terminal_info_raw, terminal_info_byte: tib1, terminal_info: { oil_electricity: tib1_bit7, // 1: oil and electricity disconnected, 0: gas oil and electricity connected gps_tracking: tib1_bit6, // 1: GPS tracking is on, 0: GPS tracking is off stts: `${tib1_bit5}${tib1_bit4}${tib1_bit3}`, // 100: SOS, 011: Low Battery Alarm, 010: Power Cut Alarm, 001: Shock Alarm, 000: Normal charge: tib1_bit2, // 1: Charge On, 0: Charge Off acc: tib1_bit1, // 1: ACC high, 0: ACC Low is_active: tib1_bit0, // 1: Activated, 0: Deactivated }, voltage_level, gsm_signal_strength, // 0x00: no signal; 0x01: extremely weak signal; 0x02: very weak signal; 0x03: good signal; 0x04: strong signal. alarm_stts, // 0x00: normal, 0x01: SOS, 0x02: Power Cut Alarm, 0x03: Shock Alarm, 0x04: Fence In Alarm, 0x05: Fence Out Alarm language, // 0x01: Chinese, 0x02: English }; return data; } static gt06AlarmResp(me) { return Buffer.from("787805160001d9dc0d0a", "hex"); } static eelinkCustomAction(me, device_id = null) { const act = { device_id: me.field1, action_type: "exist_data", cmd: "exist_data", }; if (me.field3 == "60000000" || me.field3 == "00000000") { act.action_type = "no_data_since_hardware_reset"; act.cmd = "no_data_since_hardware_reset"; // act.buffer_resp = Buffer.from('LOAD'); // act.buffer_resp = 'LOAD'; // act.buffer_resp = Buffer.from(`6767${me.field0}000A0001${me.field1}01`, 'hex'); // act.buffer_resp = `6767${me.field0}000A0001${me.field1}01`; } if (act.action_type !== "exist_data") return act; act.flag = me.field0; act.imsi = me.field2; // International Mobile Subscriber Identifier act.time_string = me.field3; const time_data = {}; time_data.year = "20" + (parseInt(me.field3.slice(0, 1), 16) + 10); time_data.month = "" + parseInt(me.field3.slice(1, 2), 16); if (time_data.month.length === 1) { time_data.month = "0" + time_data.month; } time_data.day = "" + parseInt(me.field3.slice(2, 4), 16); if (time_data.day.length === 1) { time_data.day = "0" + time_data.day; } time_data.hour = "" + parseInt(me.field3.slice(4, 6), 16); if (time_data.hour.length === 1) { time_data.hour = "0" + time_data.hour; } time_data.minute = "" + parseInt(me.field3.slice(6, 8), 16); if (time_data.minute.length === 1) { time_data.minute = "0" + time_data.minute; } act.time_data = time_data; /** * north => -9000000 to +9000000, can 1 or 2 digits of degree * west => -18000000 to +18000000, usually 3 digits degree */ act.location_string = me.field4; // original value is expressed as signed hex value, so we must to conversion to decimal // using two complement let north = ~parseInt(me.field4.slice(0, 8), 16); let west = ~parseInt(me.field4.slice(8, 16), 16); // add magnitude north = north * -1 + 1; west = west * -1 + 1; // convert to string north += ""; west += ""; // remove - from string at first and save to temporary variable let signedNorth = ""; if (north.indexOf("-") === 0) { north = north.slice(1); signedNorth = "-"; } let signedWest = ""; if (west.indexOf("-") === 0) { west = west.slice(1); signedWest = "-"; } // separate DDM => Degree Decimal Minutes let northDegree = north.slice(0, 1); let northDecimalMinute = north.slice(1, 7); if (north.length === 8) { northDegree = north.slice(0, 2); northDecimalMinute = north.slice(2, 7); } let westDegree = west.slice(0, 3); let westDecimalMinute = west.slice(3, 7); // convert DDM to DD (Decimal Degrees) act.latitude = "" + signedNorth + northDegree + "." + ("" + northDecimalMinute / 60).replace(".", ""); act.longitude = "" + signedWest + westDegree + "." + ("" + westDecimalMinute / 60).replace(".", ""); return act; } } module.exports = LibDevice;