diff --git a/Dockerbin/asound.conf b/Dockerbin/asound.conf index 7a0708d..e5ef42b 100644 --- a/Dockerbin/asound.conf +++ b/Dockerbin/asound.conf @@ -1,6 +1,22 @@ -pcm.!default { - type hw card 0 +pcm.hifiberry { +type hw card 0 } -ctl.!default { - type hw card 0 + +pcm.!default { +type plug +slave.pcm "dmixer" +} + +pcm.dmixer { +type dmix +ipc_key 1024 +slave { +pcm "hifiberry" +channels 2 +} +} + +ctl.dmixer { +type hw +card 0 } diff --git a/Dockerfile.template b/Dockerfile.template index 857362b..33de69e 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -23,7 +23,9 @@ RUN apt-get update && apt-get install -y \ alsa-utils \ gstreamer1.0-alsa \ gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-good \ gstreamer1.0-plugins-ugly \ + gstreamer1.0-tools \ gstreamer1.0-libav \ haproxy \ mopidy-spotify \ @@ -39,8 +41,8 @@ RUN apt-get update && apt-get install -y \ # Enable haproxy RUN echo 'ENABLED=1' >> /etc/default/haproxy -# Update setup-tools -RUN curl https://bootstrap.pypa.io/ez_setup.py -o - | python +# Update setup-tools and other deps +RUN pip install --upgrade pip packaging pyparsing setuptools # Install Mopidy extensions RUN pip install mopidy-gmusic Mopidy-YouTube mopidy-musicbox-webclient Mopidy-Local-SQLite Mopidy-ALSAMixer @@ -77,8 +79,8 @@ COPY ./app ./ # Compile coffee RUN ./node_modules/.bin/coffee -c ./src -# Disable haproxy service - we will manually start it later -RUN systemctl disable haproxy +# Disable haproxy and mopidy services - we will manually start it later +RUN systemctl disable haproxy mopidy ## Uncomment if you want systemd ENV INITSYSTEM on diff --git a/README.md b/README.md index 028123f..33301a8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A Raspberry Pi based smart connected speaker based on [Mopidy](https://github.co *__You can read about the making of the boombeastic and see more photos [here](https://resin.io/blog/the-making-of-boombeastic/)__* ## Parts list +## Mini #### rpi2/2+/3 version please refer to [this link](https://github.com/resin-io-playground/boombeastic/blob/master/docs/v1/mini/rpi3/bom.md) #### rpi0 version @@ -18,14 +19,21 @@ please refer to [this link](https://github.com/resin-io-playground/boombeastic/b #### rpi0 version please refer to [this link](https://github.com/resin-io-playground/boombeastic/blob/master/docs/v1/mini/rpi0/assembly.md) +## Stereo +#### rpi2/2+/3 version +please refer to [this link](https://github.com/resin-io-playground/boombeastic/blob/master/docs/v1/stereo/rpi3/bom.md) + ## Getting started +**this application is compatible with resinOS 2.0+** + - Sign up on [resin.io](https://dashboard.resin.io/signup) -- go throught the [getting started guide](http://docs.resin.io/raspberrypi/nodejs/getting-started/) and create a new RPI zero application called `boombeasticmini` +- go throught the [getting started guide](http://docs.resin.io/raspberrypi/nodejs/getting-started/) and create a new Raspberry Pi application called `boombeastic` - clone this repository to your local workspace - set these variables in the `Fleet Configuration` application side tab - `RESIN_HOST_CONFIG_dtoverlay` = `hifiberry-dac` + - `RESIN_HOST_CONFIG_device_tree_overlay` = `i2s-mmap` - add the _resin remote_ to your local workspace using the useful shortcut in the dashboard UI ![remoteadd](https://raw.githubusercontent.com/resin-io-playground/boombeastic/master/docs/gitresinremote.png) - `git push resin master` @@ -59,6 +67,7 @@ PORTAL_SSID | `ResinAP` | the name of the Access Point that [wifi-connect](https ## Videos * [YouTube 1](https://www.youtube.com/watch?v=EnLgmW8kyis) +* [YouTube 2](https://youtu.be/pKvJKaCDQW8) * [Vine 1](https://vine.co/v/5g71nzHwXvr) ## Pictures @@ -67,6 +76,10 @@ PORTAL_SSID | `ResinAP` | the name of the Access Point that [wifi-connect](https --- +![v1_stereo](https://raw.githubusercontent.com/resin-io-playground/boombeastic/master/docs/v1/stereo/photos/IMG_20170407_133846.jpg) + +--- + ![v1_rpi3_2](https://raw.githubusercontent.com/resin-io-playground/boombeastic/master/docs/v1/mini/rpi3/photos/IMG_20160929_163751.jpg) --- diff --git a/app/bower.json b/app/bower.json index 0631a9e..6b4480b 100644 --- a/app/bower.json +++ b/app/bower.json @@ -1,20 +1,20 @@ { - "name": "resin-wifi-connect", - "version": "0.0.0", - "homepage": "https://github.com/pcarranzav/resin-wifi-connect", - "authors": [ - "Pablo Carranza Vélez " - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "src/public/bower_components", - "test", - "tests" - ], - "dependencies": { - "bootstrap": "~3.3.5" - } + "name": "resin-wifi-connect", + "version": "2.0.0", + "homepage": "https://github.com/pcarranzav/resin-wifi-connect", + "authors": [ + "Pablo Carranza Vélez " + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "src/public/bower_components", + "test", + "tests" + ], + "dependencies": { + "bootstrap": "~3.3.5" + } } diff --git a/app/index.js b/app/index.js index c0d4d1c..177422b 100644 --- a/app/index.js +++ b/app/index.js @@ -1,89 +1,89 @@ { - const fs = require('fs'); - const ini = require('ini'); - const exec = require('child_process').exec; - const chalk = require("chalk"); - const request = require('request'); - const display = require(__dirname + '/libs/ledmatrix/index.js'); - const supervisor = require(__dirname + '/libs/supervisor/index.js'); - const emoji = require(__dirname + '/libs/emoji/index.js'); - const debug = require('debug')('main'); + const fs = require('fs'); + const ini = require('ini'); + const exec = require('child_process').exec; + const chalk = require("chalk"); + const request = require('request'); + const display = require(__dirname + '/libs/ledmatrix/index.js'); + const supervisor = require(__dirname + '/libs/supervisor/index.js'); + const emoji = require(__dirname + '/libs/emoji/index.js'); + const debug = require('debug')('main'); - let mopidy = ini.parse(fs.readFileSync('/etc/mopidy/mopidy.conf', 'utf-8')); - console.log(chalk.cyan('configuring Mopidy from env vars...')); - let updating = false; + let mopidy = ini.parse(fs.readFileSync('/etc/mopidy/mopidy.conf', 'utf-8')); + console.log(chalk.cyan('configuring Mopidy from env vars...')); + let updating = false; - // http config - mopidy.http.port = parseInt(process.env.MOPIDY_HTTP_PORT) || 8080; - // mpd config - mopidy.mpd.port = parseInt(process.env.MOPIDY_MPD_PORT) || 6680; - // audio config - mopidy.audio.mixer_volume = parseInt(process.env.MOPIDY_AUDIO_MIXER_VOLUME) || 50; - // Google Play Music config - mopidy.gmusic.enabled = process.env.MOPIDY_GMUSIC_ENABLED === '1' ? true : false; - mopidy.gmusic.username = process.env.MOPIDY_GMUSIC_USERNAME || "none"; - mopidy.gmusic.password = process.env.MOPIDY_GMUSIC_PASSWORD || "none"; - mopidy.gmusic.all_access = process.env.MOPIDY_GMUSIC_ALL_ACCESS === '1' ? true : false; - // Spotify config - mopidy.spotify.enabled = process.env.MOPIDY_SPOTIFY_ENABLED === '1' ? true : false; - mopidy.spotify.username = process.env.MOPIDY_SPOTIFY_USERNAME || "none"; - mopidy.spotify.password = process.env.MOPIDY_SPOTIFY_PASSWORD || "none"; - // Soundcloud config - mopidy.soundcloud.enabled = process.env.MOPIDY_SOUNDCLOUD_ENABLED === '1' ? true : false; - mopidy.soundcloud.auth_token = process.env.MOPIDY_SOUNDCLOUD_AUTH_TOKEN || "none"; - // YouTube config - mopidy.youtube.enabled = process.env.MOPIDY_YOUTUBE_ENABLED === '1' ? true : false; + // http config + mopidy.http.port = parseInt(process.env.MOPIDY_HTTP_PORT) || 8080; + // mpd config + mopidy.mpd.port = parseInt(process.env.MOPIDY_MPD_PORT) || 6680; + // audio config + mopidy.audio.mixer_volume = parseInt(process.env.MOPIDY_AUDIO_MIXER_VOLUME) || 50; + // Google Play Music config + mopidy.gmusic.enabled = process.env.MOPIDY_GMUSIC_ENABLED === '1' ? true : false; + mopidy.gmusic.username = process.env.MOPIDY_GMUSIC_USERNAME || "none"; + mopidy.gmusic.password = process.env.MOPIDY_GMUSIC_PASSWORD || "none"; + mopidy.gmusic.all_access = process.env.MOPIDY_GMUSIC_ALL_ACCESS === '1' ? true : false; + // Spotify config + mopidy.spotify.enabled = process.env.MOPIDY_SPOTIFY_ENABLED === '1' ? true : false; + mopidy.spotify.username = process.env.MOPIDY_SPOTIFY_USERNAME || "none"; + mopidy.spotify.password = process.env.MOPIDY_SPOTIFY_PASSWORD || "none"; + // Soundcloud config + mopidy.soundcloud.enabled = process.env.MOPIDY_SOUNDCLOUD_ENABLED === '1' ? true : false; + mopidy.soundcloud.auth_token = process.env.MOPIDY_SOUNDCLOUD_AUTH_TOKEN || "none"; + // YouTube config + mopidy.youtube.enabled = process.env.MOPIDY_YOUTUBE_ENABLED === '1' ? true : false; - fs.writeFileSync('/etc/mopidy/mopidy.conf', ini.stringify(mopidy)); - console.log(chalk.cyan('starting Mopidy - HTTP port:' + mopidy.http.port + ' (proxy on port 80); MPD port:' + mopidy.mpd.port)); - display.init(() => { - 'use strict'; - display.image(display.presets.splash); - }); - exec('systemctl start mopidy', (error, stdout, stderr) => { - 'use strict'; - if (error) { - console.log(chalk.red(`exec error: ${error}`)); - return; - } - console.log(chalk.green(`stdout: ${stdout}`)); - console.log(chalk.red(`stderr: ${stderr}`)); - }); + fs.writeFileSync('/etc/mopidy/mopidy.conf', ini.stringify(mopidy)); + console.log(chalk.cyan('starting Mopidy - HTTP port:' + mopidy.http.port + ' (proxy on port 80); MPD port:' + mopidy.mpd.port)); + display.init(() => { + 'use strict'; + display.image(display.presets.splash); + }); + exec('systemctl start mopidy', (error, stdout, stderr) => { + 'use strict'; + if (error) { + console.log(chalk.red(`exec error: ${error}`)); + return; + } + console.log(chalk.green(`stdout: ${stdout}`)); + console.log(chalk.red(`stderr: ${stderr}`)); + }); - supervisor.start(500, () => { - 'use strict'; - supervisor.on('status', (status) => { - console.log(chalk.white('Supervisor status update: ' + status)); - switch (status) { - case "Idle": - display.image(display.presets.smile); - break; - case "Installing": - display.image(display.presets.busy); - break; - case "Downloading": - display.image(display.presets.download); - break; - case "Starting": - display.image(display.presets.fwd); - break; - case "Stopping": - display.image(display.presets.stop); - break; - } - }); + supervisor.start(500, () => { + 'use strict'; + supervisor.on('status', (status) => { + console.log(chalk.white('Supervisor status update: ' + status)); + switch (status) { + case "Idle": + display.image(display.presets.smile); + break; + case "Installing": + display.image(display.presets.busy); + break; + case "Downloading": + display.image(display.presets.download); + break; + case "Starting": + display.image(display.presets.fwd); + break; + case "Stopping": + display.image(display.presets.stop); + break; + } }); + }); - emoji.start(() => { - 'use strict'; - emoji.on('emoji', (emoji) => { - console.log(chalk.magenta('new emoji received! applying...')); - display.image(emoji); - }); - emoji.on('reset', (emoji) => { - console.log(chalk.magenta('emoji reset request received! applying...')); - display.image(display.presets.smile); - }); + emoji.start(() => { + 'use strict'; + emoji.on('emoji', (emoji) => { + console.log(chalk.magenta('new emoji received! applying...')); + display.image(emoji); + }); + emoji.on('reset', (emoji) => { + console.log(chalk.magenta('emoji reset request received! applying...')); + display.image(display.presets.smile); }); + }); } diff --git a/app/libs/emoji/index.js b/app/libs/emoji/index.js index aef9b21..4a47335 100644 --- a/app/libs/emoji/index.js +++ b/app/libs/emoji/index.js @@ -1,98 +1,98 @@ #!/bin/env node { - const EventEmitter = require('events').EventEmitter; - const util = require('util'); - const express = require('express'); - const serveStatic = require('serve-static'); - const compression = require('compression'); - const path = require('path'); - const mime = require('mime'); - const debug = require('debug')('http'); - const bodyParser = require("body-parser"); - const _ = require('lodash'); - const app = express(); - let self; + const EventEmitter = require('events').EventEmitter; + const util = require('util'); + const express = require('express'); + const serveStatic = require('serve-static'); + const compression = require('compression'); + const path = require('path'); + const mime = require('mime'); + const debug = require('debug')('http'); + const bodyParser = require("body-parser"); + const _ = require('lodash'); + const app = express(); + let self; - errorHandler = (err, req, res, next) => { - 'use strict'; - res.status(500); - res.render('error', { - error: err - }); - }; - app.use(compression()); - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ - extended: true - })); - app.use(function(req, res, next) { - 'use strict'; - res.header("Access-Control-Allow-Origin", "*"); - res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); - res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); - next(); + errorHandler = (err, req, res, next) => { + 'use strict'; + res.status(500); + res.render('error', { + error: err }); - app.use(errorHandler); + }; + app.use(compression()); + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ + extended: true + })); + app.use(function(req, res, next) { + 'use strict'; + res.header("Access-Control-Allow-Origin", "*"); + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + next(); + }); + app.use(errorHandler); - // declaring server - let server = function() { - 'use strict'; - if (!(this instanceof server)) return new server(); - this.port = parseInt(process.env.WEB_SERVER_PORT) || 8888; - self = this; - }; - util.inherits(server, EventEmitter); + // declaring server + let server = function() { + 'use strict'; + if (!(this instanceof server)) return new server(); + this.port = parseInt(process.env.WEB_SERVER_PORT) || 8888; + self = this; + }; + util.inherits(server, EventEmitter); - server.prototype.start = function(callback) { - 'use strict'; + server.prototype.start = function(callback) { + 'use strict'; - app.use(serveStatic(__dirname + '/public', { - 'index': ['index.html'] - })); + app.use(serveStatic(__dirname + '/public', { + 'index': ['index.html'] + })); - app.post('/v1/draw/:emoji', (req, res) => { - // Draws the Emoji on the LED Display - if (!req.params.emoji) { - return res.status(400).send('Bad Request'); - } - let emoji = req.params.emoji.split(","); - let emojiParsed = []; - _.forEach(emoji, function(value) { - emojiParsed.push(parseInt(value)); - }); - self.emit("emoji", emojiParsed); - res.status(200).send('OK'); - }); + app.post('/v1/draw/:emoji', (req, res) => { + // Draws the Emoji on the LED Display + if (!req.params.emoji) { + return res.status(400).send('Bad Request'); + } + let emoji = req.params.emoji.split(","); + let emojiParsed = []; + _.forEach(emoji, function(value) { + emojiParsed.push(parseInt(value)); + }); + self.emit("emoji", emojiParsed); + res.status(200).send('OK'); + }); - app.put('/v1/draw', (req, res) => { - // Draws the Emoji on the LED Display - self.emit("reset"); - res.status(200).send('OK'); - }); + app.put('/v1/draw', (req, res) => { + // Draws the Emoji on the LED Display + self.emit("reset"); + res.status(200).send('OK'); + }); - app.delete('/v1/draw', (req, res) => { - // Clears the LED Dipsplay - let emoji = [ - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 + app.delete('/v1/draw', (req, res) => { + // Clears the LED Dipsplay + let emoji = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 - ]; - self.emit("emoji", emoji); - res.status(200).send('OK'); - }); + ]; + self.emit("emoji", emoji); + res.status(200).send('OK'); + }); - app.listen(self.port, (req, res) => { - callback(); - }); + app.listen(self.port, (req, res) => { + callback(); + }); - }; + }; - module.exports = server(); + module.exports = server(); } diff --git a/app/libs/emoji/public/css/style.css b/app/libs/emoji/public/css/style.css old mode 100755 new mode 100644 diff --git a/app/libs/emoji/public/index.html b/app/libs/emoji/public/index.html old mode 100755 new mode 100644 diff --git a/app/libs/emoji/public/js/logic.js b/app/libs/emoji/public/js/logic.js old mode 100755 new mode 100644 diff --git a/app/libs/emoji/public/js/main.js b/app/libs/emoji/public/js/main.js old mode 100755 new mode 100644 diff --git a/app/libs/ledmatrix/index.js b/app/libs/ledmatrix/index.js index 28ab26a..c3c43ef 100644 --- a/app/libs/ledmatrix/index.js +++ b/app/libs/ledmatrix/index.js @@ -120,6 +120,17 @@ 0, 0, 0, 1, 1, 0, 0, 0, ], + "justboom": [ + 1, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 0, + 0, 0, 1, 0, 1, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 1, 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ], + "blank": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -146,8 +157,7 @@ clearInterval(self.blinking); self.image(self.presets.blank); } - matrix.writeArray(img.reverse()); - img.reverse(); + matrix.writeArray(img); }; display.prototype.startBlink = function(img, interval) { 'use strict'; diff --git a/app/libs/ledmatrix/libs/ht16k33.js b/app/libs/ledmatrix/libs/ht16k33.js index 08360a0..a104e02 100644 --- a/app/libs/ledmatrix/libs/ht16k33.js +++ b/app/libs/ledmatrix/libs/ht16k33.js @@ -122,7 +122,7 @@ for (var i in self.write_buffer) { self.wire.write(Buffer([i, self.write_buffer[i]]), (err) => { if (err) { - + console.log(err); } }); } diff --git a/app/package.json b/app/package.json index 209dc97..abde019 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "boombeastic", - "version": "0.4.0", + "version": "0.5.0", "description": "A Raspberry Pi based smart fleet of connected speakers!", "main": "index.js", "directories": { @@ -41,7 +41,7 @@ }, "homepage": "https://github.com/resin-io-playground/boombeastic#readme", "jshintConfig": { - "esnext": true, + "esversion": 6, "strict": true }, "dependencies": { diff --git a/app/src/app.coffee b/app/src/app.coffee index a5a4940..4586699 100644 --- a/app/src/app.coffee +++ b/app/src/app.coffee @@ -1,14 +1,12 @@ -Promise = require 'bluebird' -fs = Promise.promisifyAll(require('fs')) express = require 'express' bodyParser = require 'body-parser' -config = require './config' - -utils = require './utils' connman = require './connman' hotspot = require './hotspot' +networkManager = require './networkManager' +systemd = require './systemd' wifiScan = require './wifi-scan' +config = require './config' app = express() @@ -18,6 +16,19 @@ app.use(express.static(__dirname + '/public')) ssids = [] +error = (e) -> + console.log(e) + if retry + console.log('Retrying') + console.log('Clearing credentials') + manager.clearCredentials() + .then(run) + .catch(error) + else + console.log('Not retrying') + console.log('Exiting') + process.exit() + app.get '/ssids', (req, res) -> res.json(ssids) @@ -25,45 +36,92 @@ app.post '/connect', (req, res) -> if not (req.body.ssid? and req.body.passphrase?) return res.sendStatus(400) - console.log('Selected ' + req.body.ssid) - res.send('OK') - data = """ - [service_home_ethernet] - Type = ethernet - Nameservers = 8.8.8.8,8.8.4.4 - - [service_home_wifi] - Type = wifi - Name = #{req.body.ssid} - Passphrase = #{req.body.passphrase} - Nameservers = 8.8.8.8,8.8.4.4 - - """ - - Promise.all [ - utils.durableWriteFile(config.connmanConfig, data) - hotspot.stop() - ] - # XXX: make it so this delay isn't needed - .delay(1000) + hotspot.stop(manager, device) .then -> - connman.waitForConnection(15000) - .then -> - utils.durableWriteFile(config.persistentConfig, data) - .then -> - process.exit() - .catch (e) -> - hotspot.start() + manager.setCredentials(req.body.ssid, req.body.passphrase) + .then(run) + .catch(error) app.use (req, res) -> res.redirect('/') -wifiScan.scanAsync() -.then (results) -> - ssids = results - - hotspot.start() +run = -> + manager.isSetup() + .then (setup) -> + if setup + console.log('Credentials found') + hotspot.stop(manager, device) + .then -> + console.log('Connecting') + manager.connect(config.connectTimeout) + .then -> + console.log('Connected') + console.log('Exiting') + process.exit() + .catch(error) + else + console.log('Credentials not found') + hotspot.stop(manager, device) + .then -> + wifiScan.scanAsync() + .then (results) -> + ssids = results + hotspot.start(manager, device) + .catch(error) app.listen(80) + +retry = true +clear = true +device = null +manager = null + +if process.argv[2] == '--clear=true' + console.log('Clear is enabled') + clear = true +else if process.argv[2] == '--clear=false' + console.log('Clear is disabled') + clear = false +else if not process.argv[2]? + console.log('No clear flag passed') + console.log('Clear is enabled') +else + console.log('Invalid clear flag passed') + console.log('Exiting') + process.exit() + +device = process.env.RESIN_DEVICE_TYPE +if !device + device = process.env.DEVICE_TYPE + if !device + console.log('Device type not found - did you set the DEVICE_TYPE environment variable?') + console.log('Exiting') + process.exit() +console.log('Device type is ' + device) + +systemd.exists('NetworkManager.service') +.then (result) -> + if result + console.log('Using NetworkManager.service') + manager = networkManager + else + console.log('Using connman.service') + manager = connman +.then -> + if clear + console.log('Clearing credentials') + manager.clearCredentials() +.then -> + manager.isSetup() + .then (setup) -> + if setup + retry = false +.then -> + manager.ready() +.then(run) +.catch (e) -> + console.log(e) + console.log('Exiting') + process.exit() diff --git a/app/src/config.coffee b/app/src/config.coffee index 58bcff1..a117866 100644 --- a/app/src/config.coffee +++ b/app/src/config.coffee @@ -6,3 +6,4 @@ module.exports = dhcpRange: process.env.PORTAL_DHCP_RANGE or '192.168.42.2,192.168.42.254' connmanConfig: process.env.PORTAL_CONNMAN_CONFIG or '/host/var/lib/connman/network.config' persistentConfig: process.env.PORTAL_PERSISTENT_CONFIG or '/data/network.config' + connectTimeout: process.env.CONNECT_TIMEOUT or 15000 diff --git a/app/src/connman.coffee b/app/src/connman.coffee index 6389e09..216ee31 100644 --- a/app/src/connman.coffee +++ b/app/src/connman.coffee @@ -1,37 +1,78 @@ Promise = require 'bluebird' DBus = require './dbus-promise' - +fs = Promise.promisifyAll(require('fs')) dbus = new DBus() - bus = dbus.getBus('system') +_ = require 'lodash' + +config = require './config' +systemd = require './systemd' +utils = require './utils' SERVICE = 'net.connman' WIFI_OBJECT = '/net/connman/technology/wifi' TECHNOLOGY_INTERFACE = 'net.connman.Technology' -exports.waitForConnection = (timeout) -> - console.log('Waiting for connman to connect..') +exports.start = -> + systemd.start('connman.service') + +exports.stop = -> + systemd.stop('connman.service') + +exports.ready = -> + systemd.waitUntilState('connman.service', 'active') + +exports.isSetup = -> + fs.statAsync(config.persistentConfig) + .then -> + utils.copyFile(config.persistentConfig, config.connmanConfig) + .return(true) + .catchReturn(false) + +exports.setCredentials = (ssid, passphrase) -> + connection = """ + [service_home_ethernet] + Type = ethernet + Nameservers = 8.8.8.8,8.8.4.4 + + [service_home_wifi] + Type = wifi + Name = #{ssid} + Passphrase = #{passphrase} + Nameservers = 8.8.8.8,8.8.4.4 + + """ + + console.log('Saving connection') + console.log(connection) + + utils.durableWriteFile(config.persistentConfig, connection) + +exports.clearCredentials = -> + fs.unlinkAsync(config.persistentConfig) + .catch(code: 'ENOENT', _.noop) +exports.connect = (timeout) -> bus.getInterfaceAsync(SERVICE, WIFI_OBJECT, TECHNOLOGY_INTERFACE) - .then (wifi) -> - new Promise (resolve, reject, onCancel) -> + .then (manager) -> + new Promise (resolve, reject) -> handler = (name, value) -> if name is 'Connected' and value is true - wifi.removeListener('PropertyChanged', handler) + manager.removeListener('PropertyChanged', handler) resolve() # Listen for 'Connected' signals - wifi.on('PropertyChanged', handler) + manager.on('PropertyChanged', handler) - # # But try to read in case we registered the event handler - # # after is was already connected - wifi.GetPropertiesAsync() + # But try to read in case we registered the event handler + # after is was already connected + manager.GetPropertiesAsync() .then ({ Connected }) -> if Connected - wifi.removeListener('PropertyChanged', handler) + manager.removeListener('PropertyChanged', handler) resolve() setTimeout -> - wifi.removeListener('PropertyChanged', handler) - reject() + manager.removeListener('PropertyChanged', handler) + reject(new Error('Timed out')) , timeout diff --git a/app/src/dbus-promise.coffee b/app/src/dbus-promise.coffee index 7251d7e..f31d380 100644 --- a/app/src/dbus-promise.coffee +++ b/app/src/dbus-promise.coffee @@ -1,6 +1,5 @@ Promise = require 'bluebird' DBus = require 'dbus' - Bus = require 'dbus/lib/bus' Interface = require 'dbus/lib/interface' diff --git a/app/src/dnsmasq.coffee b/app/src/dnsmasq.coffee index fa6cd62..aea1b3e 100644 --- a/app/src/dnsmasq.coffee +++ b/app/src/dnsmasq.coffee @@ -30,6 +30,8 @@ exports.stop = -> return Promise.resolve() new Promise (resolve, reject) -> + console.log('Stopping dnsmasq..') + ps.kill('SIGTERM') timeout = setTimeout -> diff --git a/app/src/hostapd.coffee b/app/src/hostapd.coffee index f2c7114..2d225fa 100644 --- a/app/src/hostapd.coffee +++ b/app/src/hostapd.coffee @@ -37,6 +37,8 @@ exports.stop = -> return Promise.resolve() new Promise (resolve, reject) -> + console.log('Stopping hostapd..') + ps.kill('SIGTERM') timeout = setTimeout -> diff --git a/app/src/hotspot.coffee b/app/src/hotspot.coffee index 051425b..4a14464 100644 --- a/app/src/hotspot.coffee +++ b/app/src/hotspot.coffee @@ -3,23 +3,23 @@ Promise = require 'bluebird' execAsync = Promise.promisify(exec) config = require './config' - hostapd = require './hostapd' dnsmasq = require './dnsmasq' -systemd = require './systemd' +modprobe = require './modprobe' started = false -exports.start = -> +exports.start = (manager, device) -> if started return Promise.resolve() started = true - console.log('Stopping connman..') + console.log('Starting hotspot') - systemd.stop('connman.service') - .delay(2000) + modprobe.hotspot(device) + .then -> + manager.stop() .then -> execAsync('rfkill unblock wifi') .then -> @@ -29,16 +29,23 @@ exports.start = -> hostapd.start() .then -> dnsmasq.start() + .then -> + console.log('Started hotspot') -exports.stop = -> +exports.stop = (manager, device) -> if not started return Promise.resolve() started = false - Promise.all [ + console.log('Stopping hotspot') + + modprobe.normal(device) + .then -> hostapd.stop() + .then -> dnsmasq.stop() - ] .then -> - systemd.start('connman.service') + manager.start() + .then -> + console.log('Stopped hotspot') diff --git a/app/src/modprobe.coffee b/app/src/modprobe.coffee new file mode 100644 index 0000000..dcd05ab --- /dev/null +++ b/app/src/modprobe.coffee @@ -0,0 +1,23 @@ +Promise = require 'bluebird' +{ spawn, exec } = require 'child_process' +execAsync = Promise.promisify(exec) + +exports.hotspot = (device) -> + switch device + when 'intel-edison', 'edison' + run('modprobe -r bcm4334x', 'modprobe bcm4334x op_mode=2 firmware_path="firmware/intel-edison/fw_bcmdhd.bin" nvram_path="firmware/intel-edison/bcmdhd.cal"') + else return Promise.resolve() + +exports.normal = (device) -> + switch device + when 'intel-edison', 'edison' + run('modprobe -r bcm4334x', 'modprobe bcm4334x firmware_path="firmware/intel-edison/fw_bcmdhd.bin" nvram_path="firmware/intel-edison/bcmdhd.cal"') + else return Promise.resolve() + +run = (disable, enable) -> + console.log('Loading kernel module: ' + enable) + execAsync(disable) + .delay(1000) + .then -> + execAsync(enable) + .delay(1000) diff --git a/app/src/networkManager.coffee b/app/src/networkManager.coffee new file mode 100644 index 0000000..943e18a --- /dev/null +++ b/app/src/networkManager.coffee @@ -0,0 +1,136 @@ +Promise = require 'bluebird' +{ spawn, exec } = require 'child_process' +execAsync = Promise.promisify(exec) +DBus = require './dbus-promise' +_ = require 'lodash' + +dbus = new DBus() +bus = dbus.getBus('system') + +systemd = require './systemd' + +SERVICE = 'org.freedesktop.NetworkManager' + +# This allows us to say, if there IS an existing connection id that is not in the whitelist then network manager has been set up previously. +WHITE_LIST = ['resin-sample', 'Wired connection 1'] + +NM_STATE_CONNECTED_GLOBAL = 70 +NM_DEVICE_TYPE_WIFI = 2 +NM_CONNECTIVITY_LIMITED = 4 +NM_CONNECTIVITY_FULL = 5 + +exports.start = -> + systemd.start('NetworkManager.service') + +exports.stop = -> + systemd.stop('NetworkManager.service') + +exports.ready = -> + systemd.waitUntilState('NetworkManager.service', 'active') + +exports.isSetup = -> + getConnections() + .map(isConnectionValid) + .then (results) -> + return true in results + +exports.setCredentials = (ssid, passphrase) -> + connection = { + '802-11-wireless': { + ssid: _.invokeMap(ssid, 'charCodeAt') + }, + connection: { + id: ssid, + type: '802-11-wireless', + }, + '802-11-wireless-security': { + 'auth-alg': 'open', + 'key-mgmt': 'wpa-psk', + 'psk': passphrase, + } + } + + console.log('Saving connection') + console.log(connection) + + bus.getInterfaceAsync(SERVICE, '/org/freedesktop/NetworkManager/Settings', 'org.freedesktop.NetworkManager.Settings') + .then (settings) -> + settings.AddConnectionAsync(connection) + .then -> + execAsync('sync') + +exports.clearCredentials = -> + getConnections() + .map(deleteConnection) + +exports.connect = (timeout) -> + getDevices() + .filter(isDeviceValid) + .then (validDevices) -> + if validDevices.length is 0 + throw new Error('No valid devices found.') + getConnections() + .filter(isConnectionValid) + .then (validConnections) -> + if validConnections.length is 0 + throw new Error('No valid connections found.') + bus.getInterfaceAsync(SERVICE, '/org/freedesktop/NetworkManager', 'org.freedesktop.NetworkManager') + .delay(1000) # Delay needed to avoid "Error: org.freedesktop.NetworkManager.UnknownConnection at Error (native)" when activating the connection + .then (manager) -> + manager.ActivateConnectionAsync(validConnections[0], validDevices[0], '/') + .then -> + new Promise (resolve, reject) -> + handler = (value) -> + if value == NM_STATE_CONNECTED_GLOBAL + manager.removeListener('StateChanged', handler) + resolve() + + # Listen for 'Connected' signals + manager.on('StateChanged', handler) + + # But try to read in case we registered the event handler + # after is was already connected + manager.CheckConnectivityAsync() + .then (state) -> + if state == NM_CONNECTIVITY_FULL or state == NM_CONNECTIVITY_LIMITED + manager.removeListener('StateChanged', handler) + resolve() + + setTimeout -> + manager.removeListener('StateChanged', handler) + reject(new Error('Timed out')) + , timeout + +getConnections = -> + bus.getInterfaceAsync(SERVICE, '/org/freedesktop/NetworkManager/Settings', 'org.freedesktop.NetworkManager.Settings') + .call('ListConnectionsAsync') + +getConnection = (connection) -> + bus.getInterfaceAsync(SERVICE, connection, 'org.freedesktop.NetworkManager.Settings.Connection') + +deleteConnection = (connection) -> + getConnection(connection) + .then (connection) -> + connection.GetSettingsAsync() + .then (settings) -> + if settings.connection.id not in WHITE_LIST + connection.DeleteAsync() + +isConnectionValid = (connection) -> + getConnection(connection) + .call('GetSettingsAsync') + .then (settings) -> + return settings.connection.id not in WHITE_LIST + +getDevices = -> + bus.getInterfaceAsync(SERVICE, '/org/freedesktop/NetworkManager', 'org.freedesktop.NetworkManager') + .call('GetDevicesAsync') + +getDevice = (device) -> + bus.getInterfaceAsync(SERVICE, device, 'org.freedesktop.NetworkManager.Device') + +isDeviceValid = (device) -> + getDevice(device) + .call('getPropertyAsync', 'DeviceType') + .then (property) -> + return property == NM_DEVICE_TYPE_WIFI diff --git a/app/src/public/js/index.js b/app/src/public/js/index.js index b928c58..efa9f44 100644 --- a/app/src/public/js/index.js +++ b/app/src/public/js/index.js @@ -5,7 +5,7 @@ $(function(){ $('#no-networks-message').removeClass('hidden'); } else { $.each(data, function(i, val){ - $("#ssid-select").append(""); + $("#ssid-select").append($('