├── public ├── scss │ ├── 01_base │ │ ├── _index.scss │ │ └── _base.scss │ ├── style.scss │ └── 02_components │ │ ├── _team-images.scss │ │ ├── _bb8-ctrls.scss │ │ ├── _index.scss │ │ ├── _connect.scss │ │ ├── _buttons.scss │ │ ├── _presets.scss │ │ ├── _navigation.scss │ │ ├── _move.scss │ │ └── _color.scss ├── img │ ├── bb8.png │ ├── jose.jpg │ ├── jesse.jpg │ ├── logan.jpg │ ├── ripple.gif │ ├── favicon.png │ ├── natalie.jpg │ ├── sabrina.jpg │ ├── setup-screenshot.png │ ├── arrow-down.svg │ ├── icons │ │ ├── about.svg │ │ ├── about_blue.svg │ │ ├── bluetooth.svg │ │ ├── bluetooth_blue.svg │ │ ├── book.svg │ │ ├── book_blue.svg │ │ ├── dropper.svg │ │ ├── dropper_blue.svg │ │ ├── settings.svg │ │ ├── settings_blue.svg │ │ ├── controller.svg │ │ └── controller_blue.svg │ ├── sprk.svg │ ├── bb8-silh.svg │ ├── bb8.svg │ ├── logo-vert.svg │ └── brand-logo.svg ├── js │ ├── presets.js │ ├── handle-nav.js │ ├── choose-color.js │ ├── motion-socket.js │ ├── setup-socket.js │ └── gamepad.js └── vendor │ ├── js │ ├── colorPicker.js │ └── hsvPicker.js │ └── sweetalert.min.js ├── .npmignore ├── .gitignore ├── views ├── partials │ ├── _modal.jade │ ├── _layout.jade │ └── _navigation.jade ├── about.jade ├── index.jade ├── move.jade ├── color.jade └── presets.jade ├── bin └── index.js ├── commands ├── look.js ├── move-random.js ├── dance.js ├── circle.js ├── fly.js ├── accelerometer.js ├── collision.js ├── speedometer.js ├── magic8.js └── lights.js ├── routes ├── graph_router.js └── router.js ├── lib ├── device-config.js ├── server.js └── socket-listeners.js ├── setup ├── setup-sphero.js └── setup-bb8.js ├── DEV_NOTES.md ├── gulpfile.js ├── test ├── router.spec.js ├── device-config.spec.js ├── commands.spec.js └── socket-events.spec.js ├── LICENSE.md ├── package.json ├── README.md └── .eslintrc /public/scss/01_base/_index.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | -------------------------------------------------------------------------------- /public/scss/style.scss: -------------------------------------------------------------------------------- 1 | @import '01_base/_index'; 2 | @import '02_components/_index'; 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/*.sw? 2 | **/node_modules 3 | .DS_Store 4 | **/*.log 5 | db/ 6 | coverage/ 7 | -------------------------------------------------------------------------------- /public/img/bb8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/bb8.png -------------------------------------------------------------------------------- /public/img/jose.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/jose.jpg -------------------------------------------------------------------------------- /public/img/jesse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/jesse.jpg -------------------------------------------------------------------------------- /public/img/logan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/logan.jpg -------------------------------------------------------------------------------- /public/img/ripple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/ripple.gif -------------------------------------------------------------------------------- /public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/favicon.png -------------------------------------------------------------------------------- /public/img/natalie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/natalie.jpg -------------------------------------------------------------------------------- /public/img/sabrina.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/sabrina.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.sw? 2 | **/node_modules 3 | .DS_Store 4 | **/*.log 5 | db/ 6 | /public/css/style.css 7 | coverage/ 8 | -------------------------------------------------------------------------------- /public/img/setup-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saphero/sphero-hack/HEAD/public/img/setup-screenshot.png -------------------------------------------------------------------------------- /public/scss/02_components/_team-images.scss: -------------------------------------------------------------------------------- 1 | img.team-img { 2 | border-radius: 50%; 3 | max-height: 200px; 4 | } 5 | -------------------------------------------------------------------------------- /views/partials/_modal.jade: -------------------------------------------------------------------------------- 1 | link(rel='stylesheet', type='text/css', href='static/vendor/sweetalert.css') 2 | script(src='static/vendor/sweetalert.min.js') 3 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const createAppServer = require(__dirname + '/../lib/server.js'); 6 | 7 | createAppServer(3000, true); 8 | -------------------------------------------------------------------------------- /public/scss/02_components/_bb8-ctrls.scss: -------------------------------------------------------------------------------- 1 | .ctrl-wrapper { 2 | margin: 20px; 3 | padding: 25px; 4 | border: 1px solid black; 5 | min-height: 100px; 6 | .btn-wrapper { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/scss/02_components/_index.scss: -------------------------------------------------------------------------------- 1 | @import 'buttons'; 2 | @import 'navigation'; 3 | @import 'team-images'; 4 | @import 'color'; 5 | // @import 'bb8-ctrls'; 6 | @import 'connect'; 7 | @import 'move'; 8 | @import 'presets'; 9 | -------------------------------------------------------------------------------- /commands/look.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (orb) => { 4 | orb.color('#F409CE'); 5 | return setInterval(() => { 6 | var look = Math.floor(Math.random() * 180); 7 | orb.roll(0, look); 8 | }, 500); 9 | }; 10 | -------------------------------------------------------------------------------- /commands/move-random.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (orb) => { 4 | orb.color('#00ffab'); 5 | return setInterval(() => { 6 | var direction = Math.floor(Math.random() * 360); 7 | orb.roll(1000, direction); 8 | }, 1000); 9 | }; 10 | -------------------------------------------------------------------------------- /commands/dance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (orb) => { 4 | orb.color('yellow'); 5 | var count = 0; 6 | return setInterval(() => { 7 | var deg = count % 2 ? 0 : 180; 8 | count++; 9 | orb.roll(160, deg); 10 | }, 100); 11 | }; 12 | -------------------------------------------------------------------------------- /commands/circle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (orb) => { 4 | orb.color('blue'); 5 | var counter = 0; 6 | return setInterval(() => { 7 | orb.roll(100, counter); 8 | counter = counter + 15; 9 | if (counter >= 360) return; 10 | }, 500); 11 | }; 12 | -------------------------------------------------------------------------------- /public/js/presets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global io */ 3 | 4 | var socket = io.connect('http://localhost:3000'); 5 | 6 | $('.card').on('click', 'button', function() { 7 | socket.emit('preset', { name: $(this).data('preset') }); 8 | }); 9 | 10 | $('#stop-btn').on('click', () => { 11 | socket.emit('clear-preset'); 12 | }); 13 | -------------------------------------------------------------------------------- /commands/fly.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (orb) => { 4 | orb.detectFreefall(); 5 | 6 | var fly = { 7 | freefall: () => orb.color('purple'), 8 | landed: () => orb.color('red') 9 | }; 10 | 11 | orb.on('freefall', fly.freefall); 12 | 13 | orb.on('landed', fly.landed); 14 | 15 | return fly; 16 | }; 17 | -------------------------------------------------------------------------------- /routes/graph_router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const Graph = module.exports = express.Router(); 5 | 6 | Graph.get('/move', (req, res) => { 7 | // bring in module.exported functions which make http.requests from sphero 8 | // commands on server side to client side jQuery rendering Flot graphs 9 | }); 10 | -------------------------------------------------------------------------------- /commands/accelerometer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (orb, socket) => { 4 | orb.streamAccelerometer(); 5 | 6 | orb.on('accelerometer', (data) => { 7 | var dataArr = [[]]; 8 | if (!data.xAccel.value[0] || !data.yAccel.value[0]) { 9 | dataArr[0].push([0, 0]); 10 | } else { 11 | dataArr[0].push([data.xAccel.value[0], data.yAccel.value[0]]); 12 | } 13 | socket.emit('accelerometer', dataArr); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /commands/collision.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (orb) => { 4 | orb.detectCollisions(); 5 | 6 | orb.color('teal'); 7 | orb.roll(155, Math.floor(Math.random() * 360)); 8 | 9 | var collisionObj = { 10 | collision: () => { 11 | orb.color('red'); 12 | setTimeout(() => { 13 | orb.color('teal'); 14 | }, 1000); 15 | } 16 | }; 17 | 18 | orb.on('collision', collisionObj.collision); 19 | 20 | return collisionObj; 21 | }; 22 | -------------------------------------------------------------------------------- /routes/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const Router = module.exports = express.Router(); 5 | 6 | Router.get('/', (req, res) => { 7 | res.render('index'); 8 | }); 9 | 10 | Router.get('/about', (req, res) => { 11 | res.render('about'); 12 | }); 13 | 14 | Router.get('/color', (req, res) => { 15 | res.render('color'); 16 | }); 17 | 18 | Router.get('/move', (req, res) => { 19 | res.render('move'); 20 | }); 21 | 22 | Router.get('/presets', (req, res) => { 23 | res.render('presets'); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/device-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sphero = require('sphero'); 4 | 5 | exports.spheroCreate = () => { 6 | const config = require('home-config').load('.spheroconfig'); 7 | return sphero('/dev/' + config.SPHERO_ID); 8 | }; 9 | 10 | exports.bb8Create = () => { 11 | const config = require('home-config').load('.bb8config'); 12 | if (typeof config.BB8_UUID !== 'undefined') { 13 | console.log('connecting via noble'); 14 | return sphero(config.BB8_UUID); 15 | } 16 | console.log('fail in uuid config'); 17 | return false; 18 | }; 19 | -------------------------------------------------------------------------------- /setup/setup-sphero.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | module.exports = exports = (cb) => { 6 | console.log('beginning setup'); 7 | const spheroFile = 'tty.Sphero'; 8 | fs.readdir('/dev', (err, files) => { 9 | if (err) return console.log(err); 10 | var matchy = files.filter(file => file.indexOf(spheroFile) === 0); 11 | const config = require('home-config').load('.spheroconfig', { 12 | SPHERO_ID: matchy[0] 13 | }); 14 | config.save(); 15 | console.log('saved config file to ~/.spheroconfig'); 16 | cb(); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /views/partials/_layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(name="viewport", content="width=device-width, initial-scale=1") 5 | title Sphero Hack 6 | link(rel="icon", type="image/png", href="static/img/favicon.png") 7 | link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.css") 8 | link(rel="stylesheet", type="text/css", href="//cdn.jsdelivr.net/flexboxgrid/6.3.0/flexboxgrid.min.css") 9 | link(href='https://fonts.googleapis.com/css?family=Lato:400,300,700', rel='stylesheet', type='text/css') 10 | link(rel="stylesheet", type="text/css", href="static/css/style.css") 11 | body 12 | -------------------------------------------------------------------------------- /commands/speedometer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (orb, socket) => { 4 | if (!orb) return; 5 | orb.streamVelocity(); 6 | var dataArr = [[]]; 7 | var count = 0; 8 | 9 | orb.on('velocity', (data) => { 10 | if (!data.xVelocity.value[0] || !data.yVelocity.value[0]) { 11 | dataArr[0].push([count, 0]); 12 | } else { 13 | var speed = Math.sqrt(Math.pow(data.xVelocity.value[0], 2), 14 | Math.pow(data.yVelocity.value[0], 2)); 15 | dataArr[0].push([count, speed]); 16 | } 17 | if (dataArr[0].length > 50) { 18 | dataArr[0] = dataArr[0].slice(dataArr[0].length - 50); 19 | } 20 | count++; 21 | socket.emit('speedometer', dataArr); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /commands/magic8.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var start = false; 3 | module.exports = exports = (orb) => { 4 | orb.streamVelocity(); 5 | 6 | 7 | var magic8 = { 8 | velocity: (data) => { 9 | if (data.xVelocity.value[0] > 100 || data.yVelocity.value[0] > 100) { 10 | start = true; 11 | } else if (start) { 12 | predict(); 13 | start = false; 14 | } 15 | } 16 | }; 17 | 18 | orb.on('velocity', magic8.velocity); 19 | 20 | function predict() { 21 | var number = Math.random() * 3; 22 | if (number < 1) { 23 | return orb.color('green'); 24 | } 25 | if (number < 2) { 26 | return orb.color('yellow'); 27 | } 28 | return orb.color('red'); 29 | } 30 | 31 | return magic8; 32 | }; 33 | -------------------------------------------------------------------------------- /DEV_NOTES.md: -------------------------------------------------------------------------------- 1 | # DEVELOPER NOTES 2 | 3 | #### Known Issues: 4 | 5 | - Rely on Noble bluetooth module but is not entirely compatible with this project. Causes glitches to setup of BB-8 and Ollie devices. 6 | 7 | - Currently the graphs on the ```/move``` page are not working well for BB-8 devices. This could be due to the Bluetooth LE connection which does not receive a steady stream of packets from the device and instead, dumps information back to the server. This distorts the graphs. 8 | 9 | - Intermittent Bluetooth connectivity issues when other paired devices are on. 10 | 11 | - BB-8 connection doesn't work according to Sphero module - firstly, ```orb.connect()``` does not run and we had to take additional steps to save the orb object. Also, when connecting, we have to emit ```setupDevice()``` twice. 12 | -------------------------------------------------------------------------------- /public/img/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /public/scss/02_components/_connect.scss: -------------------------------------------------------------------------------- 1 | .connect-panel { 2 | text-align: center; 3 | img { 4 | display: block; 5 | margin: 0px auto 20px auto; 6 | max-width: 200px; 7 | } 8 | button.btn, input.btn { 9 | font-family: $header-font-family; 10 | letter-spacing: 0.4px; 11 | font-weight: bold; 12 | min-width: 100px; 13 | } 14 | } 15 | .connect-req { 16 | text-align: left; 17 | color: $tert-color-slate; 18 | li { 19 | margin-bottom: 10px; 20 | font-size: 14px; 21 | } 22 | } 23 | .sweet-alert.setup-modal { 24 | h2 { 25 | font-family: $header-font-family; 26 | color: $base-color-blue; 27 | margin-bottom: 10px; 28 | } 29 | fieldset { 30 | display: none; 31 | } 32 | .sa-button-container button.cancel { 33 | background-color: $base-color-slate; 34 | font-weight: normal; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /views/about.jade: -------------------------------------------------------------------------------- 1 | include ./partials/_layout 2 | include ./partials/_navigation 3 | 4 | .content 5 | .row 6 | .col-xs.center-sm 7 | .box 8 | h2 About 9 | 10 | .row.center-sm 11 | .col-xs-12.col-sm.col-md-6.col-lg-4 12 | .box 13 | h2 Natalie Chow 14 | img.team-img(src="static/img/natalie.jpg") 15 | .col-xs-12.col-sm.col-md-6.col-lg-4 16 | .box 17 | h2 Sabrina Tee 18 | img.team-img(src="static/img/sabrina.jpg") 19 | .col-xs-12.col-sm.col-md-6.col-lg-4 20 | .box 21 | h2 Logan Tegman 22 | img.team-img(src="static/img/logan.jpg") 23 | .col-xs-12.col-sm.col-md-6.col-lg-4 24 | .box 25 | h2 Jose Tello 26 | img.team-img(src="static/img/jose.jpg") 27 | .col-xs-12.col-sm.col-md-6.col-lg-4 28 | .box 29 | h2 Jesse Thach 30 | img.team-img(src="static/img/jesse.jpg") 31 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = (port, openBrowser) => { 4 | const express = require('express'); 5 | const app = express(); 6 | const Router = require(__dirname + '/../routes/router'); 7 | const socketListeners = require(__dirname + '/socket-listeners'); 8 | const opn = require('opn'); 9 | 10 | app.set('view engine', 'jade'); 11 | app.set('views', __dirname + '/../views'); 12 | 13 | app.use('/', Router); 14 | app.use('/static', express.static(__dirname + '/../public')); 15 | 16 | const server = require('http').Server(app); 17 | const io = require('socket.io')(server); 18 | 19 | // Creates all socket.io event listeners 20 | var orb; 21 | socketListeners(io, orb); 22 | 23 | var serverInst = server.listen(port, () => { 24 | console.log('server running on port ' + port); 25 | }); 26 | 27 | if (openBrowser) opn('http://localhost:' + port); 28 | 29 | return serverInst; 30 | }; 31 | -------------------------------------------------------------------------------- /public/scss/02_components/_buttons.scss: -------------------------------------------------------------------------------- 1 | @mixin btn($color: blue) { 2 | background-color: $color; 3 | color: $base-color-white; 4 | text-transform: uppercase; 5 | letter-spacing: 0.5px; 6 | border-radius: 4px; 7 | padding: 10px 15px; 8 | border: 0; 9 | &:focus { 10 | outline: 0; 11 | } 12 | &:hover { 13 | background-color: lighten($color, 10%); 14 | } 15 | a { 16 | text-decoration: none; 17 | color: white; 18 | } 19 | } 20 | 21 | @mixin btn3D($color: blue) { 22 | @include btn($color); 23 | border-bottom: 2px solid darken($color, 10%); 24 | -webkit-box-shadow: inset 0 -2px darken($color, 10%); 25 | box-shadow: inset 0 -2px darken($color, 10%); 26 | &:active { 27 | position: relative; 28 | top: 1px; 29 | outline: 0; 30 | -webkit-box-shadow: none; 31 | box-shadow: none; 32 | } 33 | } 34 | 35 | .btn { 36 | @include btn($base-color-turq); 37 | &.shading { 38 | @include btn3D($base-color-turq); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/scss/02_components/_presets.scss: -------------------------------------------------------------------------------- 1 | .card-container { 2 | padding: 12px 6px 0px 6px; 3 | margin-right: 12px; 4 | background-color: $base-color-liteblue; 5 | .card { 6 | background-color: $base-color-white; 7 | width: 90%; 8 | text-align: center; 9 | border-radius: 4px; 10 | padding: 12px 16px; 11 | margin-right: 20px; 12 | margin-bottom: 12px; 13 | box-shadow: 2px 2px 2px $sec-color-liteblue; 14 | .card-header { 15 | margin: 6px 0px; 16 | } 17 | hr { 18 | border: none; 19 | border-bottom: 0.5px solid $base-color-slate; 20 | } 21 | p { 22 | font-size: 14px; 23 | color: $tert-color-slate; 24 | } 25 | button { 26 | font-family: $header-font-family; 27 | font-size: 14px; 28 | min-width: 100px; 29 | } 30 | } 31 | } 32 | 33 | #stop-btn { 34 | @include btn3D($sec-color-yellow); 35 | font-family: $header-font-family; 36 | font-size: 14px; 37 | margin-top: 12px; 38 | } 39 | -------------------------------------------------------------------------------- /setup/setup-bb8.js: -------------------------------------------------------------------------------- 1 | const noble = require('noble'); 2 | const _ = require('lodash'); 3 | 4 | module.exports = exports = (callback) => { 5 | console.log('beginning setup'); 6 | noble.startScanning(); 7 | noble.on('discover', (peripheral) => { 8 | if (_.includes(peripheral.advertisement.localName, 'BB-')) { 9 | var deviceUUID = peripheral.uuid; 10 | var localName = peripheral.advertisement.localName; 11 | console.log('writing to config file'); 12 | console.log('BB8 UUID - "' + deviceUUID + '"'); 13 | console.log('BB8_LOCAL_NAME: ' + localName); 14 | var config = require('home-config').load('.bb8config', { 15 | BB8_UUID: deviceUUID, 16 | BB8_LOCAL_NAME: localName 17 | }); 18 | config.save(); 19 | noble.stopScanning(); 20 | console.log('connected to ' + config.BB8_LOCAL_NAME); 21 | console.log('saved config file to ~/.bb8config'); 22 | callback(); 23 | } else { 24 | console.log('searching...'); 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /commands/lights.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.rainbow = (orb) => { 4 | var frequency = 0.3; 5 | var r, g, b; 6 | var count = 0; 7 | function nextRainbow() { 8 | r = Math.floor(Math.sin(frequency * count + 0) * 127 + 128); 9 | g = Math.floor(Math.sin(frequency * count + 2) * 127 + 128); 10 | b = Math.floor(Math.sin(frequency * count + 4) * 127 + 128); 11 | count++; 12 | if (count === 32) count = 0; 13 | } 14 | return setInterval(() => { 15 | nextRainbow(); 16 | orb.color({ red: r, green: g, blue: b }); 17 | }, 250); 18 | }; 19 | 20 | exports.xmas = (orb) => { 21 | var count = 0; 22 | return setInterval(() => { 23 | if (count % 2) { 24 | orb.color('green'); 25 | } else { 26 | orb.color('red'); 27 | } 28 | count++; 29 | }, 1000); 30 | }; 31 | 32 | exports.disco = (orb) => { 33 | var count = 0; 34 | return setInterval(() => { 35 | if (count % 2) { 36 | orb.randomColor(); 37 | } else { 38 | orb.color('black'); 39 | } 40 | count++; 41 | }, 250); 42 | }; 43 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const eslint = require('gulp-eslint'); 5 | const mocha = require('gulp-mocha'); 6 | const sass = require('gulp-sass'); 7 | const istanbul = require('gulp-istanbul'); 8 | 9 | var files = ['index.js', 'gulpfile.js', './lib/*.js', './test/*.spec.js', 10 | './public/js/*.js', './commands/*.js', '!node_modules/**']; 11 | 12 | gulp.task('lint', () => { 13 | return gulp.src(files) 14 | .pipe(eslint()) 15 | .pipe(eslint.format()); 16 | }); 17 | 18 | gulp.task('sass', () => { 19 | return gulp.src('public/scss/style.scss') 20 | .pipe(sass()) 21 | .pipe(gulp.dest('public/css')); 22 | }); 23 | 24 | gulp.task('pre-test', () => { 25 | return gulp.src(['lib/*.js', 'commands/*.js']) 26 | .pipe(istanbul()) 27 | .pipe(istanbul.hookRequire()); 28 | }); 29 | 30 | gulp.task('test', ['pre-test'], () => { 31 | return gulp.src('test/*.spec.js') 32 | .pipe(mocha()) 33 | .pipe(istanbul.writeReports()); 34 | }); 35 | 36 | gulp.task('watch', () => { 37 | gulp.watch(files, ['lint', 'test']); 38 | gulp.watch('./public/scss/**/*.scss', ['sass']); 39 | }); 40 | 41 | gulp.task('default', ['watch', 'sass', 'lint', 'test']); 42 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | include ./partials/_layout 2 | include ./partials/_navigation 3 | include ./partials/_modal 4 | script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.5/socket.io.js') 5 | 6 | .content 7 | h1.page-header Is this the droid you're looking for? 8 | .row.bottom-xs 9 | .col-sm-6.col-md-4.col-md-offset-2.connect-panel 10 | img(src='static/img/bb8.svg') 11 | input.btn.shading(id='connect-btn-bb8', type='button', value='BB-8/Ollie') 12 | .col-sm-6.col-md-4.connect-panel 13 | img(src='static/img/sprk.svg') 14 | input.btn.shading(id='connect-btn-sprk', type='button', value='Sphero/SPRK') 15 | 16 | .row 17 | .col-sm-6.col-md-4.col-md-offset-2 18 | ul.connect-req 19 | li Supported models: BB-8 by Sphero or Ollie 20 | li System requirement: OS X with Bluetooth Low Energy (BLE 4.0) 21 | li Make sure your Bluetooth is on and BB-8 is nearby 22 | 23 | .col-sm-6.col-md-4 24 | ul.connect-req 25 | li Supported models: Sphero 1.0/2.0 or SPRK 26 | li System requirement: OS X with Bluetooth Classic (Bluetooth 2.0/3.0) support 27 | li Make sure Sphero/SPRK is paired via Bluetooth 28 | 29 | script(type='text/javascript', src='static/js/setup-socket.js') 30 | -------------------------------------------------------------------------------- /public/scss/02_components/_navigation.scss: -------------------------------------------------------------------------------- 1 | #primary-nav { 2 | font-family: $header-font-family; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | background-color: $base-color-blue; 7 | min-width: 180px; 8 | height: 100%; 9 | .nav-tabs { 10 | list-style-type: none; 11 | padding-left: 0px; 12 | margin: 0px; 13 | > li { 14 | display: block; 15 | position: relative; 16 | } 17 | a { 18 | display: block; 19 | position: relative; 20 | padding: 8px 15px; 21 | font-weight: 600; 22 | text-transform: uppercase; 23 | text-decoration: none; 24 | letter-spacing: 0.5px; 25 | color: white; 26 | &:hover { 27 | background-color: $sec-color-liteblue; 28 | color: $base-color-blue; 29 | } 30 | &:focus, &:active, &.active { 31 | background-color: $base-color-white; 32 | color: $base-color-blue; 33 | } 34 | } 35 | .nav-brand { 36 | background-color: $base-color-yellow; 37 | padding: 10px; 38 | text-align: center; 39 | img { 40 | height: 90px; 41 | } 42 | } 43 | .nav-icon { 44 | height: 36px; 45 | } 46 | .row div:first-child { 47 | text-align: right; 48 | padding-right: 4px; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/img/icons/about.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 13 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /public/img/icons/about_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 13 | 17 | 19 | 20 | -------------------------------------------------------------------------------- /public/scss/01_base/_base.scss: -------------------------------------------------------------------------------- 1 | // BASE STYLES FOR SCAFFOLDING 2 | 3 | // Font Weights 4 | $light: 100; 5 | $regular: 400; 6 | $bold: 600; 7 | 8 | // Base Font(s) 9 | $base-font-family: sans-serif; 10 | $header-font-family: Lato, Arial; 11 | $base-font-size: 12px; 12 | 13 | // Color Palette 14 | $base-color-white: #F6F9FE; 15 | $base-color-liteblue: #EBF2FC; 16 | $sec-color-liteblue: #C3DFEB; 17 | $tert-color-liteblue: #A9D0DF; 18 | $base-color-turq: #36B4C2; 19 | $base-color-blue: #234358; 20 | $base-color-yellow: #FFC13E; 21 | $sec-color-yellow: #F9A11B; 22 | $base-color-slate: #BCBDC1; 23 | $sec-color-slate: #7F8082; 24 | $tert-color-slate: #5D6160; 25 | $base-gray: #1C1D21; 26 | 27 | body { 28 | background-color: $base-color-white; 29 | } 30 | .content { 31 | margin-left: 180px; 32 | padding-left: 15px; 33 | } 34 | h1, h2, h3 { 35 | font-family: Lato; 36 | color: $base-color-blue; 37 | } 38 | h4 { 39 | font-family: Lato; 40 | color: $tert-color-slate; 41 | font-weight: normal; 42 | margin-top: 0.5rem; 43 | } 44 | a { 45 | text-decoration: none; 46 | } 47 | .page-header { 48 | font-family: Lato; 49 | color: $base-color-blue; 50 | text-transform: uppercase; 51 | text-align: center; 52 | letter-spacing: 1px; 53 | margin-top: 48px; 54 | } 55 | .row { 56 | margin-right: 0px; 57 | margin-left: 0px; 58 | } 59 | -------------------------------------------------------------------------------- /public/js/handle-nav.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function setBlueIcon($el) { 4 | var $img = $el.find('img'); 5 | var path = $img.attr('src'); 6 | $img.attr('src', path.substring(0, path.length - 4) + '_blue.svg'); 7 | } 8 | 9 | function setWhiteIcon($el) { 10 | var $img = $el.find('img'); 11 | var path = $img.attr('src'); 12 | $img.attr('src', path.substring(0, path.length - 9) + '.svg'); 13 | } 14 | 15 | (function(pathname) { 16 | pathname = pathname || $(location).attr('pathname').toLowerCase(); 17 | if (pathname === '/') pathname = '/connect'; 18 | $('#primary-nav li a').removeClass('active'); 19 | var $el = $('#primary-nav li a[data-section=' + pathname.slice(1) + ']'); 20 | $el.addClass('active'); 21 | setBlueIcon($el); 22 | })(); 23 | 24 | $('#primary-nav li:not(:first-child) a').hover(function() { 25 | if (!$(this).hasClass('active')) setBlueIcon($(this)); 26 | }, function() { 27 | if (!$(this).hasClass('active')) setWhiteIcon($(this)); 28 | }); 29 | 30 | $('#primary-nav li:not(:first-child) a').click(function() { 31 | var $el = $('#primary-nav').find('a.active').removeClass('active'); 32 | setWhiteIcon($el); 33 | 34 | $(this).addClass('active'); 35 | var $img = $(this).find('img'); 36 | var path = $img.attr('src'); 37 | if (!path.endsWith('_blue.svg')) { 38 | $img.attr('src', path.substring(0, path.length - 4) + '_blue.svg'); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /public/scss/02_components/_move.scss: -------------------------------------------------------------------------------- 1 | @mixin rotate($deg: 0deg) { 2 | -ms-transform: rotate($deg); 3 | -webkit-transform: rotate($deg); 4 | transform: rotate($deg); 5 | } 6 | #accel_graph { 7 | width: 400px; 8 | height: 300px; 9 | } 10 | #speed_graph { 11 | width: 600px; 12 | height: 300px; 13 | } 14 | .ctrl-wrapper { 15 | padding: 10px; 16 | border: 1px solid $sec-color-slate; 17 | min-height: 100px; 18 | text-align: center; 19 | margin: 1.65em; 20 | } 21 | .ctrl-btn { 22 | height: 50px; 23 | width: 50px; 24 | margin-right: 4px; 25 | margin-bottom: 4px; 26 | @include btn3D($tert-color-liteblue); 27 | font-weight: bold; 28 | &:hover, &:active { 29 | background-color: $base-color-turq; 30 | border-bottom-color: darken($base-color-turq, 10%); 31 | -webkit-box-shadow: inset 0 -2px darken($base-color-turq, 10%); 32 | box-shadow: inset 0 -2px darken($base-color-turq, 10%); 33 | } 34 | &.active-btn { 35 | background-color: $base-color-turq; 36 | position: relative; 37 | top: 1px; 38 | outline: 0; 39 | -webkit-box-shadow: none; 40 | box-shadow: none; 41 | } 42 | &#up-btn img { 43 | @include rotate(180deg); 44 | } 45 | &#left-btn img { 46 | @include rotate(90deg); 47 | } 48 | &#right-btn img { 49 | @include rotate(270deg); 50 | } 51 | } 52 | .speed-indicator { 53 | width: 50px; 54 | color: $tert-color-slate; 55 | } 56 | -------------------------------------------------------------------------------- /views/partials/_navigation.jade: -------------------------------------------------------------------------------- 1 | nav#primary-nav 2 | ul.nav-tabs 3 | li.nav-brand 4 | img(src='static/img/brand-logo.svg', alt='Saphero') 5 | li 6 | a(href='/', data-section='connect') 7 | .row.middle-xs 8 | .col-xs-4 9 | img.nav-icon(src='static/img/icons/bluetooth.svg', alt='Connect') 10 | .col-xs-8 Connect 11 | li 12 | a(href='/move', data-section='move') 13 | .row.middle-xs 14 | .col-xs-4 15 | img.nav-icon(src='static/img/icons/controller.svg', alt='Motion') 16 | .col-xs-8 Motion 17 | li 18 | a(href='/color', data-section='color') 19 | .row.middle-xs 20 | .col-xs-4 21 | img.nav-icon(src='static/img/icons/dropper.svg', alt='Colors') 22 | .col-xs-8 Color 23 | li 24 | a(href='/presets', data-section='presets') 25 | .row.middle-xs 26 | .col-xs-4 27 | img.nav-icon(src='static/img/icons/book.svg', alt='Presets') 28 | .col-xs-8 Presets 29 | li 30 | a(href='/about', data-section='about') 31 | .row.middle-xs 32 | .col-xs-4 33 | img.nav-icon(src='static/img/icons/about.svg', alt='About') 34 | .col-xs-8 About 35 | 36 | script(type='text/javascript', src='https://code.jquery.com/jquery-2.2.0.min.js') 37 | script(type='text/javascript', src='static/js/handle-nav.js') 38 | -------------------------------------------------------------------------------- /views/move.jade: -------------------------------------------------------------------------------- 1 | include ./partials/_layout 2 | include ./partials/_navigation 3 | script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.5/socket.io.js') 4 | 5 | .content 6 | h1.page-header Motion Control 7 | .row.center-sm 8 | .col-xs-12.col-sm.col-md-6.col-lg-5 9 | .box 10 | h4 Accelerometer 11 | #accel_graph(style='width:400px;height:300px') 12 | .col-xs-12.col-sm.col-md-6.col-lg-7 13 | .box 14 | h4 Speedometer 15 | #speed_graph(style='width:600px;height:300px') 16 | .ctrl-wrapper.row 17 | .col-xs-12.col-sm-6 18 | h4 Direction 19 | .row.center-xs 20 | button#up-btn.ctrl-btn 21 | img(src='static/img/arrow-down.svg') 22 | .row.center-xs 23 | button#left-btn.ctrl-btn 24 | img(src='static/img/arrow-down.svg') 25 | button#down-btn.ctrl-btn 26 | img(src='static/img/arrow-down.svg') 27 | button#right-btn.ctrl-btn 28 | img(src='static/img/arrow-down.svg') 29 | .col-xs-12.col-sm-6 30 | h4 Speed 31 | .row.center-xs 32 | button#slow-btn.ctrl-btn O 33 | button#fast-btn.ctrl-btn P 34 | .row.center-xs 35 | .speed-indicator - 36 | .speed-indicator + 37 | 38 | script(src="static/vendor/jquery.min.js", charset="utf-8") 39 | script(type='text/javascript', src='static/js/motion-socket.js') 40 | script(type='text/javascript', src='static/js/gamepad.js') 41 | script(src="static/vendor/jquery.flot.min.js", charset="utf-8") 42 | -------------------------------------------------------------------------------- /public/js/choose-color.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global io hsvPicker */ 3 | 4 | var socket = io.connect('http://localhost:3000'); 5 | 6 | var defaultColor = '#4169e1'; 7 | var presetColors = [ 8 | { colorName: 'Red', colorCode: '#ff0000' }, 9 | { colorName: 'Orange', colorCode: '#ffa500' }, 10 | { colorName: 'Yellow', colorCode: '#ffff00' }, 11 | { colorName: 'Green', colorCode: '#008000' }, 12 | { colorName: 'Blue', colorCode: '#0000ff' }, 13 | { colorName: 'Purple', colorCode: '#800080' }, 14 | { colorName: 'Pink', colorCode: '#ff69b4' }, 15 | { colorName: 'Plum', colorCode: '#dda0dd' }, 16 | { colorName: 'White', colorCode: '#ffffff' }, 17 | { colorName: 'Marine', colorCode: '#7fffd4' }, 18 | { colorName: 'Bisque', colorCode: '#ffe4c4' }, 19 | { colorName: 'Violet', colorCode: '#8a2be2' }, 20 | { colorName: 'Magenta', colorCode: '#ff00ff' }, 21 | { colorName: 'Chartreuse', colorCode: '#9eff14' }, 22 | { colorName: 'Chocolate', colorCode: '#d2691e' }, 23 | { colorName: 'Crimson', colorCode: '#dc143c' }, 24 | { colorName: 'Cyan', colorCode: '#00ffff' }, 25 | { colorName: 'Royal', colorCode: '#4169e1' }, 26 | { colorName: 'Mint', colorCode: '#ccffca' }, 27 | { colorName: 'Lime', colorCode: '#00ff00' }, 28 | { colorName: 'Gold', colorCode: '#ffd700' }, 29 | { colorName: 'Sunset', colorCode: '#ff4500' }, 30 | { colorName: 'Teal', colorCode: '#008080' }, 31 | { colorName: 'Olive', colorCode: '#808000' } 32 | ]; 33 | 34 | hsvPicker(defaultColor, presetColors, (colorObj) => { 35 | console.log('color: ' + colorObj.HEX); 36 | socket.emit('color', colorObj.HEX); 37 | }); 38 | -------------------------------------------------------------------------------- /public/img/icons/bluetooth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /public/img/icons/bluetooth_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /test/router.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | chai.use(chaiHttp); 6 | const expect = chai.expect; 7 | const createAppServer = require(__dirname + '/../lib/server'); 8 | 9 | describe('UNIT: test the router endpoints', () => { 10 | before(() => this.server = createAppServer(4000)); 11 | it('should make a GET request at / (landing page)', (done) => { 12 | chai.request('localhost:4000') 13 | .get('/') 14 | .end((err, res) => { 15 | expect(err).to.eql(null); 16 | expect(res).to.have.status(200); 17 | done(); 18 | }); 19 | }); 20 | it('should make a GET request at /about', (done) => { 21 | chai.request('localhost:4000') 22 | .get('/about') 23 | .end((err, res) => { 24 | expect(err).to.eql(null); 25 | expect(res).to.have.status(200); 26 | done(); 27 | }); 28 | }); 29 | it('should make a GET request at /color', (done) => { 30 | chai.request('localhost:4000') 31 | .get('/color') 32 | .end((err, res) => { 33 | expect(err).to.eql(null); 34 | expect(res).to.have.status(200); 35 | done(); 36 | }); 37 | }); 38 | it('should make a GET request at /move', (done) => { 39 | chai.request('localhost:4000') 40 | .get('/move') 41 | .end((err, res) => { 42 | expect(err).to.eql(null); 43 | expect(res).to.have.status(200); 44 | done(); 45 | }); 46 | }); 47 | it('should make a GET request at /presets', (done) => { 48 | chai.request('localhost:4000') 49 | .get('/presets') 50 | .end((err, res) => { 51 | expect(err).to.eql(null); 52 | expect(res).to.have.status(200); 53 | done(); 54 | }); 55 | }); 56 | after((done) => { 57 | this.server.close(done); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /public/js/motion-socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global io */ 3 | 4 | var socket = io.connect('http://localhost:3000'); 5 | var resetHeading = true; 6 | 7 | window.onkeydown = (e) => { 8 | e.preventDefault(); 9 | if (resetHeading) { 10 | switch (e.keyCode) { 11 | case 37: 12 | socket.emit('roll', { direction: 'left', resetHeading }); 13 | highlightBtn('#left-btn'); 14 | break; 15 | case 38: 16 | socket.emit('roll', { direction: 'up', resetHeading }); 17 | highlightBtn('#up-btn'); 18 | break; 19 | case 39: 20 | socket.emit('roll', { direction: 'right', resetHeading }); 21 | highlightBtn('#right-btn'); 22 | break; 23 | case 40: 24 | socket.emit('roll', { direction: 'down', resetHeading }); 25 | highlightBtn('#down-btn'); 26 | break; 27 | case 79: 28 | socket.emit('speed', 'down'); 29 | highlightBtn('#slow-btn'); 30 | break; 31 | case 80: 32 | socket.emit('speed', 'up'); 33 | highlightBtn('#fast-btn'); 34 | break; 35 | default: 36 | } 37 | resetHeading = false; 38 | } 39 | }; 40 | window.onkeyup = () => { 41 | socket.emit('roll', { direction: 'stop' }); 42 | resetHeading = true; 43 | }; 44 | 45 | function highlightBtn(btnId) { 46 | $(btnId).addClass('active-btn'); 47 | setTimeout(() => { 48 | $(btnId).removeClass('active-btn'); 49 | }, 150); 50 | } 51 | 52 | socket.on('speedometer', (data) => { 53 | $.plot($('#speed_graph'), data, { 54 | yaxis: { 55 | min: 0 56 | }, 57 | xaxis: { 58 | show: false 59 | } 60 | }); 61 | }); 62 | 63 | socket.on('accelerometer', (data) => { 64 | $.plot($('#accel_graph'), data, { 65 | yaxis: { 66 | min: -2500, 67 | max: 2500 68 | }, 69 | xaxis: { 70 | min: -2500, 71 | max: 2500 72 | }, 73 | points: { 74 | show: true 75 | } 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /public/img/icons/book.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 24 | 25 | -------------------------------------------------------------------------------- /public/img/icons/book_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### License 2 | The MIT License (MIT). 3 | 4 | Copyright (c) 2016 [Natalie Chow](https://github.com/xxnatc), [Sabrina Tee](https://github.com/sabbyt/), [Logan Tegman](https://github.com/ltegman), [Jose Tello](https://github.com/josectello) and [Jesse Thach](https://github.com/jessethach). 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | #### Other Projects/External Libraries Used: 13 | Saphero uses code from other projects/external libraries which have their own licenses: 14 | - [express](https://www.npmjs.com/package/express) 15 | - [flot](https://www.npmjs.com/package/flot) 16 | - [home-config](https://www.npmjs.com/package/home-config) 17 | - [jade](https://www.npmjs.com/package/jade) 18 | - [lodash](https://www.npmjs.com/package/lodash) 19 | - [noble](https://www.npmjs.com/package/noble) 20 | - [opn](https://www.npmjs.com/package/opn) 21 | - [serialport](https://www.npmjs.com/package/serialport) 22 | - [socket.io](https://www.npmjs.com/package/socket.io) 23 | - [sphero](https://www.npmjs.com/package/sphero) 24 | - [sweetalert](https://www.npmjs.com/package/sweetalert) 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saphero", 3 | "version": "1.0.3", 4 | "description": "A Client-Side Dashboard to Easily Connect and Control Sphero/BB-8/Ollie Devices", 5 | "main": "bin/index.js", 6 | "scripts": { 7 | "start": "node bin/index.js", 8 | "test": "./node_modules/mocha/bin/mocha test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/saphero/sphero-hack.git" 13 | }, 14 | "keywords": [ 15 | "robotics", 16 | "sphero", 17 | "bb8", 18 | "bb-8", 19 | "orbotix", 20 | "hack", 21 | "robot", 22 | "robots", 23 | "spherojs", 24 | "sphero-js", 25 | "ollie", 26 | "sprk" 27 | ], 28 | "author": { "name": "Sabrina Tee", "email": "sabrinatee@me.com" }, 29 | "contributors": [ 30 | { "name": "Natalie Chow", "email": "cychownat@gmail.com" }, 31 | { "name": "Logan Tegman", "email": "ltegman@gmail.com", "url": "http://logantegman.io" }, 32 | { "name": "Jose Tello", "email": "josectello@gmail.com" }, 33 | { "name": "Jesse Thach", "email": "jessethach@gmail.com", "url": "https://about.me/jessethach"} 34 | ], 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/saphero/sphero-hack/issues" 38 | }, 39 | "homepage": "https://github.com/saphero/sphero-hack#readme", 40 | "dependencies": { 41 | "express": "^4.13.4", 42 | "home-config": "^0.1.0", 43 | "jade": "^1.11.0", 44 | "lodash": "^4.1.0", 45 | "noble": "^1.3.0", 46 | "opn": "^4.0.0", 47 | "serialport": "^2.0.6", 48 | "socket.io": "^1.4.5", 49 | "sphero": "^0.8.0" 50 | }, 51 | "devDependencies": { 52 | "chai": "^3.5.0", 53 | "chai-http": "^2.0.1", 54 | "gulp": "^3.9.0", 55 | "gulp-eslint": "^1.1.1", 56 | "gulp-istanbul": "^0.10.3", 57 | "gulp-mocha": "^2.2.0", 58 | "gulp-sass": "^2.1.1", 59 | "mocha": "^2.4.5", 60 | "socket.io-client": "^1.4.5" 61 | }, 62 | "bin": { 63 | "saphero": "bin/index.js" 64 | }, 65 | "engines": { 66 | "node": ">=4.2.0" 67 | }, 68 | "preferGlobal": true 69 | } 70 | -------------------------------------------------------------------------------- /test/device-config.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const deviceConfig = require(__dirname + '/../lib/device-config'); 5 | const homeConfig = require('home-config'); 6 | 7 | describe('device-config', () => { 8 | describe('sphero', () => { 9 | before(() => { 10 | this.oldConfig = homeConfig.load('.spheroconfig'); 11 | this.newConfig = homeConfig.load('.spheroconfig'); 12 | this.newConfig.SPHERO_ID = 'tty.Sphero-GWW-AMP-SPP'; 13 | this.newConfig.save(); 14 | }); 15 | 16 | it('should create a sphero object with given ID', () => { 17 | const testSphero = deviceConfig.spheroCreate(); 18 | expect(testSphero.constructor.name).to.eql('Sphero'); 19 | expect(testSphero.connection.conn).to.eql('/dev/tty.Sphero-GWW-AMP-SPP'); 20 | }); 21 | 22 | after(() => { 23 | this.oldConfig.save(); 24 | }); 25 | }); 26 | describe('bb8 success', () => { 27 | before(() => { 28 | this.oldConfig = homeConfig.load('.bb8config'); 29 | this.newConfig = homeConfig.load('.bb8config'); 30 | this.newConfig.BB8_UUID = '944f561f8cf441f3b5405ed48f5c63cf'; 31 | this.newConfig.save(); 32 | }); 33 | 34 | it('should create a sphero object with given ID', () => { 35 | const testBB8 = deviceConfig.bb8Create(); 36 | expect(testBB8.constructor.name).to.eql('Sphero'); 37 | expect(testBB8.connection.uuid) 38 | .to.eql('944f561f8cf441f3b5405ed48f5c63cf'); 39 | }); 40 | 41 | after(() => { 42 | this.oldConfig.save(); 43 | }); 44 | }); 45 | describe('bb8 fail', () => { 46 | before(() => { 47 | this.oldConfig = homeConfig.load('.bb8config'); 48 | this.newConfig = homeConfig.load('.bb8config'); 49 | if (typeof this.newConfig.BB8_UUID !== 'undefined') { 50 | delete this.newConfig.BB8_UUID; 51 | } 52 | this.newConfig.save(); 53 | }); 54 | 55 | it('should create a sphero object with given ID', () => { 56 | const testBB8 = deviceConfig.bb8Create(); 57 | expect(testBB8).to.eql(false); 58 | }); 59 | 60 | after(() => { 61 | this.oldConfig.save(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /views/color.jade: -------------------------------------------------------------------------------- 1 | include ./partials/_layout 2 | include ./partials/_navigation 3 | script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.5/socket.io.js') 4 | 5 | .content 6 | h1.page-header Color Picker 7 | .row 8 | .col-md-6.col-lg-8 9 | h4 Preset colors 10 | #color_squares.row 11 | .col-xs-3.col-sm-2.col-md-3.palette 12 | .col-xs-3.col-sm-2.col-md-3.palette 13 | .col-xs-3.col-sm-2.col-md-3.palette 14 | .col-xs-3.col-sm-2.col-md-3.palette 15 | .col-xs-3.col-sm-2.col-md-3.palette 16 | .col-xs-3.col-sm-2.col-md-3.palette 17 | .col-xs-3.col-sm-2.col-md-3.palette 18 | .col-xs-3.col-sm-2.col-md-3.palette 19 | .col-xs-3.col-sm-2.col-md-3.palette 20 | .col-xs-3.col-sm-2.col-md-3.palette 21 | .col-xs-3.col-sm-2.col-md-3.palette 22 | .col-xs-3.col-sm-2.col-md-3.palette 23 | .col-xs-3.col-sm-2.col-md-3.palette 24 | .col-xs-3.col-sm-2.col-md-3.palette 25 | .col-xs-3.col-sm-2.col-md-3.palette 26 | .col-xs-3.col-sm-2.col-md-3.palette 27 | .col-xs-3.col-sm-2.col-md-3.palette 28 | .col-xs-3.col-sm-2.col-md-3.palette 29 | .col-xs-3.col-sm-2.col-md-3.palette 30 | .col-xs-3.col-sm-2.col-md-3.palette 31 | .col-xs-3.col-sm-2.col-md-3.palette 32 | .col-xs-3.col-sm-2.col-md-3.palette 33 | .col-xs-3.col-sm-2.col-md-3.palette 34 | .col-xs-3.col-sm-2.col-md-3.palette 35 | 36 | .col-md-6.col-lg-4 37 | h4 Custom color 38 | #hsv_map 39 | canvas#surface(width='300', height='300') 40 | .cover 41 | .hsv-cursor 42 | .bar-bg 43 | .bar-white 44 | canvas#luminanceBar(width='25', height='300') 45 | #hsv_cursors.hsv-barcursors 46 | .hsv-barcursor-l 47 | .hsv-barcursor-r 48 | #testPatch.col-xs-12 . 49 | 50 | 51 | script(type='text/javascript', src='static/vendor/js/colors.js') 52 | script(type='text/javascript', src='static/vendor/js/colorPicker.js') 53 | script(type='text/javascript', src='static/vendor/js/hsvPicker.js') 54 | 55 | script(type='text/javascript', src='static/js/choose-color.js') 56 | -------------------------------------------------------------------------------- /views/presets.jade: -------------------------------------------------------------------------------- 1 | include ./partials/_layout 2 | include ./partials/_navigation 3 | script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.5/socket.io.js') 4 | 5 | .content 6 | h1.page-header Presets Library 7 | .row.card-container 8 | .col-xs-6.col-md-4.col-lg-3 9 | .card 10 | h3.card-header Rainbow 11 | p Flashes light with rainbow colors 12 | button.btn.shading(data-preset='rainbow') Run 13 | .col-xs-6.col-md-4.col-lg-3 14 | .card 15 | h3.card-header X'mas 16 | p Flashes red and green light 17 | button.btn.shading(data-preset='xmas') Run 18 | .col-xs-6.col-md-4.col-lg-3 19 | .card 20 | h3.card-header Disco 21 | p Flashes random lights 22 | button.btn.shading(data-preset='disco') Run 23 | .col-xs-6.col-md-4.col-lg-3 24 | .card 25 | h3.card-header Dance Party 26 | p Twerks 27 | button.btn.shading(data-preset='dance') Run 28 | .col-xs-6.col-md-4.col-lg-3 29 | .card 30 | h3.card-header Shifty 31 | p Stays in the same position and turns its head 32 | button.btn.shading(data-preset='look') Run 33 | .col-xs-6.col-md-4.col-lg-3 34 | .card 35 | h3.card-header Indecisive 36 | p Travels in a random direction every second 37 | button.btn.shading(data-preset='move-random') Run 38 | .col-xs-6.col-md-4.col-lg-3 39 | .card 40 | h3.card-header Fly 41 | p Flies in purple and on landing, changes to red 42 | button.btn.shading(data-preset='fly') Run 43 | .col-xs-6.col-md-4.col-lg-3 44 | .card 45 | h3.card-header Magic 8 Ball 46 | p Shake and ask a question - red: no, yellow: maybe and green: yes! 47 | button.btn.shading(data-preset='magic8') Run 48 | .col-xs-6.col-md-4.col-lg-3 49 | .card 50 | h3.card-header Detect Collision 51 | p Will run into walls and get hurt 52 | button.btn.shading(data-preset='collision') Run 53 | .col-xs-6.col-md-4.col-lg-3 54 | .card 55 | h3.card-header Circle 56 | p Rolls around in a full circle 57 | button.btn.shading(data-preset='circle') Run 58 | .row 59 | button#stop-btn Clear 60 | 61 | script(type='text/javascript', src='static/js/presets.js') 62 | -------------------------------------------------------------------------------- /public/js/setup-socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global io swal */ 3 | /* eslint-disable prefer-arrow-callback */ 4 | 5 | var socket = io.connect('http://localhost:3000'); 6 | var idleTimeout; 7 | 8 | function confirmConnect() { 9 | if (idleTimeout) clearTimeout(idleTimeout); 10 | swal({ 11 | title: 'Ready to go!', 12 | text: 'Your Sphero is now connected.', 13 | type: 'success', 14 | confirmButtonText: 'Let\'s explore!', 15 | confirmButtonColor: '#36B4C2', 16 | customClass: 'setup-modal' 17 | }, () => { 18 | $(location).attr('pathname', '/move'); 19 | }); 20 | } 21 | 22 | socket.on('connected-sphero', confirmConnect); 23 | socket.on('connected-bb8', confirmConnect); 24 | 25 | $('#connect-btn-sprk').on('click', () => { 26 | console.log('connecting to sprk'); 27 | socket.emit('connect-btn-sprk'); 28 | swal({ 29 | title: 'Connecting to Sphero...', 30 | imageUrl: 'static/img/ripple.gif', 31 | showCancelButton: true, 32 | showConfirmButton: false, 33 | customClass: 'setup-modal' 34 | }, function(isConfirm) { 35 | if (!isConfirm) clearTimeout(idleTimeout); 36 | }); 37 | idleError($('#connect-btn-sprk')); 38 | }); 39 | 40 | $('#connect-btn-bb8').on('click', () => { 41 | console.log('connecting to bb8'); 42 | socket.emit('connect-btn-bb8'); 43 | setTimeout(() => { 44 | socket.emit('connect-btn-bb8'); 45 | }, 2000); 46 | swal({ 47 | title: 'Connecting to BB-8/Ollie...', 48 | imageUrl: 'static/img/ripple.gif', 49 | showCancelButton: true, 50 | showConfirmButton: false, 51 | customClass: 'setup-modal' 52 | }, function(isConfirm) { 53 | if (!isConfirm) clearTimeout(idleTimeout); 54 | }); 55 | idleError($('#connect-btn-bb8')); 56 | }); 57 | 58 | function idleError($btn) { 59 | idleTimeout = setTimeout(() => { 60 | swal({ 61 | title: 'Connection failed', 62 | text: 'There is a problem connnecting to your Sphero. ' 63 | + 'Make sure your Bluetooth is on and your Sphero is awake.' 64 | + 'Try restarting the server if you keep getting this error.', 65 | type: 'error', 66 | confirmButtonText: 'Try again!', 67 | confirmButtonColor: '#36B4C2', 68 | showCancelButton: true, 69 | customClass: 'setup-modal', 70 | closeOnConfirm: false 71 | }, () => { 72 | $btn.trigger('click'); 73 | }); 74 | }, 10000); 75 | } 76 | -------------------------------------------------------------------------------- /public/scss/02_components/_color.scss: -------------------------------------------------------------------------------- 1 | #color_squares { 2 | padding-right: 24px; 3 | } 4 | 5 | .palette { 6 | font-family: Lato, sans-serif; 7 | text-transform: uppercase; 8 | text-align: center; 9 | font-size: 14px; 10 | color: white; 11 | padding: 18px 3px; 12 | border: 3px solid $base-color-white; 13 | border-radius: 1px; 14 | cursor: pointer; 15 | &.dark-text { 16 | color: $tert-color-slate; 17 | } 18 | } 19 | 20 | #hsv_map { 21 | height: 300px; 22 | position: relative; 23 | .cover { 24 | opacity: 0; 25 | background-color: #000; 26 | position: absolute; 27 | top: 0px; 28 | bottom: -1px; 29 | left: 0px; 30 | border-radius: 50%; 31 | cursor: crosshair; 32 | width: 300px; 33 | } 34 | .bar-bg, .bar-white { 35 | position: absolute; 36 | top: 0; 37 | left: 320px; 38 | width: 25px; 39 | height: 300px; 40 | } 41 | .bar-white { 42 | background-color: #fff; 43 | } 44 | .hsv-cursor { 45 | position: absolute; 46 | border: 1px solid #eee; 47 | border-radius: 50%; 48 | width: 9px; 49 | height: 9px; 50 | cursor: default; 51 | margin: -5px; 52 | cursor: crosshair; 53 | .dark { 54 | border-color: #333; 55 | } 56 | .no-cursor { 57 | cursor: none; 58 | } 59 | } 60 | #luminanceBar { 61 | position: absolute; 62 | top: 0; 63 | left: 320px; 64 | } 65 | #hsv_cursors { 66 | position: absolute; 67 | top: 0; 68 | left: 320px; 69 | width: 25px; 70 | height: 300px; 71 | overflow: hidden; 72 | } 73 | .hsv-barcursor-l, .hsv-barcursor-r { 74 | position: absolute; 75 | width: 0; 76 | height: 0; 77 | border: 4px solid transparent; 78 | margin-top: -4px; 79 | } 80 | .hsv-barcursor-l { 81 | left: 0; 82 | border-left: 4px solid #eee; 83 | } 84 | .dark .hsv-barcursor-l { 85 | border-left-color: #333; 86 | } 87 | .hsv-barcursor-r { 88 | right: 0; 89 | border-right: 4px solid #eee; 90 | } 91 | .dark .hsv-barcursor-r { 92 | border-right-color: #333; 93 | } 94 | } 95 | 96 | #testPatch { 97 | font-family: Lato, sans-serif; 98 | text-transform: uppercase; 99 | text-align: center; 100 | font-size: 14px; 101 | color: white; 102 | width: 300px; 103 | cursor: default; 104 | padding: 18px 3px; 105 | margin-top: 12px; 106 | } 107 | -------------------------------------------------------------------------------- /test/commands.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-unused-expressions 4 | */ 5 | 6 | const expect = require('chai').expect; 7 | const lights = require(__dirname + '/../commands/lights'); 8 | const look = require(__dirname + '/../commands/look'); 9 | const moveRandom = require(__dirname + '/../commands/move-random'); 10 | const dance = require(__dirname + '/../commands/dance'); 11 | const fly = require(__dirname + '/../commands/fly'); 12 | const magic8 = require(__dirname + '/../commands/magic8'); 13 | const collision = require(__dirname + '/../commands/collision'); 14 | const accelerometer = require(__dirname + '/../commands/accelerometer'); 15 | const speedometer = require(__dirname + '/../commands/speedometer'); 16 | 17 | describe('sphero commands', () => { 18 | it('lights.rainbow should send color commands', (done) => { 19 | const testInterval = lights.rainbow(this.testOrb); 20 | setTimeout(() => { 21 | expect(this.called).to.eql(1); 22 | clearInterval(testInterval); 23 | done(); 24 | }, 250); 25 | }); 26 | 27 | it('lights.disco should send multiple commands', (done) => { 28 | const testInterval = lights.disco(this.testOrb); 29 | setTimeout(() => { 30 | expect(this.called).to.eql(2); 31 | clearInterval(testInterval); 32 | done(); 33 | }, 550); 34 | }); 35 | 36 | it('look should send roll commands', (done) => { 37 | const testInterval = look(this.testOrb); 38 | setTimeout(() => { 39 | expect(this.called).to.eql(2); 40 | clearInterval(testInterval); 41 | done(); 42 | }, 500); 43 | }); 44 | 45 | it('move-random should send roll commands', (done) => { 46 | const testInterval = moveRandom(this.testOrb); 47 | setTimeout(() => { 48 | expect(this.called).to.eql(2); 49 | clearInterval(testInterval); 50 | done(); 51 | }, 1000); 52 | }); 53 | 54 | it('dance should send color and roll commands', (done) => { 55 | const testInterval = dance(this.testOrb); 56 | setTimeout(() => { 57 | expect(this.called).to.eql(2); 58 | clearInterval(testInterval); 59 | done(); 60 | }, 100); 61 | }); 62 | 63 | it('fly should turn on freefall events', () => { 64 | fly(this.testOrb); 65 | expect(this.called).to.eql(3); 66 | }); 67 | 68 | it('magic 8 should change colors and start listening', () => { 69 | magic8(this.testOrb); 70 | expect(this.called).to.eql(2); 71 | }); 72 | 73 | it('collision should change colors, start listening, and roll', () => { 74 | collision(this.testOrb); 75 | expect(this.called).to.eql(4); 76 | }); 77 | 78 | it('should turn on accelerometer', () => { 79 | accelerometer(this.testOrb); 80 | expect(this.called).to.eql(2); 81 | }); 82 | 83 | it('should turn on speedometer', () => { 84 | speedometer(this.testOrb); 85 | expect(this.called).to.eql(2); 86 | }); 87 | 88 | beforeEach(() => { 89 | this.called = 0; 90 | this.testOrb = { 91 | roll: () => this.called++, 92 | color: (color) => { 93 | expect(color).to.exist; 94 | this.called++; 95 | }, 96 | streamVelocity: () => this.called++, 97 | streamAccelerometer: () => this.called++, 98 | randomColor: () => this.called++, 99 | detectFreefall: () => this.called++, 100 | detectCollisions: () => this.called++, 101 | on: (event, cb) => { 102 | expect(event).to.be.a('string'); 103 | expect(cb).to.be.a('function'); 104 | this.called++; 105 | } 106 | }; 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /public/img/icons/dropper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 14 | 17 | 20 | 23 | 26 | 30 | 39 | 40 | -------------------------------------------------------------------------------- /public/img/icons/dropper_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 14 | 17 | 20 | 23 | 26 | 30 | 39 | 40 | -------------------------------------------------------------------------------- /public/img/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 38 | 39 | -------------------------------------------------------------------------------- /public/img/icons/settings_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 38 | 39 | -------------------------------------------------------------------------------- /public/js/gamepad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global socket highlightBtn */ 3 | 4 | var resetHeading = true; 5 | 6 | window.addEventListener('gamepadconnected', () => { 7 | console.log('gamepad connected'); 8 | }); 9 | 10 | window.addEventListener('gamepaddisconnected', () => { 11 | console.log('gamepad disconnected'); 12 | }); 13 | 14 | window.addEventListener('gamepadbuttondown', (e) => { 15 | switch (e.button) { 16 | case 14: 17 | socket.emit('roll', { direction: 'up', resetHeading }); 18 | highlightBtn('#up-btn'); 19 | break; 20 | case 15: 21 | socket.emit('roll', { direction: 'down', resetHeading }); 22 | highlightBtn('#down-btn'); 23 | break; 24 | case 16: 25 | socket.emit('roll', { direction: 'left', resetHeading }); 26 | highlightBtn('#left-btn'); 27 | break; 28 | case 17: 29 | socket.emit('roll', { direction: 'right', resetHeading }); 30 | highlightBtn('#right-btn'); 31 | break; 32 | case 0: 33 | case 4: 34 | socket.emit('speed', 'down'); 35 | highlightBtn('#slow-btn'); 36 | break; 37 | case 2: 38 | case 5: 39 | socket.emit('speed', 'up'); 40 | highlightBtn('#fast-btn'); 41 | break; 42 | default: 43 | } 44 | }); 45 | 46 | window.addEventListener('gamepadbuttonup', () => { 47 | socket.emit('roll', { direction: 'stop' }); 48 | resetHeading = true; 49 | }); 50 | 51 | var leftX = 0, leftY = 0; 52 | var rightX = 0, rightY = 0; 53 | 54 | window.addEventListener('gamepadaxismove', (e) => { 55 | // Left joystick: x-axis is #0, y-axis is #1 (reversed) 56 | // Left joystick is set to control color 57 | if ([0, 1].indexOf(e.axis) > -1) { 58 | let x = e.gamepad.axes[0].toFixed(3); 59 | let y = e.gamepad.axes[1].toFixed(3) * -1; 60 | if (Math.abs(x - leftX) > 0.45 || Math.abs(y - leftY) > 0.45) { 61 | leftX = x; 62 | leftY = y; 63 | let hue = Math.round(Math.atan2(y, x) * 180 / Math.PI); 64 | if (hue < 0) hue += 360; 65 | var hex = hsvToHex({ hue: hue, sat: 0.9, val: 0.9 }); 66 | socket.emit('color', hex); 67 | } 68 | } 69 | 70 | // Right joystick: x-axis is #2, y-axis is #5 (reversed) 71 | // Right joystick is set to control direction 72 | if ([2, 5].indexOf(e.axis) > -1) { 73 | let x = e.gamepad.axes[2].toFixed(3); 74 | let y = e.gamepad.axes[5].toFixed(3); 75 | if (Math.abs(x - rightX) > 0.55 || Math.abs(y - rightY) > 0.55) { 76 | rightX = x; 77 | rightY = y; 78 | let deg = Math.round(Math.atan2(y, x) * 180 / Math.PI) + 90; 79 | if (deg < 0) deg += 360; 80 | socket.emit('free-roll', { deg: deg }); 81 | } 82 | } 83 | }); 84 | 85 | function hsvToHex(hsv) { 86 | var h = hsv.hue, s = hsv.sat, v = hsv.val; 87 | var rgb, i, data = []; 88 | if (s === 0) { 89 | rgb = [v, v, v]; 90 | } else { 91 | h = h / 60; 92 | i = Math.floor(h); 93 | data = [ 94 | v * (1 - s), 95 | v * (1 - s * (h - i)), 96 | v * (1 - s * (1 - (h - i))) 97 | ]; 98 | switch (i) { 99 | case 0: 100 | rgb = [v, data[2], data[0]]; 101 | break; 102 | case 1: 103 | rgb = [data[1], v, data[0]]; 104 | break; 105 | case 2: 106 | rgb = [data[0], v, data[2]]; 107 | break; 108 | case 3: 109 | rgb = [data[0], data[1], v]; 110 | break; 111 | case 4: 112 | rgb = [data[2], data[0], v]; 113 | break; 114 | default: 115 | rgb = [v, data[0], data[1]]; 116 | break; 117 | } 118 | } 119 | return rgb.map((x) => { 120 | return ('0' + Math.round(x * 255).toString(16)).slice(-2); 121 | }).join(''); 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # saphero 2 | 3 | ### an orbotix-hack 4 | 5 | ### _Client-Side Dashboard to Easily Connect and Control Sphero/BB-8/Ollie Devices_ 6 | 7 | ![Control Page](/public/img/setup-screenshot.png) 8 | 9 | ### Minimum Requirements 10 | - For Sphero 1.0/2.0/SPRK models: Bluetooth Classic (Bluetooth 2.0/3.0) enabled computer 11 | - For BB-8/Ollie models: Bluetooth Low Energy (Bluetooth LE/Bluetooth Smart/Bluetooth 4.0/4.1) enabled computer 12 | - Currently only Mac (OS X) platform compatible 13 | - [Xcode 7](https://itunes.apple.com/ca/app/xcode/id497799835?mt=12) 14 | - A [Sphero 1.0/2.0](http://www.sphero.com/sphero), [SPRK](http://www.sphero.com/education), [BB-8](http://www.sphero.com/starwars) or [Ollie](http://www.sphero.com/ollie) 15 | 16 | ### Installation 17 | Download from npm: 18 | ``` 19 | npm install saphero 20 | ``` 21 | Or clone directly from GitHub: [https://github.com/saphero/sphero-hack](https://github.com/saphero/sphero-hack). 22 | 23 | If you clone from GitHub you will need to run the following commands before launching: 24 | ``` 25 | npm install 26 | ``` 27 | ``` 28 | gulp sass 29 | ``` 30 | 31 | ### Getting Started 32 | - Pair Sphero or SPRK via Bluetooth (found in System Preferences > Bluetooth - but first ensure Sphero/SPRK is not connected to any another device); you do not need to pair BB-8 or Ollie devices 33 | - Launch app from command line by typing ```saphero``` (use ```npm start``` if you cloned from GitHub) 34 | - Open browser at ```localhost:3000``` (it should launch automatically in default browser) and follow the instructions 35 | 36 | ### Features 37 | - Client-side dashboard for easy Sphero/SPRK/BB-8/Ollie connection and control 38 | - Back-end server that listens to your commands and sends them to your device 39 | - Connects and sets up a device at a click of a button! 40 | - Move your device using keypress or a game controller (game controller only compatible on [Firefox Nightly](https://nightly.mozilla.org/) browser) 41 | - Graphs speed and acceleration 42 | - Preset commands and colors 43 | - Color picker 44 | 45 | ### Game Controller 46 | - Install the [Firefox Nightly](https://nightly.mozilla.org/) browser 47 | - Launch app from command line by typing ```saphero``` (use ```npm start``` if cloned from GitHub) 48 | - Follow app instructions to connect device 49 | - Plugin a game controller via USB (currently supports PlayStation controllers only) 50 | - Control your device one the ```/move``` page: 51 | - Change color: left joystick 52 | - Directions: arrow buttons or right joystick 53 | - Increase speed: ◯ or R1 54 | - Decrease speed: ▢ or L1 55 | 56 | ### Routes 57 | Device setup and connection page: 58 | ``` 59 | localhost:3000 60 | ``` 61 | 62 | | Routes | Description | 63 | | :----------- | ------------------------------------ | 64 | | ```/move``` | Game controller and keypress control | 65 | | ```/color``` | Preset colors and color picker | 66 | | ```/preset``` | Preset commands | 67 | | ```/about``` | About the contributors | 68 | 69 | ### Acknowledgements & Modules Used 70 | - [express](https://www.npmjs.com/package/express) 71 | - [flot](https://www.npmjs.com/package/flot) 72 | - [home-config](https://www.npmjs.com/package/home-config) 73 | - [jade](https://www.npmjs.com/package/jade) 74 | - [lodash](https://www.npmjs.com/package/lodash) 75 | - [noble](https://www.npmjs.com/package/noble) 76 | - [opn](https://www.npmjs.com/package/opn) 77 | - [serialport](https://www.npmjs.com/package/serialport) 78 | - [socket.io](https://www.npmjs.com/package/socket.io) 79 | - [sphero](https://www.npmjs.com/package/sphero) 80 | - [sweetalert](https://www.npmjs.com/package/sweetalert) 81 | 82 | ### Issues? Suggestions? Comments? 83 | Submit an issue on [GitHub](https://github.com/saphero/sphero-hack/issues). 84 | 85 | Check out our [Developer's Notes](https://github.com/saphero/sphero-hack/blob/master/DEV_NOTES.md). 86 | 87 | ### Legal Notices 88 | This work is not endorsed by Orbotix. 89 | 90 | Trademarks are the property of their respective owners. 91 | 92 | ### License 93 | MIT Licensed. For more details, see the [LICENSE](https://github.com/saphero/sphero-hack/blob/master/LICENSE.md) file. 94 | -------------------------------------------------------------------------------- /public/img/icons/controller.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 13 | 19 | 21 | 41 | 43 | 45 | 46 | -------------------------------------------------------------------------------- /public/img/icons/controller_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 13 | 19 | 21 | 41 | 43 | 45 | 46 | -------------------------------------------------------------------------------- /public/img/sprk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 18 | 23 | 24 | 27 | 32 | 33 | 35 | 39 | 41 | 43 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/socket-events.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-unused-expressions 4 | */ 5 | 6 | const expect = require('chai').expect; 7 | const socketListeners = require(__dirname + '/../lib/socket-listeners'); 8 | require(__dirname + '/../lib/server'); 9 | 10 | describe('socket listener tests', () => { 11 | 12 | before(() => { 13 | const express = require('express'); 14 | const app = express(); 15 | this.server = require('http').Server(app); 16 | this.io = require('socket.io')(this.server); 17 | 18 | this.server.listen(5000, () => { 19 | console.log('server running on port 5000'); 20 | }); 21 | 22 | this.testOrb = { 23 | roll: (speed) => { 24 | expect(speed).to.exist; 25 | this.called++; 26 | }, 27 | randomColor: () => { 28 | this.called++; 29 | }, 30 | setHeading: (degrees, cb) => { 31 | expect(degrees).to.exist; 32 | this.called++; 33 | cb(); 34 | }, 35 | color: () => { }, 36 | streamVelocity: () => { }, 37 | streamAccelerometer: () => { }, 38 | on: () => { } 39 | }; 40 | socketListeners(this.io, this.testOrb); 41 | }); 42 | 43 | beforeEach((done) => { 44 | // Creates all socket.io event listeners 45 | this.called = 0; 46 | this.socket = require('socket.io-client')('http://localhost:5000'); 47 | this.socket.on('connect', () => done()); 48 | }); 49 | 50 | it('rollDirection should work', (done) => { 51 | socketListeners.rollDirection(this.testOrb, false, 0, { emit() {} }, () => { 52 | expect(this.called).to.eql(2); 53 | expect(this.testOrb).to.exist; 54 | done(); 55 | }); 56 | }); 57 | 58 | it('roll left events should work', (done) => { 59 | this.socket.emit('roll', { direction: 'left', resetHeading: true }); 60 | this.socket.on('rolled', () => { 61 | expect(this.called).to.eql(3); 62 | done(); 63 | }); 64 | }); 65 | 66 | it('roll right events should work', (done) => { 67 | this.socket.emit('roll', { direction: 'right', resetHeading: true }); 68 | this.socket.on('rolled', () => { 69 | expect(this.called).to.eql(3); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('roll down events should work', (done) => { 75 | this.socket.emit('roll', { direction: 'down', resetHeading: true }); 76 | this.socket.on('rolled', () => { 77 | expect(this.called).to.eql(3); 78 | done(); 79 | }); 80 | }); 81 | 82 | it('roll up events should work', (done) => { 83 | this.socket.emit('roll', { direction: 'up' }); 84 | this.socket.on('rolled', () => { 85 | expect(this.called).to.eql(2); 86 | done(); 87 | }); 88 | }); 89 | 90 | it('speed increase should work', (done) => { 91 | this.socket.emit('speed', 'up'); 92 | this.socket.on('speed-change', () => { 93 | expect(this.called).to.eql(1); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('speed decrease should work', (done) => { 99 | this.socket.emit('speed', 'down'); 100 | this.socket.on('speed-change', () => { 101 | expect(this.called).to.eql(1); 102 | done(); 103 | }); 104 | }); 105 | 106 | it('xmas preset should send a reply', (done) => { 107 | const currPreset = 'xmas'; 108 | this.socket.emit('preset', { name: currPreset, test: true }); 109 | this.socket.on('preset-executed', (command) => { 110 | if (command === currPreset) done(); 111 | }); 112 | }); 113 | 114 | it('rainbow preset should send a reply', (done) => { 115 | const currPreset = 'rainbow'; 116 | this.socket.emit('preset', { name: currPreset, test: true }); 117 | this.socket.on('preset-executed', (command) => { 118 | if (command === currPreset) done(); 119 | }); 120 | }); 121 | 122 | it('look preset should send a reply', (done) => { 123 | const currPreset = 'look'; 124 | this.socket.emit('preset', { name: currPreset, test: true }); 125 | this.socket.on('preset-executed', (command) => { 126 | if (command === currPreset) done(); 127 | }); 128 | }); 129 | 130 | it('move-random preset should send a reply', (done) => { 131 | const currPreset = 'move-random'; 132 | this.socket.emit('preset', { name: currPreset, test: true }); 133 | this.socket.on('preset-executed', (command) => { 134 | if (command === currPreset) done(); 135 | }); 136 | }); 137 | 138 | afterEach(() => { 139 | this.socket.destroy(); 140 | }); 141 | 142 | after(() => { 143 | this.server.close(); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "mocha": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "globals": { 9 | "window": true, 10 | "$": true, 11 | "ga": true, 12 | "jQuery": true, 13 | "router": true 14 | }, 15 | "rules": { 16 | "comma-dangle": 2, 17 | "no-cond-assign": 2, 18 | "no-console": 0, 19 | "no-constant-condition": 2, 20 | "no-control-regex": 2, 21 | "no-debugger": 2, 22 | "no-dupe-keys": 2, 23 | "no-empty": 2, 24 | "no-empty-character-class": 2, 25 | "no-ex-assign": 2, 26 | "no-extra-boolean-cast": 2, 27 | "no-extra-parens": 1, 28 | "no-extra-semi": 2, 29 | "no-func-assign": 2, 30 | "no-inner-declarations": 2, 31 | "no-invalid-regexp": 2, 32 | "no-irregular-whitespace": 2, 33 | "no-negated-in-lhs": 2, 34 | "no-obj-calls": 2, 35 | "no-regex-spaces": 2, 36 | "no-sparse-arrays": 2, 37 | "no-unreachable": 2, 38 | "use-isnan": 2, 39 | "valid-jsdoc": 2, 40 | "valid-typeof": 2, 41 | 42 | "block-scoped-var": 0, 43 | "complexity": 0, 44 | "consistent-return": 2, 45 | "curly": [2, "multi-line"], 46 | "default-case": 1, 47 | "dot-notation": 1, 48 | "eqeqeq": 1, 49 | "guard-for-in": 1, 50 | "no-alert": 1, 51 | "no-caller": 2, 52 | "no-div-regex": 2, 53 | "no-else-return": 1, 54 | "no-empty-label": 2, 55 | "no-eq-null": 1, 56 | "no-eval": 2, 57 | "no-extend-native": 2, 58 | "no-extra-bind": 2, 59 | "no-fallthrough": 2, 60 | "no-floating-decimal": 2, 61 | "no-implied-eval": 2, 62 | "no-iterator": 2, 63 | "no-labels": 2, 64 | "no-lone-blocks": 2, 65 | "no-loop-func": 0, 66 | "no-multi-spaces": 1, 67 | "no-multi-str": 2, 68 | "no-native-reassign": 2, 69 | "no-new": 2, 70 | "no-new-func": 2, 71 | "no-new-wrappers": 2, 72 | "no-octal": 2, 73 | "no-octal-escape": 2, 74 | "no-process-env": 0, 75 | "no-proto": 2, 76 | "no-redeclare": 1, 77 | "no-return-assign": 2, 78 | "no-script-url": 2, 79 | "no-self-compare": 2, 80 | "no-sequences": 2, 81 | "no-unused-expressions": 2, 82 | "no-void": 1, 83 | "no-warning-comments": [ 84 | 1, 85 | { 86 | "terms": [ 87 | "fixme" 88 | ], 89 | "location": "start" 90 | } 91 | ], 92 | "no-with": 2, 93 | "radix": 2, 94 | "vars-on-top": 0, 95 | "wrap-iife": [2, "any"], 96 | "yoda": 1, 97 | 98 | "strict": [2, "global"], 99 | 100 | "no-catch-shadow": 2, 101 | "no-delete-var": 2, 102 | "no-label-var": 2, 103 | "no-shadow": 0, 104 | "no-shadow-restricted-names": 2, 105 | "no-undef": 2, 106 | "no-undef-init": 2, 107 | "no-undefined": 1, 108 | "no-unused-vars": 2, 109 | "no-use-before-define": 0, 110 | 111 | "handle-callback-err": 2, 112 | "no-mixed-requires": 2, 113 | "no-new-require": 2, 114 | "no-path-concat": 0, 115 | "no-process-exit": 0, 116 | "no-restricted-modules": 0, 117 | "no-sync": 1, 118 | 119 | "brace-style": [ 120 | 2, 121 | "1tbs", 122 | { "allowSingleLine": true } 123 | ], 124 | "camelcase": 1, 125 | "comma-spacing": [ 126 | 2, 127 | { 128 | "before": false, 129 | "after": true 130 | } 131 | ], 132 | "comma-style": [ 133 | 2, "last" 134 | ], 135 | "consistent-this": 0, 136 | "eol-last": 2, 137 | "func-names": 0, 138 | "func-style": 0, 139 | "key-spacing": [ 140 | 2, 141 | { 142 | "beforeColon": false, 143 | "afterColon": true 144 | } 145 | ], 146 | "max-nested-callbacks": 0, 147 | "new-cap": [2, { "capIsNewExceptions": ["Server"] }], 148 | "new-parens": 2, 149 | "no-array-constructor": 2, 150 | "no-inline-comments": 1, 151 | "no-lonely-if": 1, 152 | "no-mixed-spaces-and-tabs": 2, 153 | "no-multiple-empty-lines": [ 154 | 1, 155 | { "max": 2 } 156 | ], 157 | "no-nested-ternary": 2, 158 | "no-new-object": 2, 159 | "semi-spacing": [2, { "before": false, "after": true }], 160 | "no-spaced-func": 2, 161 | "no-ternary": 0, 162 | "no-trailing-spaces": 1, 163 | "no-underscore-dangle": 0, 164 | "object-curly-spacing": [2, "always"], 165 | "one-var": 0, 166 | "operator-assignment": 0, 167 | "padded-blocks": 0, 168 | "quote-props": 0, 169 | "quotes": [ 170 | 2, 171 | "single", 172 | "avoid-escape" 173 | ], 174 | "semi": [ 175 | 2, 176 | "always" 177 | ], 178 | "sort-vars": 0, 179 | "space-after-keywords": [ 180 | 2, 181 | "always" 182 | ], 183 | "space-before-function-paren": [ 184 | 2, 185 | "never" 186 | ], 187 | "space-before-blocks": [ 188 | 2, 189 | "always" 190 | ], 191 | "space-in-parens": 0, 192 | "space-infix-ops": 2, 193 | "space-return-throw-case": 2, 194 | "space-unary-ops": [ 195 | 1, 196 | { 197 | "words": true, 198 | "nonwords": false 199 | } 200 | ], 201 | "spaced-comment": [ 202 | 2, 203 | "always", 204 | { "exceptions": ["-"] } 205 | ], 206 | "wrap-regex": 1, 207 | 208 | "max-depth": 0, 209 | "max-len": [ 210 | 1, 211 | 80, 212 | 2 213 | ], 214 | "max-params": 0, 215 | "max-statements": 0, 216 | "no-bitwise": 1, 217 | "no-plusplus": 0, 218 | "arrow-spacing": 2, 219 | "constructor-super": 2, 220 | "no-const-assign": 2, 221 | "prefer-arrow-callback": 1, 222 | "no-unneeded-ternary": [ 223 | 2, 224 | { "defaultAssignment": false } 225 | ], 226 | "arrow-parens": [ 227 | 2, 228 | "always" 229 | ] 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /public/vendor/js/colorPicker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | (function(window){ 3 | 4 | var _mouseMoveAction, 5 | _valueType, 6 | _renderTimer, 7 | _colorInstance = {}, 8 | animationFrame = 'AnimationFrame', 9 | requestAnimationFrame = 'request' + animationFrame, 10 | cancelAnimationFrame = 'cancel' + animationFrame, 11 | 12 | ColorPicker = function() {}; 13 | 14 | window.ColorPicker = ColorPicker; 15 | ColorPicker.addEvent = addEvent; 16 | ColorPicker.removeEvent = removeEvent; 17 | ColorPicker.getOrigin = getOrigin; 18 | ColorPicker.limitValue = limitValue; 19 | ColorPicker.changeClass = changeClass; 20 | 21 | // ------------------------------------------------------ // 22 | 23 | ColorPicker.prototype.setColor = function(newCol, type, alpha, forceRender) { 24 | focusInstance(this); 25 | _valueType = true; 26 | preRenderAll(_colorInstance.setColor.apply(_colorInstance, arguments)); 27 | if (forceRender) { 28 | this.startRender(true); 29 | } 30 | }; 31 | 32 | ColorPicker.prototype.saveAsBackground = function() { 33 | focusInstance(this); 34 | return saveAsBackground(true); 35 | }; 36 | 37 | ColorPicker.prototype.setCustomBackground = function(col) { 38 | focusInstance(this); 39 | return _colorInstance.setCustomBackground(col); 40 | }; 41 | 42 | ColorPicker.prototype.startRender = function(oneTime) { 43 | focusInstance(this); 44 | if (oneTime) { 45 | _mouseMoveAction = false; 46 | renderAll(); 47 | this.stopRender(); 48 | } else { 49 | _mouseMoveAction = 1; 50 | _renderTimer = window[requestAnimationFrame](renderAll); 51 | } 52 | }; 53 | 54 | ColorPicker.prototype.stopRender = function() { 55 | focusInstance(this); 56 | window[cancelAnimationFrame](_renderTimer); 57 | if (_valueType) { 58 | _mouseMoveAction = 1; 59 | stopChange(undefined, 'external'); 60 | } 61 | }; 62 | 63 | ColorPicker.prototype.setMode = function(mode) { 64 | focusInstance(this); 65 | setMode(mode); 66 | initSliders(); 67 | renderAll(); 68 | }; 69 | 70 | ColorPicker.prototype.destroyAll = function() { 71 | var html = this.nodes.colorPicker, 72 | destroyReferences = function(nodes) { 73 | for (var n in nodes) { 74 | if (nodes[n] && nodes[n].toString() === '[object Object]' || nodes[n] instanceof Array) { 75 | destroyReferences(nodes[n]); 76 | } 77 | nodes[n] = null; 78 | delete nodes[n]; 79 | } 80 | }; 81 | 82 | this.stopRender(); 83 | installEventListeners(this, true); 84 | destroyReferences(this); 85 | html.parentNode.removeChild(html); 86 | html = null; 87 | }; 88 | 89 | ColorPicker.prototype.renderMemory = function(memory) { 90 | var memos = this.nodes.memos, 91 | tmp = []; 92 | 93 | if (typeof memory === 'string') { 94 | memory = memory.replace(/^'|'$/g, '').replace(/\s*/, '').split('\',\''); 95 | } 96 | for (var n = memos.length; n--; ) { 97 | if (memory && typeof memory[n] === 'string') { 98 | tmp = memory[n].replace('rgba(', '').replace(')', '').split(','); 99 | memory[n] = {r: tmp[0], g: tmp[1], b: tmp[2], a: tmp[3]} 100 | } 101 | memos[n].style.cssText = 'background-color: ' + (memory && memory[n] !== undefined ? 102 | color2string(memory[n]) + ';' + getOpacityCSS(memory[n]['a'] || 1) : 'rgb(0,0,0);'); 103 | } 104 | }; 105 | 106 | // ------------------------------------------------------ // 107 | 108 | function limitValue(value, min, max) { 109 | return (value > max ? max : value < min ? min : value); 110 | } 111 | 112 | function changeClass(elm, cln, newCln) { 113 | return !elm ? false : elm.className = (newCln !== undefined ? 114 | elm.className.replace(new RegExp('\\s+?' + cln, 'g'), newCln ? ' ' + newCln : '') : 115 | elm.className + ' ' + cln); 116 | } 117 | 118 | function getOrigin(elm) { 119 | var box = (elm.getBoundingClientRect) ? elm.getBoundingClientRect() : {top: 0, left: 0}, 120 | doc = elm && elm.ownerDocument, 121 | body = doc.body, 122 | win = doc.defaultView || doc.parentWindow || window, 123 | docElem = doc.documentElement || body.parentNode, 124 | clientTop = docElem.clientTop || body.clientTop || 0, 125 | clientLeft = docElem.clientLeft || body.clientLeft || 0; 126 | 127 | return { 128 | left: box.left + (win.pageXOffset || docElem.scrollLeft) - clientLeft, 129 | top: box.top + (win.pageYOffset || docElem.scrollTop) - clientTop 130 | }; 131 | } 132 | 133 | function addEvent(obj, type, func) { 134 | addEvent.cache = addEvent.cache || { 135 | _get: function(obj, type, func, checkOnly) { 136 | var cache = addEvent.cache[type] || []; 137 | 138 | for (var n = cache.length; n--; ) { 139 | if (obj === cache[n].obj && '' + func === '' + cache[n].func) { 140 | func = cache[n].func; 141 | if (!checkOnly) { 142 | cache[n] = cache[n].obj = cache[n].func = null; 143 | cache.splice(n, 1); 144 | } 145 | return func; 146 | } 147 | } 148 | }, 149 | _set: function(obj, type, func) { 150 | var cache = addEvent.cache[type] = addEvent.cache[type] || []; 151 | 152 | if (addEvent.cache._get(obj, type, func, true)) { 153 | return true; 154 | } else { 155 | cache.push({ 156 | func: func, 157 | obj: obj 158 | }); 159 | } 160 | } 161 | }; 162 | 163 | if (!func.name && addEvent.cache._set(obj, type, func) || typeof func !== 'function') { 164 | return; 165 | } 166 | 167 | if (obj.addEventListener) obj.addEventListener(type, func, false); 168 | else obj.attachEvent('on' + type, func); 169 | } 170 | 171 | function removeEvent(obj, type, func) { 172 | if (typeof func !== 'function') return; 173 | if (!func.name) { 174 | func = addEvent.cache._get(obj, type, func) || func; 175 | } 176 | 177 | if (obj.removeEventListener) obj.removeEventListener(type, func, false); 178 | else obj.detachEvent('on' + type, func); 179 | } 180 | })(window); 181 | -------------------------------------------------------------------------------- /lib/socket-listeners.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const setupBB8 = require(__dirname + '/../setup/setup-bb8'); 4 | const setupSphero = require(__dirname + '/../setup/setup-sphero'); 5 | const connectFn = require(__dirname + '/device-config'); 6 | const lights = require(__dirname + '/../commands/lights'); 7 | const look = require(__dirname + '/../commands/look'); 8 | const dance = require(__dirname + '/../commands/dance'); 9 | const fly = require(__dirname + '/../commands/fly'); 10 | const magic8 = require(__dirname + '/../commands/magic8'); 11 | const moveRandom = require(__dirname + '/../commands/move-random'); 12 | const collision = require(__dirname + '/../commands/collision'); 13 | const speedometer = require(__dirname + '/../commands/speedometer'); 14 | const accelerometer = require(__dirname + '/../commands/accelerometer'); 15 | const circle = require(__dirname + '/../commands/circle'); 16 | var speed = 160; 17 | const max = 250; 18 | const min = 10; 19 | const speedChange = 10; 20 | var interval; 21 | var orbEvents; 22 | 23 | module.exports = exports = (io, orb) => { 24 | io.on('connection', (socket) => { 25 | if (typeof orb !== 'undefined') { 26 | speedometer(orb, socket); 27 | accelerometer(orb, socket); 28 | } 29 | 30 | socket.on('error', (err) => console.log(err)); 31 | 32 | socket.on('connect-btn-sprk', () => { 33 | setupSphero(() => { 34 | orb = connectFn.spheroCreate(); 35 | orb.connect(() => { 36 | console.log('connected!'); 37 | socket.emit('connected-sphero'); 38 | orb.roll(100, 0, () => { 39 | console.log('performed roll'); 40 | }); 41 | }); 42 | }); 43 | }); 44 | 45 | socket.on('connect-btn-bb8', () => { 46 | setupBB8(() => { 47 | var ninja = connectFn.bb8Create(); 48 | orb = {}; 49 | ninja.connect(() => { 50 | console.log('connected!'); 51 | socket.emit('connected-bb8'); 52 | ninja.roll(100, 0, () => { 53 | console.log('performed roll'); 54 | }); 55 | orb = ninja; 56 | }); 57 | }); 58 | }); 59 | 60 | socket.on('roll', (data) => { 61 | console.log(data.direction); 62 | clearPreset(orb); 63 | switch (data.direction) { 64 | case 'left': 65 | rollDirection(orb, true, 270, socket); 66 | break; 67 | case 'up': 68 | rollDirection(orb, false, 0, socket); 69 | break; 70 | case 'right': 71 | rollDirection(orb, true, 90, socket); 72 | break; 73 | case 'down': 74 | rollDirection(orb, true, 180, socket); 75 | break; 76 | case 'stop': 77 | orb.stop(() => { 78 | console.log('stopped'); 79 | }); 80 | break; 81 | default: 82 | console.log('received unrecognized event'); 83 | } 84 | }); 85 | 86 | socket.on('free-roll', (data) => { 87 | if (data.deg === 0) { 88 | return rollDirection(orb, false, data.deg, socket); 89 | } 90 | rollDirection(orb, true, data.deg, socket); 91 | }); 92 | 93 | socket.on('speed', (direction) => { 94 | if (direction === 'up') { 95 | if (speed === max) return console.log('at max speed'); 96 | speed += speedChange; 97 | orb.roll(speed); 98 | console.log('speed up'); 99 | } 100 | if (direction === 'down') { 101 | if (speed === min) return console.log('at min speed'); 102 | speed -= speedChange; 103 | orb.roll(speed); 104 | console.log('speed down'); 105 | } 106 | socket.emit('speed-change'); 107 | }); 108 | 109 | socket.on('color', (color) => { 110 | orb.color('#' + color); 111 | }); 112 | 113 | socket.on('preset', (command) => { 114 | console.log('command: ' + command.name); 115 | clearPreset(orb); 116 | switch (command.name) { 117 | case 'rainbow': 118 | interval = lights.rainbow(orb); 119 | break; 120 | case 'xmas': 121 | interval = lights.xmas(orb); 122 | break; 123 | case 'disco': 124 | interval = lights.disco(orb); 125 | break; 126 | case 'dance': 127 | interval = dance(orb); 128 | break; 129 | case 'look': 130 | interval = look(orb); 131 | break; 132 | case 'move-random': 133 | interval = moveRandom(orb); 134 | break; 135 | case 'collision': 136 | orbEvents = collision(orb); 137 | break; 138 | case 'fly': 139 | orbEvents = fly(orb); 140 | break; 141 | case 'magic8': 142 | orbEvents = magic8(orb); 143 | break; 144 | case 'circle': 145 | interval = circle(orb); 146 | break; 147 | default: 148 | console.log('received unrecognized event'); 149 | } 150 | if (command.test) clearPreset(orb); 151 | socket.emit('preset-executed', command.name); 152 | }); 153 | socket.on('clear-preset', () => clearPreset(orb)); 154 | }); 155 | }; 156 | 157 | function clearPreset(orb) { 158 | if (interval) clearInterval(interval); 159 | if (orbEvents) { 160 | for (let key in orbEvents) { 161 | if (orbEvents.hasOwnProperty(key)) { 162 | orb.removeListener(key, orbEvents[key]); 163 | } 164 | } 165 | orbEvents = null; 166 | } 167 | } 168 | 169 | function rollDirection(orb, resetHeading, degrees, socket, cb) { 170 | if (resetHeading) { 171 | orb.setHeading(0, () => { 172 | orb.roll(speed, degrees); 173 | orb.randomColor(); 174 | if (cb) cb(); 175 | }); 176 | } else { 177 | orb.roll(speed, degrees); 178 | orb.randomColor(); 179 | if (cb) cb(); 180 | } 181 | socket.emit('rolled'); 182 | } 183 | 184 | exports.rollDirection = rollDirection; 185 | -------------------------------------------------------------------------------- /public/img/bb8-silh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 62 | 63 | -------------------------------------------------------------------------------- /public/vendor/js/hsvPicker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var hsvPicker = function(defaultColor, presetColors, callback) { 3 | var ColorPicker = window.ColorPicker, 4 | Tools = ColorPicker || window.Tools, 5 | startPoint, 6 | currentTarget, 7 | currentTargetHeight = 0; 8 | 9 | /* ---------------------------------- */ 10 | /* ------- Render color patch ------- */ 11 | /* ---------------------------------- */ 12 | var testPatch = document.getElementById('testPatch'), 13 | renderTestPatch = function(color) { 14 | var RGB = color.RND.rgb; 15 | testPatch.style.cssText = 16 | 'background-color: rgba(' + RGB.r + ',' + RGB.g + ',' + RGB.b + ',' + color.alpha + ');' + 17 | 'color: ' + (color.rgbaMixBlack.luminance > 0.22 ? '#222' : '#fff'); 18 | testPatch.firstChild.data = '#' + color.HEX; 19 | }; 20 | 21 | /* ---------------------------------- */ 22 | /* ---------- Color squares --------- */ 23 | /* ---------------------------------- */ 24 | var colorSquares = document.getElementById('color_squares'), 25 | squares = colorSquares.children, 26 | n = squares.length; 27 | 28 | if (!presetColors) { 29 | for ( ; n--; ) { 30 | // draw random color values as background 31 | squares[n].style.backgroundColor = 'rgb(' + 32 | Math.round(Math.random() * 255) + ',' + 33 | Math.round(Math.random() * 255) + ',' + 34 | Math.round(Math.random() * 255) +')'; 35 | } 36 | } else { 37 | function hexToLightness(hex) { 38 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 39 | var lightness = (parseInt(result[1], 16) * 0.299 + parseInt(result[2], 16) * 0.587 + parseInt(result[3], 16) * 0.114) / 3; 40 | return Math.round(lightness); 41 | } 42 | 43 | for (let i = 0; i < n && i < presetColors.length; i++) { 44 | squares[i].style.backgroundColor = presetColors[i].colorCode; 45 | squares[i].innerText = presetColors[i].colorName; 46 | if (hexToLightness(presetColors[i].colorCode) > 43) { 47 | squares[i].classList.add('dark-text'); 48 | } 49 | } 50 | } 51 | 52 | // event delegation 53 | Tools.addEvent(colorSquares, 'click', function(e) { 54 | var target = e.target || e.srcElement; 55 | if (target.parentNode === this) { 56 | myColor.setColor(target.style.backgroundColor); 57 | callback(myColor.colors); 58 | startRender(true); 59 | } 60 | }); 61 | 62 | 63 | /* ---------------------------------- */ 64 | /* ---- HSV-circle color picker ----- */ 65 | /* ---------------------------------- */ 66 | var hsv_map = document.getElementById('hsv_map'), 67 | hsv_mapCover = hsv_map.children[1], // well... 68 | hsv_mapCursor = hsv_map.children[2], 69 | hsv_barBGLayer = hsv_map.children[3], 70 | hsv_barWhiteLayer = hsv_map.children[4], 71 | hsv_barCursors = hsv_map.children[6], 72 | hsv_barCursorsCln = hsv_barCursors.className, 73 | hsv_Leftcursor = hsv_barCursors.children[0], 74 | hsv_Rightcursor = hsv_barCursors.children[1], 75 | 76 | colorDisc = document.getElementById('surface'), 77 | colorDiscRadius = colorDisc.offsetHeight / 2, 78 | luminanceBar = document.getElementById('luminanceBar'), 79 | 80 | hsvDown = function(e) { // mouseDown callback 81 | var target = e.target || e.srcElement; 82 | 83 | if (e.preventDefault) e.preventDefault(); 84 | 85 | currentTarget = target.id ? target : target.parentNode; 86 | startPoint = Tools.getOrigin(currentTarget); 87 | currentTargetHeight = currentTarget.offsetHeight; 88 | Tools.addEvent(window, 'mousemove', hsvMove); 89 | hsv_map.className = 'no-cursor'; 90 | hsvMove(e); 91 | startRender(); 92 | }, 93 | hsvMove = function(e) { 94 | var r, x, y, h, s; 95 | 96 | if (currentTarget === hsv_map) { 97 | r = currentTargetHeight / 2, 98 | x = e.clientX - startPoint.left - r, 99 | y = e.clientY - startPoint.top - r, 100 | h = 360 - ((Math.atan2(y, x) * 180 / Math.PI) + (y < 0 ? 360 : 0)), 101 | s = (Math.sqrt((x * x) + (y * y)) / r) * 100; 102 | myColor.setColor({h: h, s: s}, 'hsv'); 103 | } else if (currentTarget === hsv_barCursors) { // the luminanceBar 104 | myColor.setColor({ 105 | v: (currentTargetHeight - (e.clientY - startPoint.top)) / currentTargetHeight * 100 106 | }, 'hsv'); 107 | } 108 | }, 109 | 110 | renderHSVPicker = function(color) { 111 | var pi2 = Math.PI * 2, 112 | x = Math.cos(pi2 - color.hsv.h * pi2), 113 | y = Math.sin(pi2 - color.hsv.h * pi2), 114 | r = color.hsv.s * (colorDiscRadius - 5); 115 | 116 | hsv_mapCover.style.opacity = 1 - color.hsv.v; 117 | hsv_barWhiteLayer.style.opacity = 1 - color.hsv.s; 118 | hsv_barBGLayer.style.backgroundColor = 'rgb(' + 119 | color.hueRGB.r + ',' + 120 | color.hueRGB.g + ',' + 121 | color.hueRGB.b + ')'; 122 | 123 | hsv_mapCursor.style.cssText = 124 | 'left: ' + (x * r + colorDiscRadius) + 'px;' + 125 | 'top: ' + (y * r + colorDiscRadius) + 'px;' + 126 | 'border-color: ' + (color.RGBLuminance > 0.22 ? '#333;' : '#ddd'); 127 | hsv_barCursors.className = color.RGBLuminance > 0.22 ? hsv_barCursorsCln + ' dark' : hsv_barCursorsCln; 128 | if (hsv_Leftcursor) hsv_Leftcursor.style.top = hsv_Rightcursor.style.top = ((1 - color.hsv.v) * colorDiscRadius * 2) + 'px'; 129 | }; 130 | 131 | Tools.addEvent(hsv_map, 'mousedown', hsvDown); // event delegation 132 | Tools.addEvent(document.getElementById('hsv_map'), 'mouseup', function() { 133 | Tools.removeEvent(window, 'mousemove', hsvMove); 134 | hsv_map.className = ''; 135 | stopRender(); 136 | callback(myColor.colors); 137 | }); 138 | 139 | // generic function for drawing a canvas disc 140 | var drawDisk = function(ctx, coords, radius, steps, colorCallback) { 141 | var x = coords[0] || coords, 142 | y = coords[1] || coords, 143 | a = radius[0] || radius, 144 | b = radius[1] || radius, 145 | angle = 360, 146 | rotate = 0, coef = Math.PI / 180; 147 | 148 | ctx.save(); 149 | ctx.translate(x - a, y - b); 150 | ctx.scale(a, b); 151 | 152 | steps = (angle / steps) || 360; 153 | 154 | for (; angle > 0 ; angle -= steps){ 155 | ctx.beginPath(); 156 | if (steps !== 360) ctx.moveTo(1, 1); 157 | ctx.arc(1, 1, 1, 158 | (angle - (steps / 2) - 1) * coef, 159 | (angle + (steps / 2) + 1) * coef); 160 | 161 | if (colorCallback) { 162 | colorCallback(ctx, angle); 163 | } else { 164 | ctx.fillStyle = 'black'; 165 | ctx.fill(); 166 | } 167 | } 168 | ctx.restore(); 169 | }, 170 | drawCircle = function(ctx, coords, radius, color, width) { 171 | width = width || 1; 172 | radius = [ 173 | (radius[0] || radius) - width / 2, 174 | (radius[1] || radius) - width / 2 175 | ]; 176 | drawDisk(ctx, coords, radius, 1, function(ctx, angle) { 177 | ctx.restore(); 178 | ctx.lineWidth = width; 179 | ctx.strokeStyle = color || '#000'; 180 | ctx.stroke(); 181 | }); 182 | }; 183 | 184 | if (colorDisc.getContext) { 185 | drawDisk( // draw disc 186 | colorDisc.getContext('2d'), 187 | [colorDisc.width / 2, colorDisc.height / 2], 188 | [colorDisc.width / 2 - 1, colorDisc.height / 2 - 1], 189 | 360, 190 | function(ctx, angle) { 191 | var gradient = ctx.createRadialGradient(1, 1, 1, 1, 1, 0); 192 | gradient.addColorStop(0, 'hsl(' + (360 - angle + 0) + ', 100%, 50%)'); 193 | gradient.addColorStop(1, "#FFFFFF"); 194 | 195 | ctx.fillStyle = gradient; 196 | ctx.fill(); 197 | } 198 | ); 199 | drawCircle( // gray border 200 | colorDisc.getContext('2d'), 201 | [colorDisc.width / 2, colorDisc.height / 2], 202 | [colorDisc.width / 2, colorDisc.height / 2], 203 | '#555', 204 | 1 205 | ); 206 | // draw the luminanceBar bar 207 | var ctx = luminanceBar.getContext('2d'), 208 | gradient = ctx.createLinearGradient(0, 0, 0, 300); 209 | gradient.addColorStop(0, 'transparent'); 210 | gradient.addColorStop(1, 'black'); 211 | 212 | ctx.fillStyle = gradient; 213 | ctx.fillRect(0, 0, 30, 300); 214 | } 215 | 216 | var doRender = function(color) { 217 | renderTestPatch(color); 218 | renderHSVPicker(color); 219 | }, 220 | renderTimer, 221 | 222 | startRender = function(oneTime){ 223 | if (oneTime) { // only Colors is instanciated 224 | doRender(myColor.colors); 225 | } else { 226 | renderTimer = window.setInterval( 227 | function() { 228 | doRender(myColor.colors); 229 | }, 13); // 1000 / 60); // ~16.666 -> 60Hz or 60fps 230 | } 231 | }, 232 | stopRender = function(){ 233 | window.clearInterval(renderTimer); 234 | }; 235 | 236 | var myColor = window.myColor = new Colors({color: defaultColor || 'rgba(255, 255, 37, 1)'}); 237 | 238 | // initial rendering 239 | doRender(myColor.colors); 240 | }; 241 | -------------------------------------------------------------------------------- /public/img/bb8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 12 | 15 | 17 | 20 | 22 | 25 | 27 | 29 | 33 | 39 | 44 | 47 | 49 | 51 | 53 | 55 | 58 | 60 | 63 | 66 | 72 | 76 | 79 | 81 | 84 | 87 | 102 | 104 | 105 | -------------------------------------------------------------------------------- /public/vendor/sweetalert.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t,n){"use strict";!function o(e,t,n){function a(s,l){if(!t[s]){if(!e[s]){var i="function"==typeof require&&require;if(!l&&i)return i(s,!0);if(r)return r(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var c=t[s]={exports:{}};e[s][0].call(c.exports,function(t){var n=e[s][1][t];return a(n?n:t)},c,c.exports,o,e,t,n)}return t[s].exports}for(var r="function"==typeof require&&require,s=0;s=0;)n=n.replace(" "+t+" "," ");e.className=n.replace(/^\s+|\s+$/g,"")}},i=function(e){var n=t.createElement("div");return n.appendChild(t.createTextNode(e)),n.innerHTML},u=function(e){e.style.opacity="",e.style.display="block"},c=function(e){if(e&&!e.length)return u(e);for(var t=0;t0?setTimeout(o,t):e.style.display="none"});o()},h=function(n){if("function"==typeof MouseEvent){var o=new MouseEvent("click",{view:e,bubbles:!1,cancelable:!0});n.dispatchEvent(o)}else if(t.createEvent){var a=t.createEvent("MouseEvents");a.initEvent("click",!1,!1),n.dispatchEvent(a)}else t.createEventObject?n.fireEvent("onclick"):"function"==typeof n.onclick&&n.onclick()},b=function(t){"function"==typeof t.stopPropagation?(t.stopPropagation(),t.preventDefault()):e.event&&e.event.hasOwnProperty("cancelBubble")&&(e.event.cancelBubble=!0)};a.hasClass=r,a.addClass=s,a.removeClass=l,a.escapeHtml=i,a._show=u,a.show=c,a._hide=d,a.hide=f,a.isDescendant=p,a.getTopMargin=m,a.fadeIn=v,a.fadeOut=y,a.fireClick=h,a.stopEventPropagation=b},{}],5:[function(t,o,a){Object.defineProperty(a,"__esModule",{value:!0});var r=t("./handle-dom"),s=t("./handle-swal-dom"),l=function(t,o,a){var l=t||e.event,i=l.keyCode||l.which,u=a.querySelector("button.confirm"),c=a.querySelector("button.cancel"),d=a.querySelectorAll("button[tabindex]");if(-1!==[9,13,32,27].indexOf(i)){for(var f=l.target||l.srcElement,p=-1,m=0;m"),i.innerHTML=e.html?e.text:s.escapeHtml(e.text||"").split("\n").join("
"),e.text&&s.show(i),e.customClass)s.addClass(t,e.customClass),t.setAttribute("data-custom-class",e.customClass);else{var d=t.getAttribute("data-custom-class");s.removeClass(t,d),t.setAttribute("data-custom-class","")}if(s.hide(t.querySelectorAll(".sa-icon")),e.type&&!a.isIE8()){var f=function(){for(var o=!1,a=0;ao;o++)n=parseInt(e.substr(2*o,2),16),n=Math.round(Math.min(Math.max(0,n+n*t),255)).toString(16),a+=("00"+n).substr(n.length);return a};o.extend=a,o.hexToRgb=r,o.isIE8=s,o.logStr=l,o.colorLuminance=i},{}]},{},[1]),"function"==typeof define&&define.amd?define(function(){return sweetAlert}):"undefined"!=typeof module&&module.exports&&(module.exports=sweetAlert)}(window,document); -------------------------------------------------------------------------------- /public/img/logo-vert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 15 | 18 | 21 | 24 | 26 | 29 | 31 | 35 | 37 | 41 | 43 | 45 | 47 | 48 | 54 | 58 | 61 | 64 | 66 | 69 | 71 | 75 | 78 | 82 | 84 | 86 | 88 | 89 | 92 | 94 | 97 | 99 | 102 | 104 | 107 | 109 | 111 | 115 | 121 | 126 | 129 | 131 | 133 | 135 | 137 | 140 | 142 | 145 | 148 | 154 | 157 | 160 | 162 | 165 | 168 | 183 | 185 | 186 | -------------------------------------------------------------------------------- /public/img/brand-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 15 | 18 | 21 | 24 | 26 | 29 | 31 | 35 | 37 | 41 | 43 | 45 | 47 | 48 | 54 | 58 | 61 | 64 | 66 | 69 | 71 | 75 | 78 | 82 | 84 | 86 | 88 | 89 | 92 | 94 | 97 | 99 | 102 | 104 | 107 | 109 | 111 | 115 | 121 | 126 | 129 | 131 | 133 | 135 | 137 | 140 | 142 | 145 | 148 | 154 | 157 | 160 | 162 | 165 | 168 | 183 | 185 | 186 | 194 | 201 | 208 | 214 | 221 | 226 | 232 | 233 | 234 | --------------------------------------------------------------------------------