├── .npmignore ├── .gitignore ├── tests ├── runAll.sh ├── testPingMan.js ├── testCompression.js ├── testDevMan.js ├── testWRC-RemoteProxy.js └── testWRC-LocalProxy.js ├── scripts └── build.sh ├── package.json ├── runProxyAndWebServer.js ├── runWWWController.js ├── src ├── StickyMessages.js ├── errors.js ├── PingManager.js ├── WebClientConnection.js ├── SocketTCP.js ├── messageHandler.js ├── MySmaz.js ├── ClientConnection.js ├── ServerConnection.js ├── Proxy.js ├── DeviceManager.js └── Device.js ├── runProxy.js ├── index.js ├── Protocol.md ├── README.md └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .eslintrc 3 | .tern-project 4 | tmp 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .eslintrc 3 | .tern-project 4 | npm-debug.log 5 | web-remote-control.js 6 | tmp 7 | proxyError.log 8 | wrcWebServer.log 9 | proxy.log 10 | -------------------------------------------------------------------------------- /tests/runAll.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd tests 4 | 5 | echo "Compression" 6 | node testCompression.js 7 | 8 | echo "DeviceManager" 9 | node testDevMan.js 10 | 11 | echo "PingManager" 12 | node testPingMan.js 13 | 14 | echo "Web-Remote-Control - LOCAL - UDP" 15 | node testWRC-LocalProxy.js 16 | 17 | echo "Web-Remote-Control - LOCAL - TCP" 18 | PROTOCOL=TCP node testWRC-LocalProxy.js 19 | 20 | # 21 | # The following tests require a proxy configured and running on a remote server. 22 | # 23 | # 24 | # run this script using `PROXY_ADDRESS="your.proxy.com" ./runAll.sh` 25 | # 26 | # Proxy should be started using: 27 | # var proxy = wrc.createProxy({udp4:true, allowObservers:true}); 28 | # 29 | if [ "${PROXY_ADDRESS}" != "" ]; then 30 | 31 | echo "Web-Remote-Control - REMOTE (${PROXY_ADDRESS}) - UDP" 32 | node testWRC-RemoteProxy.js 33 | 34 | echo "Web-Remote-Control - REMOTE (${PROXY_ADDRESS}) - TCP" 35 | PROTOCOL=TCP node testWRC-RemoteProxy.js 36 | 37 | fi 38 | 39 | cd .. 40 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This calls Browserify to generate code for the browser. 4 | # 5 | 6 | TARGET="web-remote-control.js" 7 | 8 | cd ../ 9 | SRC_DIR="./src" 10 | WWW_DIR="./www" 11 | 12 | mkdir -p $WWW_DIR 13 | 14 | BRSFY=`which browserify` 15 | 16 | if [ "${BRSFY}" = "" ]; then 17 | echo "Please install browserify globally (npm install -g browserify)." 18 | fi 19 | 20 | # Build web-remote-control 21 | $BRSFY --require ./index.js:web-remote-control \ 22 | --require $SRC_DIR/PingManager.js:PingManager \ 23 | --require $SRC_DIR/Device.js:Device \ 24 | --require $SRC_DIR/WebClientConnection.js:WebClientConnection \ 25 | --require $SRC_DIR/messageHandler.js:messageHandler \ 26 | --exclude $SRC_DIR/ClientConnection.js \ 27 | --exclude $SRC_DIR/Proxy.js \ 28 | --outfile $WWW_DIR/$TARGET 29 | 30 | echo "Built $TARGET" 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-remote-control", 3 | "description": "Fast, real-time, remote control of devices (drones, boats, etc) from the web.", 4 | "license": "APSL-2.0", 5 | "version": "1.9.8", 6 | "main": "index.js", 7 | "author": { 8 | "name": "Simon Werner", 9 | "url": "https://github.com/psiphi75" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/psiphi75/web-remote-control" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/psiphi75/web-remote-control/issues" 17 | }, 18 | "scripts": { 19 | "test": "./tests/runAll.sh", 20 | "prepublish": "cd scripts && ./build.sh && cd .. " 21 | }, 22 | "keywords": [ 23 | "udp", 24 | "remote", 25 | "control", 26 | "web", 27 | "control", 28 | "rc", 29 | "drone", 30 | "uav" 31 | ], 32 | "devDependencies": { 33 | "tape": "^4.6.0" 34 | }, 35 | "dependencies": { 36 | "polo": "^0.8.1", 37 | "shortid": "^2.2.6", 38 | "smaz": "1.0.0", 39 | "socket.io": "^2.2.0", 40 | "split": "^1.0.0", 41 | "wrc-controller": "0.0.12" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /runProxyAndWebServer.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2019 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | require('./runProxy'); 27 | 28 | require('./runWWWController'); 29 | 30 | 31 | -------------------------------------------------------------------------------- /runWWWController.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | /* 27 | * Create HTML/WWW Controller and host it. 28 | */ 29 | 30 | var port = 8888; 31 | var log = console.log; 32 | 33 | var WRCController = require('wrc-controller'); 34 | WRCController(port, log); 35 | -------------------------------------------------------------------------------- /src/StickyMessages.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | function StickyMessages() { 27 | this.status = {}; 28 | this.command = {}; 29 | } 30 | 31 | StickyMessages.prototype.set = function(channel, type, msgObj) { 32 | if (type !== 'command' && type !== 'status') return; 33 | if (msgObj.sticky !== true) return; 34 | this[type][channel] = { 35 | type: type, 36 | seq: msgObj.seq, 37 | data: msgObj.data 38 | }; 39 | }; 40 | 41 | StickyMessages.prototype.get = function(channel, deviceType, msgObj) { 42 | var type; 43 | if (deviceType === 'controller' || deviceType === 'observer') { 44 | type = 'status'; 45 | } else { 46 | type = 'command'; 47 | } 48 | var stickyObj = this[type][channel]; 49 | if (stickyObj === undefined) return undefined; 50 | return { 51 | type: type, 52 | seq: stickyObj.seq, 53 | data: stickyObj.data, 54 | uid: msgObj.uid, 55 | socket: msgObj.socket 56 | }; 57 | }; 58 | 59 | module.exports = StickyMessages; 60 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | module.exports = { 27 | DEVICE_NOT_REGISTERED: { 28 | type: 'DEVICE_NOT_REGISTERED', 29 | code: '1001', 30 | message: 'Device is not registered.' 31 | }, 32 | ERROR_NOT_FOUND: { 33 | type: 'ERROR_NOT_FOUND', 34 | code: '9001', 35 | message: 'The specified code error was not found.' 36 | }, 37 | PERMISSION_DENIED: { 38 | type: 'PERMISSION_DENIED', 39 | code: '5001', 40 | message: 'The action is not allowed.' 41 | }, 42 | 43 | getByCode: function (code) { 44 | 45 | switch (typeof code) { 46 | case 'number': 47 | code = code.toString(); 48 | break; 49 | case 'string': 50 | break; 51 | default: 52 | return this.ERROR_NOT_FOUND; 53 | } 54 | 55 | for (var errType in this) { 56 | var err = this[errType]; 57 | if (err.code === code) { 58 | return err; 59 | } 60 | } 61 | 62 | return this.ERROR_NOT_FOUND; 63 | 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /runProxy.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | /* 27 | * We just run the proxy. 28 | */ 29 | 30 | var DISCOVERY_PROXY_NAME = 'web-remote-control-proxy'; 31 | var polo = require('polo'); 32 | var apps = polo(); 33 | apps.put({ 34 | name: DISCOVERY_PROXY_NAME, 35 | host: getIPAddress(), 36 | port: 31234 37 | }); 38 | 39 | var wrc = require('web-remote-control'); 40 | wrc.createProxy({ 41 | udp4: true, 42 | tcp: true, 43 | socketio: true, 44 | onlyOneControllerPerChannel: true, 45 | onlyOneToyPerChannel: true, 46 | allowObservers: true, 47 | log: function() {} 48 | }); 49 | 50 | function getIPAddress() { 51 | 52 | var os = require('os'); 53 | var ifaces = os.networkInterfaces(); 54 | var addresses = []; 55 | 56 | Object.keys(ifaces).forEach(function (ifname) { 57 | var alias = 0; 58 | 59 | ifaces[ifname].forEach(function (iface) { 60 | if (iface.family !== 'IPv4' || iface.internal !== false) { 61 | // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses 62 | return; 63 | } 64 | 65 | if (alias >= 1) { 66 | // this single interface has multiple ipv4 addresses 67 | console.log(ifname + ':' + alias, iface.address); 68 | } else { 69 | // this interface has only one ipv4 adress 70 | console.log(ifname, iface.address); 71 | } 72 | addresses.push(iface.address); 73 | alias += 1; 74 | 75 | }); 76 | }); 77 | 78 | addresses.sort(function(add) { return (add === '192.168.7.1' ? 1 : -1); }); 79 | return addresses[0]; 80 | } 81 | -------------------------------------------------------------------------------- /tests/testPingMan.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | var test = require('tape'); 25 | 26 | var PingManager = require('../src/PingManager'); 27 | var settings = { 28 | log: function () {} 29 | }; 30 | 31 | test('Can create a few pings', function (t) { 32 | 33 | t.plan(2); 34 | 35 | var pm = new PingManager(settings); 36 | var fn1 = function () {}; 37 | var fn2 = function () {}; 38 | var fn3 = function () {}; 39 | pm.add(1, fn1); 40 | pm.add(2, fn2); 41 | pm.add(3, fn3); 42 | 43 | t.deepEqual(pm.pingList[1].callback, fn1); 44 | t.deepEqual(pm.pingList[3].callback, fn3); 45 | 46 | clearTimeout(pm.pingList[1].timeoutHandle); 47 | clearTimeout(pm.pingList[2].timeoutHandle); 48 | clearTimeout(pm.pingList[3].timeoutHandle); 49 | 50 | t.end(); 51 | 52 | }); 53 | 54 | test('Pings timeout (self-destruct)', function (t) { 55 | 56 | t.plan(3); 57 | 58 | var pm = new PingManager(settings); 59 | pm.MAX_PING_WAIT_TIME = 100; 60 | 61 | var pingResponseTime = 0; 62 | var fn1 = function (time) { 63 | pingResponseTime = time; 64 | t.equal(time, -1, 'pingResponse should return -1 (unsuccessful ping)'); 65 | t.end(); 66 | }; 67 | pm.add(1, fn1); 68 | 69 | t.equal(pingResponseTime, 0, 'pingResponse should not have changed yet'); 70 | t.deepEqual(pm.pingList[1].callback, fn1); 71 | 72 | }); 73 | 74 | test('Pings can be resolved', function (t) { 75 | 76 | t.plan(2); 77 | 78 | var pm = new PingManager(settings); 79 | 80 | var fn1 = function (time) { 81 | t.equal(time, 123, 'pingResponse should be positive'); 82 | 83 | setTimeout(function () { 84 | t.equal(typeof pm.pingList[1], 'undefined', 'Ping should be removed from list'); 85 | t.end(); 86 | }, 100); 87 | }; 88 | pm.add(1, fn1); 89 | pm.handleIncomingPing(1, 123); 90 | 91 | }); 92 | -------------------------------------------------------------------------------- /src/PingManager.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | /** 25 | * PingManager ensures all pings are matched to a response. Lost pings are 26 | * forgotten over time. 27 | */ 28 | function PingManager(settings) { 29 | this.pingList = {}; 30 | this.MAX_PING_WAIT_TIME = 60 * 1000; 31 | this.log = settings.log; 32 | } 33 | 34 | /** 35 | * Add a new ping and handle the callback when it times out. 36 | * @param {number} pingId The ping sequenceNumber 37 | * @param {Function} callback The callback (returns time or -1 on timeoutHandle) 38 | */ 39 | PingManager.prototype.add = function(pingId, callback) { 40 | 41 | if (typeof pingId !== 'number') { 42 | this.log('PingManager.add(): pingId must be a number.'); 43 | return; 44 | } 45 | if (this.pingList[pingId]) { 46 | this.log('PingManager.add(): pingId has already been supplied.'); 47 | return; 48 | } 49 | 50 | // Create the ping, and make it self destruct. 51 | var self = this; 52 | var ping = { 53 | callback: callback, 54 | timeoutHandle: setTimeout(function removeStalePing() { 55 | // and delete the stale ping 56 | self.handleIncomingPing(pingId, -1); 57 | }, this.MAX_PING_WAIT_TIME) 58 | }; 59 | 60 | this.pingList[pingId] = ping; 61 | 62 | }; 63 | 64 | /** 65 | * We call this when we receive a ping. This will close the ping. This will 66 | * run the callback we called with the add() method. 67 | * @param {number} pingId The ping sequenceNumber. 68 | * @param {number} time The elapsed time. 69 | */ 70 | PingManager.prototype.handleIncomingPing = function(pingId, time) { 71 | var ping = this.pingList[pingId]; 72 | if (!ping) { 73 | this.log('PingManager.respond(): pingId not found.'); 74 | return; 75 | } 76 | 77 | clearTimeout(ping.timeoutHandle); 78 | 79 | if (typeof ping.callback === 'function'){ 80 | ping.callback(time); 81 | } 82 | 83 | try { 84 | delete this.pingList[pingId]; 85 | } catch (ex) { 86 | this.log('Did not expect this.'); 87 | } 88 | 89 | }; 90 | 91 | PingManager.prototype.close = function () { 92 | for (var pingId in this.pingList) { 93 | this.handleIncomingPing(pingId, -1); 94 | } 95 | }; 96 | 97 | module.exports = PingManager; 98 | -------------------------------------------------------------------------------- /src/WebClientConnection.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var messageHandler = require('./messageHandler'); 27 | 28 | var EventEmitter = require('events').EventEmitter; 29 | var util = require('util'); 30 | 31 | /** 32 | * The connection manager will handle the TCP and UDP transport. As well as 33 | * the protocol. 34 | */ 35 | function WebClientConnection(options) { 36 | 37 | var url = window.location.host.split(':')[0]; 38 | if (options && options.proxyUrl) { 39 | url = options.proxyUrl; 40 | } 41 | 42 | this.createProxySocket('http://' + url, 33331); 43 | 44 | EventEmitter.call(this); 45 | } 46 | util.inherits(WebClientConnection, EventEmitter); 47 | 48 | 49 | /** 50 | * Set up the UDP listener. 51 | */ 52 | WebClientConnection.prototype.createProxySocket = function (address, port) { 53 | 54 | var self = this; 55 | this.remoteAddress = address; 56 | this.remotePort = port; 57 | 58 | this.socket = window.io(address + ':' + port); 59 | this.socket.on('connect', function() { 60 | console.log('connected'); 61 | }); 62 | this.socket.on('error', function(ex) { 63 | this.emit('error', ex); 64 | }); 65 | this.socket.on('event', function(message) { 66 | if (message && typeof message.byteLength === 'number') { 67 | message = String.fromCharCode.apply(null, new Uint8Array(message)); 68 | } 69 | handleMessage.bind(self)(message); 70 | }); 71 | this.socket.on('disconnect', function() {}); 72 | }; 73 | 74 | 75 | function handleMessage(message) { 76 | 77 | var msgObj; 78 | try { 79 | msgObj = messageHandler.parseIncomingMessage(message); 80 | } catch (ex) { 81 | this.emit('error', ex); 82 | return; 83 | } 84 | 85 | this.emit(msgObj.type, msgObj); 86 | 87 | } 88 | 89 | 90 | /** 91 | * Sends a message to the remote device. 92 | * @param {string} err The error string 93 | * @param {string} address The remote address. 94 | * @param {number} remote The remote port. 95 | */ 96 | WebClientConnection.prototype._send = function(msgObj) { 97 | 98 | var sendBuffer = messageHandler.packOutgoingMessage(msgObj); 99 | 100 | try { 101 | this.socket.emit('event', sendBuffer); 102 | } catch (ex) { 103 | console.error('WebClientConnection._send(): ', ex); 104 | } 105 | 106 | }; 107 | 108 | module.exports = WebClientConnection; 109 | -------------------------------------------------------------------------------- /src/SocketTCP.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | var EventEmitter = require('events').EventEmitter; 26 | var util = require('util'); 27 | 28 | function SocketTCP (options) { 29 | 30 | this.log = options.log; 31 | this.port = options.port; 32 | 33 | this.listenTCP(); 34 | 35 | EventEmitter.call(this); 36 | 37 | } 38 | util.inherits(SocketTCP, EventEmitter); 39 | 40 | 41 | /** 42 | * Start the TCP server on the given port and address (optional). 43 | */ 44 | SocketTCP.prototype.listenTCP = function() { 45 | 46 | var net = require('net'); 47 | var split = require('split'); 48 | var self = this; 49 | this.socket = net.createServer(function(socket) { 50 | var socketInfo = { 51 | protocol: 'tcp', 52 | address: socket.remoteAddress, 53 | port: socket.remotePort, 54 | socketId: socket.remoteAddress + ':' + socket.remotePort, 55 | socket: socket, 56 | close: function close() { 57 | try { 58 | socket.destroy(); 59 | } catch (ex) { 60 | // okay if there is an error - socket may already be closed. 61 | } 62 | } 63 | }; 64 | var stream = socket.pipe(split('\n')); 65 | stream.on('data', function(message) { 66 | self.emit('data', message); 67 | self.handleMessage.bind(self)(message, socketInfo); 68 | }); 69 | stream.on('close', function() { 70 | self.emit('socket-close', socketInfo.socketId); 71 | }); 72 | stream.on('error', self.handleMessage.bind(self)); 73 | 74 | }); 75 | this.socket.on('error', this.handleError); 76 | this.socket.on('listening', function () { 77 | self.emit('listening', self.port, self.proxyUrl, 'tcp'); 78 | }); 79 | this.socket.listen(self.port); 80 | 81 | }; 82 | 83 | SocketTCP.prototype.handleError = function(err) { 84 | if (typeof this.log === 'function') { 85 | this.log(err); 86 | } else { 87 | console.error(err); 88 | } 89 | this.emit('error', err); 90 | }; 91 | 92 | /** 93 | * Close all connections. 94 | */ 95 | SocketTCP.prototype.close = function() { 96 | 97 | this.socket.close(); 98 | this.removeAllListeners(); 99 | 100 | }; 101 | 102 | /** 103 | * Sends a message to the remote device. 104 | */ 105 | SocketTCP.prototype.write = function(data) { 106 | 107 | try { 108 | this.socket.write(data, undefined, function() { 109 | self.write 110 | }); 111 | } catch (ex) { 112 | this.log('SocketTCP._send():error: ', ex); 113 | } 114 | 115 | }; 116 | 117 | module.exports = SocketTCP; 118 | -------------------------------------------------------------------------------- /tests/testCompression.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var test = require('tape'); 27 | 28 | var msgHandler = require('../src/messageHandler'); 29 | 30 | var mySmaz = require('../src/MySmaz'); 31 | 32 | test('Smaz codebook hack is valid', function(t) { 33 | t.plan(1); 34 | t.doesNotThrow(mySmaz.validateCodebook, undefined, 'Codebook Validation'); 35 | t.end(); 36 | }); 37 | 38 | messageHandlerCompUncomp(true); 39 | messageHandlerCompUncomp(false); 40 | 41 | 42 | function messageHandlerCompUncomp(enable_compression) { 43 | 44 | // This is not dependant on local, but we don't need to over test it. 45 | test('messageHandler compress and uncompresses', function(t) { 46 | t.plan(3); 47 | 48 | var obj = { type: 'ping', seq: 1234, uid: '123422', data: '1453020903937' }; 49 | var o = msgHandler.parseIncomingMessage(msgHandler.packOutgoingMessage(obj, enable_compression), enable_compression); 50 | t.deepEqual(o, obj, 'Can compress and decompress'); 51 | 52 | obj.data = { 'moredata': { 'yetmore': {} } }; 53 | o = msgHandler.parseIncomingMessage(msgHandler.packOutgoingMessage(obj, enable_compression), enable_compression); 54 | t.deepEqual(o, obj, 'Can compress and decompress nested objects'); 55 | 56 | obj.data.newline = { 'yetmoredata': 'data wi\nth newlines\n' }; 57 | o = msgHandler.parseIncomingMessage(msgHandler.packOutgoingMessage(obj, enable_compression), enable_compression); 58 | t.deepEqual(o, obj, 'Can compress and decompress with newline characters'); 59 | 60 | t.end(); 61 | 62 | }); 63 | } 64 | 65 | // This is not dependant on local, but we don't need to over test it. 66 | test('Compression actually compresses', function(t) { 67 | t.plan(1); 68 | 69 | var obj = { type: 'ping', seq: 1234, uid: '123422', data: '1453020903937' }; 70 | obj.data = { 'moredata': { 'yetmore': {} } }; 71 | obj.data.newline = { 'yetmoredata': 'data wi\nth newlines\n' }; 72 | 73 | var objStr = JSON.stringify(obj); 74 | 75 | var lenBefore = objStr.length; 76 | var lenAfter = mySmaz.compress(objStr).length; 77 | t.true(lenAfter < lenBefore * 0.7, 'Size is smaller after compression.'); 78 | 79 | console.log('Bytes before: ', lenBefore); 80 | console.log('Bytes after: ', lenAfter); 81 | 82 | t.end(); 83 | 84 | }); 85 | 86 | 87 | // This is not dependant on local, but we don't need to over test it. 88 | test('(De)Compression works', function(t) { 89 | 90 | var tests = [ 91 | 'This is a simple test', 92 | '', 93 | 'And\nsome new\n lines\n', 94 | '%9823h&3j*jd', 95 | '\t' 96 | ]; 97 | 98 | t.plan(tests.length); 99 | 100 | tests.forEach(function (str, i) { 101 | t.equal(mySmaz.decompress(mySmaz.compress(str)), str, 'Test string #' + (i + 1)); 102 | }); 103 | 104 | t.end(); 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /tests/testDevMan.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | var test = require('tape'); 25 | 26 | var DevMan = require('../src/DeviceManager'); 27 | var devices = new DevMan(); 28 | 29 | var deviceType = 'toy'; 30 | var channel1 = 'chan1'; 31 | var channel2 = 'chan2'; 32 | var uid2; 33 | var uid5; 34 | var fakeSocket1 = { 35 | socketId: '1', 36 | close: function() {} 37 | }; 38 | var fakeSocket2 = { 39 | socketId: '2', 40 | close: function() {} 41 | }; 42 | var fakeSocket3 = { 43 | socketId: '3', 44 | close: function() {} 45 | }; 46 | var fakeSocket4 = { 47 | socketId: '4', 48 | close: function() {} 49 | }; 50 | 51 | test('Can create a list of devices and add many', function(t) { 52 | 53 | t.plan(6); 54 | 55 | uid2 = devices.add(deviceType, channel1, fakeSocket1, 1); 56 | uid5 = devices.add(deviceType, channel2, fakeSocket2, 1); 57 | 58 | t.true(typeof devices.add(deviceType, channel1, fakeSocket1, 1) === 'string', 'added a toy'); 59 | t.true(typeof uid2 === 'string', 'added a toy'); 60 | t.true(typeof devices.add(deviceType, channel1, fakeSocket3, 1) === 'string', 'added a toy'); 61 | 62 | t.true(typeof devices.add(deviceType, channel2, fakeSocket4, 1) === 'string', 'added a toy'); 63 | t.true(typeof uid5 === 'string', 'added a toy'); 64 | 65 | t.equal(devices.add(), undefined, 'must pass parameters to add'); 66 | 67 | t.end(); 68 | 69 | 70 | }); 71 | 72 | test('Can retreive the toys', function(t) { 73 | 74 | t.plan(2); 75 | 76 | t.deepEqual(devices.get(uid2), { 77 | deviceType: deviceType, 78 | channel: channel1, 79 | socket: fakeSocket1, 80 | seqNum: 1 81 | }); 82 | t.deepEqual(devices.get(uid5), { 83 | deviceType: deviceType, 84 | channel: channel2, 85 | socket: fakeSocket2, 86 | seqNum: 1 87 | }); 88 | 89 | t.end(); 90 | 91 | }); 92 | 93 | test('Can get devices on channel and remove devices', function(t) { 94 | 95 | t.plan(4); 96 | 97 | t.equal(devices.getAll(deviceType, channel1).length, 3); 98 | t.true(devices.remove(uid2), 'can remove device'); 99 | t.equal(devices.getAll(deviceType, channel1).length, 2); 100 | t.false(devices.remove(uid2), 'can not remove device'); 101 | 102 | t.end(); 103 | 104 | }); 105 | 106 | test('Can update a device', function(t) { 107 | 108 | t.plan(1); 109 | 110 | devices.update(uid5, fakeSocket1, 3005); 111 | 112 | t.deepEqual(devices.get(uid5), { 113 | deviceType: deviceType, 114 | channel: channel2, 115 | socket: fakeSocket1, 116 | seqNum: 3005 117 | }, 'device updates correctly'); 118 | 119 | t.end(); 120 | 121 | }); 122 | 123 | 124 | test('Sequence number checks', function(t) { 125 | 126 | t.plan(2); 127 | 128 | devices.update(uid5, fakeSocket1, 3005); 129 | t.ok(devices.isLatestSeqNum(uid5, 3006), 'Sequence numbers increment okay.'); 130 | 131 | devices.update(uid5, fakeSocket1, 3006); 132 | t.notOk(devices.isLatestSeqNum(uid5, 3005), 'Old Sequence numbers fail.'); 133 | 134 | t.end(); 135 | 136 | }); 137 | -------------------------------------------------------------------------------- /tests/testWRC-RemoteProxy.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | 27 | // 28 | // Imports 29 | // 30 | 31 | 32 | var test = require('tape'); 33 | var wrc = require('../index'); 34 | 35 | 36 | // 37 | // Configuration 38 | // 39 | 40 | var channel1 = 'channel-1'; 41 | 42 | // Allows us to change the tests (UDP vs TCP) from the command line 43 | var UDP = true; 44 | var TCP = false; 45 | if (process.env.PROTOCOL && process.env.PROTOCOL.toUpperCase() === 'TCP') { 46 | UDP = false; 47 | TCP = true; 48 | } 49 | 50 | // Enable detailed logging 51 | var ENABLE_LOGGING = false; 52 | var logging; 53 | if (!ENABLE_LOGGING) { 54 | logging = function() {}; 55 | } 56 | 57 | 58 | // 59 | // Tests 60 | // 61 | 62 | 63 | var options = { 64 | channel: channel1, 65 | log: logging, 66 | keepalive: 0, 67 | udp4: UDP, 68 | tcp: TCP, 69 | proxyUrl: process.env.PROXY_ADDRESS 70 | }; 71 | var controller = wrc.createController(options); 72 | var toy = wrc.createToy(options); 73 | 74 | // Wait until both devices are registered 75 | var countRegs = 0; 76 | controller.once('register', regCounter); 77 | toy.once('register', regCounter); 78 | function regCounter () { 79 | countRegs += 1; 80 | if (countRegs === 2) { 81 | startTests(); 82 | } 83 | } 84 | 85 | function startTests () { 86 | 87 | 88 | test('Test we can ping the Proxy and get a result', function(t) { 89 | 90 | t.plan(4); 91 | 92 | controller.ping(function (time) { 93 | t.true(typeof time === 'number', 'controller: ping time is a number'); 94 | t.true(time >= 0, 'controller: ping time is in the past'); 95 | }); 96 | 97 | toy.ping(function (time) { 98 | t.true(typeof time === 'number', 'toy: ping time is a number'); 99 | t.true(time >= 0, 'toy: ping time is in the past'); 100 | }); 101 | 102 | }); 103 | 104 | 105 | test('Test controller-1 can send commands to toy-1 (text)', function(t) { 106 | 107 | t.plan(1); 108 | 109 | var cmdTxt = 'simon say\'s do this'; 110 | 111 | toy.once('command', function(respCmdTxt) { 112 | 113 | // Slow things down just a bit 114 | setTimeout(function() { 115 | t.equal(respCmdTxt, cmdTxt, 'command received is correct'); 116 | t.end(); 117 | }, 50); 118 | 119 | }); 120 | 121 | controller.command(cmdTxt); 122 | 123 | }); 124 | 125 | test('Test controller-1 can send commands to toy-1 (object)', function(t) { 126 | 127 | t.plan(1); 128 | 129 | var cmdObj = { 130 | a: 'simon say\'s do this', 131 | b: 'c' 132 | }; 133 | 134 | toy.once('command', function(respCmdObj) { 135 | t.deepEqual(respCmdObj, cmdObj, 'command was correct'); 136 | t.end(); 137 | }); 138 | 139 | controller.command(cmdObj); 140 | 141 | }); 142 | 143 | 144 | test('Test toy-1 can send status updates to controller-1 (text)', function(t) { 145 | 146 | t.plan(1); 147 | 148 | var statusTxt = 'Hi, I am here'; 149 | 150 | controller.once('status', function fn (respStatusTxt) { 151 | t.equal(respStatusTxt, statusTxt, 'command was correct'); 152 | t.end(); 153 | }); 154 | 155 | toy.status(statusTxt); 156 | 157 | }); 158 | 159 | test.onFinish(function () { 160 | 161 | setTimeout(function () { 162 | 163 | toy.close(); 164 | controller.close(); 165 | 166 | }, 250); 167 | 168 | }); 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/messageHandler.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var mySmaz = require('./MySmaz'); 27 | 28 | /** 29 | * Parse an incoming message and ensure it's valid. Convert it to an object that 30 | * can ben sent to other listeners. 31 | * 32 | * @param {[uint8]?} message The message from the datastream 33 | * @param {object} remote The remote host 34 | * @param {string} protocol The protocol we are using 35 | * @return {object} The valid object. 36 | * @throws Error when the message is invalid. 37 | */ 38 | exports.parseIncomingMessage = function(message, enable_compression) { 39 | 40 | if (message.length === 0) { 41 | // Empty packet arrived, this happens when remote closes connection 42 | return null; 43 | } 44 | 45 | var msgObj; 46 | 47 | try { 48 | msgObj = decompress(message, enable_compression); 49 | } catch (ex) { 50 | throw new Error('There was an error parsing the incoming message: ' + ex + JSON.stringify(message.toString())); 51 | } 52 | 53 | if (typeof msgObj !== 'object') { 54 | throw new Error('The incoming message is corrupt'); 55 | } 56 | 57 | /* Check the type is valid */ 58 | var requiresList; 59 | switch (msgObj.type) { 60 | case 'register': 61 | requiresList = ['type', 'seq', 'data']; 62 | break; 63 | 64 | case 'status': 65 | case 'command': 66 | case 'ping': 67 | case 'error': 68 | requiresList = ['type', 'seq', 'data', 'uid']; 69 | break; 70 | default: 71 | throw new Error('An invalid incoming message arrived: ' + msgObj.toString()); 72 | } 73 | 74 | /* Check the properties are all valid */ 75 | requiresList.forEach(function(req) { 76 | if (!msgObj.hasOwnProperty(req)) { 77 | throw new Error('The message that arrived is not valid, it does not contain a property: ' + req); 78 | } 79 | }); 80 | 81 | return msgObj; 82 | }; 83 | 84 | exports.packOutgoingMessage = function(msgObj, enable_compression) { 85 | 86 | var cleanMsgObj = {}; 87 | if (msgObj.hasOwnProperty('type')) { 88 | cleanMsgObj.type = msgObj.type; 89 | } 90 | if (msgObj.hasOwnProperty('seq')) { 91 | cleanMsgObj.seq = msgObj.seq; 92 | } 93 | if (msgObj.hasOwnProperty('data')) { 94 | cleanMsgObj.data = msgObj.data; 95 | } 96 | if (msgObj.hasOwnProperty('uid')) { 97 | cleanMsgObj.uid = msgObj.uid; 98 | } 99 | if (msgObj.hasOwnProperty('sticky') && msgObj.sticky === true) { 100 | cleanMsgObj.sticky = true; 101 | } 102 | 103 | return compress(cleanMsgObj, enable_compression); 104 | 105 | }; 106 | 107 | 108 | function compress(data, enable_compression) { 109 | 110 | var result = JSON.stringify(data); 111 | result = result + '\n'; 112 | 113 | if (enable_compression) { 114 | result = mySmaz.compress(result); 115 | } 116 | 117 | return new Buffer(result); 118 | } 119 | 120 | function decompress(compressedData, enable_compression) { 121 | 122 | var str; 123 | if (enable_compression) { 124 | str = mySmaz.decompress(compressedData); 125 | } else { 126 | str = compressedData.toString(); 127 | } 128 | 129 | /* socket.io has a tendancy to concatinate messages */ 130 | // FIXME: This looses information. But do we care? 131 | var strArray = str.split('\n'); 132 | var offset = 1; 133 | if (strArray[strArray.length - 1] === '') { 134 | offset = 2; 135 | } 136 | str = strArray[strArray.length - offset]; 137 | 138 | return JSON.parse(str); 139 | } 140 | -------------------------------------------------------------------------------- /src/MySmaz.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | /***************************************************************************************************** 27 | * 28 | * Hack the smaz codebook 29 | * 30 | * smaz has a codebook, but it does not work well with our implementation. So we hack it a bit. Now 31 | * it works a much better with numbers and JSON. We also take out the 13th char (new line). 32 | * 33 | *****************************************************************************************************/ 34 | 35 | // We only hack the reverse_codebook, then generate the codebook from that. 36 | var reverse_codebook = [' ', 'the', 'e', 't', 'a', 'of', 'o', 'and', 'i', 'n', 37 | '\n', /* We keep the newline in position 0x0A, because it will be used there */ 38 | 'e ', 'r', 's', 39 | ' t', 'in', 'he', 'th', 'h', 'he ', 'to', '\r\n', 'l', 's ', 'd', ' a', 'an","er', 'c', ' o', 'd ', 40 | 'on', ' of', 're', 'of ', 't ', ', ', 'is', 'u', 'at', ' ', 'n ', 'or', 'which', 'f', 'm', 'as', 41 | 'it', 'that', '$', 'was', 'en', ' ', ' w', 'es', ' an', ' i', '\r', 'f ', 'g', 'p', 'nd', ' s', 42 | 'nd ', 'ed ', 'w', 'ed', 'http://', 'for', 'te', 'ing', 'y ', 'The', ' c', 'ti', 'r ', 'his', 'st', 43 | ' in', 'ar', 'nt', ',', ' to', 'y', 'ng', ' h', 'with', 'le', 'al', 'to ', 'b', 'ou', 'be', 'were', 44 | ' b', 'se', 'o ', 'ent', 'ha', 'ng ', 'their', '"', 'hi', 'from', ' f', 'in ', 'de', 'ion', 'me', 45 | 'v', '.', 've', 'all', 're ', 'ri', 'ro', 'is ', 'co', 'f t', 'are', 'ea', '. ', 'her', ' m', 46 | 'er ', ' p', 'es ', 'by', 'they', 'di', 'ra', 'ic', 'not', 's, ', 'd t', 'at ', 'ce', 'la', 'h ', 47 | 'ne', 'as ', 'tio', 'on ', 'n t', 'io', 'we', ' a ', 'om', ', a', 's o', 'ur', 'li', 'll', 'ch', 48 | 'had', 'this', 'e t', 'g ', 'e\r\n', ' wh', 'ere', ' co', 'e o', 'a ', 'us', ' d', 'ss', '\n\r\n', 49 | '\r\n\r', '="', ' be', ' e', 's a', 'ma', 'one', 't t', 'or ', 'but', 'el', 'so', 'l ', 'e s', 50 | 's,', 'no', 'ter', ' wa', 'iv', 'ho', 'e a', ' r', 'hat', 's t', 'ns', 'ch ', 'wh', 'tr', 'ut', 51 | '/', 'have', 'ly ', 'ta', ' ha', ' on', 'tha', '-', ' l', 'ati', 'en ', 'pe', ' re', 'there', 52 | 'ass', 'si', ' fo', 'wa', 'ec', 'our', 'who', 'its', 'z', 'fo', 'rs', '>', 'ot', 'un', '<', 'im', 53 | 'th ', 'j', '\'', '{"type":"', '"}', '","', '":{"', '":"', '"seq":', '{"type":"status",', 54 | '{"type":"error",', '{"type":"command",', '{"type":"register",', '{"type":"ping",', ':"', '":', 55 | '{', '}', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 56 | 57 | var smaz = require('smaz'); 58 | smaz.codebook = generateCodebook(); 59 | smaz.reverse_codebook = reverse_codebook; 60 | 61 | module.exports = { 62 | 63 | compress: smaz.compress, 64 | decompress: smaz.decompress, 65 | 66 | // decompress: function decompress(smazedBuf) { 67 | // var s = new Buffer(smazedBuf); 68 | // var uncompressedString = smaz.decompress(s); 69 | // return uncompressedString; 70 | // }, 71 | 72 | 73 | /** 74 | * Mainly exposed for testing purposes. This needs to be tested each time an update to the codebook is made. 75 | * @throws Error if there is an issue with the codebook. 76 | */ 77 | validateCodebook: function validateCodebook() { 78 | if (reverse_codebook.length !== 254) { 79 | throw new Error('reverse_codebook should have 254 characters, yet it has ' + reverse_codebook.length); 80 | } 81 | reverse_codebook.forEach(function (value, i) { 82 | 83 | if (typeof value !== 'string') { 84 | throw new Error('Value is not a string. value: "' + value + '", index: ' + i); 85 | } 86 | 87 | // If there are two or more similar entries it will catch one of them. 88 | if (reverse_codebook.indexOf(value) !== i) { 89 | throw new Error('Duplicate entry. value: "' + value + '", index: ' + i); 90 | } 91 | }); 92 | } 93 | }; 94 | 95 | 96 | /** 97 | * Generate the codebook from the reverse_codebook. 98 | * @return {[string]} The codebook 99 | */ 100 | function generateCodebook() { 101 | 102 | var codebook = {}; 103 | reverse_codebook.forEach(function (value, i) { 104 | codebook[value] = i; 105 | }); 106 | 107 | return codebook; 108 | } 109 | -------------------------------------------------------------------------------- /src/ClientConnection.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var messageHandler = require('./messageHandler'); 27 | 28 | var EventEmitter = require('events').EventEmitter; 29 | var util = require('util'); 30 | 31 | /** 32 | * The connection manager will handle the TCP and UDP transport. As well as 33 | * the protocol. 34 | */ 35 | function ClientConnection(options) { 36 | 37 | if (options.udp4 === true && options.tcp === true) { 38 | throw new Error('Both udp and tcp are set as protocol. Devices can only communicate in one protocol.'); 39 | } 40 | if (options.udp4 === false && options.tcp === false) { 41 | throw new Error('Neither UDP or TCP is set. Devices must communicate in one protocol.'); 42 | } 43 | 44 | if (options.udp4) { 45 | this.enable_compression = true; 46 | this.createProxySocket('udp4', options.proxyUrl, options.port); 47 | } else { 48 | this.enable_compression = false; 49 | this.createProxySocket('tcp', options.proxyUrl, options.port); 50 | } 51 | 52 | this.log = options.log; 53 | 54 | EventEmitter.call(this); 55 | } 56 | util.inherits(ClientConnection, EventEmitter); 57 | 58 | /** 59 | * Set up the UDP listener. 60 | */ 61 | ClientConnection.prototype.createProxySocket = function (protocol, address, port) { 62 | 63 | this.remoteAddress = address; 64 | this.remotePort = port; 65 | var self = this; 66 | 67 | switch (protocol) { 68 | case 'udp4': 69 | var dgram = require('dgram'); 70 | this.udp4 = dgram.createSocket('udp4'); 71 | this.udp4.on('error', handleError.bind(this)); 72 | this.udp4.on('message', handleMessage.bind(this)); 73 | break; 74 | 75 | case 'tcp': 76 | var net = require('net'); 77 | 78 | this.tcp = new net.Socket(); 79 | this.tcp.connect(this.remotePort, this.remoteAddress); 80 | this.tcp.on('error', handleError); 81 | this.tcp.on('data', handleMessage); 82 | this.tcp.on('close', function() { 83 | delete self.tcp; 84 | }); 85 | break; 86 | 87 | default: 88 | throw new Error('invalid protocol: ', protocol); 89 | } 90 | 91 | function handleError(err) { 92 | self.log(err); 93 | self.emit('error', err); 94 | } 95 | 96 | function handleMessage(message) { 97 | 98 | var msgObj; 99 | try { 100 | msgObj = messageHandler.parseIncomingMessage(message, this.enable_compression); 101 | } catch (ex) { 102 | self.emit('error', ex); 103 | return; 104 | } 105 | 106 | // Empty packet arrived, this happens when remote closes connection 107 | if (msgObj === null) { 108 | return; 109 | } 110 | 111 | self.emit(msgObj.type, msgObj); 112 | 113 | } 114 | }; 115 | 116 | 117 | /** 118 | * Close all connections. 119 | */ 120 | ClientConnection.prototype.closeAll = function() { 121 | 122 | if (this.udp4) { 123 | try { 124 | this.udp4.close(); 125 | } catch (ex) { 126 | this.log('ERROR: ClientConnection.closeAll(): error closing udp4: ', ex); 127 | } 128 | } 129 | 130 | if (this.tcp) { 131 | this.tcp.destroy(); 132 | } 133 | 134 | }; 135 | 136 | 137 | /** 138 | * Sends a message to the remote device. 139 | * @param {object} msgObj The message and socket. 140 | * @param {function} callback (optional) 141 | */ 142 | ClientConnection.prototype._send = function(msgObj, callback) { 143 | 144 | callback = callback || function () {}; 145 | 146 | var sendBuffer = messageHandler.packOutgoingMessage(msgObj, this.enable_compression); 147 | 148 | if (this.udp4) { 149 | this.udp4.send(sendBuffer, 0, sendBuffer.length, this.remotePort, this.remoteAddress); 150 | callback(null, 'sent'); 151 | return; 152 | } 153 | 154 | if (this.tcp) { 155 | this.tcp.write(sendBuffer, undefined, function () { 156 | callback(null, 'sent'); 157 | }); 158 | return; 159 | } 160 | 161 | callback('Trying to send a message when a protocol has not been configured.', null); 162 | 163 | }; 164 | 165 | module.exports = ClientConnection; 166 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var defaults = { 27 | // This is the URL were the proxy is located. Only Toys and Controllers can 28 | // configure this. 29 | proxyUrl: 'localhost', 30 | 31 | // This is the port of the proxy. All three components (proxy, controller, 32 | // and toy) need to be configured on the same port. 33 | port: 33330, 34 | 35 | // This is the channel to use. The proxy will ensure that only devices on 36 | // the same channel can communicate together. The controller and toy need 37 | // to be on the same channel. You can make the channel a unique string. 38 | channel: '1', 39 | 40 | // How often the device pings the proxy. This helps ensure the connection 41 | // is kept alive. You can disable this by setting it to 0 (zero). Time is 42 | // in milliseconds. 43 | keepalive: 30 * 1000, 44 | 45 | // This determines the logging to use. By default it logs to the standard 46 | // console. 47 | log: function () { 48 | console.log.apply(console, arguments); 49 | }, 50 | 51 | // Use the TCP Protocol - only the proxy can use both TCP and UDP. 52 | tcp: true, 53 | 54 | // Use the UDP protocol - only the proxy can use both TCP and UDP. 55 | udp4: false, 56 | 57 | // Allow connections to proxy via Socket.IO 58 | socketio: true, 59 | 60 | // Options for the proxy - should there be only one device (toy/controller) per channel? 61 | onlyOneControllerPerChannel: false, 62 | onlyOneToyPerChannel: false, 63 | 64 | // A listener is can only see the Toy's status. It cannot send control commands. 65 | allowObservers: false, 66 | 67 | }; 68 | 69 | module.exports = { 70 | createProxy: init('proxy'), 71 | createToy: init('toy'), 72 | createController: init('controller'), 73 | createObserver: init('observer') 74 | }; 75 | 76 | 77 | /** 78 | * Helper function to create an initialised device or proxy server. 79 | * @param {string} type 'proxy', 'toy', or 'controller'. 80 | * @return {function} The initialisation function that can be called later. 81 | */ 82 | function init(type) { 83 | 84 | return function(params) { 85 | if (!params) { 86 | params = {}; 87 | } 88 | 89 | var settings = { 90 | proxyUrl: params.proxyUrl || defaults.proxyUrl, 91 | channel: params.channel || defaults.channel, 92 | keepalive: parseFalsey(params.keepalive, defaults.keepalive), 93 | port: params.port || defaults.port, 94 | log: params.log || defaults.log, 95 | tcp: parseFalsey(params.tcp, defaults.tcp), 96 | udp4: parseFalsey(params.udp4, defaults.udp4), 97 | socketio: parseFalsey(params.socketio, defaults.socketio), 98 | onlyOneControllerPerChannel: parseFalsey(params.onlyOneControllerPerChannel, defaults.onlyOneControllerPerChannel), 99 | onlyOneToyPerChannel: parseFalsey(params.onlyOneToyPerChannel, defaults.onlyOneToyPerChannel), 100 | allowObservers: parseFalsey(params.allowObservers, defaults.allowObservers), 101 | deviceType: type 102 | }; 103 | 104 | if (typeof params.log !== 'function') { 105 | params.log = defaults.log; 106 | } 107 | 108 | switch (type) { 109 | 110 | case 'proxy': 111 | if (isBrowser()) { 112 | console.error('Cannot create proxy in browser'); 113 | } 114 | var Proxy = require('./src/Proxy'); 115 | return new Proxy(settings); 116 | 117 | case 'toy': 118 | case 'controller': 119 | case 'observer': 120 | var connectionModule; 121 | if (isBrowser()) { 122 | connectionModule = require('./src/WebClientConnection'); 123 | settings.udp4 = false; 124 | settings.tcp = false; 125 | settings.socketio = true; 126 | } else { 127 | connectionModule = require('./src/ClientConnection'); 128 | settings.socketio = false; 129 | } 130 | var Device = require('./src/Device'); 131 | return new Device(settings, connectionModule); 132 | 133 | default: 134 | throw new Error('Could not determine server type.'); 135 | } 136 | 137 | }; 138 | } 139 | 140 | function parseFalsey(val1, val2) { 141 | if (typeof val1 === 'undefined') { 142 | return val2; 143 | } 144 | return val1; 145 | } 146 | 147 | function isBrowser() { 148 | return !(typeof window === 'undefined'); 149 | } 150 | -------------------------------------------------------------------------------- /src/ServerConnection.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var EventEmitter = require('events').EventEmitter; 27 | var util = require('util'); 28 | 29 | var messageHandler = require('./messageHandler'); 30 | var SOCKET_IO_PORT = 33331; 31 | 32 | /** 33 | * The connection manager will handle the TCP and UDP transport. As well as 34 | * the protocol. 35 | * @param {object} options (optional) 36 | */ 37 | function ServerConnection (options) { 38 | 39 | this.log = options.log; 40 | this.port = options.port; 41 | this.proxyUrl = options.proxyUrl; 42 | 43 | if (!(options.udp4 === false)) { 44 | this.listenUDP4(); 45 | } 46 | if (!(options.tcp === false)) { 47 | this.listenTCP(); 48 | } 49 | if (!(options.socketio === false)) { 50 | this.listenSocketIO(); 51 | } 52 | 53 | EventEmitter.call(this); 54 | 55 | } 56 | util.inherits(ServerConnection, EventEmitter); 57 | 58 | /** 59 | * Start the SocketIO server on the given port and address (optional) 60 | * @param {number} port The port number to listen on. 61 | * @param {string} address (optional) The IP address to listen on. 62 | */ 63 | ServerConnection.prototype.listenSocketIO = function() { 64 | 65 | var socketio = require('socket.io'); 66 | var self = this; 67 | this.socketio = socketio(); 68 | this.socketio.on('connection', function(socket) { 69 | var socketInfo = { 70 | protocol: 'socketio', 71 | address: socket.client.conn.remoteAddress, 72 | port: '????', 73 | socketId: socket.id, 74 | socket: socket, 75 | close: socket.close 76 | }; 77 | socket.on('event', function(message) { 78 | 79 | // Socket.IO sends messages a bit different 80 | if (message && message.type === 'Buffer') { 81 | message = new Buffer(message.data); 82 | } 83 | 84 | self.handleMessage.bind(self)(message, socketInfo); 85 | }); 86 | socket.on('close', function() { 87 | self.emit('socket-close', socketInfo.socketId); 88 | }); 89 | }); 90 | this.socketio.on('error', this.handleError); 91 | this.socketio.listen(SOCKET_IO_PORT); 92 | 93 | 94 | // Wait until the event listener is attached to THIS. 95 | setTimeout(function () { 96 | self.emit('listening', SOCKET_IO_PORT, self.proxyUrl, 'socketio'); 97 | }, 100); 98 | 99 | }; 100 | 101 | 102 | /** 103 | * Start the UDP server on the given port and address (optional). 104 | */ 105 | ServerConnection.prototype.listenUDP4 = function() { 106 | 107 | var dgram = require('dgram'); 108 | this.udp4 = dgram.createSocket('udp4'); 109 | this.udp4.on('error', this.handleError); 110 | 111 | var self = this; 112 | this.udp4.on('message', function (message, remote) { 113 | var socketInfo = { 114 | protocol: 'udp4', 115 | address: remote.address, 116 | port: remote.port, 117 | socketId: remote.address + ':' + remote.port, 118 | close: function () {} 119 | }; 120 | self.handleMessage.bind(self)(message, socketInfo); 121 | }); 122 | 123 | this.udp4.on('listening', function () { 124 | self.emit('listening', self.port, self.proxyUrl, 'udp4'); 125 | }); 126 | this.udp4.bind(self.port); 127 | }; 128 | 129 | 130 | /** 131 | * Start the TCP server on the given port and address (optional). 132 | */ 133 | ServerConnection.prototype.listenTCP = function() { 134 | 135 | var net = require('net'); 136 | var split = require('split'); 137 | var self = this; 138 | this.tcp = net.createServer(function(socket) { 139 | var socketInfo = { 140 | protocol: 'tcp', 141 | address: socket.remoteAddress, 142 | port: socket.remotePort, 143 | socketId: socket.remoteAddress + ':' + socket.remotePort, 144 | socket: socket, 145 | close: function close() { 146 | try { 147 | socket.destroy(); 148 | } catch (ex) { 149 | // okay if there is an error - socket may already be closed. 150 | } 151 | } 152 | }; 153 | var stream = socket.pipe(split('\n')); 154 | stream.on('data', function(message) { 155 | self.handleMessage.bind(self)(message, socketInfo); 156 | }); 157 | stream.on('close', function() { 158 | self.emit('socket-close', socketInfo.socketId); 159 | }); 160 | stream.on('error', self.handleMessage.bind(self)); 161 | 162 | }); 163 | this.tcp.on('error', this.handleError); 164 | this.tcp.on('listening', function () { 165 | self.emit('listening', self.port, self.proxyUrl, 'tcp'); 166 | }); 167 | this.tcp.listen(self.port); 168 | 169 | }; 170 | 171 | ServerConnection.prototype.handleError = function(err) { 172 | if (typeof this.log === 'function') { 173 | this.log(err); 174 | } else { 175 | console.error(err); 176 | } 177 | this.emit('error', err); 178 | }; 179 | 180 | ServerConnection.prototype.handleMessage = function(message, socketInfo) { 181 | 182 | var enable_compression = socketInfo.protocol === 'udp4'; 183 | var msgObj; 184 | try { 185 | msgObj = messageHandler.parseIncomingMessage(message, enable_compression); 186 | } catch (ex) { 187 | this.emit('error', ex); 188 | return; 189 | } 190 | 191 | // Empty packet arrived, this happens when remote closes connection 192 | if (msgObj === null) { 193 | return; 194 | } 195 | 196 | msgObj.socket = socketInfo; 197 | this.emit(msgObj.type, msgObj); 198 | 199 | this.log(new Date(), socketInfo.address + ':' + socketInfo.port, msgObj.type, msgObj.channel || msgObj.uid, msgObj.seq, msgObj.data); 200 | }; 201 | 202 | 203 | /** 204 | * Close all connections. 205 | */ 206 | ServerConnection.prototype.closeAll = function() { 207 | 208 | if (this.udp4) { 209 | this.udp4.close(); 210 | } 211 | 212 | if (this.tcp) { 213 | this.tcp.close(); 214 | } 215 | 216 | this.removeAllListeners(); 217 | 218 | }; 219 | 220 | /** 221 | * Sends a message to the remote device. 222 | * @param {string} err The error string 223 | * @param {string} address The remote address. 224 | * @param {number} remote The remote port. 225 | */ 226 | ServerConnection.prototype._send = function(msgObj) { 227 | 228 | var socketInfo = msgObj.socket; 229 | var enable_compression = socketInfo.protocol === 'udp4'; 230 | var msgComp = messageHandler.packOutgoingMessage(msgObj, enable_compression); 231 | 232 | if (socketInfo.protocol === 'udp4') { 233 | this.udp4.send(msgComp, 0, msgComp.length, socketInfo.port, socketInfo.address); 234 | return; 235 | } 236 | 237 | if (socketInfo.protocol === 'tcp') { 238 | try { 239 | socketInfo.socket.write(msgComp); 240 | } catch (ex) { 241 | this.log('ServerConnection._send():error: ', ex); 242 | } 243 | return; 244 | } 245 | 246 | if (socketInfo.protocol === 'socketio') { 247 | socketInfo.socket.emit('event', msgComp); 248 | return; 249 | } 250 | 251 | }; 252 | 253 | module.exports = ServerConnection; 254 | -------------------------------------------------------------------------------- /src/Proxy.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var EventEmitter = require('events').EventEmitter; 27 | var util = require('util'); 28 | 29 | var DeviceManager = require('./DeviceManager'); 30 | var ServerConnection = require('./ServerConnection'); 31 | var errors = require('./errors.js'); 32 | var StickyMessages = require('./StickyMessages'); 33 | 34 | /** 35 | * This is the proxy server. It is the "man in the middle". Devices (toys, 36 | * controllers and observers) connect to the proxy server. 37 | * @param {object} settings (optional) Settings as defined by the help. 38 | */ 39 | function Prox(settings) { 40 | 41 | var self = this; 42 | this.log = settings.log; 43 | 44 | this.devices = new DeviceManager(settings); 45 | this.server = new ServerConnection(settings); 46 | 47 | this.server.on('listening', function (localPort, localAddress, protocol) { 48 | self.log('Web-Remote-Control Proxy Server listening to "' + protocol + '" requests on ' + localAddress + ':' + localPort); 49 | }); 50 | 51 | this.server.on('error', this.handleError.bind(this)); 52 | 53 | this.server.on('socket-close', function (socketId) { 54 | self.devices.removeBySocketId(socketId); 55 | }); 56 | 57 | this.server.on('register', this.registerDevice.bind(this)); 58 | this.server.on('ping', this.respondToPing.bind(this)); 59 | this.server.on('status', this.forwardStatus.bind(this)); 60 | this.server.on('command', this.forwardCommand.bind(this)); 61 | 62 | this.stickers = new StickyMessages(); 63 | 64 | EventEmitter.call(this); 65 | 66 | } 67 | util.inherits(Prox, EventEmitter); 68 | 69 | 70 | /** 71 | * Close all connections. 72 | */ 73 | Prox.prototype.close = function() { 74 | this.server.closeAll(); 75 | this.removeAllListeners(); 76 | }; 77 | 78 | 79 | /** 80 | * Register a new device on a given channel. 81 | * @param {object} msgObj Message object with channel info in the 'data' parameter. 82 | * @param {object} remote The sender socket 83 | */ 84 | Prox.prototype.registerDevice = function(msgObj) { 85 | 86 | if (!msgObj.data) { 87 | this.log('msgObj has no data: ', msgObj); 88 | return; 89 | } 90 | 91 | var deviceType = msgObj.data.deviceType; 92 | var channel = msgObj.data.channel; 93 | 94 | if (!this.devices.validDeviceType(deviceType)) { 95 | this.log('Invalid device type: ', deviceType); 96 | return; 97 | } 98 | 99 | if (typeof channel === 'undefined') { 100 | this.log('registerDevice: device channel is undefined'); 101 | return; 102 | } 103 | 104 | var uid = this.devices.add(deviceType, channel, msgObj.socket, msgObj.seq); 105 | msgObj.uid = uid; 106 | msgObj.data = { 107 | channel: channel, 108 | uid: uid 109 | }; 110 | 111 | this._send(msgObj); 112 | this.emit(msgObj.type, msgObj); 113 | 114 | // 115 | // Send sticky stuff once registerd 116 | // 117 | var stickyMsgObj = this.stickers.get(channel, deviceType, msgObj); 118 | if (stickyMsgObj === undefined) return; 119 | var self = this; 120 | setTimeout(function () { 121 | self._send(stickyMsgObj); 122 | }, 20); 123 | }; 124 | 125 | 126 | /** 127 | * Return a ping to a toy/controller/observer. 128 | * @param {object} msgObj The message object sent by the toy/controller/observer. 129 | * @param {object} remote The sender socket 130 | */ 131 | Prox.prototype.respondToPing = function(msgObj) { 132 | 133 | var device = this.devices.update(msgObj.uid, msgObj.socket, msgObj.seq); 134 | 135 | if (!device) { 136 | this.respondError(msgObj, errors.DEVICE_NOT_REGISTERED); 137 | this.log('Unable to find the device to update: ', msgObj); 138 | return; 139 | } 140 | 141 | this._send(msgObj); 142 | this.emit(msgObj.type, msgObj); 143 | }; 144 | 145 | 146 | /** 147 | * Forward a command from a controller to a device. 148 | * @param {object} msgObj The message object we are forwarding. 149 | * @param {object} remote The sender socket. 150 | */ 151 | Prox.prototype.forwardCommand = function(msgObj) { 152 | this.forward('command', 'toy', msgObj); 153 | }; 154 | 155 | 156 | /** 157 | * Forward a status update from a toy to a controller/observer. 158 | * @param {object} msgObj The message object we are forwarding. 159 | * @param {object} remote The sender socket. 160 | */ 161 | Prox.prototype.forwardStatus = function(msgObj) { 162 | this.forward('status', 'controller', msgObj); 163 | this.forward('status', 'observer', msgObj); 164 | }; 165 | 166 | 167 | /** 168 | * Forward a command from a controller/toy to a toy/controller. This will 169 | * forward to all toys/controllers on the given channel. 170 | * 171 | * @param {string} forwardToType The type of item we are forwarding to. 172 | * @param {object} msgObj The message object we are forwarding. 173 | * @param {object} remote The sender socket. 174 | */ 175 | Prox.prototype.forward = function(actionType, forwardToType, msgObj) { 176 | 177 | var self = this; 178 | 179 | var sendingDevice = this.devices.get(msgObj.uid); 180 | if (!sendingDevice) { 181 | this.respondError(msgObj, errors.DEVICE_NOT_REGISTERED); 182 | this.log('Prox.forwardCommand(): remote device not found: ', msgObj.uid); 183 | return; 184 | } 185 | 186 | // Check the device is allowed this type of action 187 | if (!this.devices.isAllowedAction(msgObj.uid, actionType)) { 188 | this.respondError(msgObj, errors.PERMISSION_DENIED); 189 | this.log('Prox.forwardCommand(): Action not allowed for ' + sendingDevice.deviceType + ': ', msgObj.uid); 190 | return; 191 | } 192 | 193 | // Drop the packet if it's not the latest (highest seqNum) 194 | if (!this.devices.isLatestSeqNum(msgObj.uid, msgObj.seq)) { 195 | this.log('Dropped a packet from: ' + msgObj.uid); 196 | return; 197 | } 198 | 199 | this.devices.update(msgObj.uid, msgObj.socket, msgObj.seq); 200 | 201 | var uidList = this.devices.getAll(forwardToType, sendingDevice.channel); 202 | 203 | uidList.forEach(function(uid) { 204 | var receivingDevice = { 205 | type: msgObj.type, 206 | seq: msgObj.seq, 207 | uid: uid, 208 | data: msgObj.data, 209 | socket: self.devices.getSocket(uid) 210 | }; 211 | 212 | self._send(receivingDevice); 213 | }); 214 | self.stickers.set(sendingDevice.channel, msgObj.type, msgObj); 215 | 216 | this.emit(msgObj.type, msgObj); 217 | 218 | }; 219 | 220 | 221 | /** 222 | * This will send a message to the remote device. 223 | * @param {object} msgObj The object to send as JSON. 224 | */ 225 | Prox.prototype._send = function(msgObj) { 226 | this.server._send(msgObj); 227 | }; 228 | 229 | 230 | /** 231 | * Respond to the given socket with an error. 232 | * @param {object} msgObj The outgoing message deatils. 233 | * @param {object} errorType The error to send 234 | */ 235 | Prox.prototype.respondError = function (msgObj, errorType) { 236 | var responseObj = { 237 | type: 'error', 238 | seq: msgObj.seq, 239 | uid: null, 240 | data: errorType.code, 241 | socket: msgObj.socket 242 | }; 243 | this._send(responseObj); 244 | }; 245 | 246 | Prox.prototype.handleError = function(err) { 247 | var errMsg = 'Proxy: There was an error:\t' + JSON.stringify(err); 248 | this.log(errMsg); 249 | this.emit(errMsg); 250 | }; 251 | 252 | module.exports = Prox; 253 | -------------------------------------------------------------------------------- /Protocol.md: -------------------------------------------------------------------------------- 1 | # Communication Protocol 2 | 3 | In case you want to build your own client/server (device/proxy) in another language the Protocol for web-remote-control 4 | is described below. 5 | 6 | ## Overview 7 | 8 | The devices (toys, controllers and observers) communicate with each other through the proxy. A proxy must be available with the 9 | network ports set and correct UDP/TCP/SocketIO protocols enabled both on the devices and the proxy. The proxy can 10 | communicate in any of the network protocols (UDP/TCP/SocketIO), while a device can only use one protocol. 11 | 12 | The devices register on a channel, the channel can be any valid JavaScript string. For a given channel there can be 13 | only one toy and only one controller. There can be any number of observers. The proxy will disconnect the last toy/controller if a new toy/controller 14 | registers. All devices on a channel can only talk between themselves. 15 | 16 | ## The Protocol 17 | 18 | This section describes the protocol in detail. All messages are JSON strings and passed over the respective network 19 | layer: 20 | - UDP the packet must fit into one UDP packet. See the *Note on UDP and smaz* section below. Default port is 33330. 21 | - A TCP socket. Default port is 33330. 22 | - Socket.io is used for web browser to proxy communication. Because web browser communication does not support 23 | raw TCP or UDP. Default port is TCP 33331. 24 | 25 | Below is the structure of all messages. The `Unique ID` is not always present. 26 | ```Text 27 | { 28 | "type": [String: Message type], 29 | "seq": [Integer: Sequence Number], 30 | "uid": [Number: Unique ID], 31 | "data": [any: Data] 32 | } 33 | [NEWLINE] 34 | ``` 35 | 36 | The message needs to be standard JSON format, so white space does not matter. For all 37 | examples in the documentation whitespace is included for clarity. 38 | 39 | A newline character must be at the end of every message. This helps separate messages 40 | when communicating over Socket.io, although it is required for all protocols. 41 | 42 | No acknowledgment packets are sent at all, this is left up to the network layer. 43 | 44 | ### New Lines 45 | 46 | As mentioned above, all messages should end with newline character. 47 | 48 | There must be no new-line characters in the JSON anywhere, including the text fields. 49 | 50 | 51 | ### Message fields 52 | 53 | The fields in the JSON message are described below. 54 | 55 | **type:** Message Type 56 | 57 | This string defines the type of message being received. The value must be one of the following: 58 | - `register` - sent by all devices and by proxy as response. 59 | - `command` - sent by controller devices. 60 | - `status` - sent by toy devices. 61 | - `ping` - sent by devices. 62 | - `error` - sent by proxy. 63 | 64 | **seq:** Sequence Number 65 | 66 | This number defines the number of packet sent by the Device. It allows old packets 67 | to be dropped. Because when you are controlling devices in real time, old packets contain old information that is likely not relevant. 68 | 69 | **uid:** Unique ID 70 | 71 | This string is the unique ID of the device provided by the proxy, it provides a 72 | light authentication method. It will be in the response message on registration. 73 | It must be in every message type (except 'register') sent by the device to the proxy. 74 | 75 | **data:** Data 76 | 77 | This field can be any type. It is the payload from the sender to the recipient. 78 | The recipient may be another device or the proxy. 79 | 80 | ### Message types 81 | 82 | #### register 83 | 84 | This type of message only occurs once, it must be the first communication point. 85 | 86 | * *Senders*: toy, controller, observer, proxy 87 | * *Final recipient*: proxy or the sending device 88 | * *Mandatory fields*: `type`, `seq`, `data` 89 | * *Data*: The `data` field must contain an object with the following fields: 90 | * `deviceType`: String (mandatory): this must be one of `toy`, `controller`, `observer`. 91 | * `channel`: String (optional): the channel to register on. If not provided the proxy will create a default channel. 92 | * *Response from proxy to sender*: After the request is received a `register` or `error` message is returned by the proxy. The `register` message indicates success and will contain the following: 93 | * `type`: "register". 94 | * `seq`: will be the same sequence number as in the request message. 95 | * `uid`: The Unique ID string. This must be stored for future requests. 96 | 97 | Example message to proxy: 98 | ```JSON 99 | { 100 | "type": "register", 101 | "seq": 1, 102 | "data": { 103 | "deviceType": "toy", 104 | "channel": 1 105 | } 106 | }\n 107 | ``` 108 | 109 | Example response from proxy. 110 | ```JSON 111 | { 112 | "type": "register", 113 | "seq": 1, 114 | "uid":"S1ebSunv" 115 | }\n 116 | ``` 117 | 118 | #### command 119 | 120 | This type of message is sent by controller devices and propagated to toy devices. 121 | 122 | * *Senders*: controller 123 | * *Final recipient*: toy (via proxy) 124 | * *Mandatory fields*: `type`, `seq`, `uid`, `data` 125 | * *Data*: The `data` field can contain any valid JSON type 126 | * *Response from proxy to sender*: none 127 | 128 | Example request from controller to proxy (which forwards to toy): 129 | ```JSON 130 | { 131 | "type": "command", 132 | "seq": 199, 133 | "data": "This is my command", 134 | "uid": "S1ebSunv" 135 | }\n 136 | ``` 137 | 138 | Example from proxy to toy (based on `command` example above): 139 | ```JSON 140 | { 141 | "type": "command", 142 | "seq": 199, 143 | "data": "This is my command", 144 | "uid": "HkrJt_nv" 145 | }\n 146 | ``` 147 | 148 | 149 | #### status 150 | 151 | This type of message is sent by toy devices and propagated to controller and observer devices. 152 | 153 | * *Senders*: toy 154 | * *Final recipient*: controller, observer 155 | * *Mandatory fields*: `type`, `seq`, `uid`, `data` 156 | * *Data*: The `data` field can contain any valid JSON type 157 | * *Response from proxy to sender*: none 158 | 159 | Example request from toy to proxy (which forwards to controller / observer): 160 | ```JSON 161 | { 162 | "type": "status", 163 | "seq": 1045, 164 | "data": { 165 | "any-old-value": "This is my any-old-status" 166 | }, 167 | "uid": "HkrJt_nv" 168 | }\n 169 | ``` 170 | 171 | Example from proxy to controller (based on `status` example above): 172 | ```JSON 173 | { 174 | "type": "status", 175 | "seq": 1045, 176 | "data": { 177 | "any-old-value": "This is my any-old-status" 178 | }, 179 | "uid": "S1ebSunv" 180 | }\n 181 | ``` 182 | 183 | #### ping 184 | 185 | This type of message is only between the device and proxy. It can occur any number of times. 186 | It can be used to keep the network connection active, which may be necessary for some networks. 187 | 188 | * *Senders*: toy, controller, observer, proxy 189 | * *Final recipient*: The sending device 190 | * *Mandatory fields*: `type`, `seq`, `uid`, `data` 191 | * *Data*: The `data` field can contain any valid JSON type. 192 | * *Response from proxy to sender*: On receiving a ping the proxy 193 | will return the the message immediately. The fields returned are: 194 | * `type`: "ping". 195 | * `seq`: Same value as in the request message. 196 | * `uid`: Same value as in the request message. 197 | * `data`: Same value as in the request message. 198 | 199 | Note: you can send a time value and then calculate the round trip time. 200 | 201 | Example message to proxy: 202 | ```JSON 203 | { 204 | "type": "ping", 205 | "seq": 999, 206 | "uid": "S1ebSunv", 207 | "data": 1234567890 208 | }\n 209 | ``` 210 | 211 | Example response from proxy. 212 | ```JSON 213 | { 214 | "type": "ping", 215 | "seq": 999, 216 | "uid": "S1ebSunv", 217 | "data": 1234567890 218 | }\n 219 | ``` 220 | 221 | 222 | #### error 223 | 224 | This message is sent by the proxy only, and always in response to a message from the device. 225 | 226 | * *Senders*: proxy, on a request from a device 227 | * *Final recipient*: The sending device 228 | * *Mandatory fields*: n/a 229 | * *Data*: The `data` field can contain any valid JSON type. 230 | * *Response from proxy to sender*: On receiving a ping the proxy will return the the message immediately. The fields returned are: 231 | * `type`: "error". 232 | * `seq`: Same value as in the request message. 233 | * `uid`: null 234 | * `data`: Error code (see [errors.js](https://github.com/psiphi75/web-remote-control/blob/master/src/errors.js) for all error codes). 235 | 236 | Example error from proxy. 237 | ```JSON 238 | { 239 | "type": "error", 240 | "seq": 123, 241 | "uid": null, 242 | "data": 1001 243 | }\n 244 | ``` 245 | 246 | 247 | ## Note on UDP and smaz 248 | 249 | Currently for the UDP protocol a customised [smaz](https://github.com/antirez/smaz) algorithm is used. The purpose of this is to reduce the size of the data transmitted. General purpose compression algorithms usually are very poor for small payloads. The smaz algorithm is used and generally obtains compression ratios of around 50%. A custom built smaz map is used. How this works is not currently documented, you will need to read the code. 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Remote Control 2 | 3 | This module allows you to control an IoT (Internet of Things) device from the web (e.g. your mobile phone). This is a complete remote control solution that includes the following components: 4 | 5 | - A Proxy - runs on a server and needs to be accessible to the Controller and Toy. 6 | - The Controller - this can be via web or node.js. 7 | - The Toy - the device being controlled (this should run node.js). 8 | 9 | This solution is ideal for controlling devices over a cellular network (i.e where you cannot directly access by an IP). It uses the UDP protocol instead of TCP by default to make the communication more efficient. Note: UDP protocol is only available for node.js, currently no browsers support UDP for standard webpages. 10 | 11 | Connection methods are: 12 | 13 | - TCP - between node.js client/controller and proxy. 14 | - UDP - between node.js client/controller and proxy. This protocol is optimised with a compression protocol using [smaz](https://www.npmjs.com/package/smaz). 15 | - Socket.io - between Browser client and proxy. 16 | 17 | ## Installing from npmjs.org 18 | 19 | ```bash 20 | npm install web-remote-control 21 | ``` 22 | 23 | ## Installing from github 24 | 25 | ```bash 26 | git clone https://github.com/psiphi75/web-remote-control 27 | 28 | # Run the following commands if you want the web-remote-control.js for use in the browser. 29 | sudo npm install -g browserify 30 | cd scripts 31 | build.sh 32 | ``` 33 | 34 | 35 | ## Basic Usage 36 | 37 | **The Proxy:** 38 | 39 | The proxy is required to relay `command`s from the controller to device. It also accepts `ping`s and relays `status` messages from the device/controller to the controller/device. 40 | 41 | The default port for TCP and UDP is 33330 and for socket.io it's 33331. 42 | 43 | ```javascript 44 | var wrc = require('web-remote-control'); 45 | var proxy = wrc.createProxy(); 46 | ``` 47 | 48 | **The Device (node.js):** 49 | 50 | The device is what is being controlled. It accepts `command`s, `message`s, and `ping` responses. It can send `ping`s and `message`s. 51 | 52 | ```javascript 53 | var wrc = require('web-remote-control'); 54 | var toy = wrc.createToy({ proxyUrl: 'your proxy url'}); 55 | 56 | // Should wait until we are registered before doing anything else 57 | toy.on('register', function() { 58 | 59 | // Ping the proxy and get the response time (in milliseconds) 60 | toy.ping(function (time) { 61 | console.log(time); 62 | }); 63 | 64 | // Send a status update to the controller 65 | toy.status('Hi, this is a message to the controller.'); 66 | 67 | }); 68 | 69 | // Listens to commands from the controller 70 | toy.on('command', function(cmd) { 71 | console.log('The controller sent me this command: ', cmd); 72 | }); 73 | ``` 74 | 75 | **The Controller (from browser):** 76 | 77 | ```html 78 | 79 | 80 | 97 | ``` 98 | 99 | **The Controller (from node.js):** 100 | 101 | The controller is what controls the device via a `command`. It accepts `status`s and `ping` responses. It can send `command`s and `status` updates. 102 | 103 | ```javascript 104 | var wrc = require('web-remote-control'); 105 | var controller = wrc.createController({ proxyUrl: 'your proxy url' }); 106 | 107 | // Should wait until we are registered before doing anything else 108 | controller.on('register', function() { 109 | 110 | controller.command('Turn Left'); 111 | 112 | }); 113 | 114 | controller.on('status', function (status) { 115 | console.log('I got a status message: ', status); 116 | }); 117 | ``` 118 | 119 | # More Advanced Usage 120 | 121 | The default values are shown below. This can be found in `index.js`. 122 | 123 | ```javascript 124 | var defaults = { 125 | 126 | // This is the URL were the proxy is located. Only Toys and Controllers can 127 | // configure this. 128 | proxyUrl: 'localhost', 129 | 130 | // This is the port of the proxy. All three components (proxy, controller, 131 | // and toy) need to be configured on the same port. 132 | port: 33330, 133 | 134 | // This is the channel to use. The proxy will ensure that only devices on 135 | // the same channel can communicate together. The controller and toy need 136 | // to be on the same channel. You can make the channel a unique string. 137 | channel: '1', 138 | 139 | // How often the device pings the proxy. This helps ensure the connection 140 | // is kept alive. You can disable this by setting it to 0 (zero). Time is 141 | // in milliseconds. 142 | keepalive: 30 * 1000, 143 | 144 | // This determines the logging to use. By default it logs to the standard 145 | // console. 146 | log: console.log, 147 | 148 | // Use the TCP Protocol - only the proxy can use both TCP and UDP. 149 | tcp: true, 150 | 151 | // Use the UDP protocol - only the proxy can use both TCP and UDP. 152 | udp4: true, 153 | 154 | // Allow connections to proxy via Socket.IO 155 | socketio: true 156 | 157 | // Options for the proxy - should there be only one device (toy/controller) per channel? 158 | onlyOneControllerPerChannel: false, 159 | onlyOneToyPerChannel: false 160 | 161 | // A listener is can only see the Toy's status. It cannot send control commands. 162 | allowObservers: false, 163 | 164 | }; 165 | ``` 166 | 167 | Below is an example for creating a custom proxy. 168 | 169 | ```javascript 170 | var wrc = require('web-remote-control'); 171 | var settings = { 172 | port: 12345, 173 | log: function () {} // turn logging off 174 | }; 175 | var proxy = wrc.createProxy(settings); 176 | ``` 177 | 178 | ## Sticky messages 179 | 180 | It is possible to create sticky messages of type 'command' and 'status' using `stickyCommand()` and `stickyStatus()`. A sticky message will be held by the proxy for a given channel and by provided to all devices registering on that channel. Only one sticky message will be kept at a time, new sticky messages will overwrite the old. 181 | 182 | ## Communication Protocol 183 | 184 | In case you want to build your own client/server (Device/Proxy) in another language the Protocol for web-remote-control has been described [here](https://github.com/psiphi75/web-remote-control/blob/master/Protocol.md). 185 | 186 | ## Known Issues and To-Do items 187 | 188 | Below are known issues, feel free to fix them. 189 | 190 | - Proxy default of UDP, fallback to TCP. 191 | - Refactor Socket connections - seperate the socket open, write and close from ServerConnection.js. 192 | - TCP sockets may be crash the system when two TCP write events occur at the same time. Need to wait for TCP drain event. 193 | - **Done**: Sticky messages added. 194 | - **Done**: Create observer device that can't remote control the device. 195 | - **Done**: Allow only one controller per channel. 196 | - **Done**: Integrate the static fileserver (WebServer.js) with the proxy. This simplifies the creation of the whole web-remote-control functionality. 197 | - **Done**: Out of order packets are not handled, we should only use the most recent command packet. 198 | - **Done**: Add the creation of "web-remote-control.js" to the install (need to run build.sh) and include browserify as a global. 199 | - **Done**: The web component needs creating and documented. 200 | - **Done** (for UDP): Compression currently does not work. Because the packet length is so short (can be less than 50 bytes) standard compression algorithms don't work, in-fact the make the data payload bigger. [smaz](https://www.npmjs.com/package/smaz) is a neat library that accommodates this and can compress short strings. 201 | - **Fixed**: TCP functionality missing. 202 | - **Fixed**: If we are not registered, try again in 30 seconds. 203 | - **Fixed**: Each ping creates a new listener. 204 | 205 | ## License 206 | 207 | Copyright 2016 Simon M. Werner 208 | 209 | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 210 | 211 | 212 | 213 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 214 | -------------------------------------------------------------------------------- /src/DeviceManager.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var makeUID = require('shortid').generate; 27 | 28 | 29 | /** 30 | * The DeviceManager manages toys and controllers (devices). Devices operate on 31 | * a given channel, both the toy and controller require the same channel. There 32 | * can be multiple toys and controllers on the same channel. 33 | */ 34 | function DeviceManager(settings) { 35 | settings = settings ? settings : {}; 36 | this.onlyOneControllerPerChannel = settings.onlyOneControllerPerChannel === true ? true : false; 37 | this.onlyOneToyPerChannel = settings.onlyOneToyPerChannel === true ? true : false; 38 | this.allowObservers = settings.allowObservers === true ? true : false; 39 | this.list = {}; 40 | } 41 | 42 | 43 | /** 44 | * Add a new toy/controller to the device manager. 45 | * @param {string} deviceType 'toy' or 'controller' or 'observer'. 46 | * @param {string} channel The channel to operate on (this is provided by the device). 47 | * @param {object} socket The socket details. 48 | * @param {number} seqNum The sequence number from the device. 49 | * @param {string} protocol 'tcp' or 'udp4' 50 | * @return {uid} The UID of the device. 51 | */ 52 | DeviceManager.prototype.add = function(deviceType, channel, socket, seqNum) { 53 | 54 | switch (undefined) { // eslint-disable-line default-case 55 | case deviceType: 56 | case channel: 57 | case socket: 58 | case seqNum: 59 | console.error('DeviceManager.add(): one of the inputs are undefined.'); 60 | return undefined; 61 | } 62 | 63 | if (!this.validDeviceType(deviceType)) { 64 | console.error('DeviceManager.getAll(): "deviceType" should be "toy" or "controller" or "observer" (if allowObservers is true), not: ', deviceType); 65 | return undefined; 66 | } 67 | 68 | // Remove the controller / toy that already exists 69 | if ((deviceType === 'controller' && this.onlyOneControllerPerChannel) || 70 | (deviceType === 'toy' && this.onlyOneToyPerChannel)) { 71 | var devUID = this.findByDeviceTypeAndChannel(deviceType, channel); 72 | if (devUID) { 73 | this.remove(devUID); 74 | } 75 | } 76 | 77 | var uid = makeUID(); 78 | this.list[uid] = { 79 | deviceType: deviceType, 80 | channel: channel, 81 | socket: socket, 82 | seqNum: seqNum 83 | }; 84 | return uid; 85 | 86 | }; 87 | 88 | 89 | /** 90 | * Remove a device from the device manager. 91 | * @param {string} uid The UID 92 | * @return {boolean} true if successful, otherwise false. 93 | */ 94 | DeviceManager.prototype.remove = function(uid) { 95 | 96 | var dev = this.list[uid]; 97 | 98 | if (typeof dev === 'undefined') { 99 | return false; 100 | } 101 | 102 | try { 103 | dev.socket.close(); 104 | } catch (ex) { 105 | // okay if there is an error - socket may already be closed. 106 | } 107 | 108 | try { 109 | delete this.list[uid]; 110 | } catch (ex) { 111 | return false; 112 | } 113 | return true; 114 | }; 115 | 116 | 117 | /** 118 | * Checks if the device is allowed the given action 119 | * @param {string} uid The UID of the device 120 | * @param {string} actionType The action to test 121 | * @return {boolean} true if action is allowed, otherwise false. 122 | */ 123 | DeviceManager.prototype.isAllowedAction = function(uid, actionType) { 124 | 125 | var dev = this.list[uid]; 126 | 127 | if (typeof dev === 'undefined') { 128 | return false; 129 | } 130 | 131 | if (dev.deviceType === 'toy' && actionType === 'status') return true; 132 | if (dev.deviceType === 'controller' && actionType === 'command') return true; 133 | 134 | return false; 135 | 136 | }; 137 | 138 | 139 | /** 140 | * Remove a device from the device manager. 141 | * @param {string} uid The UID 142 | * @return {boolean} true if successful, otherwise false. 143 | */ 144 | DeviceManager.prototype.removeBySocketId = function(socketId) { 145 | 146 | var uid = this.findBySocketId(socketId); 147 | return this.remove(uid); 148 | 149 | }; 150 | 151 | 152 | DeviceManager.prototype.findByDeviceTypeAndChannel = function(deviceType, channel) { 153 | var self = this; 154 | var foundUid = null; 155 | Object.keys(this.list).forEach( function(uid) { 156 | var dev = self.list[uid]; 157 | if (dev.deviceType === deviceType && dev.channel === channel) { 158 | foundUid = uid; 159 | } 160 | }); 161 | return foundUid; 162 | }; 163 | 164 | 165 | DeviceManager.prototype.findBySocketId = function(socketId) { 166 | var self = this; 167 | var foundUid = null; 168 | Object.keys(this.list).forEach( function(uid) { 169 | var dev = self.list[uid]; 170 | if (dev.socket.socketId === socketId) { 171 | foundUid = uid; 172 | } 173 | }); 174 | return foundUid; 175 | }; 176 | 177 | 178 | /** 179 | * Get a device from a UID. 180 | * @param {string} uid The UID. 181 | * @return {object} The device details. 182 | */ 183 | DeviceManager.prototype.get = function(uid) { 184 | return this.list[uid]; 185 | }; 186 | 187 | 188 | /** 189 | * Get a socket from the device with the given UID. 190 | * @param {string} uid The UID. 191 | * @return {object} The socket details. 192 | */ 193 | DeviceManager.prototype.getSocket = function(uid) { 194 | return this.list[uid].socket; 195 | }; 196 | 197 | 198 | /** 199 | * Get all devices on a channel. But only by device type (toy/controller). 200 | * @param {string} deviceType 'toy' or 'controllers' 201 | * @param {string} channel The channel. 202 | * @return {array} An array of UID strings. 203 | */ 204 | DeviceManager.prototype.getAll = function(deviceType, channel) { 205 | 206 | if (!this.validDeviceType(deviceType)) { 207 | console.error('DeviceManager.getAll(): "deviceType" should be "toy" or "controller" or "observer" (if allowObservers is true), not: ', deviceType); 208 | return []; 209 | } 210 | 211 | var devList = []; 212 | var self = this; 213 | Object.keys(this.list).forEach( function(uid) { 214 | var dev = self.list[uid]; 215 | if (dev.deviceType === deviceType && dev.channel === channel) { 216 | devList.push(uid); 217 | } 218 | }); 219 | return devList; 220 | }; 221 | 222 | 223 | /** 224 | * Update the device with the given ip and port parameters. Then return the 225 | * device. 226 | * @param {string} uid The UID for the device. 227 | * @param {string} ip The IP the device was last seen on. 228 | * @param {number} port The port the device was last seen on. 229 | * @return {object} The updated device details. 230 | */ 231 | DeviceManager.prototype.update = function(uid, socket, seqNum) { 232 | 233 | if (isNaN(seqNum)) { 234 | console.error('DeviceManager.update(): provided "seqNum" is not a number'); 235 | return undefined; 236 | } 237 | 238 | var device = this.list[uid]; 239 | if (!device) { 240 | return undefined; 241 | } 242 | 243 | device.socket = socket || device.socket; 244 | device.seqNum = seqNum; 245 | return device; 246 | }; 247 | 248 | 249 | /** 250 | * Make sure the sequence number is okay. 251 | * @param {string} uid UID for the device 252 | * @param {number} seqNum The seqNum to check 253 | * @return {boolean} True or false 254 | */ 255 | DeviceManager.prototype.isLatestSeqNum = function(uid, seqNum) { 256 | 257 | if (isNaN(seqNum)) { 258 | console.error('DeviceManager.isLatestSeqNum(): provided "seqNum" is not a number'); 259 | return false; 260 | } 261 | 262 | var device = this.list[uid]; 263 | if (!device) { 264 | return false; 265 | } 266 | if (device.seqNum > seqNum) { 267 | return false; 268 | } 269 | return true; 270 | }; 271 | 272 | 273 | /** 274 | * Confirm the device type is correct. 275 | * @param {string} type The name of the device to check. 276 | * @return {boolean} True if the device type is valid. 277 | */ 278 | DeviceManager.prototype.validDeviceType = function(deviceType) { 279 | switch (deviceType) { 280 | case 'toy': 281 | case 'controller': 282 | return true; 283 | case 'observer': // observer only valid if allowObserver === true 284 | return this.allowObservers; 285 | default: 286 | return false; 287 | } 288 | }; 289 | 290 | module.exports = DeviceManager; 291 | -------------------------------------------------------------------------------- /src/Device.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var EventEmitter = require('events').EventEmitter; 27 | var util = require('util'); 28 | 29 | var PingManager = require('./PingManager'); 30 | var errors = require('./errors.js'); 31 | 32 | var NET_TIMEOUT = 5 * 1000; 33 | 34 | function Device(settings, ClientConnection) { 35 | 36 | this.proxyUrl = settings.proxyUrl; 37 | this.port = settings.port; 38 | 39 | switch (typeof settings.channel) { 40 | case 'undefined': 41 | this.channel = '1'; 42 | break; 43 | case 'number': 44 | this.channel = parseFloat(settings.channel); 45 | break; 46 | case 'string': 47 | this.channel = settings.channel; 48 | break; 49 | default: 50 | throw new Error('Channel has an invalid type: ', typeof settings.channel); 51 | } 52 | 53 | this.keepalive = settings.keepalive; 54 | this.deviceType = settings.deviceType || 'controller'; 55 | this.log = settings.log || function() {}; 56 | 57 | this.pingManager = new PingManager(settings); 58 | this.connection = new ClientConnection(settings); 59 | this.uid = undefined; 60 | 61 | // This keeps a track of ther controller sequenceNumber. If a command with 62 | // a smaller number is received, we drop it. 63 | this.remoteSeqNum = 0; 64 | this.mySeqNum = 1; 65 | 66 | if (this.keepalive > 0) { 67 | this.timeoutHandle = setInterval(this.ping.bind(this), this.keepalive); 68 | } 69 | 70 | var self = this; 71 | this.connection.on('error', handleCommError); 72 | this.connection.on('register', handleRegisterResponse); 73 | this.connection.on('status', reEmit); 74 | this.connection.on('command', reEmit); 75 | this.connection.on('ping', handlePing); 76 | 77 | // Register the device, this announces us. 78 | this.register(); 79 | 80 | function reEmit(responseMsgObj) { 81 | self.emit(responseMsgObj.type, responseMsgObj.data, responseMsgObj.seq); 82 | } 83 | 84 | function handleRegisterResponse(responseMsgObj) { 85 | 86 | if (!self.uid) { 87 | if (typeof responseMsgObj.uid !== 'string') { 88 | throw new Error('unable to initailise'); 89 | } 90 | self.log('Registered. UID:', responseMsgObj.uid); 91 | self.uid = responseMsgObj.uid; 92 | } 93 | 94 | self.clearRegisterTimeout(); 95 | reEmit(responseMsgObj); 96 | } 97 | 98 | function handlePing(responseMsgObj) { 99 | var pingTime = (new Date()).getTime() - parseInt(responseMsgObj.data); 100 | self.pingManager.handleIncomingPing(responseMsgObj.seq, pingTime); 101 | } 102 | 103 | function handleCommError(responseMsgObj) { 104 | var errorCode = responseMsgObj.data; 105 | var error = errors.getByCode(errorCode); 106 | if (error.type === 'DEVICE_NOT_REGISTERED') { 107 | // Need to re-register 108 | self.uid = undefined; 109 | self.register(); 110 | } 111 | self.emit('error', new Error('Device: There was an error: ' + JSON.stringify(responseMsgObj))); 112 | } 113 | 114 | // Make ourself an emiter 115 | EventEmitter.call(this); 116 | 117 | } 118 | util.inherits(Device, EventEmitter); 119 | 120 | 121 | /** 122 | * Register with the proxy only. Expect an immediate response from proxy. We 123 | * send the channel we are on, then expect a UID in return. 124 | * 125 | * Note: if 'uid' is set, then we are registered. 126 | */ 127 | Device.prototype.register = function () { 128 | 129 | // Don't try to re-register if we are already trying 130 | if (this.recheckRegisteryTimeout) { 131 | return; 132 | } 133 | this._send('register', { 134 | deviceType: this.deviceType, 135 | channel: this.channel 136 | }); 137 | 138 | // Check the registery again in RECHECK_REGISTER seconds if we do not get a response 139 | var self = this; 140 | this.recheckRegisteryTimeout = setTimeout(function checkRegistery() { 141 | self.log(self.deviceType + ': unable to register with proxy (timeout), trying again. (' + self.proxyUrl + ' on "' + self.channel + '")'); 142 | self.clearRegisterTimeout(); 143 | self.register(); 144 | }, NET_TIMEOUT); 145 | 146 | }; 147 | 148 | Device.prototype.clearRegisterTimeout = function () { 149 | if (!this.recheckRegisteryTimeout) { 150 | return; 151 | } 152 | clearTimeout(this.recheckRegisteryTimeout); 153 | this.recheckRegisteryTimeout = undefined; 154 | }; 155 | 156 | 157 | /** 158 | * Send a ping to the proxy only. Expect an immediate response from proxy. 159 | * @param {function} callback This function gets called on completion of the ping. 160 | */ 161 | Device.prototype.ping = function(callback) { 162 | 163 | // Can only ping if we are registered 164 | if (!this.uid && typeof callback === 'function') { 165 | callback(-1); 166 | return; 167 | } 168 | 169 | this.pingManager.add(this.mySeqNum, callback); 170 | 171 | var timeStr = (new Date().getTime()).toString(); 172 | this._send('ping', timeStr); 173 | 174 | }; 175 | 176 | 177 | /** 178 | * Send a status update to the remote proxy - which gets forwarded to the 179 | * controller(s). 180 | * @param {string} type The status update type we are sending. 181 | * @param {string} data The data, it must be a string. 182 | */ 183 | Device.prototype.status = function (msgString) { 184 | this._send('status', msgString); 185 | }; 186 | 187 | 188 | /** 189 | * Send a status to the remote proxy that is sticky - which gets forwarded to the 190 | * receiver(s). A sticky status will be held on the proxy on the given channel until 191 | * the next message comes through. 192 | * @param {string} type The sticky type we are sending. 193 | * @param {string} data The data, it must be a string. 194 | */ 195 | Device.prototype.stickyStatus = function (msgString) { 196 | this._send('status', msgString, {sticky: true}); 197 | }; 198 | 199 | 200 | /** 201 | * Send a command to the remote proxy - which gets forwarded to the 202 | * receiver(s). 203 | * @param {string} type The command type we are sending. 204 | * @param {string} data The data, it must be a string. 205 | */ 206 | Device.prototype.command = function (msgString) { 207 | 208 | if (this.deviceType !== 'controller') { 209 | throw new Error('Only controllers can send commands.'); 210 | } 211 | 212 | this._send('command', msgString); 213 | }; 214 | 215 | /** 216 | * Send a command to the remote proxy that is sticky - which gets forwarded to the 217 | * receiver(s). A sticky command will be held on the proxy on the given channel until 218 | * the next message comes through. 219 | * @param {string} type The command type we are sending. 220 | * @param {string} data The data, it must be a string. 221 | * @param {object} options Additional options - read the code. 222 | */ 223 | Device.prototype.stickyCommand = function (msgString) { 224 | 225 | if (this.deviceType !== 'controller') { 226 | throw new Error('Only controllers can send commands.'); 227 | } 228 | 229 | this._send('command', msgString, {sticky: true}); 230 | }; 231 | 232 | 233 | /** 234 | * Send data to the remote proxy. 235 | * @param {string} type The message type we are sending. 236 | * @param {string} data The data, it must be a string. 237 | */ 238 | Device.prototype._send = function(type, data, options) { 239 | 240 | if (!this.uid && type !== 'register') { 241 | this.log('Device._send(): Not yet registered.'); 242 | return; 243 | } 244 | 245 | var msgObj = { 246 | type: type, 247 | uid: this.uid, 248 | seq: this.mySeqNum, 249 | data: data 250 | }; 251 | 252 | if ((type === 'status' || type === 'command') && options && options.sticky === true) { 253 | msgObj.sticky = true; 254 | } 255 | 256 | // Send the message to the proxy. Use the IP have we have determined it. 257 | this.connection._send(msgObj); 258 | this.mySeqNum += 1; 259 | }; 260 | 261 | 262 | /** 263 | * Close all connections. 264 | */ 265 | Device.prototype.close = function() { 266 | this.uid = undefined; 267 | this.clearRegisterTimeout(); 268 | if (this.timeoutHandle) { 269 | clearTimeout(this.timeoutHandle); 270 | } 271 | if (this.connection && typeof this.connection.closeAll === 'function') { 272 | this.connection.closeAll(); 273 | } 274 | this.removeAllListeners(); 275 | this.connection.removeAllListeners(); 276 | this.pingManager.close(); 277 | }; 278 | 279 | /** 280 | * Checks if the device is registered or not. 281 | */ 282 | Device.prototype.isRegistered = function() { 283 | return typeof this.uid === 'string'; 284 | }; 285 | 286 | 287 | module.exports = Device; 288 | -------------------------------------------------------------------------------- /tests/testWRC-LocalProxy.js: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * * 3 | * Copyright 2016 Simon M. Werner * 4 | * * 5 | * Licensed to the Apache Software Foundation (ASF) under one * 6 | * or more contributor license agreements. See the NOTICE file * 7 | * distributed with this work for additional information * 8 | * regarding copyright ownership. The ASF licenses this file * 9 | * to you under the Apache License, Version 2.0 (the * 10 | * "License"); you may not use this file except in compliance * 11 | * with the License. You may obtain a copy of the License at * 12 | * * 13 | * http://www.apache.org/licenses/LICENSE-2.0 * 14 | * * 15 | * Unless required by applicable law or agreed to in writing, * 16 | * software distributed under the License is distributed on an * 17 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 18 | * KIND, either express or implied. See the License for the * 19 | * specific language governing permissions and limitations * 20 | * under the License. * 21 | * * 22 | *********************************************************************/ 23 | 24 | 'use strict'; 25 | 26 | var test = require('tape'); 27 | var wrc = require('../index'); 28 | 29 | 30 | // 31 | // Configuration 32 | // 33 | 34 | 35 | // Allows us to change the tests (UDP vs TCP) from the command line 36 | var UDP = true; 37 | var TCP = false; 38 | if (process.env.PROTOCOL && process.env.PROTOCOL.toUpperCase() === 'TCP') { 39 | UDP = false; 40 | TCP = true; 41 | } 42 | 43 | // Enable detailed logging 44 | var ENABLE_LOGGING = false; 45 | var logging; 46 | if (!ENABLE_LOGGING) { 47 | logging = function() {}; 48 | } 49 | 50 | var channel1 = 'channel-1'; 51 | 52 | 53 | // 54 | // Tests 55 | // 56 | 57 | 58 | var proxy = createProxy(); 59 | var toy; 60 | var controller; 61 | 62 | function createProxy() { 63 | return wrc.createProxy({log: logging, 64 | udp4: UDP, 65 | tcp: TCP, 66 | socketio: false, 67 | onlyOneControllerPerChannel: true, 68 | onlyOneToyPerChannel: true, 69 | allowObservers: true }); 70 | } 71 | function createController(channel) { 72 | channel = channel || channel1; 73 | return wrc.createController({ channel: channel, log: logging, keepalive: 0, udp4: UDP, tcp: TCP }); 74 | } 75 | function createToy(channel) { 76 | channel = channel || channel1; 77 | return wrc.createToy({ channel: channel, log: logging, keepalive: 0, udp4: UDP, tcp: TCP }); 78 | } 79 | function createObserver(channel) { 80 | channel = channel || channel1; 81 | return wrc.createObserver({ channel: channel, log: logging, keepalive: 0, udp4: UDP, tcp: TCP }); 82 | } 83 | 84 | test('Test Proxy can be created and a toy can be registered', function(t) { 85 | 86 | t.plan(5); 87 | var tests = 0; 88 | 89 | var uid1; 90 | var uid2; 91 | 92 | proxy.once('register', function fn(msgObj) { 93 | t.equal(msgObj.type, 'register', 'message is correct type'); 94 | uid1 = msgObj.uid; 95 | t.true(typeof msgObj.uid === 'string', 'the uid is the correct type'); 96 | 97 | tests += 2; 98 | wrapUp(); 99 | }); 100 | 101 | toy = createToy(); 102 | toy.once('register', function fnReg(msgObj) { 103 | t.true(typeof msgObj === 'object', 'the response msgObj is the correct type'); 104 | t.equal(channel1, msgObj.channel, 'The channel is correct'); 105 | uid2 = msgObj.uid; 106 | 107 | tests += 1; 108 | wrapUp(); 109 | }); 110 | 111 | function wrapUp() { 112 | if (tests === 3) { 113 | t.equal(uid1, uid2, 'The UIDs are the same'); 114 | t.end(); 115 | } 116 | } 117 | 118 | }); 119 | 120 | test('Test controller can send commands to proxy', function(t) { 121 | 122 | t.plan(2); 123 | 124 | var cmdTxt = 'simon say\'s do this'; 125 | 126 | proxy.once('command', function fn (cmdObj) { 127 | t.equal(cmdObj.type, 'command', 'message is correct type'); 128 | t.equal(cmdObj.data, cmdTxt, 'command was correct'); 129 | t.end(); 130 | }); 131 | 132 | controller = createController(); 133 | controller.once('register', function fnReg() { 134 | controller.command(cmdTxt); 135 | }); 136 | 137 | }); 138 | 139 | 140 | test('Test observer can register and receive status updates from the toy', function(t) { 141 | 142 | t.plan(2); 143 | 144 | var statusTxt = 'This is my status'; 145 | 146 | var observer = createObserver(); 147 | observer.once('register', function fnReg() { 148 | controller.command(statusTxt); 149 | t.pass('Observer can register'); 150 | toy.status(statusTxt); 151 | }); 152 | observer.once('status', function fnReg(status) { 153 | observer.close(); 154 | t.equal(status, statusTxt, 'status is correct'); 155 | t.end(); 156 | }); 157 | 158 | 159 | }); 160 | 161 | 162 | test('toy-x registers, proxy crashes, then toy-1 pings and gets error and re-registers', function(t) { 163 | 164 | t.plan(4); 165 | 166 | // "Crash" the proxy - we simulate by removing the toy from DevMan 167 | delete proxy.devices.list[toy.uid]; 168 | 169 | toy.once('error', function() { 170 | t.pass('proxy sent an error response, as expected'); 171 | }); 172 | 173 | toy.once('register', function(msgObj) { 174 | t.true(typeof msgObj === 'object', '... and we re-registered okay'); 175 | t.true(typeof msgObj.uid === 'string', '... and uid returned'); 176 | t.true(typeof msgObj.channel === 'string', '... and channel returned'); 177 | 178 | t.end(); 179 | }); 180 | 181 | toy.ping(); 182 | 183 | }); 184 | 185 | 186 | test('The sequence numbers are handled and passed from device to toy', function(t) { 187 | 188 | // Seq Plan, item '1003' should be dropped by proxy and not heard by controller. 189 | var seqPlan = [1001, 1002, 1004, 1003, 1005]; 190 | var seqPlanCnt = 0; 191 | 192 | toy.on('command', fnToy); 193 | controller.on('status', fnCtrlr); 194 | controller.command('start'); 195 | 196 | toy.mySeqNum = seqPlan[seqPlanCnt]; 197 | 198 | // Handle all commands 199 | function fnToy (cmdObj) { 200 | 201 | if (seqPlanCnt >= seqPlan.length) { 202 | wrapUp(); 203 | return; 204 | } 205 | 206 | switch (seqPlanCnt) { 207 | 208 | case 0: 209 | case 1: 210 | case 2: 211 | case 4: 212 | t.pass('Toy sequence number increments okay.'); 213 | break; 214 | 215 | case 3: 216 | t.equal(cmdObj, 'dummy', 'Dummy command passed through okay.'); 217 | break; 218 | 219 | default: 220 | t.fail('We really should never reach this.'); 221 | } 222 | 223 | // Fabricate the sequence number to simulate slow packets 224 | toy.mySeqNum = seqPlan[seqPlanCnt]; 225 | toy.status('' + seqPlan[seqPlanCnt]); 226 | 227 | seqPlanCnt += 1; 228 | } 229 | 230 | function fnCtrlr (toySeq) { 231 | 232 | switch (toySeq) { 233 | 234 | case '1001': 235 | case '1002': 236 | case '1004': 237 | case '1005': 238 | t.pass('In order sequences arrive at controller.'); 239 | break; 240 | 241 | case '1003': 242 | t.fail('Out of order sequences should not arrive at controller.'); 243 | break; 244 | 245 | default: 246 | t.fail('We really should never reach this.'); 247 | } 248 | 249 | controller.command(toySeq); 250 | 251 | // Need to push through another command - since the status won't reach 252 | if (seqPlanCnt === 2) { 253 | controller.command('dummy'); 254 | } 255 | } 256 | 257 | function wrapUp() { 258 | toy.removeListener('command', fnToy); 259 | controller.removeListener('status', fnCtrlr); 260 | 261 | t.end(); 262 | } 263 | 264 | }); 265 | 266 | 267 | test('There can be only one device per channel', function(t){ 268 | t.plan(4); 269 | 270 | t.equal(proxy.devices.getAll('toy', channel1).length, 1, 'There is only one toy to start with.'); 271 | var toy2 = createToy(); 272 | toy2.once('register', function() { 273 | t.equal(proxy.devices.getAll('toy', channel1).length, 1, 'There is only one toy to end with.'); 274 | toy2.close(); 275 | }); 276 | 277 | t.equal(proxy.devices.getAll('controller', channel1).length, 1, 'There is only one controller to start with.'); 278 | var controller2 = createController(); 279 | controller2.once('register', function() { 280 | t.equal(proxy.devices.getAll('controller', channel1).length, 1, 'There is only one controller to end with.'); 281 | controller2.close(); 282 | }); 283 | 284 | }); 285 | 286 | testTwoStickies('command'); 287 | testTwoStickies('status'); 288 | 289 | function testTwoStickies(type) { 290 | 291 | test('Test the sticky status messages for ' + type, function(t) { 292 | 293 | var myStatus = 'What is the meaning of life?'; 294 | var myChannel = Math.random().toString(); 295 | t.plan(1); 296 | 297 | var dev; 298 | var stickyFn; 299 | var createObserverFn; 300 | if (type === 'command') { 301 | dev = createController(myChannel); 302 | stickyFn = dev.stickyCommand.bind(dev); 303 | createObserverFn = createToy; 304 | } else { 305 | dev = createToy(myChannel); 306 | stickyFn = dev.stickyStatus.bind(dev); 307 | createObserverFn = createObserver; 308 | } 309 | 310 | dev.once('register', function() { 311 | stickyFn(myStatus); 312 | 313 | var observer2 = createObserverFn(myChannel); 314 | observer2.once(type, function(status) { 315 | t.equal(myStatus, status, 'Status message received after observer connected.'); 316 | observer2.close(); 317 | dev.close(); 318 | t.end(); 319 | }); 320 | }); 321 | 322 | }); 323 | } 324 | 325 | 326 | test('toy registers but proxy not there, then toy re-registers automatically', function(t) { 327 | 328 | t.plan(1); 329 | 330 | proxy.close(); 331 | 332 | console.log('HANG ON, this test takes a few seconds.'); 333 | var myToy = createToy('RandomChannel' + Math.random().toString()); 334 | 335 | myToy.on('register', function () { 336 | t.pass('We could register with a proxy that came up late.'); 337 | t.end(); 338 | myToy.removeAllListeners(); 339 | myToy.close(); 340 | }); 341 | 342 | myToy.on('error', function() { 343 | t.pass('We handled the error.'); 344 | t.end(); 345 | myToy.removeAllListeners(); 346 | myToy.close(); 347 | clearTimeout(h); 348 | }); 349 | 350 | // Start the proxy after the first retry 351 | var h = setTimeout(function(){ 352 | proxy = createProxy(); 353 | }, 6000); 354 | 355 | }); 356 | 357 | 358 | test.onFinish(function () { 359 | 360 | setTimeout(function () { 361 | proxy.close(); 362 | toy.close(); 363 | controller.close(); 364 | }, 100); 365 | 366 | }); 367 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Simon M. Werner 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------