From 0ea228ab658abc5acbb5ce20c5a799de4ee67d1f Mon Sep 17 00:00:00 2001
From: Adhidarma Hadiwinoto <me@adhisimon.org>
Date: Mon, 9 Sep 2019 12:52:25 +0700
Subject: [PATCH] sms and ussd

---
 lib/modem-commands/index.js | 146 +++++++++++++++++++++++++++++++++++---------
 lib/modem-info.js           |   8 +--
 lib/serialport-parsers.js   |  45 +++++++++++---
 modem-tester.js             |  11 +++-
 package-lock.json           |  11 +++-
 package.json                |   5 +-
 6 files changed, 180 insertions(+), 46 deletions(-)

diff --git a/lib/modem-commands/index.js b/lib/modem-commands/index.js
index 2d15a77..84d8046 100644
--- a/lib/modem-commands/index.js
+++ b/lib/modem-commands/index.js
@@ -4,10 +4,14 @@ exports.MUTEX_COMMAND = MUTEX_COMMAND;
 const MUTEX_SUBCOMMAND = 'SUBCOMMAND';
 exports.MUTEX_SUBCOMMAND = MUTEX_SUBCOMMAND;
 
+const CTRLZ = '\u001a';
+
 const pdu = require('node-pdu');
+const uuidv1 = require('uuid/v1');
 
 const ParserReadline = require('@serialport/parser-readline');
 const ParserRegex = require('@serialport/parser-regex');
+const ParserReady = require('@serialport/parser-ready');
 
 const logger = require('komodo-sdk/logger');
 const mutex = require('../mutex-common');
@@ -27,8 +31,9 @@ function writeToPort(data) {
         });
     });
 }
+exports.writeToPort = writeToPort;
 
-exports.writeToPortAndWaitForReadline = function writeToPortAndWaitForReadline(cmd, lockName) {
+function writeToPortAndWaitForReadline(cmd, lockName) {
     let resolved = false;
 
     return new Promise(async (resolve) => {
@@ -46,9 +51,10 @@ exports.writeToPortAndWaitForReadline = function writeToPortAndWaitForReadline(c
         port.pipe(parser);
         await writeToPort(cmd);
     });
-};
+}
+exports.writeToPortAndWaitForReadline = writeToPortAndWaitForReadline;
 
-exports.writeToPortAndWaitForOkOrError = function writeToPortAndWaitForOkOrError(cmd, lockName) {
+function writeToPortAndWaitForOkOrError(cmd, lockName) {
     return new Promise(async (resolve) => {
         const parser = new ParserRegex({ regex: /(?:OK|ERROR)\r\n/ });
         parser.on('data', (data) => {
@@ -61,22 +67,23 @@ exports.writeToPortAndWaitForOkOrError = function writeToPortAndWaitForOkOrError
         port.pipe(parser);
         await writeToPort(cmd);
     });
-};
+}
+exports.writeToPortAndWaitForOkOrError = writeToPortAndWaitForOkOrError;
 
-exports.sleep = function sleep(ms) {
+function sleep(ms) {
     return new Promise((resolve) => {
         setTimeout(() => {
             resolve();
         }, ms || 0);
     });
-};
-
+}
+exports.sleep = sleep;
 
 exports.setPort = function setPort(val) {
     port = val;
 };
 
-exports.querySignalQuality = function querySignalQuality() {
+function querySignalQuality() {
     return new Promise(async (resolve) => {
         if (!mutex.tryLock(MUTEX_COMMAND, 'querySignalQuality')) {
             resolve(false);
@@ -87,18 +94,20 @@ exports.querySignalQuality = function querySignalQuality() {
         mutex.unlock(MUTEX_COMMAND, 'querySignalQuality');
         resolve(true);
     });
-};
+}
+exports.querySignalQuality = querySignalQuality;
 
-exports.queryCOPS = function queryCOPS(lockName) {
+function queryCOPS(lockName) {
     return new Promise(async (resolve) => {
         await mutex.lock(lockName || MUTEX_COMMAND, 'queryCOPS');
         await writeToPort('AT+COPS?\r');
         mutex.unlock(lockName || MUTEX_COMMAND, 'queryCOPS');
         resolve(true);
     });
-};
+}
+exports.queryCOPS = queryCOPS;
 
-exports.queryCOPSAndSignalQuality = function queryCOPSAndSignalQuality(skipOnLocked) {
+function queryCOPSAndSignalQuality(skipOnLocked) {
     return new Promise(async (resolve) => {
         if (!skipOnLocked) {
             await mutex.lock(MUTEX_COMMAND);
@@ -107,15 +116,16 @@ exports.queryCOPSAndSignalQuality = function queryCOPSAndSignalQuality(skipOnLoc
             return;
         }
 
-        await this.writeToPortAndWaitForOkOrError('AT+COPS?\r', MUTEX_SUBCOMMAND);
-        await this.writeToPortAndWaitForOkOrError('AT+CSQ\r', MUTEX_SUBCOMMAND);
+        await writeToPortAndWaitForOkOrError('AT+COPS?\r', MUTEX_SUBCOMMAND);
+        await writeToPortAndWaitForOkOrError('AT+CSQ\r', MUTEX_SUBCOMMAND);
 
         mutex.unlock(MUTEX_COMMAND, 'queryCopsAndSignalQuality');
         resolve(true);
     });
-};
+}
+exports.queryCOPSAndSignalQuality = queryCOPSAndSignalQuality;
 
-exports.queryIMEI = function queryIMEI(lockName) {
+function queryIMEI(lockName) {
     return new Promise(async (resolve) => {
         const parser = new ParserRegex({ regex: parsers.PARSER_WAIT_FOR_OK_OR_ERROR_REGEX });
         parser.on('data', (data) => {
@@ -132,9 +142,10 @@ exports.queryIMEI = function queryIMEI(lockName) {
         port.pipe(parser);
         await writeToPort('AT+CGSN\r');
     });
-};
+}
+exports.queryIMEI = queryIMEI;
 
-exports.queryIMSI = function queryIMSI(lockName) {
+function queryIMSI(lockName) {
     return new Promise(async (resolve) => {
         const parser = new ParserRegex({ regex: parsers.PARSER_WAIT_FOR_OK_OR_ERROR_REGEX });
         parser.on('data', (data) => {
@@ -151,13 +162,14 @@ exports.queryIMSI = function queryIMSI(lockName) {
         port.pipe(parser);
         await writeToPort('AT+CIMI\r');
     });
-};
+}
+exports.queryIMSI = queryIMSI;
 
 exports.queryIMEIAndIMSI = async function queryIMEIAndIMSI() {
     await mutex.lock(MUTEX_COMMAND, 'queryIMEIAndIMSI');
 
-    const imei = await this.queryIMEI(MUTEX_SUBCOMMAND);
-    const imsi = await this.queryIMSI(MUTEX_SUBCOMMAND);
+    const imei = await queryIMEI(MUTEX_SUBCOMMAND);
+    const imsi = await queryIMSI(MUTEX_SUBCOMMAND);
 
     await mutex.unlock(MUTEX_COMMAND, 'queryIMEIAndIMSI');
     return { imei, imsi };
@@ -201,16 +213,94 @@ exports.queryModel = function queryModel(lockName) {
     });
 };
 
-exports.sendSMS = function sendSMS(destination, msg) {
-    return new Promise(async (resolve) => {
-        
-    });
-};
+async function sendCtrlZ() {
+    await writeToPort(CTRLZ);
+}
+exports.sendCtrlZ = sendCtrlZ;
 
-exports.initATCommands = async function initATCommands() {
+async function initATCommands() {
     await mutex.lock(MUTEX_COMMAND, 'INIT MODEM');
-    await this.writeToPortAndWaitForOkOrError('\rATE0\r', MUTEX_SUBCOMMAND);
+    await this.writeToPortAndWaitForOkOrError(`${CTRLZ}ATE0\r`, MUTEX_SUBCOMMAND);
     await this.writeToPortAndWaitForOkOrError('AT+CMGF=0\r', MUTEX_SUBCOMMAND);
     await this.writeToPortAndWaitForOkOrError('AT+CNMI=1,2,0,1,0\r', MUTEX_SUBCOMMAND);
     mutex.unlock(MUTEX_COMMAND, 'INIT MODEM');
+}
+exports.initATCommands = initATCommands;
+
+function sendCMGSPdu(pduLength) {
+    return new Promise((resolve) => {
+        const parser = new ParserReady({ delimiter: '>' });
+        parser.on('data', () => {
+            logger.verbose('Got ">" message prompt, gonna to write PDU message');
+            port.unpipe(parser);
+            mutex.unlock(MUTEX_SUBCOMMAND, 'sendSmsPduCommand');
+            resolve(true);
+        });
+
+        mutex.lock(MUTEX_SUBCOMMAND, 'sendSmsPduCommand');
+        port.pipe(parser);
+        writeToPort(`AT+CMGS=${pduLength}\r`);
+    });
+}
+
+exports.sendSMS = function sendSMS(destination, msg) {
+    return new Promise(async (resolve) => {
+        await mutex.lock(MUTEX_COMMAND, 'sendSMS');
+
+        if (!destination || !destination.trim()) {
+            resolve(false);
+            return;
+        }
+
+        if (!msg || !msg.trim()) {
+            resolve(false);
+            return;
+        }
+
+        const correctedDestination = `+${destination.replace(/^0/, '62')}`.replace(/^\++/, '+');
+        logger.verbose(`Sending sms to ${correctedDestination}`, { msg });
+
+        await this.writeToPortAndWaitForOkOrError('AT+CMGF=0\r', MUTEX_SUBCOMMAND);
+
+        const submit = pdu.Submit();
+        submit.setAddress(correctedDestination);
+        submit.setData(msg.trim());
+        submit.getType().setSrr(0);
+
+        await sendCMGSPdu(Math.floor(submit.toString().length / 2) - 1);
+        await writeToPort(`${submit.toString()}${CTRLZ}`);
+
+        mutex.unlock(MUTEX_COMMAND, 'sendSMS');
+        resolve(true);
+    });
+};
+
+exports.executeUSSD = function executeUSSD(code, _includeCUSD2, _sessionId) {
+    return new Promise(async (resolve) => {
+        const includeCUSD2 = _includeCUSD2 || 0;
+        const sessionId = _sessionId || uuidv1();
+
+        async function responseHandler(data) {
+            logger.verbose('Processing USSD response', { data });
+            parsers.setUssdCallback(null);
+
+            if (includeCUSD2 === 1 || includeCUSD2 === 2) {
+                await writeToPortAndWaitForOkOrError('AT+CUSD=2\r', MUTEX_SUBCOMMAND);
+            }
+
+            mutex.unlock(MUTEX_COMMAND, `executeUSSD ${sessionId}`);
+            resolve(data);
+        }
+
+        mutex.lock(MUTEX_COMMAND, `executeUSSD ${sessionId}`);
+        parsers.setUssdCallback(responseHandler);
+
+        await this.writeToPortAndWaitForOkOrError(`${CTRLZ}AT+CMGF=0\r`, MUTEX_SUBCOMMAND);
+
+        if (includeCUSD2 === -1 || includeCUSD2 === 2) {
+            await this.writeToPortAndWaitForOkOrError('AT+CUSD=2\r', MUTEX_SUBCOMMAND);
+        }
+
+        await writeToPort(`AT+CUSD=1,"${code}",15\r`, MUTEX_SUBCOMMAND);
+    });
 };
diff --git a/lib/modem-info.js b/lib/modem-info.js
index 6a087ec..309f84c 100644
--- a/lib/modem-info.js
+++ b/lib/modem-info.js
@@ -1,5 +1,5 @@
-const MAX_LAST_DATA_AGE_MS = 2 * 60 * 1000;
-const INTERVAL_BETWEEN_IDLE_CHECK_MS = 30 * 1000;
+const DEFAULT_MAX_LAST_DATA_AGE_MS = 2 * 60 * 1000;
+const DEFAULT_INTERVAL_BETWEEN_IDLE_CHECK_MS = 30 * 1000;
 
 const config = require('komodo-sdk/config');
 const logger = require('komodo-sdk/logger');
@@ -24,11 +24,11 @@ const modemInfo = {
 if (!config.disable_idle_check) {
     setInterval(() => {
         const deltaMs = new Date() - Math.max(modemInfo.lastWriteTs, modemInfo.startTime);
-        if (deltaMs >= (config.max_last_data_age_ms || MAX_LAST_DATA_AGE_MS)) {
+        if (deltaMs >= (config.max_last_data_age_ms || DEFAULT_MAX_LAST_DATA_AGE_MS)) {
             logger.warn(`Modem idle for ${deltaMs} ms. Modem stucked? Terminating!`);
             process.exit(1);
         }
-    }, INTERVAL_BETWEEN_IDLE_CHECK_MS);
+    }, config.interval_beetwen_signal_strength_ms || DEFAULT_INTERVAL_BETWEEN_IDLE_CHECK_MS);
 }
 
 module.exports = modemInfo;
diff --git a/lib/serialport-parsers.js b/lib/serialport-parsers.js
index e684ff9..0a777f9 100644
--- a/lib/serialport-parsers.js
+++ b/lib/serialport-parsers.js
@@ -23,11 +23,34 @@ exports.getPort = function getPort() {
     return port;
 };
 
-function parsePdu(data) {
+let ussdCallback = null;
+function setUssdCallback(cb) {
+    ussdCallback = cb;
+}
+exports.setUssdCallback = setUssdCallback;
+
+function isAlphaNumeric(str) {
+    const len = str.length;
+    // eslint-disable-next-line no-plusplus
+    for (let i = 0; i < len; i++) {
+        const code = str.charCodeAt(i);
+        if (!(code > 47 && code < 58) // numeric (0-9)
+            && !(code > 64 && code < 91) // upper alpha (A-Z)
+            && !(code > 96 && code < 123)) { // lower alpha (a-z)
+            return false;
+        }
+    }
+    return true;
+}
+
+function parsePdu(_data) {
+    const data = _data && _data.toString().trim().toUpperCase();
+
     if (!data) return null;
+    if (!isAlphaNumeric(data)) return null;
 
     try {
-        const result = nodePdu.parse(data.toString().trim() || '');
+        const result = nodePdu.parse(data);
         return result;
     } catch (e) {
         return null;
@@ -112,16 +135,14 @@ parserReadline.on('data', (data) => {
     if (!data) return;
 
     const pduParsed = parsePdu(data);
-    if (pduParsed) {
-        logger.verbose('PDU parsed', { type: (typeof pduParsed.getType === 'function') && pduParsed.getType() });
-    }
-
     if (pduParsed && pduParsed.constructor.name !== 'Deliver') {
-        logger.warn('WARN-9DA32C41: Unknown PDU message type name. PLEASE REPORT IT TO DEVELOPER AT TEKTRANS', { typeName: pduParsed.constructor.name, type: pduParsed.getType(), data: data.toString().trim() });
+        const pduType = pduParsed.getType();
+        logger.warn('WARN-9DA32C41: Unknown PDU message type name. PLEASE REPORT IT TO DEVELOPER AT TEKTRANS', { typeName: pduParsed.constructor.name, pduType, data: data.toString().trim() });
     }
 
     if (pduParsed && pduParsed.constructor.name === 'Deliver' && pduParsed.getData().getSize()) {
-        logger.verbose('Got a PDU SMS-DELIVER', { type: pduParsed.getType() });
+        const pduType = pduParsed.getType();
+        logger.verbose('Got a PDU SMS-DELIVER', { pduType });
         onPduDeliver(data, pduParsed);
     } else if (isResultCodeIs(data, 'CSQ')) {
         logger.verbose('Got a signal quality report', { data: data.toString() });
@@ -133,6 +154,14 @@ parserReadline.on('data', (data) => {
         logger.verbose('Got a new message report', { data: data.toString() });
     } else if (isResultCodeIs(data, 'CMTI')) {
         logger.verbose('Got a new message notification report', { data: data.toString() });
+    } else if (isResultCodeIs(data, 'CUSD')) {
+        logger.verbose('Got a USSD command response', { data: data.toString() });
+        if (ussdCallback && typeof ussdCallback === 'function') {
+            logger.verbose('Calling USSD callback');
+            ussdCallback(data.toString());
+        } else {
+            logger.verbose('Skip unwanted USSD response');
+        }
     }
 });
 
diff --git a/modem-tester.js b/modem-tester.js
index 87b0b36..7eaadef 100644
--- a/modem-tester.js
+++ b/modem-tester.js
@@ -20,7 +20,8 @@ const port = new SerialPort(config.modem.device, { baudRate: 115200 }, async (er
         process.exit(1);
     }
 
-    await modemCommands.writeToPortAndWaitForOkOrError('\rAT\r');
+    await modemCommands.sendCtrlZ();
+    // await modemCommands.writeToPortAndWaitForOkOrError('\rAT\r');
     await modemCommands.writeToPortAndWaitForOkOrError('AT&FE0\r');
     await modemCommands.writeToPortAndWaitForOkOrError('AT+CMGF=0\r');
     await modemCommands.writeToPortAndWaitForOkOrError('AT+CNMI=1,2,0,1,0\r');
@@ -32,6 +33,12 @@ const port = new SerialPort(config.modem.device, { baudRate: 115200 }, async (er
     await modemCommands.queryCOPSAndSignalQuality();
     logger.info('Modem state', modemInfo);
 
+    // await modemCommands.sendSMS('628128364883', `coba pakai pdu ${new Date()}`);
+    // await modemCommands.sendSMS('+6282210008543', `coba pakai pdu ${new Date()}`);
+    // await modemCommands.sendSMS('6281809903333', `coba pakai pdu ${new Date()}`);
+    // const ussdResponse = await modemCommands.executeUSSD('*888#', 2);
+    // logger.info('USSD RESPONSE', { ussdResponse });
+
     setInterval(async () => {
         await modemCommands.initATCommands();
         await modemCommands.queryManufacturer();
@@ -39,7 +46,7 @@ const port = new SerialPort(config.modem.device, { baudRate: 115200 }, async (er
         await modemCommands.queryIMEIAndIMSI();
         await modemCommands.queryCOPSAndSignalQuality();
         logger.info('Modem state', modemInfo);
-    }, (config && config.interval_beetwen_signal_strength_ms) || 30000);
+    }, config.interval_beetwen_signal_strength_ms || 30000);
 });
 
 global.MODEM_PORT = port;
diff --git a/package-lock.json b/package-lock.json
index ddef173..48b71f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4155,6 +4155,11 @@
         "is-fullwidth-code-point": "^2.0.0"
       }
     },
+    "sms-pdu-node": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/sms-pdu-node/-/sms-pdu-node-0.1.2.tgz",
+      "integrity": "sha1-lyaFkBVvBYdOBNN4NpIZ6FlfhdM="
+    },
     "snapdragon": {
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -4844,9 +4849,9 @@
       "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
     },
     "uuid": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
-      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
+      "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
     },
     "validate-npm-package-license": {
       "version": "3.0.4",
diff --git a/package.json b/package.json
index 89a7093..b18a827 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
     "@serialport/parser-delimiter": "^2.0.2",
     "@serialport/parser-inter-byte-timeout": "^1.1.0",
     "@serialport/parser-readline": "^2.0.2",
+    "@serialport/parser-ready": "^2.0.2",
     "@serialport/parser-regex": "^2.0.2",
     "express": "^4.17.1",
     "komodo-sdk": "git+http://gitlab.kodesumber.com/komodo/komodo-sdk.git",
@@ -38,6 +39,8 @@
     "pdu": "^1.1.0",
     "request": "^2.88.0",
     "serialport": "^7.1.5",
-    "serialport-gsm": "^3.2.0"
+    "serialport-gsm": "^3.2.0",
+    "sms-pdu-node": "^0.1.2",
+    "uuid": "^3.3.3"
   }
 }
-- 
1.9.0