Commit 8ebd9e2579e99d9f3a721d643514b16b8efc8784

Authored by Adhidarma Hadiwinoto
1 parent 75b30422e9
Exists in master

Coba delimiter \n

Showing 1 changed file with 1 additions and 1 deletions Inline Diff

1 'use strict'; 1 'use strict';
2 2
3 const INTERVAL_BEETWEN_SIGNAL_STRENGTH_MS = 60000; 3 const INTERVAL_BEETWEN_SIGNAL_STRENGTH_MS = 60000;
4 const MAX_LAST_DATA_AGE_MS = 3 * 60 * 1000; 4 const MAX_LAST_DATA_AGE_MS = 3 * 60 * 1000;
5 const REGEX_WAIT_FOR_OK_OR_ERROR = /[\r\n]+(?:OK|ERROR)\r/; 5 const REGEX_WAIT_FOR_OK_OR_ERROR = /\n(?:OK|ERROR)\r/;
6 6
7 const moment = require('moment'); 7 const moment = require('moment');
8 const SerialPort = require('serialport'); 8 const SerialPort = require('serialport');
9 const ParserReadline = require('@serialport/parser-readline'); 9 const ParserReadline = require('@serialport/parser-readline');
10 // const ParserDelimiter = require('@serialport/parser-delimiter'); 10 // const ParserDelimiter = require('@serialport/parser-delimiter');
11 11
12 const ParserRegex = require('@serialport/parser-regex'); 12 const ParserRegex = require('@serialport/parser-regex');
13 13
14 const config = require('komodo-sdk/config'); 14 const config = require('komodo-sdk/config');
15 const logger = require('komodo-sdk/logger'); 15 const logger = require('komodo-sdk/logger');
16 16
17 const mutex = require('./mutex'); 17 const mutex = require('./mutex');
18 const common = require('./common'); 18 const common = require('./common');
19 const sms = require('./sms'); 19 const sms = require('./sms');
20 const dbCops = require('./db-cops'); 20 const dbCops = require('./db-cops');
21 const reportSender = require('./report-sender'); 21 const reportSender = require('./report-sender');
22 // const msisdn = require('./msisdn'); 22 // const msisdn = require('./msisdn');
23 const registerModem = require('./register-modem'); 23 const registerModem = require('./register-modem');
24 // const counters = require('./counters'); 24 // const counters = require('./counters');
25 25
26 const modemInfo = { 26 const modemInfo = {
27 device: config.modem.device, 27 device: config.modem.device,
28 manufacturer: null, 28 manufacturer: null,
29 model: null, 29 model: null,
30 imei: null, 30 imei: null,
31 imsi: null, 31 imsi: null,
32 msisdn: null, 32 msisdn: null,
33 cops: null, 33 cops: null,
34 networkId: null, 34 networkId: null,
35 networkName: null, 35 networkName: null,
36 signalStrength: null, 36 signalStrength: null,
37 signalStrengthTs: null, 37 signalStrengthTs: null,
38 signalStrengthTsReadable: null, 38 signalStrengthTsReadable: null,
39 // messageSentCounter: null, 39 // messageSentCounter: null,
40 // messageReceivedCounter: null, 40 // messageReceivedCounter: null,
41 }; 41 };
42 42
43 let lastTs = new Date(); 43 let lastTs = new Date();
44 44
45 let port; 45 let port;
46 46
47 const parserReadLine = new ParserReadline(); 47 const parserReadLine = new ParserReadline();
48 48
49 const parserWaitForOK = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR }); 49 const parserWaitForOK = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
50 parserWaitForOK.on('data', () => { 50 parserWaitForOK.on('data', () => {
51 mutex.releaseLockWaitForCommand(); 51 mutex.releaseLockWaitForCommand();
52 }); 52 });
53 53
54 function writeToPort(data) { 54 function writeToPort(data) {
55 return new Promise((resolve) => { 55 return new Promise((resolve) => {
56 port.write(data, (err, bytesWritten) => { 56 port.write(data, (err, bytesWritten) => {
57 if (err) logger.warn(`ERROR: ${err.toString()}`); 57 if (err) logger.warn(`ERROR: ${err.toString()}`);
58 logger.verbose(`* OUT: ${data}`); 58 logger.verbose(`* OUT: ${data}`);
59 resolve(bytesWritten); 59 resolve(bytesWritten);
60 }); 60 });
61 }); 61 });
62 } 62 }
63 63
64 async function readSMS(slot) { 64 async function readSMS(slot) {
65 const parserCMGR = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR }); 65 const parserCMGR = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
66 parserCMGR.on('data', (data) => { 66 parserCMGR.on('data', (data) => {
67 if (data) { 67 if (data) {
68 try { 68 try {
69 reportSender.incomingSMS(sms.extract(data.toString().trim()), modemInfo); 69 reportSender.incomingSMS(sms.extract(data.toString().trim()), modemInfo);
70 } catch (e) { 70 } catch (e) {
71 logger.warn(`Exception on reporting new message. ${e.toString()}`, { smsObj: e.smsObj, dataFromModem: data }); 71 logger.warn(`Exception on reporting new message. ${e.toString()}`, { smsObj: e.smsObj, dataFromModem: data });
72 72
73 process.exit(0); 73 process.exit(0);
74 } 74 }
75 } 75 }
76 port.unpipe(parserCMGR); 76 port.unpipe(parserCMGR);
77 mutex.releaseLockWaitForCommand(); 77 mutex.releaseLockWaitForCommand();
78 }); 78 });
79 79
80 // const parserCMGD = new ParserDelimiter({ delimiter: DELIMITER_WAIT_FOR_OK }); 80 // const parserCMGD = new ParserDelimiter({ delimiter: DELIMITER_WAIT_FOR_OK });
81 const parserCMGD = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR }); 81 const parserCMGD = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
82 parserCMGD.on('data', () => { 82 parserCMGD.on('data', () => {
83 port.unpipe(parserCMGD); 83 port.unpipe(parserCMGD);
84 mutex.releaseLockWaitForCommand(); 84 mutex.releaseLockWaitForCommand();
85 }); 85 });
86 86
87 logger.info(`Reading SMS on slot ${slot}`); 87 logger.info(`Reading SMS on slot ${slot}`);
88 await mutex.setLockWaitForCommand(); 88 await mutex.setLockWaitForCommand();
89 port.pipe(parserCMGR); 89 port.pipe(parserCMGR);
90 await writeToPort(`AT+CMGR=${slot}\r`); 90 await writeToPort(`AT+CMGR=${slot}\r`);
91 logger.info(`Finished reading SMS on slot ${slot}`); 91 logger.info(`Finished reading SMS on slot ${slot}`);
92 92
93 logger.info(`Deleting message on slot ${slot}`); 93 logger.info(`Deleting message on slot ${slot}`);
94 await mutex.setLockWaitForCommand(); 94 await mutex.setLockWaitForCommand();
95 port.pipe(parserCMGD); 95 port.pipe(parserCMGD);
96 await writeToPort(`AT+CMGD=${slot}\r`); 96 await writeToPort(`AT+CMGD=${slot}\r`);
97 logger.info('Message processing has completed'); 97 logger.info('Message processing has completed');
98 } 98 }
99 99
100 function onIncomingSMS(data) { 100 function onIncomingSMS(data) {
101 const value = common.extractValueFromReadLineData(data); 101 const value = common.extractValueFromReadLineData(data);
102 if (!value) return; 102 if (!value) return;
103 103
104 const chunks = value.split(','); 104 const chunks = value.split(',');
105 if (!chunks && !chunks[1]) return; 105 if (!chunks && !chunks[1]) return;
106 106
107 const slot = chunks[1]; 107 const slot = chunks[1];
108 108
109 logger.info(`Incoming SMS on slot ${slot}`); 109 logger.info(`Incoming SMS on slot ${slot}`);
110 readSMS(slot); 110 readSMS(slot);
111 } 111 }
112 112
113 function onCOPS(data) { 113 function onCOPS(data) {
114 modemInfo.cops = common.extractValueFromReadLineData(data).trim(); 114 modemInfo.cops = common.extractValueFromReadLineData(data).trim();
115 logger.info(`Connected Network: ${modemInfo.cops}`); 115 logger.info(`Connected Network: ${modemInfo.cops}`);
116 116
117 if (!modemInfo.cops) return; 117 if (!modemInfo.cops) return;
118 118
119 [, , modemInfo.networkId] = modemInfo.cops.split(','); 119 [, , modemInfo.networkId] = modemInfo.cops.split(',');
120 120
121 if (modemInfo.networkId) { 121 if (modemInfo.networkId) {
122 modemInfo.networkName = dbCops[modemInfo.networkId] || modemInfo.networkId; 122 modemInfo.networkName = dbCops[modemInfo.networkId] || modemInfo.networkId;
123 } 123 }
124 } 124 }
125 125
126 parserReadLine.on('data', (data) => { 126 parserReadLine.on('data', (data) => {
127 logger.verbose(`* IN: ${data}`); 127 logger.verbose(`* IN: ${data}`);
128 if (data) { 128 if (data) {
129 lastTs = new Date(); 129 lastTs = new Date();
130 if (data.indexOf('+CSQ: ') === 0) { 130 if (data.indexOf('+CSQ: ') === 0) {
131 const signalStrength = common.extractValueFromReadLineData(data).trim(); 131 const signalStrength = common.extractValueFromReadLineData(data).trim();
132 if (signalStrength) { 132 if (signalStrength) {
133 modemInfo.signalStrength = signalStrength; 133 modemInfo.signalStrength = signalStrength;
134 modemInfo.signalStrengthTs = new Date(); 134 modemInfo.signalStrengthTs = new Date();
135 modemInfo.signalStrengthTsReadable = moment(modemInfo.signalStrengthTs).format('YYYY-MM-DD HH:mm:ss'); 135 modemInfo.signalStrengthTsReadable = moment(modemInfo.signalStrengthTs).format('YYYY-MM-DD HH:mm:ss');
136 logger.info(`Signal strength: ${modemInfo.signalStrength}`); 136 logger.info(`Signal strength: ${modemInfo.signalStrength}`);
137 registerModem(modemInfo); 137 registerModem(modemInfo);
138 } 138 }
139 } else if (data.indexOf('+CMTI: ') === 0) { 139 } else if (data.indexOf('+CMTI: ') === 0) {
140 // counters.increment('MESSAGE_RECEIVED', modemInfo); 140 // counters.increment('MESSAGE_RECEIVED', modemInfo);
141 onIncomingSMS(data); 141 onIncomingSMS(data);
142 } else if (data.indexOf('+COPS: ') === 0) { 142 } else if (data.indexOf('+COPS: ') === 0) {
143 onCOPS(data); 143 onCOPS(data);
144 } 144 }
145 } 145 }
146 }); 146 });
147 147
148 async function simpleSubCommand(cmd, callback) { 148 async function simpleSubCommand(cmd, callback) {
149 const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR }); 149 const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
150 parser.on('data', (data) => { 150 parser.on('data', (data) => {
151 port.unpipe(parser); 151 port.unpipe(parser);
152 mutex.releaseLockWaitForSubCommand(); 152 mutex.releaseLockWaitForSubCommand();
153 153
154 if (data) { 154 if (data) {
155 if (callback) callback(null, data.toString().trim()); 155 if (callback) callback(null, data.toString().trim());
156 } 156 }
157 }); 157 });
158 158
159 return new Promise(async (resolve) => { 159 return new Promise(async (resolve) => {
160 await mutex.setLockWaitForSubCommand(); 160 await mutex.setLockWaitForSubCommand();
161 port.pipe(parser); 161 port.pipe(parser);
162 writeToPort(cmd); 162 writeToPort(cmd);
163 163
164 await mutex.setLockWaitForSubCommand(); 164 await mutex.setLockWaitForSubCommand();
165 mutex.releaseLockWaitForSubCommand(); 165 mutex.releaseLockWaitForSubCommand();
166 166
167 resolve(); 167 resolve();
168 }); 168 });
169 } 169 }
170 170
171 function readManufacturer() { 171 function readManufacturer() {
172 return new Promise((resolve) => { 172 return new Promise((resolve) => {
173 simpleSubCommand('AT+CGMI\r', (err, result) => { 173 simpleSubCommand('AT+CGMI\r', (err, result) => {
174 modemInfo.manufacturer = result; 174 modemInfo.manufacturer = result;
175 logger.info(`Manufacturer: ${result}`); 175 logger.info(`Manufacturer: ${result}`);
176 resolve(result); 176 resolve(result);
177 }); 177 });
178 }); 178 });
179 } 179 }
180 180
181 function readModel() { 181 function readModel() {
182 return new Promise((resolve) => { 182 return new Promise((resolve) => {
183 simpleSubCommand('AT+CGMM\r', (err, result) => { 183 simpleSubCommand('AT+CGMM\r', (err, result) => {
184 modemInfo.model = result; 184 modemInfo.model = result;
185 logger.info(`Model: ${result}`); 185 logger.info(`Model: ${result}`);
186 resolve(result); 186 resolve(result);
187 }); 187 });
188 }); 188 });
189 } 189 }
190 190
191 function readIMEI() { 191 function readIMEI() {
192 return new Promise((resolve) => { 192 return new Promise((resolve) => {
193 simpleSubCommand('AT+CGSN\r', (err, result) => { 193 simpleSubCommand('AT+CGSN\r', (err, result) => {
194 modemInfo.imei = result; 194 modemInfo.imei = result;
195 logger.info(`IMEI: ${result}`); 195 logger.info(`IMEI: ${result}`);
196 resolve(result); 196 resolve(result);
197 }); 197 });
198 }); 198 });
199 } 199 }
200 200
201 function readIMSI() { 201 function readIMSI() {
202 return new Promise((resolve) => { 202 return new Promise((resolve) => {
203 simpleSubCommand('AT+CIMI\r', (err, result) => { 203 simpleSubCommand('AT+CIMI\r', (err, result) => {
204 modemInfo.imsi = result; 204 modemInfo.imsi = result;
205 logger.info(`IMSI: ${result}`); 205 logger.info(`IMSI: ${result}`);
206 206
207 if (result) { 207 if (result) {
208 /* 208 /*
209 modemInfo.msisdn = msisdn[result]; 209 modemInfo.msisdn = msisdn[result];
210 if (modemInfo.msisdn) { 210 if (modemInfo.msisdn) {
211 logger.info(`MSISDN: ${modemInfo.msisdn}`); 211 logger.info(`MSISDN: ${modemInfo.msisdn}`);
212 registerModem(modemInfo); 212 registerModem(modemInfo);
213 } 213 }
214 */ 214 */
215 } else { 215 } else {
216 logger.warn(`IMSI not detected. Please insert a sim card to your modem. Terminating ${config.modem.device}.`); 216 logger.warn(`IMSI not detected. Please insert a sim card to your modem. Terminating ${config.modem.device}.`);
217 process.exit(2); 217 process.exit(2);
218 } 218 }
219 resolve(result); 219 resolve(result);
220 }); 220 });
221 }); 221 });
222 } 222 }
223 223
224 function readCOPS() { 224 function readCOPS() {
225 return new Promise((resolve) => { 225 return new Promise((resolve) => {
226 simpleSubCommand('AT+COPS?\r', (err, result) => { 226 simpleSubCommand('AT+COPS?\r', (err, result) => {
227 resolve(result); 227 resolve(result);
228 }); 228 });
229 }); 229 });
230 } 230 }
231 231
232 function deleteInbox() { 232 function deleteInbox() {
233 return new Promise((resolve) => { 233 return new Promise((resolve) => {
234 simpleSubCommand('AT+CMGD=0,4\r', (err, result) => { 234 simpleSubCommand('AT+CMGD=0,4\r', (err, result) => {
235 resolve(result); 235 resolve(result);
236 }); 236 });
237 }); 237 });
238 } 238 }
239 239
240 async function querySignalStrength() { 240 async function querySignalStrength() {
241 const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR }); 241 const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
242 parser.on('data', () => { 242 parser.on('data', () => {
243 port.unpipe(parser); 243 port.unpipe(parser);
244 mutex.releaseLockWaitForCommand(); 244 mutex.releaseLockWaitForCommand();
245 }); 245 });
246 246
247 if (mutex.tryLockWaitForCommand()) { 247 if (mutex.tryLockWaitForCommand()) {
248 port.pipe(parser); 248 port.pipe(parser);
249 await writeToPort('AT+CSQ\r'); 249 await writeToPort('AT+CSQ\r');
250 } 250 }
251 } 251 }
252 252
253 function registerModemToCenterPeriodically() { 253 function registerModemToCenterPeriodically() {
254 registerModem(modemInfo); 254 registerModem(modemInfo);
255 255
256 setInterval(() => { 256 setInterval(() => {
257 registerModem(modemInfo); 257 registerModem(modemInfo);
258 }, 60 * 1000); 258 }, 60 * 1000);
259 } 259 }
260 260
261 async function registerSignalStrengthBackgroundQuery() { 261 async function registerSignalStrengthBackgroundQuery() {
262 logger.info('Registering background signal strength query'); 262 logger.info('Registering background signal strength query');
263 263
264 querySignalStrength(); 264 querySignalStrength();
265 265
266 setInterval(() => { 266 setInterval(() => {
267 querySignalStrength(); 267 querySignalStrength();
268 }, config.interval_beetwen_signal_strength_ms || INTERVAL_BEETWEN_SIGNAL_STRENGTH_MS); 268 }, config.interval_beetwen_signal_strength_ms || INTERVAL_BEETWEN_SIGNAL_STRENGTH_MS);
269 } 269 }
270 270
271 async function sendSMS(destination, msg) { 271 async function sendSMS(destination, msg) {
272 if (typeof destination !== 'string' || typeof msg !== 'string' || !destination.trim() || !msg.trim()) return; 272 if (typeof destination !== 'string' || typeof msg !== 'string' || !destination.trim() || !msg.trim()) return;
273 273
274 const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR }); 274 const parser = new ParserRegex({ regex: REGEX_WAIT_FOR_OK_OR_ERROR });
275 parser.on('data', () => { 275 parser.on('data', () => {
276 port.unpipe(parser); 276 port.unpipe(parser);
277 mutex.releaseLockWaitForSubCommand(); 277 mutex.releaseLockWaitForSubCommand();
278 }); 278 });
279 279
280 logger.verbose('Waiting for command lock to send message'); 280 logger.verbose('Waiting for command lock to send message');
281 await mutex.setLockWaitForCommand(); 281 await mutex.setLockWaitForCommand();
282 282
283 logger.info('Sending message', { destination, msg }); 283 logger.info('Sending message', { destination, msg });
284 // counters.increment('MESSAGE_SENT', modemInfo); 284 // counters.increment('MESSAGE_SENT', modemInfo);
285 285
286 const correctedDestination = `+${destination}`.replace(/^0/, '62').replace(/^\++/, '+'); 286 const correctedDestination = `+${destination}`.replace(/^0/, '62').replace(/^\++/, '+');
287 287
288 logger.verbose('Waiting for lock before set to text mode'); 288 logger.verbose('Waiting for lock before set to text mode');
289 await mutex.setLockWaitForSubCommand(); 289 await mutex.setLockWaitForSubCommand();
290 port.pipe(parser); 290 port.pipe(parser);
291 await writeToPort('AT+CMGF=1\r'); 291 await writeToPort('AT+CMGF=1\r');
292 292
293 logger.verbose('Waiting for lock before writing message'); 293 logger.verbose('Waiting for lock before writing message');
294 await mutex.setLockWaitForSubCommand(); 294 await mutex.setLockWaitForSubCommand();
295 port.pipe(parser); 295 port.pipe(parser);
296 await writeToPort(`AT+CMGS="${correctedDestination}"\r${msg}${Buffer.from([0x1A])}`); 296 await writeToPort(`AT+CMGS="${correctedDestination}"\r${msg}${Buffer.from([0x1A])}`);
297 297
298 await mutex.setLockWaitForSubCommand(); 298 await mutex.setLockWaitForSubCommand();
299 mutex.releaseLockWaitForSubCommand(); 299 mutex.releaseLockWaitForSubCommand();
300 300
301 logger.info('Message has been sent'); 301 logger.info('Message has been sent');
302 302
303 setTimeout(() => { 303 setTimeout(() => {
304 logger.verbose('Releasing command lock'); 304 logger.verbose('Releasing command lock');
305 mutex.releaseLockWaitForCommand(); 305 mutex.releaseLockWaitForCommand();
306 }, 2000); 306 }, 2000);
307 } 307 }
308 308
309 function init() { 309 function init() {
310 port = new SerialPort(config.modem.device, { baudRate: 115200 }, (err) => { 310 port = new SerialPort(config.modem.device, { baudRate: 115200 }, (err) => {
311 if (err) { 311 if (err) {
312 logger.warn(`Error opening modem. ${err}. Terminating modem ${config.modem.device}.`); 312 logger.warn(`Error opening modem. ${err}. Terminating modem ${config.modem.device}.`);
313 process.exit(1); 313 process.exit(1);
314 } 314 }
315 315
316 registerModem(modemInfo); 316 registerModem(modemInfo);
317 }); 317 });
318 port.pipe(parserReadLine); 318 port.pipe(parserReadLine);
319 319
320 setInterval(() => { 320 setInterval(() => {
321 if ((new Date() - lastTs) > MAX_LAST_DATA_AGE_MS) { 321 if ((new Date() - lastTs) > MAX_LAST_DATA_AGE_MS) {
322 logger.warn(`No data for more than ${MAX_LAST_DATA_AGE_MS} ms. Modem might be unresponsive. Terminating modem ${config.modem.device}.`); 322 logger.warn(`No data for more than ${MAX_LAST_DATA_AGE_MS} ms. Modem might be unresponsive. Terminating modem ${config.modem.device}.`);
323 process.exit(0); 323 process.exit(0);
324 } 324 }
325 }, 30 * 1000); 325 }, 30 * 1000);
326 326
327 port.on('open', async () => { 327 port.on('open', async () => {
328 await mutex.setLockWaitForCommand(); 328 await mutex.setLockWaitForCommand();
329 329
330 logger.info('Modem opened'); 330 logger.info('Modem opened');
331 await writeToPort('\r'); 331 await writeToPort('\r');
332 await simpleSubCommand('AT\r'); 332 await simpleSubCommand('AT\r');
333 333
334 logger.info('Initializing modem to factory set'); 334 logger.info('Initializing modem to factory set');
335 await simpleSubCommand('AT&F\r'); 335 await simpleSubCommand('AT&F\r');
336 336
337 logger.info('Disabling echo'); 337 logger.info('Disabling echo');
338 await simpleSubCommand('ATE0\r'); 338 await simpleSubCommand('ATE0\r');
339 339
340 logger.info('Set to text mode'); 340 logger.info('Set to text mode');
341 await simpleSubCommand('AT+CMGF=1\r'); 341 await simpleSubCommand('AT+CMGF=1\r');
342 342
343 await readCOPS(); 343 await readCOPS();
344 344
345 await readManufacturer(); 345 await readManufacturer();
346 await readModel(); 346 await readModel();
347 await readIMEI(); 347 await readIMEI();
348 await readIMSI(); 348 await readIMSI();
349 349
350 if (!config.disable_delete_inbox_on_startup) { 350 if (!config.disable_delete_inbox_on_startup) {
351 logger.info('Deleting existing messages'); 351 logger.info('Deleting existing messages');
352 await deleteInbox(); 352 await deleteInbox();
353 } 353 }
354 354
355 mutex.releaseLockWaitForCommand(); 355 mutex.releaseLockWaitForCommand();
356 logger.verbose('Init completed'); 356 logger.verbose('Init completed');
357 357
358 registerModemToCenterPeriodically(); 358 registerModemToCenterPeriodically();
359 registerSignalStrengthBackgroundQuery(); 359 registerSignalStrengthBackgroundQuery();
360 }); 360 });
361 } 361 }
362 362
363 init(); 363 init();
364 364
365 exports.modemInfo = modemInfo; 365 exports.modemInfo = modemInfo;
366 exports.sendSMS = sendSMS; 366 exports.sendSMS = sendSMS;
367 367