├── 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 |
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 |
20 |
--------------------------------------------------------------------------------
/public/img/icons/about_blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
24 |
--------------------------------------------------------------------------------
/public/img/icons/bluetooth_blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
25 |
--------------------------------------------------------------------------------
/public/img/icons/book_blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
40 |
--------------------------------------------------------------------------------
/public/img/icons/dropper_blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
40 |
--------------------------------------------------------------------------------
/public/img/icons/settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
39 |
--------------------------------------------------------------------------------
/public/img/icons/settings_blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | 
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 |
46 |
--------------------------------------------------------------------------------
/public/img/icons/controller_blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
46 |
--------------------------------------------------------------------------------
/public/img/sprk.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
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 |
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 |
186 |
--------------------------------------------------------------------------------
/public/img/brand-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
234 |
--------------------------------------------------------------------------------