├── .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 |
--------------------------------------------------------------------------------