modem.js 10.6 KB
'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\n/;
const REGEX_WAIT_FOR_OK_OR_ERROR = /(?: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}"\n${msg}${Buffer.from([0x1A])}`);

    await mutex.setLockWaitForSubCommand();
    mutex.releaseLockWaitForSubCommand();

    logger.info('Message has been sent');

    setTimeout(() => {
        logger.verbose('Releasing command lock');
        mutex.releaseLockWaitForCommand();
    }, 2000);
}

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;