UNPKG

@jujax/node-quectel-ec25-sms-test

Version:
1,368 lines (1,235 loc) 38.4 kB
/* ------------------------------------------------------------------ * node-sms-transceiver - sms-transceiver.js * * Copyright (c) 2020, Futomi Hatano, All rights reserved. * Copyright (c) 2022, Julien Pelletier, All rights reserved. * Released under the MIT license * Date: 2022-08-20 * ---------------------------------------------------------------- */ 'use strict'; const EventEmitter = require('events'); const ValExp = require('node-oas-valexp'); const SmsPdu = require('node-sms-pdu'); const SmsModem = require('./sms-modem.js'); const OpenCellid = require('./open-cellid.js'); class SmsTransceiver extends EventEmitter { /* ------------------------------------------------------------------ * Constructor * * [Arguments] * - path | String | Required | System path of the serial port (e.g., "/dev/ttyMODEM0") * - options | Object | Optional | * - baudRate | Integer | Optional | Baud rate. The Default is 115200. * ---------------------------------------------------------------- */ constructor(path, options) { super(); this._modem = new SmsModem(path, options); this._concat = true; this._event_concat_messages = {}; } get path() { return this._modem.path; } get baudRate() { return this._modem.baudRate; } get concat() { return this._concat; } set concat(flag) { let valexp = new ValExp({ name: 'concat', required: true, schema: { type: 'boolean' } }); if (valexp.test(flag)) { this._concat = flag; } else { throw valexp.error; } } /* ------------------------------------------------------------------ * wait(msec) { * - Wait for the specified time (msec) * * [Arguments] * - msec | Integer | Required | Msec. * * [Returen value] * - Promise object * - Nothing will be passed to the `resolve()`. * ---------------------------------------------------------------- */ wait(msec) { return new Promise((resolve, reject) => { let valexp_msec = new ValExp({ name: 'msec', required: true, schema: { type: 'integer', minimum: 0 } }); if (!valexp_msec.test(msec)) { reject(valexp_msec.error); return; } setTimeout(resolve, msec); }); } /* ------------------------------------------------------------------ * open() * - Opens the serial port and sets the configurations * * [Arguments] * - None * * [Returen value] * - Promise object * - Nothing will be passed to the `resolve()`. * ---------------------------------------------------------------- */ open() { return (async () => { if (this._modem.isOpen === true) { return; } this._modem.once('serial-open', () => { this.emit('serial-open'); }); this._modem.once('serial-close', () => { this._modem.removeAllListeners(); this.emit('serial-close'); }); this._modem.on('serial-data', (buf) => { this.emit('serial-data', buf); }); this._modem.on('at-command', (cmd) => { this.emit('at-command', cmd); }); this._modem.on('at-response', (res) => { this.emit('at-response', res); }); this._modem.on('at-notification', (notification) => { this.emit('at-notification', notification); }); this._modem.on('sms-message', (notification) => { this._handleSmsMessageEvent(notification); }); await this._modem.open(); let cmd_list = [ 'ATE0', // Command Echo: Disable 'ATQ0', // Quiet Result Codes: Enable 'ATV1', // Response Format: full headers and trailers and verbose format of result codes 'ATS0=0', // Number Of Rings To Auto Answer: auto answer disabled 'AT+CNMI=2,1,0,0,0', // New Message Indications To Terminal Equipment: Enable indications 'AT+CMGF=0' // Message Format: PDU mode ]; for (let cmd of cmd_list) { await this._modem.execCommand(cmd); } })(); } _handleSmsMessageEvent(notification) { (async () => { let index = notification.index; let msg = await this._readMessage(index); if (!msg.concat || this._concat === false) { this.emit('sms-message', msg); return; } let key = [ msg.type, msg.from || msg.to, msg.concat.reference, msg.concat.total ].join('-'); if (!(key in this._event_concat_messages)) { let texts = []; let indexes = []; for (let i = 0; i < msg.concat.total; i++) { indexes.push(undefined); texts.push(undefined); } this._event_concat_messages[key] = { indexes: indexes, texts: texts, message: msg, total: msg.concat.total, num: 0 }; } let idx = msg.concat.sequence - 1; let cmsg = this._event_concat_messages[key]; cmsg.indexes[idx] = msg.index; cmsg.texts[idx] = msg.text; cmsg.num++; if (cmsg.num === cmsg.total) { cmsg.message.text = cmsg.texts.join(''); delete cmsg.message.concat.sequence; cmsg.message.concat.indexes = cmsg.indexes; this.emit('sms-message', cmsg.message); delete this._event_concat_messages[key]; return; } })().catch((error) => { console.error(error); }); } /* ------------------------------------------------------------------ * close() * - Close the serial port * * [Arguments] * - None * * [Returen value] * - Promise object * - Nothing will be passed to the `resolve()`. * ---------------------------------------------------------------- */ close() { return this._modem.close(); } /* ------------------------------------------------------------------ * execCommand(cmd, options) * - Execute an AT command * * [Arguments] * - cmd | String | Required | AT command (e.g., "AT+COPS?") * - options | Object | Optional | * - timeout | Integer | Optional | Response timeout in msec. * | | | The value must be in the range of 1 to 60000. * | | | The default value is 10000. * - terminator | Integer | Optional | Terminator character of AT command. * | | | The default value is 0x0D (Carriage return). * * [Returen value] * - Promise object * - A message coming from the modem will be passed to the `resolve()`. * - Even if the modem returns an error, the `resolve()` will be called. * ---------------------------------------------------------------- */ execCommand(cmd, options) { return this._modem.execCommand(cmd, options); } /* ------------------------------------------------------------------ * getModemInfo() * - Get the information of the modem * * [Arguments] * - None * * [Returen value] * - Promise object * - An object will be passed to the `resolve()`: * { * "manufacturer": "SIMCOM INCORPORATED", * "model": "SIMCOM_SIM7100JC", * "revision": "4534B05SIM7100JC", * "serial": "866803000000000" * } * ---------------------------------------------------------------- */ getModemInfo() { return (async () => { // Manufacturer Identification let cgmi_res = await this.execCommand('AT+CGMI'); let cgmi = this._trimLastOkLine(cgmi_res); // Model Identification let cgmm_res = await this.execCommand('AT+CGMM'); let cgmm = this._trimLastOkLine(cgmm_res); // Revision Identification let cgmr_res = await this.execCommand('AT+CGMR'); cgmr_res = this._trimLastOkLine(cgmr_res); let cgmr = cgmr_res.replace(/^\+CGMR\:\s*/, ''); // Product Serial Number Identification let cgsn_res = await this.execCommand('AT+CGSN'); let cgsn = this._trimLastOkLine(cgsn_res); let info = { manufacturer: cgmi, model: cgmm, revision: cgmr, serial: cgsn }; return info; })(); } /* ------------------------------------------------------------------ * getNetworkInfo() * - Get the information of the network * * [Arguments] * - None * * [Returen value] * - Promise object * - An object will be passed to the `resolve()`: * { * "subscriber": "08000000000", * "operator": { * "name": "NTT DOCOMO NTT DOCOMO", * "mcc": 440, // Mobile country code * "mnc": 10 // Mobile network code * }, * "pdpContexts": [ // PDP (Packet Data Protocol) contexts * { * "id": "1", * "type": "IP", // PDP type ("IP" or "PPP") * "apn": "lte.exemple.ne.jp", // Access Point Name * "active": true, * "addr": "0.0.0.0", // PDP address * } * ] * } * ---------------------------------------------------------------- */ getNetworkInfo() { return (async () => { // Subscriber Number let cnum_res = await this.execCommand('AT+CNUM'); cnum_res = this._trimLastOkLine(cnum_res); let cnum_rows = this._parseCsvResponseLine('CNUM', cnum_res); let cnum = ''; if (cnum_rows && cnum_rows.length >= 2) { cnum = cnum_rows[1]; } // Operator Selection let operator = { name: '', mcc: 0, // Mobile country code mnc: 0 // Mobile network code }; await this.execCommand(`AT+COPS=3,0`); let cops0_res = await this.execCommand('AT+COPS?'); let cops0_data = this._parseCops(cops0_res); if (cops0_data) { operator.name = cops0_data.oper; } await this.execCommand(`AT+COPS=3,2`); let cops2_res = await this.execCommand('AT+COPS?'); let cops2_data = this._parseCops(cops2_res); if (cops2_data) { operator.mcc = parseInt(cops2_data.oper.substring(0, 3), 10); operator.mnc = parseInt(cops2_data.oper.substring(3, 5), 10); } // PDP contexts let cgdcont_res = await this.execCommand('AT+CGDCONT?'); let contexts = this._parseCgdcont(cgdcont_res); // PDP Context Activate Or Deactivate let cgact_res = await this.execCommand('AT+CGACT?'); let context_acts = this._parseCgact(cgact_res); for (let [cid, active] of Object.entries(context_acts)) { if (contexts[cid]) { contexts[cid].active = active; } } // Show PDP Address let cgpaddr_res = await this.execCommand('AT+CGPADDR=' + Object.keys(contexts).join(',')); let context_addrs = this._parseCgpaddr(cgpaddr_res); for (let [cid, addr] of Object.entries(context_addrs)) { if (contexts[cid]) { contexts[cid].addr = addr; } } let info = { subscriber: cnum, operator: operator, contexts: Object.values(contexts) }; return info; })(); } _parseCops(data) { let ln = this._trimLastOkLine(data); let rows = this._parseCsvResponseLine('COPS', ln); if (!rows || rows.length < 3) { return null; } let res = { mode: rows[0], format: rows[1], oper: rows[2] }; return res; } _parseCgdcont(data) { data = this._trimLastOkLine(data); let lines = data.split(/\n+/); let res = {}; for (let ln of lines) { let rows = this._parseCsvResponseLine('CGDCONT', ln); if (!rows || rows.length < 3) { continue; } let id = rows[0]; res[id] = { cid: rows[0], // context identifier type: rows[1], apn: rows[2] }; } return res; } _parseCgact(data) { data = this._trimLastOkLine(data); let lines = data.split(/\n+/); let res = {}; for (let ln of lines) { let rows = this._parseCsvResponseLine('CGACT', ln); if (!rows || rows.length < 2) { continue; } let cid = rows[0]; let active = (rows[1] === '1') ? true : false; res[cid] = active; } return res; } _parseCgpaddr(data) { data = this._trimLastOkLine(data); let lines = data.split(/\n+/); let res = {}; for (let ln of lines) { let rows = this._parseCsvResponseLine('CGPADDR', ln); if (!rows || rows.length < 2) { continue; } let cid = rows[0]; let addr = rows[1]; res[cid] = addr; } return res; } _trimLastOkLine(data) { data = data.replace(/\n+OK$/, ''); return data; } _parseCsvResponseLine(cmd, line) { let prefix_re = new RegExp('^\\+' + cmd + '\\:\\s*'); if (prefix_re.test(line) === false) { return null; } line = line.replace(prefix_re, ''); let cols = []; let is_in_quoted_col = false; let col = ''; let chars = line.split(''); for (let char of chars) { if (char === ',') { if (!is_in_quoted_col) { cols.push(col); col = ''; continue; } } else if (char === '"') { is_in_quoted_col = !is_in_quoted_col; continue; } col += char; } if (col) { cols.push(col); } return cols; } /* ------------------------------------------------------------------ * getSignalQuality() * - Get the Signal Quality * * [Arguments] * - None * * [Returen value] * - Promise object * - An object will be passed to the `resolve()`: * { * "rssi": -67, * "ber": null * } * * ber: * - 0: less than 0.2% * - 1: 0.2% to 0.4% * - 2: 0.4% to 0.8% * - 3: 0.8% to 1.6% * - 4: 1.6% to 3.2% * - 5: 3.2% to 6.4% * - 6: 6.4% to 12.8% * - 7: more than 12.8% * - null: Unknown * ---------------------------------------------------------------- */ getSignalQuality() { return (async () => { // Signal Quality let csq = await this.execCommand('AT+CSQ'); csq = this._trimLastOkLine(csq); let regexp = /^\+CSQ\:\s*/; let res = { rssi: null, ber: null }; if (regexp.test(csq)) { let [rssi, ber] = csq.replace(regexp, '').split(','); if (/^\d+$/.test(rssi)) { rssi = parseInt(rssi, 10); if (rssi === 0) { rssi = -113; // -113 dBm or less } else if (rssi >= 1 && rssi <= 30) { rssi = -113 + (rssi * 2); // in the range of -111 to -53 dBm } else if (rssi = 31) { rssi = -51; // -51 dBm or greater } else { rssi = null; // not known or not detectable } res.rssi = rssi; } if (/^\d+$/.test(ber)) { res.ber = parseInt(ber, 10); } } return res; })(); } /* ------------------------------------------------------------------ * getLocationInfo() * - Get the information of the location * * [Arguments] * - params | Object | Optional | * - opencellid | Object | Optional | * - token | String | Required | Your API token * - region | Integer | OPtional | Region code of the end point (1 - 4) * | | | The default value is 1. * - lang | String | Optional | Language code * | | | The default value is "en" * * The `region` must be one in the table below: * - 1: US East (Northern Virginia) * - 2: US West (San Francisco) * - 3: Europe (France) * - 4: Asia Pacific (Singapore) * * [Returen value] * - Promise object * - An object will be passed to the `resolve()`: * { * "mcc": 440, // Mobile country code * "mnc": 10, // Mobile network code * "lac": 4368, // location area code * "cid": 49983061 // Cell ID * } * - If the `opencellid` is specified and the `opencellid` property * will be added in the response as follows: * { * "mcc": 440, // Mobile country code * "mnc": 10, // Mobile network code * "lac": 4368, // location area code * "cid": 49983061, // Cell ID * "opencellid": { * "status": "ok", * "balance": 4999, * "lat": 35.814246, * "lon": 139.395624, * "accuracy": 905, * "message": "This cell tower was not found in OpenCelliD. ...", * "address": "Iruma, Saitama Prefecture, 358-0011, Japan" * } * } * ---------------------------------------------------------------- */ getLocationInfo(params = {}) { return (async () => { // Check the `params` if (typeof (params) !== 'object') { throw new Error('The `params` must be an object.'); } // Set the presentation of an unsolicited result code of `AT +CREG?` let creg2_res = await this.execCommand('AT+CREG=2'); if (/(^|\n)OK/.test(creg2_res) === false) { throw new Error('Unexpected response: ' + creg2_res); } // Get the location let creg_res = await this.execCommand('AT+CREG?'); let creg_res_line = this._trimLastOkLine(creg_res); let rows = this._parseCsvResponseLine('CREG', creg_res_line); if (!rows || rows.length < 4) { return null; } let lac_hex = rows[2]; let cid_hex = rows[3]; if (!lac_hex || /^[0-9A-Fa-f]{4}$/.test(lac_hex) === false) { throw new Error('Unexpected response: ' + creg_res_line); } if (!cid_hex || /^[0-9A-Fa-f]+$/.test(cid_hex) === false) { throw new Error('Unexpected response: ' + creg_res_line); } let lac = parseInt(lac_hex, 16); let cid = parseInt(cid_hex, 16); // Operator Selection await this.execCommand(`AT+COPS=3,2`); let cops2_res = await this.execCommand('AT+COPS?'); let cops2_data = this._parseCops(cops2_res); if (!cops2_data) { throw new Error('Failed to execute the command: AT+COPS?'); } let mcc = parseInt(cops2_data.oper.substring(0, 3), 10); let mnc = parseInt(cops2_data.oper.substring(3, 5), 10); let info = { mcc: mcc, // Mobile country code mnc: mnc, // Mobile network code lac: lac, // location area code cid: cid // Cell ID }; // OpenCelliD if (params.opencellid) { let poci = params.opencellid; let newp = { token: poci.token, }; if (poci.region) { newp.region = poci.region; } if (poci.lang) { newp.lang = poci.lang; } let opencellid = new OpenCellid(newp); let res = await opencellid.geolocationCell1Gsm({ mcc: mcc, mnc: mnc, lac: lac, cid: cid, }); info.opencellid = res; } return info; })(); } /* ------------------------------------------------------------------ * getMessageStorage() * - Get the preferred message storage * * [Arguments] * - None * * [Returen value] * - Promise object * - An object will be passed to the `resolve()`: * { * "r": { * "mem": "ME", * "total": 8, * "used": 23 * }, * "w": { * "mem": "ME", * "total": 8, * "used": 23 * }, * "s": { * "mem": "ME", * "total": 8, * "used": 23 * } * } * ---------------------------------------------------------------- */ getMessageStorage() { return (async () => { // Preferred Message Storage let cpms_res = await this.execCommand('AT+CPMS?'); cpms_res = this._trimLastOkLine(cpms_res); let rows = this._parseCsvResponseLine('CPMS', cpms_res); if (!rows || rows.length < 9) { throw new Error('Unexpected response: ' + cpms_res); } let res = { r: { mem: '', used: 0, total: 0 }, w: { mem: '', used: 0, total: 0 }, s: { mem: '', used: 0, total: 0 } }; ['r', 'w', 's'].forEach((k, i) => { // memr, memw, mems res[k].mem = rows[i * 3 + 0]; // usedr, usedw, useds res[k].used = parseInt(rows[i * 3 + 1], 10); // totalr, totalw, totals res[k].total = parseInt(rows[i * 3 + 2], 10); }); return res; })(); } /* ------------------------------------------------------------------ * setMessageStorage(memr, memw, mems) * - Set the preferred message storage * * [Arguments] * - memr | String | Required | Memory type for reading messages. * - memw | String | Optional | Memory type for writing and sending messages. * - mems | String | Optional | Memory type for storing received messages. * * Each argument must be "SM" or "ME" if specified. * If the `memw` and `mems` are not specified, the values are set to the value of the `memr`. * * [Returen value] * - Promise object * - An object will be passed to the `resolve()`: * { * "r": { * "mem": "ME", * "total": 8, * "used": 23 * }, * "w": { * "mem": "ME", * "total": 8, * "used": 23 * }, * "s": { * "mem": "ME", * "total": 8, * "used": 23 * } * } * ---------------------------------------------------------------- */ setMessageStorage(memr, memw, mems) { return (async () => { // Check the `memr`, `memw`, `mems` if (!memr) { throw new Error('The `memr` is required.'); } let params = { memr: memr, memw: memw || memr, mems: mems || memr }; for (let [k, v] of Object.entries(params)) { let valexp = new ValExp({ name: k, schema: { type: 'string', enum: ['SM', 'ME'] } }); if (!valexp.test(v)) { throw valexp.error; } } // Preferred Message Storage let command = `AT+CPMS="${memr}","${memw}","${mems}"`; let cpms = await this.execCommand(command); if (/\nOK/.test(cpms) === false) { throw new Error('Unexpected response: ' + cpms); } let res = await this.getMessageStorage(); return res; })(); } /* ------------------------------------------------------------------ * listMessages(options) * - List messages stored in the selected message storage * * [Arguments] * - options | Object | Optional | * - stat | Integer | Optional | Message status * | | | See the description below * * The `stat` must be one in the table below: * - 0: new message * - 1: read message * - 2: stored message not yet sent * - 3: stored message already sent * - 4: all messages (Default) * * [Returen value] * - Promise object * - An array will be passed to the `resolve()`: * [ * { * "index": 0, * "stat": 1, * "concat": null; * "number": "09000000000", * "text": "Hello", * "pdus": [{ * "smsc": "+8190000000000", * "type": "SMS-DELIVER", * "origination": "09000000000", * "timestamp": "2020-02-21T14:07:06+09:00", * "concat": null, * "text": "Hello" * }: * } * ] * ---------------------------------------------------------------- */ listMessages(options = {}) { return (async () => { let valexp_options = new ValExp({ name: 'options', schema: { type: 'object', properties: { stat: { type: 'integer', enum: [0, 1, 2, 3, 4], default: 4 } } } }); let valexp_options_res = valexp_options.exec(options); if (!valexp_options_res) { throw valexp_options.error; } options = valexp_options_res[0]; // List Messages let command = 'AT+CMGL=' + options.stat.toString(); let cmgl = await this.execCommand(command); if (/(^|\n)OK/.test(cmgl) === false) { throw new Error('Unexpected response: ' + cmgl); } let message_list = this._parseMessageList(cmgl); return message_list; })(); } _parseMessageList(cmgl) { let line_list = cmgl.split(/\n+/); let message_list = []; let current_message = null; let concat_num = 0; for (let line of line_list) { if (/^\+CMGL\:/.test(line)) { let rows = this._parseCsvResponseLine('CMGL', line); if (rows.length < 2) { continue; } let index = parseInt(rows[0], 10); let stat = parseInt(rows[1], 10); current_message = { index: index, stat: stat, type: '', from: null, to: null, date: null, concat: null, text: '' }; } else if (current_message && /^[A-F0-9]+$/.test(line)) { let pdu = SmsPdu.parse(line); delete pdu.details; current_message.type = pdu.type; if (pdu.type === 'SMS-DELIVER') { current_message.from = pdu.origination; current_message.date = pdu.timestamp; } else if (pdu.type === 'SMS-SUBMIT') { current_message.to = pdu.destination; } if (pdu.concat) { current_message.concat = { reference: pdu.concat.reference, sequence: pdu.concat.sequence, total: pdu.concat.total }; concat_num++; } current_message.text = pdu.text; message_list.push(current_message); } else { current_message = null; } } // Sort the messages by `index` message_list.sort((a, b) => { return (a.index > b.index) ? 1 : -1; }); // Merge concatinated messages if (this._concat === true && concat_num > 0) { message_list = this._mergeMessageList(message_list); } return message_list; } _mergeMessageList(message_list) { let merged_list = []; let concat_msg_map = {}; for (let msg of message_list) { if (!msg.concat) { merged_list.push(msg); continue; } let key = [ msg.type, msg.from || msg.to, msg.concat.reference, msg.concat.total ].join('-'); if (!(key in concat_msg_map)) { let texts = []; let indexes = []; for (let i = 0; i < msg.concat.total; i++) { indexes.push(undefined); texts.push(undefined); } concat_msg_map[key] = { indexes: indexes, texts: texts, message: msg, total: msg.concat.total, num: 0 }; merged_list.push(msg); } let idx = msg.concat.sequence - 1; concat_msg_map[key].indexes[idx] = msg.index; concat_msg_map[key].texts[idx] = msg.text; concat_msg_map[key].num++; if (concat_msg_map[key].num === concat_msg_map[key].total) { let message = concat_msg_map[key].message; message.text = concat_msg_map[key].texts.join(''); delete message.concat.sequence; message.concat.indexes = concat_msg_map[key].indexes; delete concat_msg_map[key]; } } for (let concat_msg of Object.values(concat_msg_map)) { let merged_text = ''; for (let txt of concat_msg.texts) { if (txt === undefined) { txt = '[?]' } merged_text += txt; } concat_msg.message.text = merged_text; delete concat_msg.message.concat.sequence; concat_msg.message.concat.indexes = concat_msg.indexes; } return merged_list; } /* ------------------------------------------------------------------ * readMessage(index) * - Read a message stored in the currnet message storage * * [Arguments] * - index | integer | Required | index number in the current message storage * * [Returen value] * - Promise object * - An object will be passed to the `resolve()`: * { * "index": 0, * "stat": 1, * "data": { * "smsc": "+8190000000000", * "type": "SMS-DELIVER", * "origination": "09000000000", * "timestamp": "2020-02-21T14:07:06+09:00", * "concat": null, * "text": "Hello" * } * } * - If the specified `index` is not available, `null` will be passed * to the `resolve()`. * ---------------------------------------------------------------- */ readMessage(index) { return (async () => { let msg = await this._readMessage(index); if (!msg) { return null; } if (!msg.concat || this._concat === false) { return msg; } let message_list = await this.listMessages(); let target_message = null; for (let message of message_list) { if (message.index === index) { target_message = message; break; } } return target_message; })(); } _readMessage(index) { return (async () => { let valexp_index = new ValExp({ name: 'index', required: true, schema: { type: 'integer', minimum: 0 } }); if (!valexp_index.test(index)) { throw valexp_index.error; } // Read Message let command = 'AT+CMGR=' + index.toString(); let cmgr_res = await this.execCommand(command); if (/(^|\n)OK/.test(cmgr_res) === false) { throw new Error('Unexpected response: ' + cmgr_res); } cmgr_res = this._trimLastOkLine(cmgr_res); let lines = cmgr_res.split(/\n+/); if (/^\+CMGR\:/.test(lines[0]) === false || /^[A-F0-9]+$/.test(lines[1]) === false) { return null; } let rows = this._parseCsvResponseLine('CMGR', lines[0]); let stat = parseInt(rows[0], 10); let pdu = SmsPdu.parse(lines[1]); let msg = { index: index, stat: stat, type: pdu.type, from: null, to: null, date: null, concat: null, text: pdu.text }; if (pdu.concat) { msg.concat = { reference: pdu.concat.reference, sequence: pdu.concat.sequence, total: pdu.concat.total }; } if (pdu.type === 'SMS-DELIVER') { msg.from = pdu.origination; msg.date = pdu.timestamp; } else if (pdu.type === 'SMS-SUBMIT') { msg.to = pdu.destination; } return msg; })(); } /* ------------------------------------------------------------------ * deleteMessage(index) * - Delete a message stored in the currnet message storage * * [Arguments] * - index | Integer | Required | Index number in the current message storage. * * [Returen value] * - Promise object * - An object will be passed to the `resolve()`: * { * "index": 0, * "stat": 1, * "data": { * "smsc": "+8190000000000", * "type": "SMS-DELIVER", * "origination": "09000000000", * "timestamp": "2020-02-21T14:07:06+09:00", * "concat": null, * "text": "Hello" * } * } * - If the specified `index` is not available, `null` will be passed * to the `resolve()`. * ---------------------------------------------------------------- */ deleteMessage(index) { return (async () => { let valexp_index = new ValExp({ name: 'index', required: true, schema: { type: 'integer', minimum: 0 } }); if (!valexp_index.test(index)) { throw valexp_index.error; } // Read Message let msg = await this.readMessage(index); if (!msg) { return null; } // Delete Message // - in the case that the message is not a concatenated message // or the concat mode is disabled if (!msg.concat || this._concat === false) { let command = 'AT+CMGD=' + index.toString(); let cmgd = await this.execCommand(command); if (/(^|\n)OK/.test(cmgd) === false) { throw new Error('Unexpected response: ' + cmgr); } return msg; } // Delete Messages // - in the case that the message is a concatenated message // and the concat mode is enabled let message_list = await this.listMessages(); let target_message = null; for (let message of message_list) { if (message.index === index) { target_message = message; break; } } if (!target_message) { return null; } for (let idx of target_message.concat.indexes) { let command = 'AT+CMGD=' + idx.toString(); let cmgd = await this.execCommand(command); if (/(^|\n)OK/.test(cmgd) === false) { throw new Error('Unexpected response: ' + cmgr); } } return target_message; })(); } /* ------------------------------------------------------------------ * deleteAllMessages() * - Delete all messages stored in the currnet message storage * * [Arguments] * - None * * [Returen value] * - Promise object * - Nothing will be passed to the `resolve()`: * ---------------------------------------------------------------- */ deleteAllMessages() { return (async () => { // Delete All Messages let command = 'AT+CMGD=0,4'; let cmgd = await this.execCommand(command); if (/(^|\n)OK/.test(cmgd)) { return; } else { throw new Error('Unexpected response: ' + cmgr); } })(); } /* ------------------------------------------------------------------ * sendMessage(dest, text) * - Send a message to the specified telephone number * * [Arguments] * - dest | String | Required | Destination telephone number * - text | String | Required | Message text * * [Returen value] * - Promise object * - Nothing will be passed to the `resolve()`: * ---------------------------------------------------------------- */ sendMessage(dest, text) { return (async () => { // Create PDUs let pdu_list = this._generateSubmitPdus(dest, text); // Send Message let err = ''; for (let pdu of pdu_list) { let command = 'AT+CMGS=' + pdu.length; let cmgs = await this.execCommand(command); if (/^\>/.test(cmgs)) { let result = await this.execCommand(pdu.hex, { terminator: 0x1A // `0x1A` means "Ctrl+z" }); if (/(^|\n)OK/.test(result) === false) { err = 'Unexpected response: ' + result; break; } } else { err = 'Unexpected response: ' + cmgs; break; } } if (err) { throw new Error(err); } return; })(); } _generateSubmitPdus(dest, text) { let valexp_dest = new ValExp({ name: 'dest', required: true, schema: { type: 'string', pattern: /^\+?\d+$/ } }); if (!valexp_dest.test(dest)) { throw valexp_dest.error; } let valexp_text = new ValExp({ name: 'text', required: true, schema: { type: 'string', minLength: 1 } }); if (!valexp_text.test(text)) { throw valexp_text.error; } // Create PDUs let pdu_list = SmsPdu.generateSubmit(dest, text); return pdu_list; } /* ------------------------------------------------------------------ * writeSubmitMessage(dest, text) * - Write a SMS-SUBMIT message to the selected storage * * [Arguments] * - dest | String | Required | Destination telephone number * - text | String | Required | Message text * * [Returen value] * - Promise object * - A list of the stored message index number will be passed to the `resolve()`: * ---------------------------------------------------------------- */ writeSubmitMessage(dest, text) { return (async () => { // Create PDUs let pdu_list = this._generateSubmitPdus(dest, text); // Write the PDUs to the selected storage let err = ''; let index_list = []; for (let pdu of pdu_list) { let command = 'AT+CMGW=' + pdu.length.toString() + ',2'; let cmgw_res = await this.execCommand(command); if (/^\>/.test(cmgw_res) === false) { err = 'Unexpected response: ' + cmgw_res; break; } let result = await this.execCommand(pdu.hex, { terminator: 0x1A // `0x1A` means "Ctrl+z" }); if (/(^|\n)OK/.test(result) === false) { err = 'Unexpected response: ' + result; break; } result = this._trimLastOkLine(result); let rows = this._parseCsvResponseLine('CMGW', result); if (!rows || rows.length < 1 || /^\d+$/.test(rows[0]) === false) { err = 'Unexpected response: ' + result; break; } let parsed_pud = SmsPdu.parse(pdu.hex); delete parsed_pud.details; index_list.push(parseInt(rows[0], 10)); } if (err) { throw new Error(err); } return index_list; })(); } /* ------------------------------------------------------------------ * sendStoredMessage(index) * - Send a SMS-SUBMIT message stored in the selected storage * * [Arguments] * - index | Integer | Required | Message index number or * * [Returen value] * - Promise object * - Nothing will be passed to the `resolve()`: * ---------------------------------------------------------------- */ sendStoredMessage(index) { return (async () => { // Check the `index` let valexp_index = new ValExp({ name: 'index', required: true, schema: { type: 'integer', minimum: 0 } }); if (!valexp_index.test(index)) { throw valexp_index.error; } // Read the message let message = await this.readMessage(index); if (!message) { throw new Error('The `index` was not found.'); } let index_list = [index]; if (message.concat && this._concat === true) { index_list = message.concat.indexes; } // Send the message for (let idx of index_list) { let command = 'AT+CMSS=' + idx.toString(); let cmss_res = await this.execCommand(command); if (/(^|\n)OK/.test(cmss_res) === false) { throw new Error('Unexpected response: ' + cmss_res); } } })(); } } module.exports = SmsTransceiver;