diff --git a/dev/autorun.js b/dev/autorun.js index a1541e1..a50e527 100644 --- a/dev/autorun.js +++ b/dev/autorun.js @@ -26,11 +26,17 @@ var tasks = [ watch: /^local\//, exec: function () { info('Local: Starting...'); - exec('node', ['arduino.js'], { cwd: './local', pipe: true, name: 'local' }, function (err) { + var env = { LOCAL_SERVER: 'mock' }; + var cmd = [ + 'killall --quiet -SIGHUP --user ' + process.env.USER + ' petbot_local', + 'sleep 0.5', // wait for the petbot process to shut down gracefully + 'node localserver' + ].join(';'); + exec('sh', ['-c', cmd], { cwd: 'local/', pipe: true, name: 'local', env: env }, function (err) { info('Local: Exited.'); }); }, - execOnStart: true + execOnStart: 500 }, { name: 'reset remote', @@ -82,7 +88,9 @@ _.each(tasks, function(opts) { return false; }; if (opts.execOnStart) { - opts.exec(); + setTimeout(function () { + opts.exec(); + }, typeof opts.execOnStart === 'number' ? opts.execOnStart : 0); } }); diff --git a/local/localserver.js b/local/localserver.js index e2e7ccf..4b4203c 100644 --- a/local/localserver.js +++ b/local/localserver.js @@ -1,61 +1,82 @@ +process.title = 'petbot_local'; + var config = require('../common/config'); var client = require('socket.io-client'); var _ = require('underscore'); -var LocalServer = function () { - this.socket = client.connect(config.HOST); - this.BOARD_LOW = 0; - this.BOARD_HIGH = 1; - this.pinMap = {}; -}; - -LocalServer.prototype.handleSignal = function (data) { - var self = this; - if (data.name == 'killswitch') { - self.kill(); - } else { - var level = (data.active) ? self.BOARD_HIGH : self.BOARD_LOW; - self.write(data.name, level); +var T = require('tbone').tbone; +var tbone = T; + +var servers = tbone.collections.base.make({ + lookupById: true +}); + +var localServer = tbone.make(); +T('servers', servers); + +localServer('drive', function () { + var driving = _.filter(_.pluck(T('servers') || {}, 'drive'), function (drive) { + return drive && (!!drive.right || !!drive.forward); + }); + return driving.length === 0 ? {} : driving[0]; +}); +localServer('awake', function () { + return _.keys(T('servers')).length > 0; +}); + +var nextId = 1; +var socket = client.connect(config.HOST, { transports: ['xhr-polling'] }); +socket.on('connect', function() { + var me = tbone.make(); + me('id', nextId++); + servers.add(me); + + console.log('connected to remote server'); + socket.emit('clientId', {id: config.DEVICE_ID}); + + // Expect the server to say "keepDriving" every ~500 ms. + var driveTimeout; + function stopDrivingAfterTimeout() { + if (driveTimeout) { + clearTimeout(driveTimeout); + } + driveTimeout = setTimeout(function () { + me('drive', {}); + }, 1000); } -} - -LocalServer.prototype.sleep = function () { - console.log('local server is idle'); -} - -LocalServer.prototype.wake = function () { - console.log('local server is awake'); -} - -LocalServer.prototype.write = function (direction, level) { - console.log('direction ' + direction + ' at level ' + level); -} - -LocalServer.prototype.kill = function () { - var self = this; - console.log('KILLSWITCH ENGAGE'); - _.each(pinMap, function (k, v) { - self.write(v.pin, self.BOARD_LOW) - }); -} - -LocalServer.prototype.run = function () { - var self = this; - self.socket.on('connect', function() { - console.log('connected to remote server'); - self.socket.emit('clientId', {id: config.DEVICE_ID}); - - self.wake(); - - self.socket.on('direction', function(data) { - self.handleSignal(data); - }); + socket.on('drive', function(data) { + me('drive', data); + stopDrivingAfterTimeout(); + }); + socket.on('keepDriving', function () { + stopDrivingAfterTimeout(); + }); + T(function () { + if (!socket.disconnected) { + socket.emit('pins', localServer('pins')); + } + }); + T(function () { + if (!socket.disconnected) { + socket.emit('drive', localServer('drive')); + } + }); - self.socket.on('disconnect', function() { - console.log('disconnected from remote server'); - self.sleep(); - }); + socket.on('disconnect', function() { + console.log('disconnected from remote server'); + servers.remove(me); }); -}; +}); + +require('./' + process.env.LOCAL_SERVER + '.js')(localServer); -module.exports = LocalServer; +// Gracefully shutdown on SIGHUP +process.on('SIGHUP', function () { + try { + socket.disconnect(function() { + process.exit(0); + }); + } catch(e) { + process.exit(0); + } +}); diff --git a/local/mock.js b/local/mock.js new file mode 100644 index 0000000..c6aec2a --- /dev/null +++ b/local/mock.js @@ -0,0 +1,15 @@ +var T = require('tbone').tbone; +var tbone = T; + +module.exports = function (me) { + T(function () { + var drive = me('drive'); + me('pins.right', drive.right === 1 ? 5 : 0); + me('pins.left', drive.right === -1 ? 5 : 0); + me('pins.forward', drive.forward === 1 ? 5 : 0); + me('pins.backward', drive.forward === -1 ? 5 : 0); + }); + T(function () { + me('pins.ready', !!me('awake')); + }); +}; diff --git a/public/index.html b/public/index.html index 4b03c61..b7a4e5f 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ -
+

PETBOT v0.0.1

@@ -25,15 +25,20 @@

PETBOT v0.0.1

- diff --git a/public/static/bindings.js b/public/static/bindings.js index 20deee9..7b533da 100644 --- a/public/static/bindings.js +++ b/public/static/bindings.js @@ -1,38 +1,15 @@ -var socket = io.connect(); +var socket = io.connect('/', { + transports: ['xhr-polling'], + 'reconnection delay': 100, + 'max reconnection attempts': 10000 +}); socket.emit('clientId', {id: 'browser'}); -// var keys = { -// 37: { -// name: 'left', -// active: false, -// mode: 'steer' -// }, -// 38: { -// name: 'forward', -// active: false, -// mode: 'move' -// }, -// 39: { -// name: 'right', -// active: false, -// mode: 'steer' -// }, -// 40: { -// name: 'back', -// active: false, -// mode: 'move' -// } -// }; var keys = { 37: 'left', 38: 'forward', 39: 'right', 40: 'backward' }; -T('localStatus', { - online: false, - status: 'connecting', - message: 'Attempting to connect to PETBOT...' -}); socket.on('yourId', function (data) { T('browserId', data); }); @@ -42,9 +19,52 @@ socket.on('localStatus', function (data) { socket.on('browsers', function (data) { T('browsers', JSON.stringify(data)); }); +socket.on('bots', function (data) { + T('bots', JSON.stringify(data)); +}); socket.on('drive', function (data) { T('actualDrive', data); }); +socket.on('botOnline', function (data) { + T('botOnline', data); +}); +socket.on('connect', function () { + T('online', true); +}); +socket.on('reconnect', function () { + T('online', true); +}); +socket.on('disconnect', function () { + T('online', false); +}); + +T('onlineStr', function () { + return T('online') + ''; +}); +T('botOnlineStr', function () { + return T('botOnline') + ''; +}); +T('status', function () { + if (T('online')) { + if (T('botOnline')) { + return { + name: 'online', + message: 'Connected.' + }; + } else { + return { + name: 'connecting', + message: 'Connecting to petbot...' + }; + } + } else { + return { + name: 'offline', + message: 'Connecting to server...' + }; + } +}); + socket.on('reloadui', function () { console.log('Reloading...') location.reload(); @@ -70,10 +90,14 @@ T(function () { setTimeout(function () { T.toggle('isDrivingTimer'); }, 500); - } else { - } }); +tbone.createView('topBanner', function () { + this.$el.removeClass('online'); + this.$el.removeClass('connecting'); + this.$el.removeClass('offline'); + this.$el.addClass(T('status.name')); +}); tbone.createView('direction', function () { var $el = this.$el; var isOtherDriver = !!T('actualDrive.browserId') && T('browserId') !== T('actualDrive.browserId'); diff --git a/public/static/style.less b/public/static/style.less index 0585f7a..dbde274 100644 --- a/public/static/style.less +++ b/public/static/style.less @@ -70,7 +70,7 @@ a, a:visited, a:active { .backward.left:before { content: '\2199'; } -.top-banner { +.topBanner { width: 100%; height: 40px; display: block; @@ -81,6 +81,11 @@ a, a:visited, a:active { &.online { background: #bada55; + overflow: hidden; + height: 0px; + padding: 0px; + transition: height 1s, padding 1s; + transition-delay: 1s; } &.offline { diff --git a/remote/petbot.js b/remote/petbot.js index bb06953..3204734 100644 --- a/remote/petbot.js +++ b/remote/petbot.js @@ -21,6 +21,17 @@ T('drive', function () { }); return driving.length === 0 ? {} : driving[0]; }); +T('isDriving', function () { + return !!T('drive.forward') || !!T('drive.right'); +}); +T('botOnline', function () { + return _.keys(T('bots')).length > 0; +}); + +var bots = tbone.collections.base.make({ + lookupById: true +}); +T('bots', bots); app.configure(function () { var username = process.env.AUTH_USERNAME; @@ -72,21 +83,45 @@ io.sockets.on('connection', function (socket) { } }); +var nextId = 1; var handleLocalServer = function (socket) { console.log('local server is connected'); - localServerUp = true; - broadcastLocalStatus(localServerUp, socket); + var me = tbone.make(); + var myId = nextId++; + socket.emit('yourId', myId); + me.query('id', myId); + bots.add(me); + T(function () { + if (!socket.disconnected) { + socket.emit('drive', T('drive')); + } + }); + T(function () { + if (!socket.disconnected) { + if (T('isDriving')) { + socket.emit('keepDriving'); + T('isDrivingTimer'); + setTimeout(function () { + T.toggle('isDrivingTimer'); + }, 500); + } + } + }); + socket.on('pins', function (data) { + me('pins', data); + }); + socket.on('drive', function (data) { + me('drive', data); + }); socket.on('disconnect', function () { + bots.remove(me); console.log('local server disconnected'); - localServerUp = false; - broadcastLocalStatus(localServerUp, socket); }); }; -var nextBrowserId = 1; function handleBrowser (socket) { - var me = tbone.models.base.make(); - var myId = nextBrowserId++; + var me = tbone.make(); + var myId = nextId++; socket.emit('yourId', myId); me.query('id', myId); browsers.add(me); @@ -95,11 +130,21 @@ function handleBrowser (socket) { socket.emit('browsers', T('browsers')); } }); + T(function () { + if (!socket.disconnected) { + socket.emit('bots', T('bots')); + } + }); T(function () { if (!socket.disconnected) { socket.emit('drive', T('drive')); } }); + T(function () { + if (!socket.disconnected) { + socket.emit('botOnline', !!T('botOnline')); + } + }); // Expect the client to say "keepDriving" every ~500 ms. var driveTimeout; function stopDrivingAfterTimeout() { @@ -121,60 +166,3 @@ function handleBrowser (socket) { browsers.remove(me); }); } - -var broadcastLocalStatus = function (isUp, socket) { - var statusObj = {}; - if (isUp === true) { - statusObj = { - online: true, - status: 'online', - message: 'PETBOT is online and ready to go!' - }; - } else if (isUp === false) { - statusObj = { - online: false, - status: 'offline', - message: 'PETBOT is offline' - }; - } else { - statusObj = { - online: false, - status: 'connecting', - message: 'Attempting to connect to PETBOT...' - }; - } - socket.broadcast.emit('localStatus', statusObj); -}; - -// fire when a new connection moves into the #0 spot -var openCurrentSocket = function (member) { - console.log('now listening to connection '+member.id); - broadcastLocalStatus(localServerUp, member.socket); - member.socket.on('direction', function (data) { - console.log('broadcasting: '+data); - member.socket.broadcast.emit('direction', data); - }); - member.socket.on('disconnect', function () { - console.log('connection ' + member.id + ' is disconnecting'); - // remove this connection from the queue - updateQueue(member, false); - }); -}; - -// handle changes in the queue. add or remove, then start listening to a new connection -// if necessary. -var updateQueue = function (member, shouldAdd) { - var firstItemId = (queue.length > 0) ? queue[0].id : null; - if (shouldAdd) { - queue.push(member); - } else { - queue = _.reject(queue, function(item) { - return item.id === member.id; - }); - } - // XXX possible race condition? - // if guid of first item has changed, there's a new connection in first place - if (queue.length > 0 && firstItemId !== queue[0].id) { - openCurrentSocket(queue[0]); - } -};