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