├── index.js ├── examples └── app.js ├── README.md ├── package.json ├── .gitignore ├── lib └── phone.js └── phone.advanced.js /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/phone'); 2 | -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | let Phone = require('../index'); 2 | 3 | let phone = new Phone({ 4 | port: '/dev/tty.usbmodem0000001', 5 | line: '3624422471', 6 | description: 'House' 7 | }); 8 | 9 | phone.on('ringing', function (call) { 10 | console.log(`Ringing: ${call.number}`); 11 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # caller-id 2 | 3 | Node.js Phone caller Id using modem 4 | 5 | Example 6 | 7 | ``` javascript 8 | 9 | let Phone = require('phone-caller-id'); 10 | 11 | let phone = new Phone({ 12 | port: '/dev/tty.usbmodem0000001', 13 | line: '3624422471', 14 | description: 'House' 15 | }); 16 | 17 | phone.on('ringing', function (call) { 18 | console.log(`Ringing: ${call.number}`); 19 | }); 20 | ``` 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phone-caller-id", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:arg20/phone-caller-id.git" 12 | }, 13 | "author": "Gabriel Zimmermann", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/arg20/phone-caller-id/issues" 17 | }, 18 | "homepage": "https://github.com/arg20/phone-caller-id#readme", 19 | "dependencies": { 20 | "bluebird": "^3.5.0", 21 | "moment": "^2.18.1", 22 | "serialport": "^5.0.0-beta3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # Logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Docs 8 | js.docs 9 | api.docs 10 | doc/ 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # testing 18 | coverage 19 | $CIRCLE_TEST_REPORTS 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency and build directories 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | #ide and os 37 | .idea 38 | .project 39 | .DS_STORE 40 | 41 | #compressed 42 | *.zip 43 | 44 | # Uploaded and temp files 45 | temp/* 46 | !temp/.gitkeep 47 | 48 | uploads/*/* 49 | !uploads/.gitkeep 50 | !uploads/*/.gitkeep 51 | configs/certs 52 | 53 | #Lambdas 54 | lambda/node_modules/ 55 | lambda/.serverless/ 56 | 57 | # Elastic Beanstalk Files 58 | .elasticbeanstalk/* 59 | !.elasticbeanstalk/*.cfg.yml 60 | !.elasticbeanstalk/*.global.yml 61 | -------------------------------------------------------------------------------- /lib/phone.js: -------------------------------------------------------------------------------- 1 | let EventEmitter = require('events').EventEmitter; 2 | let bluebird = require('bluebird'); 3 | let promisifyAll = bluebird.promisifyAll; 4 | let SerialPort = require('serialport'); 5 | promisifyAll(SerialPort); 6 | 7 | let moment = require('moment'); 8 | 9 | let codes = { 10 | ring: '\u0010R', 11 | number: 'NMBR=', 12 | dial: '\u0010d', 13 | busy: '\u0010b', 14 | }; 15 | 16 | let secondsInBetweenRings = 4; 17 | 18 | 19 | function extractPhoneNumber(string) { 20 | let number = string.indexOf(codes.number); 21 | if (number < 0) { 22 | return null; 23 | } 24 | let phoneNumber = string.substring(number + 5, string.length - 1); 25 | phoneNumber = phoneNumber.replace(/\D/g, ''); 26 | return phoneNumber; 27 | } 28 | 29 | class Phone extends EventEmitter { 30 | 31 | constructor({port, line, description}) { 32 | super(); 33 | this.line = line; 34 | this.description = description; 35 | this.serialPort = new SerialPort(port); 36 | promisifyAll(this.serialPort); 37 | this.serialPort.on('open', this.init.bind(this)); 38 | } 39 | 40 | async init() { 41 | await this.activateVoiceMode(); 42 | await this.activateCallerId(); 43 | await this.setUpListener(); 44 | await this.hangUp(); 45 | this.emit('init'); 46 | } 47 | 48 | async activateVoiceMode() { 49 | await this.serialPort.writeAsync('AT+FCLASS=8\r'); 50 | await this.serialPort.drainAsync(); 51 | } 52 | 53 | async activateCallerId() { 54 | await this.serialPort.writeAsync('AT+VCID=1\r'); 55 | await this.serialPort.drainAsync(); 56 | } 57 | 58 | async setUpListener() { 59 | this.serialPort.on('data', async data => { 60 | let msg = Buffer.from(data, 'ascii').toString(); 61 | this.lastEventAt = moment(); 62 | // If data contains a phone number extract it and trigger current call. 63 | let phoneNumber = extractPhoneNumber(msg); 64 | 65 | if (phoneNumber) { 66 | this.startCall(phoneNumber); 67 | return; 68 | } 69 | 70 | if (msg === codes.ring) { 71 | this.ring(); 72 | return; 73 | } 74 | }); 75 | } 76 | 77 | async startCall(phoneNumber) { 78 | this.ongoingCall = { 79 | line: this.line, 80 | description: this.description, 81 | number: phoneNumber, 82 | startedAt: moment(), 83 | rings: 1, 84 | }; 85 | 86 | this.emit('ringing.start', this.ongoingCall); 87 | this.emit('ringing', this.ongoingCall); 88 | this.setCheck(); 89 | } 90 | 91 | async endCall() { 92 | if (this.ongoingCall) { 93 | this.emit('ringing.end', this.ongoingCall); 94 | } 95 | this.ongoingCall = null; 96 | } 97 | 98 | async ring() { 99 | if (this.lastRingAt) { 100 | let secondsFromLastRing = moment().diff(this.lastRingAt, 'seconds'); 101 | if (secondsFromLastRing <= secondsInBetweenRings) { 102 | this.ongoingCall.rings++; 103 | this.emit('ringing', this.ongoingCall); 104 | } 105 | } 106 | this.lastRingAt = moment(); 107 | this.setCheck(); 108 | } 109 | 110 | async setCheck() { 111 | let checkAgainIn = secondsInBetweenRings * 1000 + 1000; // 1 more seconds after it stops ringing 112 | 113 | if (this.check) { 114 | clearTimeout(this.check); 115 | } 116 | this.check = setTimeout(async () => { 117 | let ringSecondsAgo = moment().diff(this.lastRingAt, 'seconds'); 118 | if (ringSecondsAgo <= secondsInBetweenRings) { 119 | return this.setCheck(); 120 | } 121 | this.endCall(); 122 | }, checkAgainIn); 123 | } 124 | } 125 | 126 | Phone.listPorts = function () { 127 | return SerialPort.listAsync(); 128 | }; 129 | 130 | module.exports = Phone; -------------------------------------------------------------------------------- /phone.advanced.js: -------------------------------------------------------------------------------- 1 | let EventEmitter = require('events').EventEmitter; 2 | let bluebird = require('bluebird'); 3 | let promisifyAll = bluebird.promisifyAll; 4 | let SerialPort = require('serialport'); 5 | let moment = require('moment'); 6 | 7 | let codes = { 8 | ring: '\u0010R', 9 | number: 'NMBR=', 10 | dial: '\u0010d', 11 | busy: '\u0010b', 12 | }; 13 | 14 | /** WIP **/ 15 | 16 | function extractPhoneNumber(string) { 17 | let number = string.indexOf(codes.number); 18 | if (number < 0) { 19 | return null; 20 | } 21 | let phoneNumber = string.substring(number + 5, string.length - 1); 22 | phoneNumber = phoneNumber.replace(/\D/g, ''); 23 | return phoneNumber; 24 | } 25 | 26 | class Phone extends EventEmitter { 27 | 28 | constructor(port) { 29 | super(); 30 | this.serialPort = new SerialPort(port); 31 | promisifyAll(this.serialPort); 32 | this.serialPort.on('open', this.init.bind(this)); 33 | } 34 | 35 | async init() { 36 | await this.setToVoiceMode(); 37 | await this.setUpListener(); 38 | this.emit('init'); 39 | } 40 | 41 | async setToVoiceMode() { 42 | await this.serialPort.writeAsync('AT+FCLASS=8\r'); 43 | await this.serialPort.drainAsync(); 44 | await this.serialPort.writeAsync('AT+VCID=1\r'); 45 | await this.serialPort.drainAsync(); 46 | await this.hangUp(); 47 | } 48 | 49 | async setUpListener() { 50 | this.serialPort.on('data', async data => { 51 | let msg = Buffer.from(data, 'ascii').toString(); 52 | this.lastEventAt = moment(); 53 | // If data contains a phone number extract it and trigger current call. 54 | let phoneNumber = extractPhoneNumber(msg); 55 | 56 | if (phoneNumber) { 57 | this.startCall(phoneNumber); 58 | return; 59 | } 60 | 61 | // console.log(JSON.stringify(msg)); 62 | 63 | if (msg === codes.ring) { 64 | this.ring(); 65 | return; 66 | } 67 | 68 | if (msg === codes.dial) { 69 | this.endCall(); 70 | } 71 | 72 | if (msg === codes.busy) { 73 | this.endCall(); 74 | } 75 | }); 76 | } 77 | 78 | async startCall(phoneNumber) { 79 | this.ongoingCall = { 80 | number: phoneNumber, 81 | startedAt: moment(), 82 | rings: 1, 83 | pickedUpAt: null, 84 | hungUpAt: null, 85 | }; 86 | 87 | this.emit('call.incoming', this.ongoingCall); 88 | this.emit('ringing', this.ongoingCall); 89 | this.setCheck(); 90 | } 91 | 92 | async endCall() { 93 | if (this.ongoingCall) { 94 | if (this.ongoingCall.pickedUpAt) { 95 | this.ongoingCall.hungUpAt = moment(); 96 | this.ongoingCall.duration = this.ongoingCall.pickedUpAt.diff(this.ongoingCall.hungUpAt, 'seconds'); 97 | this.emit('call.end', this.ongoingCall); 98 | } else { 99 | this.emit('ringing.end', this.ongoingCall); 100 | } 101 | } 102 | this.ongoingCall = null; 103 | await this.hangUp(); 104 | } 105 | 106 | async ring() { 107 | if (this.lastRingAt) { 108 | let lastRingAgo = moment().diff(this.lastRingAt, 'seconds'); 109 | if (lastRingAgo === 4) { 110 | this.ongoingCall.rings++; 111 | this.emit('ringing', this.ongoingCall); 112 | } 113 | } 114 | this.lastRingAt = moment(); 115 | this.setCheck(); 116 | } 117 | 118 | async pickUp() { 119 | await this.serialPort.writeAsync('AT+VLS=5\r'); 120 | await this.serialPort.drainAsync(); 121 | } 122 | 123 | async hangUp() { 124 | await this.serialPort.writeAsync('AT+VLS=0\r'); 125 | await this.serialPort.drainAsync(); 126 | } 127 | 128 | async setCheck() { 129 | if (this.check) { 130 | clearTimeout(this.check); 131 | } 132 | this.check = setTimeout(async () => { 133 | let ringSecondsAgo = moment().diff(this.lastRingAt, 'seconds'); 134 | if (ringSecondsAgo > 4) { 135 | await this.pickUp(); 136 | setTimeout(() => { 137 | if (this.ongoingCall) { 138 | this.ongoingCall.pickedUpAt = moment(); 139 | this.emit('call.start', this.ongoingCall); 140 | } 141 | }, 4000); 142 | } 143 | }, 5000); 144 | } 145 | 146 | async checkIfPickedUp() { 147 | 148 | } 149 | } 150 | 151 | module.exports = Phone; --------------------------------------------------------------------------------