├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── docs ├── AT-Command.pdf ├── AT_Commands_Reference_Guide.pdf ├── pdu.md └── pdu_format.gif ├── example ├── index.js └── network.js ├── gprs.js ├── index.js ├── modem.js ├── package.json ├── pdu.js ├── sim900.jpg ├── sms.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_* 2 | *.log 3 | yarn.lock 4 | logs 5 | **/*.backup.* 6 | **/*.back.* 7 | 8 | node_modules 9 | bower_componets 10 | 11 | *.sublime* 12 | 13 | psd 14 | thumb 15 | sketch 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_* 2 | *.log 3 | logs 4 | **/*.backup.* 5 | **/*.back.* 6 | 7 | node_modules 8 | bower_componets 9 | 10 | *.sublime* 11 | 12 | psd 13 | thumb 14 | sketch 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 lsong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## gsm2 ![NPM version](https://img.shields.io/npm/v/gsm2.svg?style=flat) 2 | 3 | > GSM Modem module for Node.js 4 | 5 | ### Installation 6 | 7 | ```bash 8 | $ npm i gsm2 --save 9 | ``` 10 | 11 | ### Example 12 | 13 | ```js 14 | import * as gsm from 'gsm2'; 15 | 16 | const modem = new gsm.Modem('/dev/gsm-modem'); 17 | 18 | modem.on('+CRING', console.log.bind('Ringing')) 19 | modem.on('+CLIP', number => { 20 | console.log('Incoming Call', number); 21 | }) 22 | modem.on('+CMTI', msg => { 23 | console.log('Incoming Message', msg); 24 | }); 25 | 26 | modem.open(async () => { 27 | 28 | await modem.reset() 29 | await modem.sms_mode(1) 30 | await modem.sms_send( 31 | '+8618510100102', 32 | 'This is a test from gsm2' 33 | ); 34 | 35 | }); 36 | ``` 37 | 38 | ![sim900](./sim900.jpg) 39 | 40 | ### API 41 | check this file: `index.js` 42 | 43 | ### Contributing 44 | - Fork this Repo first 45 | - Clone your Repo 46 | - Install dependencies by `$ npm install` 47 | - Checkout a feature branch 48 | - Feel free to add your features 49 | - Make sure your features are fully tested 50 | - Publish your local branch, Open a pull request 51 | - Enjoy hacking <3 52 | 53 | ### MIT license 54 | Copyright (c) 2016 lsong 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy 57 | of this software and associated documentation files (the "Software"), to deal 58 | in the Software without restriction, including without limitation the rights 59 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 60 | copies of the Software, and to permit persons to whom the Software is 61 | furnished to do so, subject to the following conditions: 62 | 63 | The above copyright notice and this permission notice shall be included in 64 | all copies or substantial portions of the Software. 65 | 66 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 67 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 68 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 69 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 70 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 71 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 72 | THE SOFTWARE. 73 | 74 | --- 75 | -------------------------------------------------------------------------------- /docs/AT-Command.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/node-gsm/652295b9a268c28d38fcc56caff8aaf99635e2fb/docs/AT-Command.pdf -------------------------------------------------------------------------------- /docs/AT_Commands_Reference_Guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/node-gsm/652295b9a268c28d38fcc56caff8aaf99635e2fb/docs/AT_Commands_Reference_Guide.pdf -------------------------------------------------------------------------------- /docs/pdu.md: -------------------------------------------------------------------------------- 1 | 2 | ![PDU Format](/pdu_format.gif) 3 | -------------------------------------------------------------------------------- /docs/pdu_format.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/node-gsm/652295b9a268c28d38fcc56caff8aaf99635e2fb/docs/pdu_format.gif -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import * as gsm from '../index.js'; 2 | 3 | const modem = new gsm.Modem( 4 | '/dev/tty.usbserial', { 5 | retry: 1000 6 | } 7 | ); 8 | 9 | modem.on('+CRING', console.log.bind('Ringing')) 10 | modem.on('+CLIP', (number) => { 11 | console.log('Incoming Call', number); 12 | }) 13 | modem.on('+CMTI', (msg) => { 14 | console.log('Incoming Message', msg); 15 | }); 16 | modem.on('message', (message, m) => { 17 | // console.log(message); 18 | }); 19 | 20 | modem.open(async () => { 21 | 22 | await modem.reset() 23 | await modem.sms_mode(1) 24 | await modem.sms_send( 25 | '+8618510100102', 26 | 'This is a test from gsm2' 27 | ); 28 | 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /example/network.js: -------------------------------------------------------------------------------- 1 | import { GPRS } from "../index.js"; 2 | 3 | const network = new GPRS( 4 | '/dev/tty.usbserial', { 5 | retry: 1000 6 | } 7 | ); 8 | 9 | (async () => { 10 | 11 | await network.init(); 12 | 13 | const res = await network.request({ 14 | hostname: 'lsong.org', 15 | method: 'GET', 16 | path: '/' 17 | }); 18 | console.log(res); 19 | 20 | })(); -------------------------------------------------------------------------------- /gprs.js: -------------------------------------------------------------------------------- 1 | import { Modem } from './modem.js'; 2 | 3 | export class GPRS extends Modem { 4 | request() { 5 | 6 | } 7 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | export * from './pdu.js'; 3 | export * from './sms.js'; 4 | export * from './gprs.js'; 5 | export * from './modem.js'; -------------------------------------------------------------------------------- /modem.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { debuglog } from 'util'; 3 | import * as async from "async"; 4 | import { SerialPort } from 'serialport'; 5 | 6 | const debug = debuglog('gsm2'); 7 | /** 8 | * [Modem description] 9 | * @param {[type]} options [description] 10 | */ 11 | export class Modem extends SerialPort { 12 | constructor(port, options) { 13 | Object.assign(options, { 14 | retry: 0, 15 | timeout: 5000, 16 | dataBits: 8, 17 | stopBits: 1, 18 | baudRate: 115200, 19 | autoOpen: false 20 | }, options); 21 | super({ 22 | path: port, 23 | ...options 24 | }); 25 | this.options = options; 26 | var output = ''; 27 | var regexp = /(\r?(.+)\r)?\r\n(.+)\r\n$/; 28 | this.on('data', chunk => { 29 | output += chunk; 30 | if (/>/.test(output)) { 31 | this.emit('message', output); 32 | output = ''; 33 | return; 34 | } 35 | if (regexp.test(output)) { 36 | var m = regexp.exec(output); 37 | this.emit('message', m[3], m); 38 | output = ''; 39 | } 40 | }).on('message', message => { 41 | var p = message.split(/:\s?/); 42 | if (p.length === 2) this.emit(p[0], p[1]); 43 | }); 44 | this.queue = async.queue((task, done) => { 45 | debug('task', task); 46 | // Syntax: 47 | // AT 48 | this.write(task.data + '\r', () => this.drain()); 49 | function onMessage(message) { 50 | clearInterval(this.retry); 51 | clearTimeout(this.timeout); 52 | if (/ERROR/.test(message)) { 53 | task.reject(message); 54 | } else { 55 | task.accept(message); 56 | } 57 | this.removeListener('message', onMessage); 58 | done(); 59 | } 60 | this.on('message', onMessage); 61 | // to temporary disable timeout use: null, 0, false 62 | if ((task.timeout !== undefined ? task.timeout : this.options.timeout)) { 63 | this.timeout = setTimeout(() => { 64 | clearInterval(this.retry); 65 | task.reject(new Error('Timeout exceeded')); 66 | this.removeListener('message', onMessage); 67 | done(); 68 | }, +(task.timeout || this.options.timeout)); 69 | } 70 | // to temporary disable retry use: null, 0, false 71 | if ((task.retry !== undefined ? task.retry : this.options.retry)) { 72 | this.retry = setInterval(() => { 73 | this.write(task.data + '\r', () => this.drain()); 74 | }, +(task.retry || this.options.retry)); 75 | } 76 | }); 77 | } 78 | send(data, options) { 79 | var command = Object.assign({ data }, options || {}); 80 | command.promise = new Promise((accept, reject) => { 81 | command.accept = accept; 82 | command.reject = reject; 83 | }); 84 | this.queue.push(command); 85 | return command.promise; 86 | } 87 | test(cmd) { 88 | return this.send(`AT+${cmd}?`); 89 | } 90 | exec(cmd) { 91 | return this.send(`AT+${cmd}`); 92 | } 93 | get(name) { 94 | return this.send(`AT+${name}=?`); 95 | } 96 | set(name, value, options) { 97 | return this.send(`AT+${name}=${value}`, options); 98 | } 99 | reset() { 100 | return this.send('ATZ'); 101 | } 102 | save() { 103 | return this.send('AT&W'); 104 | } 105 | factory() { 106 | return this.send('AT&F'); 107 | } 108 | clock() { 109 | return this.test('CCLK'); 110 | } 111 | debug(n = 0) { 112 | return this.set('CMEE', n); 113 | } 114 | echo(n = 0) { 115 | return this.exec('ATE' + n); 116 | } 117 | id() { 118 | return this.send('ATI'); 119 | } 120 | sn() { 121 | // TODO: 122 | } 123 | imsi() { 124 | return this.test('CIMI').then(isOK => isOK && this.exec('CIMI')); 125 | } 126 | model() { 127 | return this.test('CGMM').then(isOK => isOK && this.exec('CGMM')) 128 | } 129 | version() { 130 | // AT+CGMR 131 | return this.test('GMR').then(isOK => isOK && this.exec('GMR')) 132 | } 133 | manufacturer() { 134 | // AT+CGMI 135 | return this.test('GMI').then(isOK => isOK && this.exec('GMI')) 136 | } 137 | signal_strength() { 138 | return this.test('CSQ').then(() => { 139 | // this.get('CSQ').then(res => { 140 | // return res.match(/\+CSQ:\s(.+)/)[1]; 141 | // }); 142 | return this.exec('CSQ').then(res => { 143 | res = res.match(/\+CSQ:\s*(.+)/); 144 | res = res[1].split(','); 145 | return { 146 | rssi: res[0], 147 | ber: res[1] 148 | }; 149 | }) 150 | }); 151 | } 152 | dial(number) { 153 | return this.send(`ATD${number};`); 154 | } 155 | hangup() { 156 | return this.send('ATH'); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gsm2", 3 | "version": "1.0.4", 4 | "type": "module", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node ./test/test.js" 8 | }, 9 | "author": "lsong", 10 | "license": "MIT", 11 | "keywords": [ 12 | "gsm", 13 | "modem", 14 | "pdu" 15 | ], 16 | "description": "gsm modem module for node.js", 17 | "dependencies": { 18 | "async": "^3.2.4", 19 | "serialport": "^10.4.0" 20 | }, 21 | "directories": { 22 | "example": "example" 23 | }, 24 | "devDependencies": {}, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/song940/node-gsm-modem.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/song940/node-gsm-modem/issues" 31 | }, 32 | "homepage": "https://github.com/song940/node-gsm-modem#readme" 33 | } 34 | -------------------------------------------------------------------------------- /pdu.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const parse = pdu => { 4 | //Cursor points to the last octet we've read. 5 | var cursor = 0; 6 | 7 | var buffer = new Buffer(pdu.slice(0, 4), 'hex'); 8 | var smscSize = buffer[0]; 9 | var smscType = buffer[1].toString(16); 10 | cursor = (smscSize * 2 + 2); 11 | var smscNum = deSwapNibbles(pdu.slice(4, cursor)); 12 | 13 | var buffer = new Buffer(pdu.slice(cursor, cursor + 6), 'hex'); 14 | cursor += 6; 15 | var smsDeliver = buffer[0]; 16 | 17 | var smsDeliverBits = ("00000000" + parseInt(smsDeliver).toString(2)).slice(-8); 18 | var udhi = smsDeliverBits.slice(1, 2) === "1"; 19 | 20 | var senderSize = buffer[1]; 21 | if (senderSize % 2 === 1) 22 | senderSize++; 23 | 24 | var senderType = parseInt(buffer[2]).toString(16) 25 | 26 | var encodedSender = pdu.slice(cursor, cursor + senderSize); 27 | var senderNum; 28 | if (senderType === '91') { 29 | senderNum = deSwapNibbles(encodedSender); 30 | } else if (senderType === 'd0') { 31 | senderNum = this.decode7Bit(encodedSender).replace(/\0/g, ''); 32 | } else { 33 | console.error('unsupported sender type.'); 34 | } 35 | 36 | cursor += senderSize; 37 | 38 | var protocolIdentifier = pdu.slice(cursor, cursor + 2); 39 | cursor += 2; 40 | 41 | var dataCodingScheme = pdu.slice(cursor, cursor + 2); 42 | cursor = cursor + 2; 43 | 44 | var encoding = detectEncoding(dataCodingScheme); 45 | 46 | var timestamp = deSwapNibbles(pdu.slice(cursor, cursor + 14)); 47 | 48 | 49 | var time = new Date; 50 | time.setUTCFullYear('20' + timestamp.slice(0, 2)); 51 | time.setUTCMonth(timestamp.slice(2, 4) - 1); 52 | time.setUTCDate(timestamp.slice(4, 6)); 53 | time.setUTCHours(timestamp.slice(6, 8)); 54 | time.setUTCMinutes(timestamp.slice(8, 10)); 55 | time.setUTCSeconds(timestamp.slice(10, 12)); 56 | 57 | var firstTimezoneOctet = parseInt(timestamp.slice(12, 13)); 58 | var binary = ("0000" + firstTimezoneOctet.toString(2)).slice(-4); 59 | var factor = binary.slice(0, 1) === '1' ? 1 : -1; 60 | var binary = '0' + binary.slice(1, 4); 61 | var firstTimezoneOctet = parseInt(binary, 2).toString(10); 62 | var timezoneDiff = parseInt(firstTimezoneOctet + timestamp.slice(13, 14)); 63 | var time = new Date(time.getTime() + (timezoneDiff * 15 * 1000 * 60 * factor)); 64 | 65 | cursor += 14; 66 | 67 | var dataLength = parseInt(pdu.slice(cursor, cursor + 2), 16).toString(10); 68 | cursor += 2; 69 | 70 | if (udhi) { //User-Data-Header-Indicator: means there's some User-Data-Header. 71 | var udhLength = pdu.slice(cursor, cursor + 2); 72 | var iei = pdu.slice(cursor + 2, cursor + 4); 73 | if (iei == "00") { //Concatenated sms. 74 | var headerLength = pdu.slice(cursor + 4, cursor + 6); 75 | var referenceNumber = pdu.slice(cursor + 6, cursor + 8); 76 | var parts = pdu.slice(cursor + 8, cursor + 10); 77 | var currentPart = pdu.slice(cursor + 10, cursor + 12); 78 | } 79 | 80 | if (iei == "08") { //Concatenaded sms with a two-bytes reference number 81 | var headerLength = pdu.slice(cursor + 4, cursor + 6); 82 | var referenceNumber = pdu.slice(cursor + 6, cursor + 10); 83 | var parts = pdu.slice(cursor + 10, cursor + 12); 84 | var currentPart = pdu.slice(cursor + 12, cursor + 14); 85 | } 86 | 87 | if (encoding === '16bit') 88 | if (iei == '00') 89 | cursor += (udhLength - 2) * 4; 90 | else if (iei == '08') 91 | cursor += ((udhLength - 2) * 4) + 2; 92 | else 93 | cursor += (udhLength - 2) * 2; 94 | } 95 | 96 | if (encoding === '16bit') 97 | var text = decode16Bit(pdu.slice(cursor), dataLength); 98 | else if (encoding === '7bit') 99 | var text = decode7Bit(pdu.slice(cursor), dataLength); 100 | else if (encoding === '8bit') 101 | var text = ''; //TODO 102 | 103 | var data = { 104 | 'smsc': smscNum, 105 | 'smsc_type': smscType, 106 | 'sender': senderNum, 107 | 'sender_type': senderType, 108 | 'encoding': encoding, 109 | 'time': time, 110 | 'text': text 111 | }; 112 | 113 | if (udhi) { 114 | data['udh'] = { 115 | 'length': udhLength, 116 | 'iei': iei, 117 | }; 118 | 119 | if (iei == '00' || iei == '08') { 120 | data['udh']['reference_number'] = referenceNumber; 121 | data['udh']['parts'] = parseInt(parts); 122 | data['udh']['current_part'] = parseInt(currentPart); 123 | } 124 | } 125 | 126 | return data; 127 | } 128 | 129 | export const detectEncoding = function (dataCodingScheme) { 130 | var binary = ('00000000' + (parseInt(dataCodingScheme, 16).toString(2))).slice(-8); 131 | 132 | if (binary == '00000000') 133 | return '7bit'; 134 | 135 | if (binary.slice(0, 2) === '00') { 136 | var compressed = binary.slice(2, 1) === '1'; 137 | var bitsHaveMeaning = binary.slice(3, 1) === '1'; 138 | 139 | if (binary.slice(4, 6) === '00') 140 | return '7bit'; 141 | 142 | if (binary.slice(4, 6) === '01') 143 | return '8bit'; 144 | 145 | if (binary.slice(4, 6) === '10') 146 | return '16bit'; 147 | } 148 | } 149 | 150 | export const decode16Bit = function (data, length) { 151 | //We are getting ucs2 characters. 152 | var ucs2 = ''; 153 | for (var i = 0; i <= data.length; i = i + 4) { 154 | ucs2 += String.fromCharCode("0x" + data[i] + data[i + 1] + data[i + 2] + data[i + 3]); 155 | } 156 | 157 | return ucs2; 158 | } 159 | 160 | export const deSwapNibbles = function (nibbles) { 161 | var out = ''; 162 | for (var i = 0; i < nibbles.length; i = i + 2) { 163 | if (nibbles[i] === 'F') //Dont consider trailing F. 164 | out += parseInt(nibbles[i + 1], 16).toString(10); 165 | else 166 | out += parseInt(nibbles[i + 1], 16).toString(10) + parseInt(nibbles[i], 16).toString(10); 167 | } 168 | return out; 169 | } 170 | 171 | export const decode7Bit = function (code, count) { 172 | //We are getting 'septeps'. We should decode them. 173 | var binary = ''; 174 | for (var i = 0; i < code.length; i++) 175 | binary += ('0000' + parseInt(code.slice(i, i + 1), 16).toString(2)).slice(-4); 176 | 177 | var bin = Array(); 178 | var cursor = 0; 179 | var fromPrevious = ''; 180 | var i = 0; 181 | while (binary[i]) { 182 | var remaining = 7 - fromPrevious.length; 183 | var toNext = 8 - remaining; 184 | bin[i] = binary.slice(cursor + toNext, cursor + toNext + remaining) + fromPrevious; 185 | var fromPrevious = binary.slice(cursor, cursor + toNext); 186 | if (toNext === 8) 187 | fromPrevious = ''; 188 | else 189 | cursor += 8; 190 | i++; 191 | } 192 | 193 | var ascii = ''; 194 | for (i in bin) 195 | ascii += String.fromCharCode(parseInt(bin[i], 2)); 196 | 197 | return ascii; 198 | } 199 | 200 | export const encode7Bit = function (ascii) { 201 | //We should create septeps now. 202 | var octets = new Array(); 203 | for (var i = 0; i < ascii.length; i++) 204 | octets.push(('0000000' + (ascii.charCodeAt(i).toString(2))).slice(-7)); 205 | 206 | for (var i in octets) { 207 | var i = parseInt(i); 208 | var freeSpace = 8 - octets[i].length; 209 | 210 | if (octets[i + 1] && freeSpace !== 8) { 211 | octets[i] = octets[i + 1].slice(7 - freeSpace) + octets[i]; 212 | octets[i + 1] = octets[i + 1].slice(0, 7 - freeSpace); 213 | } 214 | } 215 | 216 | var hex = ''; 217 | for (i in octets) 218 | if (octets[i].length > 0) 219 | hex += ('00' + (parseInt(octets[i], 2).toString(16))).slice(-2); 220 | return hex; 221 | } 222 | 223 | //TODO: TP-Validity-Period (Delivery) 224 | export const generate = function (message) { 225 | var pdu = '00'; 226 | 227 | var parts = 1; 228 | if (message.encoding === '16bit' && message.text.length > 70) 229 | parts = message.text.length / 66; 230 | 231 | else if (message.encoding === '7bit' && message.text.length > 160) 232 | parts = message.text.length / 153; 233 | 234 | parts = Math.ceil(parts); 235 | 236 | TPMTI = 1; 237 | TPRD = 4; 238 | TPVPF = 8; 239 | TPSRR = 32; 240 | TPUDHI = 64; 241 | TPRP = 128; 242 | 243 | var submit = TPMTI; 244 | 245 | if (parts > 1) //UDHI 246 | submit = submit | TPUDHI; 247 | 248 | submit = submit | TPSRR; 249 | 250 | pdu += submit.toString(16); 251 | 252 | pdu += '00'; //TODO: Reference Number; 253 | 254 | var receiverSize = ('00' + (parseInt(message.receiver.length, 10).toString(16))).slice(-2); 255 | var receiver = swapNibbles(message.receiver); 256 | var receiverType = 81; //TODO: NOT-Hardcoded PDU generation. Please note that Hamrah1 doesnt work if we set it to 91 (International). 257 | 258 | pdu += receiverSize.toString(16) + receiverType + receiver; 259 | 260 | pdu += '00'; //TODO TP-PID 261 | 262 | if (message.encoding === '16bit') 263 | pdu += '08'; 264 | else if (message.encoding === '7bit') 265 | pdu += '00'; 266 | 267 | var pdus = new Array(); 268 | 269 | var csms = randomHexa(2); // CSMS allows to give a reference to a concatenated message 270 | 271 | for (var i = 0; i < parts; i++) { 272 | pdus[i] = pdu; 273 | 274 | if (message.encoding === '16bit') { 275 | /* If there are more than one messages to be sent, we are going to have to put some UDH. Then, we would have space only 276 | * for 66 UCS2 characters instead of 70 */ 277 | if (parts === 1) 278 | var length = 70; 279 | else 280 | var length = 66; 281 | 282 | } else if (message.encoding === '7bit') { 283 | /* If there are more than one messages to be sent, we are going to have to put some UDH. Then, we would have space only 284 | * for 153 ASCII characters instead of 160 */ 285 | if (parts === 1) 286 | var length = 160; 287 | else 288 | var length = 153; 289 | } 290 | var text = message.text.slice(i * length, (i * length) + length); 291 | 292 | if (message.encoding === '16bit') { 293 | user_data = encode16Bit(text); 294 | var size = (user_data.length / 2); 295 | 296 | if (parts > 1) 297 | size += 6; //6 is the number of data headers we append. 298 | 299 | } else if (message.encoding === '7bit') { 300 | user_data = encode7Bit(text); 301 | var size = user_data.length / 2; 302 | } 303 | 304 | pdus[i] += ('00' + parseInt(size).toString(16)).slice(-2); 305 | 306 | if (parts > 1) { 307 | pdus[i] += '05'; 308 | pdus[i] += '00'; 309 | pdus[i] += '03'; 310 | pdus[i] += csms; 311 | pdus[i] += ('00' + parts.toString(16)).slice(-2); 312 | pdus[i] += ('00' + (i + 1).toString(16)).slice(-2); 313 | } 314 | pdus[i] += user_data; 315 | } 316 | 317 | return pdus; 318 | } 319 | 320 | 321 | export const encode16Bit = function (text) { 322 | var out = ''; 323 | for (var i = 0; i < text.length; i++) { 324 | out += ('0000' + (parseInt(text.charCodeAt(i), 10).toString(16))).slice(-4); 325 | } 326 | return out; 327 | } 328 | 329 | export const swapNibbles = function (nibbles) { 330 | var out = ''; 331 | for (var i = 0; i < nibbles.length; i = i + 2) { 332 | if (typeof (nibbles[i + 1]) === 'undefined') // Add a trailing F. 333 | out += 'F' + parseInt(nibbles[i], 16).toString(10); 334 | else 335 | out += parseInt(nibbles[i + 1], 16).toString(10) + parseInt(nibbles[i], 16).toString(10); 336 | } 337 | return out; 338 | } 339 | 340 | export const parseStatusReport = function (pdu) { 341 | //Cursor points to the last octet we've read. 342 | var cursor = 0; 343 | 344 | var smscSize = parseInt(pdu.slice(0, 2), 16); 345 | cursor += 2; 346 | 347 | var smscType = parseInt(pdu.slice(cursor, cursor + 2), 16); 348 | cursor += 2; 349 | 350 | var smscNum = deSwapNibbles(pdu.slice(cursor, (smscSize * 2) + 2)); 351 | cursor = (smscSize * 2 + 2); 352 | 353 | var header = parseInt(pdu.slice(cursor, cursor + 2)); 354 | cursor += 2; 355 | 356 | var reference = parseInt(pdu.slice(cursor, cursor + 2), 16); 357 | cursor += 2; 358 | 359 | var senderSize = parseInt(pdu.slice(cursor, cursor + 2), 16); 360 | if (senderSize % 2 === 1) 361 | senderSize++; 362 | cursor += 2; 363 | 364 | var senderType = parseInt(pdu.slice(cursor, cursor + 2)); 365 | cursor += 2; 366 | 367 | var sender = deSwapNibbles(pdu.slice(cursor, cursor + senderSize)); 368 | 369 | var status = pdu.slice(-2); 370 | 371 | return { 372 | smsc: smscNum, 373 | reference: reference, 374 | sender: sender, 375 | status: status 376 | } 377 | } 378 | 379 | export function randomHexa(size) { 380 | var text = ""; 381 | var possible = "0123456789ABCDEF"; 382 | for (var i = 0; i < size; i++) 383 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 384 | return text; 385 | } -------------------------------------------------------------------------------- /sim900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/node-gsm/652295b9a268c28d38fcc56caff8aaf99635e2fb/sim900.jpg -------------------------------------------------------------------------------- /sms.js: -------------------------------------------------------------------------------- 1 | import { Modem } from './modem.js'; 2 | 3 | export class SMS extends Modem { 4 | sms_center() { 5 | return this.test('CSCA'); 6 | } 7 | sms_mode(mode) { 8 | return this.set('CMGF', mode || 0); 9 | } 10 | sms_list(mode) { 11 | return this.get('CMGL').then(str => { 12 | return /\((.+)\)/.exec(str)[1] 13 | .split(',') 14 | .map(s => s.replace(/["']/g, '')); 15 | }).then(modes => { 16 | return this.set('CMGL', modes[mode || 0]); 17 | }); 18 | } 19 | sms_read(index) { 20 | return this.set('CMGR', index); 21 | } 22 | sms_send(number, content) { 23 | // temporary disable retry. 24 | return this.set('CMGS', `"${number}"`, { retry: 0 }).then(x => { 25 | return this.send(content + '\u001a'); 26 | }); 27 | } 28 | sms_delete(index) { 29 | return this.set('CMGD', index); 30 | } 31 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const PDU = require('../pdu'); 3 | 4 | var message = PDU.generate({ 5 | text:'Some text', 6 | receiver:999999999999, //MSISDN 7 | encoding:'16bit' //Or 7bit if you're sending an ascii message. 8 | }); 9 | assert.equal('002100aN810008120053006f006d006500200074006500780074', message[0]); 10 | 11 | var message = PDU.parse('06918919015000240C9189194238148900003110211052254117CAB03D3C1FCBD3703AA81D5E97E7A079D93D2FBB00'); 12 | assert.equal(message.sender, '989124834198'); --------------------------------------------------------------------------------