'use strict';
const INTERVAL_BEETWEN_SIGNAL_STRENGTH_MS = 60000;
const MAX_LAST_DATA_AGE_MS = 3 * 60 * 1000;
const REGEX_WAIT_FOR_OK_OR_ERROR = /\n(?:OK|ERROR)\r/;
// const REGEX_WAIT_FOR_OK_OR_ERROR_USSD = /\n(?:OK|ERROR)\r/;
const moment = require('moment');
const SerialPort = require('serialport');
const ParserReadline = require('@serialport/parser-readline');
// const ParserDelimiter = require('@serialport/parser-delimiter');
const ParserRegex = require('@serialport/parser-regex');
const config = require('komodo-sdk/config');
const logger = require('komodo-sdk/logger');
const mutex = require('./mutex');
const common = require('./common');
const sms = require('./sms');
const dbCops = require('./db-cops');
const reportSender = require('./report-sender');
// const msisdn = require('./msisdn');
const registerModem = require('./register-modem');
// const counters = require('./counters');
const modemInfo = {
device: config.modem.device,
manufacturer: null,
model: null,
imei: null,
imsi: null,
msisdn: null,
cops: null,
networkId: null,
networkName: null,
signalStrength: null,
signalStrengthTs: null,
signalStrengthTsReadable: null,
// messageSentCounter: null,
// messageReceivedCounter: null,
};
let lastTs = new Date();
let port;
const parserReadLine = new ParserReadline();
const parserWaitForOK = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
parserWaitForOK.on('data', () => {
mutex.releaseLockWaitForCommand();
});
function writeToPort(data) {
return new Promise((resolve) => {
port.write(data, (err, bytesWritten) => {
if (err) logger.warn(`ERROR: ${err.toString()}`);
logger.verbose(`* OUT: ${data}`);
resolve(bytesWritten);
});
});
}
async function readSMS(slot) {
const parserCMGR = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
parserCMGR.on('data', (data) => {
if (data) {
try {
reportSender.incomingSMS(sms.extract(data.toString().trim()), modemInfo);
} catch (e) {
logger.warn(`Exception on reporting new message. ${e.toString()}`, { smsObj: e.smsObj, dataFromModem: data });
process.exit(0);
}
}
port.unpipe(parserCMGR);
mutex.releaseLockWaitForCommand();
});
// const parserCMGD = new ParserDelimiter({ delimiter: DELIMITER_WAIT_FOR_OK });
const parserCMGD = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
parserCMGD.on('data', () => {
port.unpipe(parserCMGD);
mutex.releaseLockWaitForCommand();
});
logger.info(`Reading SMS on slot ${slot}`);
await mutex.setLockWaitForCommand();
port.pipe(parserCMGR);
await writeToPort(`AT+CMGR=${slot}\r`);
logger.info(`Finished reading SMS on slot ${slot}`);
logger.info(`Deleting message on slot ${slot}`);
await mutex.setLockWaitForCommand();
port.pipe(parserCMGD);
await writeToPort(`AT+CMGD=${slot}\r`);
logger.info('Message processing has completed');
}
function onIncomingSMS(data) {
const value = common.extractValueFromReadLineData(data);
if (!value) return;
const chunks = value.split(',');
if (!chunks && !chunks[1]) return;
const slot = chunks[1];
logger.info(`Incoming SMS on slot ${slot}`);
readSMS(slot);
}
function onCOPS(data) {
modemInfo.cops = common.extractValueFromReadLineData(data).trim();
logger.info(`Connected Network: ${modemInfo.cops}`);
if (!modemInfo.cops) return;
[, , modemInfo.networkId] = modemInfo.cops.split(',');
if (modemInfo.networkId) {
modemInfo.networkName = dbCops[modemInfo.networkId] || modemInfo.networkId;
}
}
parserReadLine.on('data', (data) => {
logger.verbose(`* IN: ${data}`);
if (data) {
lastTs = new Date();
if (data.indexOf('+CSQ: ') === 0) {
const signalStrength = common.extractValueFromReadLineData(data).trim();
if (signalStrength) {
modemInfo.signalStrength = signalStrength;
modemInfo.signalStrengthTs = new Date();
modemInfo.signalStrengthTsReadable = moment(modemInfo.signalStrengthTs).format('YYYY-MM-DD HH:mm:ss');
logger.info(`Signal strength: ${modemInfo.signalStrength}`);
registerModem(modemInfo);
}
} else if (data.indexOf('+CMTI: ') === 0) {
// counters.increment('MESSAGE_RECEIVED', modemInfo);
onIncomingSMS(data);
} else if (data.indexOf('+COPS: ') === 0) {
onCOPS(data);
}
}
});
async function simpleSubCommand(cmd, callback) {
const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
parser.on('data', (data) => {
port.unpipe(parser);
mutex.releaseLockWaitForSubCommand();
if (data) {
if (callback) callback(null, data.toString().trim());
}
});
return new Promise(async (resolve) => {
await mutex.setLockWaitForSubCommand();
port.pipe(parser);
writeToPort(cmd);
await mutex.setLockWaitForSubCommand();
mutex.releaseLockWaitForSubCommand();
resolve();
});
}
function readManufacturer() {
return new Promise((resolve) => {
simpleSubCommand('AT+CGMI\r', (err, result) => {
modemInfo.manufacturer = result;
logger.info(`Manufacturer: ${result}`);
resolve(result);
});
});
}
function readModel() {
return new Promise((resolve) => {
simpleSubCommand('AT+CGMM\r', (err, result) => {
modemInfo.model = result;
logger.info(`Model: ${result}`);
resolve(result);
});
});
}
function readIMEI() {
return new Promise((resolve) => {
simpleSubCommand('AT+CGSN\r', (err, result) => {
modemInfo.imei = result;
logger.info(`IMEI: ${result}`);
resolve(result);
});
});
}
function readIMSI() {
return new Promise((resolve) => {
simpleSubCommand('AT+CIMI\r', (err, result) => {
modemInfo.imsi = result;
logger.info(`IMSI: ${result}`);
if (result) {
/*
modemInfo.msisdn = msisdn[result];
if (modemInfo.msisdn) {
logger.info(`MSISDN: ${modemInfo.msisdn}`);
registerModem(modemInfo);
}
*/
} else {
logger.warn(`IMSI not detected. Please insert a sim card to your modem. Terminating ${config.modem.device}.`);
process.exit(2);
}
resolve(result);
});
});
}
function readCOPS() {
return new Promise((resolve) => {
simpleSubCommand('AT+COPS?\r', (err, result) => {
resolve(result);
});
});
}
function deleteInbox() {
return new Promise((resolve) => {
simpleSubCommand('AT+CMGD=0,4\r', (err, result) => {
resolve(result);
});
});
}
async function querySignalStrength() {
const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
parser.on('data', () => {
port.unpipe(parser);
mutex.releaseLockWaitForCommand();
});
if (mutex.tryLockWaitForCommand()) {
port.pipe(parser);
await writeToPort('AT+CSQ\r');
}
}
function registerModemToCenterPeriodically() {
registerModem(modemInfo);
setInterval(() => {
registerModem(modemInfo);
}, 60 * 1000);
}
async function registerSignalStrengthBackgroundQuery() {
logger.info('Registering background signal strength query');
querySignalStrength();
setInterval(() => {
querySignalStrength();
}, config.interval_beetwen_signal_strength_ms || INTERVAL_BEETWEN_SIGNAL_STRENGTH_MS);
}
async function sendSMS(destination, msg) {
if (typeof destination !== 'string' || typeof msg !== 'string' || !destination.trim() || !msg.trim()) return;
const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
parser.on('data', () => {
port.unpipe(parser);
mutex.releaseLockWaitForSubCommand();
});
logger.verbose('Waiting for command lock to send message');
await mutex.setLockWaitForCommand();
logger.info('Sending message', { destination, msg });
// counters.increment('MESSAGE_SENT', modemInfo);
const correctedDestination = `+${destination}`.replace(/^0/, '62').replace(/^\++/, '+');
logger.verbose('Waiting for lock before set to text mode');
await mutex.setLockWaitForSubCommand();
port.pipe(parser);
await writeToPort('AT+CMGF=1\r');
logger.verbose('Waiting for lock before writing message');
await mutex.setLockWaitForSubCommand();
port.pipe(parser);
await writeToPort(`AT+CMGS="${correctedDestination}"\r${msg}${Buffer.from([0x1A])}`);
await mutex.setLockWaitForSubCommand();
mutex.releaseLockWaitForSubCommand();
logger.info('Message has been sent');
setTimeout(() => {
logger.verbose('Releasing command lock');
mutex.releaseLockWaitForCommand();
}, config.wait_for_release_lock_wait_for_command_ms || 2000);
}
/**
* Ekseksusi kode USSD.
*
* Pilihan includeCUSD2:
* -1: sebelum
* 0: tidak (default)
* 1: sesudah
* 2: sebelum dan sesudah
*
* @param {string} code - Kode USSD
* @param {number} [includeCUSD2=0] - Apakah ingin otomatis memasukkan CUSD=2
*/
function executeUSSD(code, includeCUSD2) {
return new Promise(async (resolve) => {
const parserMain = new ParserReadline({ delimiter: '\r\n' });
// const parserMain = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
parserMain.on('data', (data) => {
port.unpipe(parserMain);
mutex.releaseLockWaitForSubCommand();
resolve(data);
});
const parserCUSD2 = new ParserReadline({ delimiter: '\r\n' });
// const parserCUSD2 = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
parserCUSD2.on('data', () => {
port.unpipe(parserCUSD2);
mutex.releaseLockWaitForSubCommand();
});
logger.verbose('Waiting for command lock to execute USSD');
await mutex.setLockWaitForCommand();
if (includeCUSD2 === -1 || includeCUSD2 === 2) {
logger.info('Terminating existing USSD session');
await mutex.setLockWaitForSubCommand();
port.pipe(parserCUSD2);
await writeToPort('AT+CUSD=2\r');
}
await mutex.setLockWaitForSubCommand();
port.pipe(parserMain);
await writeToPort(`AT+CUSD=1,"${code}",15\r`);
if (includeCUSD2 === 1 || includeCUSD2 === 2) {
logger.info('Terminating existing USSD session');
await mutex.setLockWaitForSubCommand();
port.pipe(parserCUSD2);
await writeToPort('AT+CUSD=2\r');
}
await mutex.setLockWaitForSubCommand();
mutex.releaseLockWaitForSubCommand();
mutex.releaseLockWaitForCommand();
});
}
function init() {
port = new SerialPort(config.modem.device, { baudRate: 115200 }, (err) => {
if (err) {
logger.warn(`Error opening modem. ${err}. Terminating modem ${config.modem.device}.`);
process.exit(1);
}
registerModem(modemInfo);
});
port.pipe(parserReadLine);
setInterval(() => {
if ((new Date() - lastTs) > MAX_LAST_DATA_AGE_MS) {
logger.warn(`No data for more than ${MAX_LAST_DATA_AGE_MS} ms. Modem might be unresponsive. Terminating modem ${config.modem.device}.`);
process.exit(0);
}
}, 30 * 1000);
port.on('open', async () => {
await mutex.setLockWaitForCommand();
logger.info('Modem opened');
await writeToPort('\r');
await simpleSubCommand('AT\r');
logger.info('Initializing modem to factory set');
await simpleSubCommand('AT&F\r');
logger.info('Disabling echo');
await simpleSubCommand('ATE0\r');
logger.info('Set to text mode');
await simpleSubCommand('AT+CMGF=1\r');
await readCOPS();
await readManufacturer();
await readModel();
await readIMEI();
await readIMSI();
if (!config.disable_delete_inbox_on_startup) {
logger.info('Deleting existing messages');
await deleteInbox();
}
mutex.releaseLockWaitForCommand();
logger.verbose('Init completed');
registerModemToCenterPeriodically();
registerSignalStrengthBackgroundQuery();
});
}
init();
exports.modemInfo = modemInfo;
exports.sendSMS = sendSMS;
exports.executeUSSD = executeUSSD;