├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── docs ├── fuge-logo.png ├── presentation.pptx ├── screen.png ├── step0.graffle ├── step0.png ├── step1.graffle ├── step1.png ├── step2.graffle ├── step2.png ├── step4.graffle ├── step4.png ├── step5.graffle ├── step5.png ├── targe.graffle └── target.png ├── package.json ├── step0 ├── README.md └── frontend │ ├── api │ ├── index.js │ ├── package.json │ └── webStream.js │ └── public │ ├── index.html │ ├── js │ ├── bundle.js │ ├── chart.js │ └── package.json │ └── lib │ ├── d3.min.js │ ├── jquery.js │ ├── lodash.min.js │ ├── rickshaw.min.css │ └── rickshaw.min.js ├── step1 ├── README.md ├── frontend │ ├── api │ │ ├── index.js │ │ ├── package.json │ │ └── webStream.js │ └── public │ │ ├── index.html │ │ ├── js │ │ ├── bundle.js │ │ ├── chart.js │ │ └── package.json │ │ └── lib │ │ ├── d3.min.js │ │ ├── jquery.js │ │ ├── lodash.min.js │ │ ├── rickshaw.min.css │ │ └── rickshaw.min.js └── services │ ├── influx │ ├── run.bat │ └── run.sh │ └── serializer │ ├── influxUtil.js │ ├── package.json │ ├── serializer.js │ └── test │ └── test.js ├── step2 ├── README.md ├── frontend │ ├── .babelrc │ ├── .csscomb.json │ ├── .csslintrc │ ├── .editorconfig │ ├── .eslintrc │ ├── .flowconfig │ ├── .gitattributes │ ├── .jscsrc │ ├── .npmignore │ ├── .travis.yml │ ├── api │ │ ├── index.js │ │ ├── package.json │ │ └── webStream.js │ └── public │ │ ├── index.html │ │ ├── js │ │ ├── bundle.js │ │ ├── chart.js │ │ └── package.json │ │ └── lib │ │ ├── d3.min.js │ │ ├── jquery.js │ │ ├── lodash.min.js │ │ ├── rickshaw.min.css │ │ └── rickshaw.min.js └── services │ ├── influx │ ├── run.bat │ └── run.sh │ └── serializer │ ├── influxUtil.js │ ├── package.json │ ├── run.bat │ ├── run.sh │ ├── serializer.js │ ├── test │ └── test.js │ ├── testWrite.bat │ └── testWrite.sh ├── step3 ├── README.md ├── frontend │ ├── .babelrc │ ├── .csscomb.json │ ├── .csslintrc │ ├── .editorconfig │ ├── .eslintrc │ ├── .flowconfig │ ├── .gitattributes │ ├── .jscsrc │ ├── .npmignore │ ├── .travis.yml │ ├── Dockerfile │ ├── api │ │ ├── index.js │ │ ├── package.json │ │ └── webStream.js │ ├── public │ │ ├── index.html │ │ ├── js │ │ │ ├── bundle.js │ │ │ ├── chart.js │ │ │ └── package.json │ │ └── lib │ │ │ ├── d3.min.js │ │ │ ├── jquery.js │ │ │ ├── lodash.min.js │ │ │ ├── rickshaw.min.css │ │ │ └── rickshaw.min.js │ ├── run.bat │ └── run.sh ├── fuge │ ├── compose-dev.yml │ └── fuge-config.js └── services │ ├── influx │ ├── run.bat │ └── run.sh │ └── serializer │ ├── Dockerfile │ ├── influxUtil.js │ ├── package.json │ ├── run.bat │ ├── run.sh │ ├── serializer.js │ ├── test │ └── test.js │ ├── testWrite.bat │ └── testWrite.sh ├── step4 ├── README.md ├── frontend │ ├── .babelrc │ ├── .csscomb.json │ ├── .csslintrc │ ├── .editorconfig │ ├── .eslintrc │ ├── .flowconfig │ ├── .gitattributes │ ├── .jscsrc │ ├── .npmignore │ ├── .travis.yml │ ├── Dockerfile │ ├── api │ │ ├── index.js │ │ ├── package.json │ │ └── webStream.js │ └── public │ │ ├── index.html │ │ ├── js │ │ ├── bundle.js │ │ ├── chart.js │ │ └── package.json │ │ └── lib │ │ ├── d3.min.js │ │ ├── jquery.js │ │ ├── lodash.min.js │ │ ├── rickshaw.min.css │ │ └── rickshaw.min.js ├── fuge │ ├── compose-dev.yml │ └── fuge-config.js └── services │ ├── broker │ ├── Dockerfile │ ├── broker.js │ └── package.json │ ├── influx │ ├── run.bat │ └── run.sh │ ├── sensor │ ├── Dockerfile │ ├── package.json │ └── sensor.js │ └── serializer │ ├── Dockerfile │ ├── influxUtil.js │ ├── package.json │ ├── serializer.js │ ├── test │ └── test.js │ ├── testWrite.bat │ └── testWrite.sh ├── step5 ├── README.md ├── frontend │ ├── .babelrc │ ├── .csscomb.json │ ├── .csslintrc │ ├── .editorconfig │ ├── .eslintrc │ ├── .flowconfig │ ├── .gitattributes │ ├── .jscsrc │ ├── .npmignore │ ├── .travis.yml │ ├── Dockerfile │ ├── api │ │ ├── index.js │ │ ├── package.json │ │ └── webStream.js │ └── public │ │ ├── index.html │ │ ├── js │ │ ├── bundle.js │ │ ├── chart.js │ │ └── package.json │ │ └── lib │ │ ├── d3.min.js │ │ ├── jquery.js │ │ ├── lodash.min.js │ │ ├── rickshaw.min.css │ │ └── rickshaw.min.js ├── fuge │ ├── compose-dev.yml │ └── fuge-config.js └── services │ ├── actuator │ ├── Dockerfile │ ├── actuator.js │ └── package.json │ ├── broker │ ├── Dockerfile │ ├── broker.js │ └── package.json │ ├── influx │ ├── run.bat │ └── run.sh │ ├── sensor │ ├── Dockerfile │ ├── package.json │ └── sensor.js │ └── serializer │ ├── Dockerfile │ ├── influxUtil.js │ ├── package.json │ ├── serializer.js │ ├── test │ └── test.js │ ├── testWrite.bat │ └── testWrite.sh ├── step6 ├── README.md ├── frontend │ ├── .babelrc │ ├── .csscomb.json │ ├── .csslintrc │ ├── .editorconfig │ ├── .eslintrc │ ├── .flowconfig │ ├── .gitattributes │ ├── .jscsrc │ ├── .npmignore │ ├── .travis.yml │ ├── Dockerfile │ ├── api │ │ ├── index.js │ │ ├── package.json │ │ └── webStream.js │ └── public │ │ ├── index.html │ │ ├── js │ │ ├── bundle.js │ │ ├── chart.js │ │ └── package.json │ │ └── lib │ │ ├── d3.min.js │ │ ├── jquery.js │ │ ├── lodash.min.js │ │ ├── rickshaw.min.css │ │ └── rickshaw.min.js ├── fuge │ ├── compose-dev.yml │ └── fuge-config.js └── services │ ├── actuator │ ├── Dockerfile │ ├── actuator.js │ └── package.json │ ├── broker │ ├── Dockerfile │ ├── broker.js │ └── package.json │ ├── influx │ ├── run.bat │ └── run.sh │ ├── sensor │ ├── Dockerfile │ ├── package.json │ └── sensor.js │ └── serializer │ ├── Dockerfile │ ├── influxUtil.js │ ├── package.json │ ├── serializer.js │ ├── test │ └── test.js │ ├── testWrite.bat │ └── testWrite.sh ├── step7 ├── README.md ├── frontend │ ├── .babelrc │ ├── .csscomb.json │ ├── .csslintrc │ ├── .editorconfig │ ├── .eslintrc │ ├── .flowconfig │ ├── .gitattributes │ ├── .jscsrc │ ├── .npmignore │ ├── .travis.yml │ ├── Dockerfile │ ├── api │ │ ├── index.js │ │ ├── package.json │ │ └── webStream.js │ └── public │ │ ├── index.html │ │ ├── js │ │ ├── bundle.js │ │ ├── chart.js │ │ └── package.json │ │ └── lib │ │ ├── d3.min.js │ │ ├── jquery.js │ │ ├── lodash.min.js │ │ ├── rickshaw.min.css │ │ └── rickshaw.min.js ├── fuge │ ├── compose-dev.yml │ ├── docker-compose.yml │ ├── env │ └── fuge-config.js └── services │ ├── actuator │ ├── Dockerfile │ ├── actuator.js │ └── package.json │ ├── broker │ ├── Dockerfile │ ├── broker.js │ └── package.json │ ├── influx │ ├── run.bat │ └── run.sh │ ├── sensor │ ├── Dockerfile │ ├── package.json │ └── sensor.js │ └── serializer │ ├── Dockerfile │ ├── influxUtil.js │ ├── package.json │ ├── serializer.js │ ├── test │ └── test.js │ ├── testWrite.bat │ └── testWrite.sh └── step8 ├── README.md ├── frontend ├── .babelrc ├── .csscomb.json ├── .csslintrc ├── .editorconfig ├── .eslintrc ├── .flowconfig ├── .gitattributes ├── .jscsrc ├── .npmignore ├── .travis.yml ├── Dockerfile ├── api │ ├── index.js │ ├── package.json │ └── webStream.js └── public │ ├── index.html │ ├── js │ ├── bundle.js │ ├── chart.js │ └── package.json │ └── lib │ ├── d3.min.js │ ├── jquery.js │ ├── lodash.min.js │ ├── rickshaw.min.css │ └── rickshaw.min.js ├── fuge ├── compose-dev.yml ├── docker-compose.yml ├── env └── fuge-config.js └── services ├── actuator ├── Dockerfile ├── actuator.js └── package.json ├── broker ├── Dockerfile ├── broker.js └── package.json ├── influx ├── run.bat └── run.sh ├── sensor ├── Dockerfile ├── package.json └── sensor.js └── serializer ├── Dockerfile ├── influxUtil.js ├── package.json ├── serializer.js ├── test └── test.js ├── testWrite.bat └── testWrite.sh /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | pids 11 | logs 12 | results 13 | node_modules 14 | npm-debug.log 15 | mochahelper.js 16 | .idea/ 17 | .settings/ 18 | dist 19 | .tmp 20 | .sass-cache 21 | app/bower_components 22 | options.mine.js 23 | db/ 24 | data/ 25 | .DS_Store 26 | log 27 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "white": false, 3 | "node": true, 4 | "browser": true, 5 | "esnext": true, 6 | "bitwise": true, 7 | "camelcase": true, 8 | "curly": true, 9 | "eqeqeq": true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef" : "nofunc", 13 | "newcap": true, 14 | "noarg": true, 15 | "quotmark": "single", 16 | "regexp": true, 17 | "undef": true, 18 | "unused": true, 19 | "strict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "globals": { 23 | "document": true, 24 | "$":true, 25 | "_":true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 the microbial-lab team 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /docs/fuge-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/fuge-logo.png -------------------------------------------------------------------------------- /docs/presentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/presentation.pptx -------------------------------------------------------------------------------- /docs/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/screen.png -------------------------------------------------------------------------------- /docs/step0.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step0.graffle -------------------------------------------------------------------------------- /docs/step0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step0.png -------------------------------------------------------------------------------- /docs/step1.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step1.graffle -------------------------------------------------------------------------------- /docs/step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step1.png -------------------------------------------------------------------------------- /docs/step2.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step2.graffle -------------------------------------------------------------------------------- /docs/step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step2.png -------------------------------------------------------------------------------- /docs/step4.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step4.graffle -------------------------------------------------------------------------------- /docs/step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step4.png -------------------------------------------------------------------------------- /docs/step5.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step5.graffle -------------------------------------------------------------------------------- /docs/step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/step5.png -------------------------------------------------------------------------------- /docs/targe.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/targe.graffle -------------------------------------------------------------------------------- /docs/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/micro-services-tutorial-iot/c436b6e50d74d98282fe45c2ebc234e75620c363/docs/target.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microservices-tutorial-iot", 3 | "version": "1.0.0", 4 | "description": "Microservices Tutorial IoT", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/nearform/micro-services-tutorial-iot" 14 | }, 15 | "dependencies": { 16 | "end-of-stream": "1.1.x", 17 | "express": "4.13.x", 18 | "influx": "4.1.x", 19 | "lodash": "4.2.x", 20 | "moment": "2.11.x", 21 | "mosca": "1.0.x", 22 | "mqtt": "1.7.x", 23 | "seneca": "1.1.x", 24 | "websocket-stream": "3.1.x" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /step0/README.md: -------------------------------------------------------------------------------- 1 | # Step 0 2 | 3 | ![image](../docs/step0.png) 4 | 5 | In this step we have a simple front end for our system. Lets start off by installing some dependencies and running it. Do the following: 6 | 7 | If you haven't done so already, start by running `npm install` in the top level directory of the _micro-services-tutorial-iot_ repo. Then run the frontend as follows: 8 | 9 | 1. `cd frontend/api` 10 | 2. `node index.js` 11 | 12 | Point your browser to http://localhost:10001. You should see a chart. Simple! 13 | 14 | ## Challenge 15 | Next we are going to start up our database. To do this we are going to take advantage of Docker. If you haven't done this already you can fetch this container by running: 16 | 17 | ``` 18 | docker pull tutum/influxdb 19 | ``` 20 | 21 | Documentation on how to start the container can be found here: https://hub.docker.com/r/tutum/influxdb/ 22 | 23 | Your challenge is to startup the influxDB container. Once you have it running successfully open your browser and check out the influx web interface. 24 | 25 | ## Next Up [step1](../step1/README.md) 26 | -------------------------------------------------------------------------------- /step0/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var app = express(); 5 | var http = require('http').Server(app); 6 | var webStream = require('./webStream')(http); 7 | var i = 0; 8 | 9 | app.use('/', express.static(__dirname + '/../public')); 10 | 11 | 12 | 13 | setInterval(function() { 14 | var randInt = Math.floor(Math.random() * 100); 15 | var temp = Math.round((Math.sin(i++ / 40) + 4) * (randInt + 200)); 16 | webStream.emit([{time: (new Date()).getTime(), sensorId: '1', temperature: temp}]); 17 | }, 1000); 18 | 19 | 20 | 21 | http.listen(10001, function(){ 22 | console.log('listening on *:10001'); 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /step0/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "end-of-stream": "1.1.x", 13 | "express": "4.13.x", 14 | "lodash": "4.2.x", 15 | "websocket-stream": "3.1.x" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /step0/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | }); 18 | }); 19 | }; 20 | 21 | 22 | 23 | var handleStream = function(stream) { 24 | stream.id = streamCounter++; 25 | streams[stream.id] = stream; 26 | 27 | eos(stream, function () { 28 | delete streams[stream.id]; 29 | }); 30 | }; 31 | 32 | 33 | 34 | websocket.createServer({server: http}, handleStream); 35 | 36 | return { 37 | emit: emit 38 | }; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /step0/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /step0/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | $(document).ready(function() { 80 | initChart(); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /step0/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "websocket-stream": "3.1.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step0/frontend/public/lib/rickshaw.min.css: -------------------------------------------------------------------------------- 1 | .rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} -------------------------------------------------------------------------------- /step1/README.md: -------------------------------------------------------------------------------- 1 | # Step 1 2 | 3 | ## Solution to step 0 4 | 5 | 1. The container can be started with `docker run -d -p 8083:8083 -p 8086:8086 tutum/influxdb` 6 | 2. The files in step1/services/influx/ contain commands to start the influx container for your convenience. 7 | 3. Run `docker-machine ip default` to obtain the docker-machine ip address 8 | 4. Point your browser to http://\:8083/ to open the influx console 9 | 10 | The `-p` argument exposes ports 8083 and 8086 from the container to the host. The `-d` argument tells docker to run the container in [detached mode](https://docs.docker.com/engine/reference/run/#detached-d). 11 | 12 | Note that when using docker locally, you are always dealing with a separate VM, with it's own IP address (at least on Mac and Windows). 13 | 14 | You can stop the container at any time by using the `docker kill` command. 15 | 16 | ## Challenge 17 | ![image](../docs/step1.png) 18 | 19 | Now that we have our database running, we are going to create a micro-service to read and write to it. A serialization service has been created for you in step1/services/serializer. 20 | 21 | Your challenge is to write a small script to start this process up and use it to write temperature values into influx DB. Once the service is up and running you can use the following command to send data points to the service. 22 | 23 | ```sh 24 | curl -X POST -d '{"role": "serialize", "cmd": "write", "sensorId": "1", "temperature": 32}' http://localhost:10000/act --header "Content-Type:application/json" 25 | ``` 26 | 27 | __hint__ If you look at the code in `serializer.js` you will notice that it uses the following environment variables: 28 | 29 | * INFLUX_HOST 30 | * serializer_PORT 31 | 32 | Your startup script will need to set these variables to the correct values. 33 | 34 | You can check that the data points are indeed written to influx by pointing your browser to the influx web interface and running this query: 35 | 36 | ``` 37 | use temperature; 38 | select * from temperature; 39 | ``` 40 | 41 | ## Next Up [step2](../step2/README.md) 42 | -------------------------------------------------------------------------------- /step1/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var app = express(); 5 | var http = require('http').Server(app); 6 | var webStream = require('./webStream')(http); 7 | var i = 0; 8 | 9 | app.use('/', express.static(__dirname + '/../public')); 10 | 11 | 12 | 13 | setInterval(function() { 14 | var randInt = Math.floor(Math.random() * 100); 15 | var temp = Math.round((Math.sin(i++ / 40) + 4) * (randInt + 200)); 16 | webStream.emit([{time: (new Date()).getTime(), sensorId: '1', temperature: temp}]); 17 | }, 1000); 18 | 19 | 20 | 21 | http.listen(10001, function(){ 22 | console.log('listening on *:10001'); 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /step1/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "end-of-stream": "1.1.x", 13 | "express": "4.13.x", 14 | "lodash": "4.2.x", 15 | "websocket-stream": "3.1.x" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /step1/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | }); 18 | }); 19 | }; 20 | 21 | 22 | 23 | var handleStream = function(stream) { 24 | stream.id = streamCounter++; 25 | streams[stream.id] = stream; 26 | 27 | eos(stream, function () { 28 | delete streams[stream.id]; 29 | }); 30 | }; 31 | 32 | 33 | 34 | websocket.createServer({server: http}, handleStream); 35 | 36 | return { 37 | emit: emit 38 | }; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /step1/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /step1/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | $(document).ready(function() { 80 | initChart(); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /step1/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "websocket-stream": "3.1.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step1/frontend/public/lib/rickshaw.min.css: -------------------------------------------------------------------------------- 1 | .rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} -------------------------------------------------------------------------------- /step1/services/influx/run.bat: -------------------------------------------------------------------------------- 1 | docker run -d -p 8083:8083 -p 8086:8086 tutum/influxdb 2 | -------------------------------------------------------------------------------- /step1/services/influx/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run -d -p 8083:8083 -p 8086:8086 tutum/influxdb 3 | -------------------------------------------------------------------------------- /step1/services/serializer/influxUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (client) { 4 | 5 | const writePoint = function (sensorId, temperature, cb) { 6 | client.writePoint('temperature', {sensorId: sensorId, temperature: temperature}, {}, cb); 7 | }; 8 | 9 | 10 | 11 | const readPoints = function (sensorId, start, end, cb) { 12 | const query = `select * from temperature where sensorId='${sensorId}' and time > '${start}' and time < '${end}'`; 13 | client.query(query, cb); 14 | }; 15 | 16 | 17 | 18 | return { 19 | writePoint, 20 | readPoints 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /step1/services/serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "serializer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "influx": "4.1.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step1/services/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Seneca = require('seneca'); 4 | const influx = require('influx'); 5 | const influxUtil = require('./influxUtil'); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | var createDatabase = function (cb) { 11 | setTimeout(() => { 12 | const initDb = influx({host: process.env.INFLUX_HOST, username: 'root', password: 'root'}); 13 | initDb.createDatabase('temperature', (err) => { 14 | if (err) { 15 | console.error(`ERROR: ${err}`); 16 | } 17 | 18 | cb(); 19 | }); 20 | }, 3000); 21 | }; 22 | 23 | 24 | 25 | createDatabase(() => { 26 | var db = influx({host: process.env.INFLUX_HOST, username: 'root', password: 'root', database: 'temperature'}); 27 | var ifx = influxUtil(db); 28 | 29 | seneca.add({role: 'serialize', cmd: 'read'}, (args, cb) => { 30 | ifx.readPoints(args.sensorId, args.start, args.end, cb); 31 | }); 32 | 33 | 34 | seneca.add({role: 'serialize', cmd: 'write'}, (args, cb) => { 35 | ifx.writePoint(args.sensorId, args.temperature, cb); 36 | }); 37 | 38 | 39 | seneca.listen({port: process.env.serializer_PORT}); 40 | }); 41 | 42 | 43 | module.exports.seneca = seneca; 44 | -------------------------------------------------------------------------------- /step1/services/serializer/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.serializer_PORT = 3001; 4 | process.env.INFLUX_HOST = '192.168.59.103'; 5 | 6 | var assert = require('assert'); 7 | var ser = require('../serializer'); 8 | 9 | describe('read write test', function () { 10 | 11 | it('should write data to influx', function (done){ 12 | ser.seneca.act({role: 'serialize', cmd: 'write', sensorId: '1', temperature: 32}, function (err) { 13 | assert(!err); 14 | done(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /step2/README.md: -------------------------------------------------------------------------------- 1 | # Step 2 2 | 3 | ## solution to step 1 4 | 5 | 1. The influx container can be started with the scripts provided in step2/services/influx 6 | 2. The folder step2/services/serializer contains the code for the serialization service 7 | 2. Start this service with the script step2/services/serializer/run.sh (or run.bat) 8 | 3. Send some test data using the script step2/services/serializer/testWrite.sh (or testWrite.bat) 9 | 4. use the influx console to view data points 10 | 11 | __note__ The serializer code is a seneca micro-service. Seneca provides an abstraction layer over various transport mechanisms including TCP, HTTP, RabbitMQ, Redis, NATS, etc... In this tutorial we are using HTTP as the transport mechanism and sending JSON based messages. 12 | 13 | ## Challenge 14 | ![image](../docs/step2.png) 15 | 16 | The next thing we will need to do is to hook up our front end to our serialization service in order to read data values for charting. 17 | 18 | An updated front end that talks to the serialization service is provided in step2/frontend. 19 | 20 | Your challenge is to start up influxDB, the frontend and the serialization service. To do this you will need to write a startup script for the frontend service in the same way as the serialization service. 21 | 22 | Once you have these up and running use the testWrite.sh script to send data to influx and see it appear in the front end. 23 | 24 | __hint__ If you look at the updated frontend code you will see that it uses the following environment variables 25 | 26 | * PROXY_HOST 27 | * serializer_PORT 28 | * frontend_PORT 29 | 30 | Your script will need to set these values prior to starting the frontend. 31 | 32 | ## Next Up [step3](../step3/README.md) 33 | -------------------------------------------------------------------------------- /step2/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /step2/frontend/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /step2/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /step2/frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "__DEV__": true, 13 | "__SERVER__": true 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "rules": { 19 | // Strict mode 20 | "strict": [2, "never"], 21 | 22 | // Code style 23 | "indent": [2, 2], 24 | "quotes": [2, "single"], 25 | 26 | // React 27 | "react/display-name": 0, 28 | "react/jsx-boolean-value": 1, 29 | "react/jsx-no-duplicate-props": 1, 30 | "react/jsx-no-undef": 1, 31 | "react/jsx-quotes": 1, 32 | "react/jsx-sort-prop-types": 0, 33 | "react/jsx-sort-props": 0, 34 | "react/jsx-uses-react": 1, 35 | "react/jsx-uses-vars": 1, 36 | "react/no-danger": 0, 37 | "react/no-did-mount-set-state": 1, 38 | "react/no-did-update-set-state": 1, 39 | "react/no-multi-comp": 1, 40 | "react/no-unknown-property": 1, 41 | "react/prop-types": 1, 42 | "react/react-in-jsx-scope": 1, 43 | "react/require-extension": 1, 44 | "react/self-closing-comp": 1, 45 | "react/sort-comp": 1, 46 | "react/wrap-multilines": 1 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /step2/frontend/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build 3 | .*/config 4 | .*/node_modules 5 | .*/gulpfile.js 6 | 7 | [include] 8 | -------------------------------------------------------------------------------- /step2/frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.less text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.txt text eol=lf 19 | *.xml text eol=lf 20 | -------------------------------------------------------------------------------- /step2/frontend/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "validateIndentation": 2, 6 | "excludeFiles": ["build/**", "node_modules/**"], 7 | "esprima": "esprima-fb" 8 | } 9 | -------------------------------------------------------------------------------- /step2/frontend/.npmignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /step2/frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | - '0.12' 5 | - '0.10' 6 | matrix: 7 | allow_failures: 8 | - node_js: iojs 9 | - node_js: '0.12' 10 | -------------------------------------------------------------------------------- /step2/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var seneca = require('seneca')(); 5 | var app = express(); 6 | var http = require('http').Server(app); 7 | var webStream = require('./webStream')(http); 8 | var moment = require('moment'); 9 | var _ = require('lodash'); 10 | 11 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'read'}}); 12 | app.use('/', express.static(__dirname + '/../public')); 13 | 14 | var lastEmitted = 0; 15 | setInterval(function() { 16 | seneca.act({role: 'serialize', cmd: 'read', sensorId: '1', start: moment().subtract(10, 'minutes').utc().format(), end: moment().utc().format()}, function(err, data) { 17 | var toEmit = []; 18 | 19 | _.each(data[0], function(point) { 20 | if (moment(point.time).unix() > lastEmitted) { 21 | lastEmitted = moment(point.time).unix(); 22 | point.time = (new Date(point.time)).getTime(); 23 | toEmit.push(point); 24 | } 25 | }); 26 | if (toEmit.length > 0) { 27 | console.log('will emit'); 28 | console.log(toEmit); 29 | webStream.emit(toEmit); 30 | } 31 | }); 32 | }, 1000); 33 | 34 | 35 | http.listen(process.env.frontend_PORT, function(){ 36 | console.log('listening on *:' + process.env.frontend_PORT); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /step2/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "end-of-stream": "1.1.x", 13 | "express": "4.13.x", 14 | "lodash": "4.2.x", 15 | "moment": "2.11.x", 16 | "seneca": "1.1.x", 17 | "websocket-stream": "3.1.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /step2/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | console.log('data sent'); 18 | }); 19 | }); 20 | }; 21 | 22 | 23 | 24 | var handleStream = function(stream) { 25 | stream.id = streamCounter++; 26 | streams[stream.id] = stream; 27 | 28 | eos(stream, function () { 29 | delete streams[stream.id]; 30 | }); 31 | }; 32 | 33 | 34 | 35 | websocket.createServer({server: http}, handleStream); 36 | 37 | return { 38 | emit: emit 39 | }; 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /step2/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /step2/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | $(document).ready(function() { 80 | initChart(); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /step2/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "websocket-stream": "3.1.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step2/frontend/public/lib/rickshaw.min.css: -------------------------------------------------------------------------------- 1 | .rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} -------------------------------------------------------------------------------- /step2/services/influx/run.bat: -------------------------------------------------------------------------------- 1 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 2 | -------------------------------------------------------------------------------- /step2/services/influx/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 3 | -------------------------------------------------------------------------------- /step2/services/serializer/influxUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (client) { 4 | 5 | const writePoint = function (sensorId, temperature, cb) { 6 | client.writePoint('temperature', {sensorId: sensorId, temperature: temperature}, {}, cb); 7 | }; 8 | 9 | 10 | 11 | const readPoints = function (sensorId, start, end, cb) { 12 | const query = `select * from temperature where sensorId='${sensorId}' and time > '${start}' and time < '${end}'`; 13 | client.query(query, cb); 14 | }; 15 | 16 | 17 | 18 | return { 19 | writePoint, 20 | readPoints 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /step2/services/serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "serializer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "influx": "4.1.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step2/services/serializer/run.bat: -------------------------------------------------------------------------------- 1 | set serializer_PORT=10000 2 | docker-machine ip default > dock 3 | set /p INFLUX_HOST= < dock 4 | del dock 5 | node serializer.js 6 | -------------------------------------------------------------------------------- /step2/services/serializer/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export serializer_PORT=10000 3 | export INFLUX_HOST=$(docker-machine ip default) 4 | node serializer.js 5 | -------------------------------------------------------------------------------- /step2/services/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Seneca = require('seneca'); 4 | const influx = require('influx'); 5 | const influxUtil = require('./influxUtil'); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | var createDatabase = function (cb) { 11 | setTimeout(() => { 12 | const initDb = influx({host: process.env.INFLUX_HOST, username: 'root', password: 'root'}); 13 | initDb.createDatabase('temperature', (err) => { 14 | if (err) { 15 | console.error(`ERROR: ${err}`); 16 | } 17 | 18 | cb(); 19 | }); 20 | }, 3000); 21 | }; 22 | 23 | 24 | 25 | createDatabase(() => { 26 | var db = influx({host: process.env.INFLUX_HOST, username: 'root', password: 'root', database: 'temperature'}); 27 | var ifx = influxUtil(db); 28 | 29 | seneca.add({role: 'serialize', cmd: 'read'}, (args, cb) => { 30 | ifx.readPoints(args.sensorId, args.start, args.end, cb); 31 | }); 32 | 33 | 34 | seneca.add({role: 'serialize', cmd: 'write'}, (args, cb) => { 35 | ifx.writePoint(args.sensorId, args.temperature, cb); 36 | }); 37 | 38 | 39 | seneca.listen({port: process.env.serializer_PORT}); 40 | }); 41 | 42 | 43 | module.exports.seneca = seneca; 44 | -------------------------------------------------------------------------------- /step2/services/serializer/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SERVICE_HOST = 'localhost'; 4 | process.env.SERVICE_PORT = 3001; 5 | process.env.INFLUX_HOST = '192.168.59.103'; 6 | 7 | var assert = require('assert'); 8 | var ser = require('../serializer'); 9 | 10 | describe('read write test', function() { 11 | 12 | it('should write data to influx', function(done){ 13 | ser.seneca.act({role: 'serialize', cmd: 'write', sensorId: '1', temperature: 32}, function(err) { 14 | assert(!err); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /step2/services/serializer/testWrite.bat: -------------------------------------------------------------------------------- 1 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 123}" --header "Content-Type:application/json" http://localhost:10000/act 2 | -------------------------------------------------------------------------------- /step2/services/serializer/testWrite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": $RANDOM}" http://localhost:10000/act --header "Content-Type:application/json" 3 | 4 | -------------------------------------------------------------------------------- /step3/README.md: -------------------------------------------------------------------------------- 1 | # Step 3 2 | 3 | ## solution to step 2 4 | 5 | 1. The influx container can be started with the scripts provided in step3/services/influx 6 | 2. Start the serialization service with the script step3/services/serializer/run.sh (or run.bat) 7 | 3. Start the frontend with the script step3/frontend/run.sh (or run.bat) 8 | 4. open the frontend at http://localhost:10001/ 9 | 5. Send some test data using the script step3/services/serializer/testWrite.sh (or testWrite.bat) 10 | 6. The data points should appear on the frontend chart 11 | 12 | ## Challenge 13 | ![image](../docs/fuge-logo.png) 14 | 15 | Right now we only have 3 moving parts in our system, but already it is becoming a pain to manage. In this step, your challenge is to get the system running using Fuge. Fuge is a micro-service development environment that helps ease the process of local development running processes and docker containers. 16 | 17 | The folder step3/fuge contains two files: 18 | 19 | * compose-dev.yml - a docker compose format file that specifies the processes that make up our system 20 | * fuge-config.json - global fuge settings 21 | 22 | Your challenge is to run the system using the fuge shell. You can find some documentation on fuge here: [https://github.com/apparatus/fuge](https://github.com/apparatus/fuge). Once you have the system started up you can check that everything is working by using the script step3/services/serializer/testWrite.sh (or testWrite.bat). You should see data coming through to the frontend chart. 23 | 24 | __hint__ you will need to stop all of the previously running processes and containers first. 25 | 26 | __hint__ make sure that you have installed fuge using `npm install -g fuge`. 27 | 28 | __hint__ you can start the fuge shell using the `fuge shell` command 29 | 30 | 31 | ## Next Up [step4](../step4/README.md) 32 | -------------------------------------------------------------------------------- /step3/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /step3/frontend/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /step3/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /step3/frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "__DEV__": true, 13 | "__SERVER__": true 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "rules": { 19 | // Strict mode 20 | "strict": [2, "never"], 21 | 22 | // Code style 23 | "indent": [2, 2], 24 | "quotes": [2, "single"], 25 | 26 | // React 27 | "react/display-name": 0, 28 | "react/jsx-boolean-value": 1, 29 | "react/jsx-no-duplicate-props": 1, 30 | "react/jsx-no-undef": 1, 31 | "react/jsx-quotes": 1, 32 | "react/jsx-sort-prop-types": 0, 33 | "react/jsx-sort-props": 0, 34 | "react/jsx-uses-react": 1, 35 | "react/jsx-uses-vars": 1, 36 | "react/no-danger": 0, 37 | "react/no-did-mount-set-state": 1, 38 | "react/no-did-update-set-state": 1, 39 | "react/no-multi-comp": 1, 40 | "react/no-unknown-property": 1, 41 | "react/prop-types": 1, 42 | "react/react-in-jsx-scope": 1, 43 | "react/require-extension": 1, 44 | "react/self-closing-comp": 1, 45 | "react/sort-comp": 1, 46 | "react/wrap-multilines": 1 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /step3/frontend/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build 3 | .*/config 4 | .*/node_modules 5 | .*/gulpfile.js 6 | 7 | [include] 8 | -------------------------------------------------------------------------------- /step3/frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.less text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.txt text eol=lf 19 | *.xml text eol=lf 20 | -------------------------------------------------------------------------------- /step3/frontend/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "validateIndentation": 2, 6 | "excludeFiles": ["build/**", "node_modules/**"], 7 | "esprima": "esprima-fb" 8 | } 9 | -------------------------------------------------------------------------------- /step3/frontend/.npmignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /step3/frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | - '0.12' 5 | - '0.10' 6 | matrix: 7 | allow_failures: 8 | - node_js: iojs 9 | - node_js: '0.12' 10 | -------------------------------------------------------------------------------- /step3/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN cd api && npm install --ignore-scripts 4 | CMD node api/index.js 5 | -------------------------------------------------------------------------------- /step3/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var seneca = require('seneca')(); 5 | var app = express(); 6 | var http = require('http').Server(app); 7 | var webStream = require('./webStream')(http); 8 | var moment = require('moment'); 9 | var _ = require('lodash'); 10 | 11 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'read'}}); 12 | app.use('/', express.static(__dirname + '/../public')); 13 | 14 | var lastEmitted = 0; 15 | setInterval(function() { 16 | seneca.act({role: 'serialize', cmd: 'read', sensorId: '1', start: moment().subtract(10, 'minutes').utc().format(), end: moment().utc().format()}, function(err, data) { 17 | var toEmit = []; 18 | 19 | _.each(data[0], function(point) { 20 | if (moment(point.time).unix() > lastEmitted) { 21 | lastEmitted = moment(point.time).unix(); 22 | point.time = (new Date(point.time)).getTime(); 23 | toEmit.push(point); 24 | } 25 | }); 26 | if (toEmit.length > 0) { 27 | console.log('will emit'); 28 | console.log(toEmit); 29 | webStream.emit(toEmit); 30 | } 31 | else { 32 | console.log('.'); 33 | } 34 | }); 35 | }, 1000); 36 | 37 | 38 | 39 | http.listen(process.env.frontend_PORT, function(){ 40 | console.log('listening on *:' + process.env.frontend_PORT); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /step3/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "end-of-stream": "1.1.x", 13 | "express": "4.13.x", 14 | "lodash": "4.2.x", 15 | "moment": "2.11.x", 16 | "seneca": "1.1.x", 17 | "websocket-stream": "3.1.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /step3/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | console.log('data sent'); 18 | }); 19 | }); 20 | }; 21 | 22 | 23 | 24 | var handleStream = function(stream) { 25 | stream.id = streamCounter++; 26 | streams[stream.id] = stream; 27 | 28 | eos(stream, function () { 29 | delete streams[stream.id]; 30 | }); 31 | }; 32 | 33 | 34 | 35 | websocket.createServer({server: http}, handleStream); 36 | 37 | return { 38 | emit: emit 39 | }; 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /step3/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /step3/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | $(document).ready(function() { 80 | initChart(); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /step3/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "moment": "2.11.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step3/frontend/public/lib/rickshaw.min.css: -------------------------------------------------------------------------------- 1 | .rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} -------------------------------------------------------------------------------- /step3/frontend/run.bat: -------------------------------------------------------------------------------- 1 | set PROXY_HOST=127.0.0.1 2 | set serializer_PORT=10000 3 | set frontend_PORT=10001 4 | node api/index.js 5 | -------------------------------------------------------------------------------- /step3/frontend/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export PROXY_HOST=127.0.0.1 3 | export serializer_PORT=10000 4 | export frontend_PORT=10001 5 | cd api 6 | node index.js 7 | 8 | -------------------------------------------------------------------------------- /step3/fuge/compose-dev.yml: -------------------------------------------------------------------------------- 1 | influx: 2 | image: tutum/influxdb 3 | ports: 4 | - 8086:8086 5 | - 8083:8083 6 | serializer: 7 | build: ../services/serializer/ 8 | container_name: serializer 9 | frontend: 10 | build: ../frontend/ 11 | container_name: frontend 12 | -------------------------------------------------------------------------------- /step3/fuge/fuge-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // run docker containers if false containers with image attribute will not be run 4 | runDocker: true, 5 | 6 | // proxy settings - one of docker | process | all | none 7 | proxy: 'docker', 8 | 9 | // if true tail running process to the shell by default 10 | tail: true, 11 | 12 | // if true monitor running processes for changes by default 13 | monitor: true, 14 | 15 | // exclude these patterns from the monitor 16 | exclude: /node_modules|\.git|\.log/mgi, 17 | 18 | // override section. Allows the default build, run and debug commands 19 | // to be overriden on a service by service basis. These commands are 20 | // normally generated by inspecting the Dockerfile for a service 21 | /* 22 | overrides: { 23 | service1: { build: 'sh build.sh' } 24 | } 25 | */ 26 | }; 27 | -------------------------------------------------------------------------------- /step3/services/influx/run.bat: -------------------------------------------------------------------------------- 1 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 2 | -------------------------------------------------------------------------------- /step3/services/influx/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 3 | -------------------------------------------------------------------------------- /step3/services/serializer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node serializer.js 5 | 6 | -------------------------------------------------------------------------------- /step3/services/serializer/influxUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (client) { 4 | 5 | const writePoint = function (sensorId, temperature, cb) { 6 | client.writePoint('temperature', {sensorId: sensorId, temperature: temperature}, {}, cb); 7 | }; 8 | 9 | 10 | 11 | const readPoints = function (sensorId, start, end, cb) { 12 | const query = `select * from temperature where sensorId='${sensorId}' and time > '${start}' and time < '${end}'`; 13 | client.query(query, cb); 14 | }; 15 | 16 | 17 | 18 | return { 19 | writePoint, 20 | readPoints 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /step3/services/serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "serializer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "influx": "4.1.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step3/services/serializer/run.bat: -------------------------------------------------------------------------------- 1 | set serializer_PORT=10000 2 | docker-machine ip default > dock 3 | set /p PROXY_HOST= < dock 4 | del dock 5 | node serializer.js 6 | -------------------------------------------------------------------------------- /step3/services/serializer/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export serializer_PORT=10000 3 | export PROXY_HOST=$(docker-machine ip default) 4 | node serializer.js 5 | -------------------------------------------------------------------------------- /step3/services/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Seneca = require('seneca'); 4 | const influx = require('influx'); 5 | const influxUtil = require('./influxUtil'); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | var createDatabase = function (cb) { 11 | setTimeout(() => { 12 | const initDb = influx({host: process.env.PROXY_HOST, username: 'root', password: 'root'}); 13 | initDb.createDatabase('temperature', (err) => { 14 | if (err) { 15 | console.error(`ERROR: ${err}`); 16 | } 17 | 18 | cb(); 19 | }); 20 | }, 3000); 21 | }; 22 | 23 | 24 | 25 | createDatabase(() => { 26 | var db = influx({host: process.env.PROXY_HOST, username: 'root', password: 'root', database: 'temperature'}); 27 | var ifx = influxUtil(db); 28 | 29 | seneca.add({role: 'serialize', cmd: 'read'}, (args, cb) => { 30 | ifx.readPoints(args.sensorId, args.start, args.end, cb); 31 | }); 32 | 33 | 34 | seneca.add({role: 'serialize', cmd: 'write'}, (args, cb) => { 35 | ifx.writePoint(args.sensorId, args.temperature, cb); 36 | }); 37 | 38 | 39 | seneca.listen({port: process.env.serializer_PORT}); 40 | }); 41 | 42 | 43 | module.exports.seneca = seneca; 44 | -------------------------------------------------------------------------------- /step3/services/serializer/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SERVICE_HOST = 'localhost'; 4 | process.env.SERVICE_PORT = 3001; 5 | process.env.INFLUX_HOST = '192.168.59.103'; 6 | 7 | var assert = require('assert'); 8 | var ser = require('../serializer'); 9 | 10 | describe('read write test', function() { 11 | 12 | it('should write data to influx', function(done){ 13 | ser.seneca.act({role: 'serialize', cmd: 'write', sensorId: '1', temperature: 32}, function(err) { 14 | assert(!err); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /step3/services/serializer/testWrite.bat: -------------------------------------------------------------------------------- 1 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 123}" --header "Content-Type:application/json" http://localhost:10000/act 2 | -------------------------------------------------------------------------------- /step3/services/serializer/testWrite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": $RANDOM}" http://localhost:10000/act --header "Content-Type:application/json" 3 | 4 | -------------------------------------------------------------------------------- /step4/README.md: -------------------------------------------------------------------------------- 1 | # Step 4 2 | 3 | ## solution to step 3 4 | 5 | 1. the folder step4/fuge contains configuration files for the fuge shell 6 | 2. start fuge up by running `fuge shell fuge/compose-dev.yml` 7 | 3. start the system in the shell by running `start all` 8 | 4. open http://localhost:10001 to view the chart 9 | 5. use the script step4/services/serializer/testWrite.sh to send some data to the serialization service 10 | 11 | __note__ Fuge is now running a mixture of processes and docker containers. It does this by: 12 | 13 | * injecting environment variables into each process 14 | * running an internal proxy to bridge network connections between processes and containers 15 | 16 | ## Challenge 17 | ![image](../docs/step4.png) 18 | 19 | Now that we have our serializer service running, lets add in the dummy sensor and 20 | our MQTT broker. The code for the sensor is in step4/services/sensor and for the 21 | broker in step4/services/broker. 22 | 23 | Your challenge is to add this into the fuge yml file and get the system running. 24 | Once you do this you should be able to start the front end, influx and your 25 | microservices from the fuge shell and see data streaming from the sensor to the 26 | front end. 27 | 28 | ## Next Up [step5](../step5/README.md) 29 | -------------------------------------------------------------------------------- /step4/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /step4/frontend/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /step4/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /step4/frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "__DEV__": true, 13 | "__SERVER__": true 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "rules": { 19 | // Strict mode 20 | "strict": [2, "never"], 21 | 22 | // Code style 23 | "indent": [2, 2], 24 | "quotes": [2, "single"], 25 | 26 | // React 27 | "react/display-name": 0, 28 | "react/jsx-boolean-value": 1, 29 | "react/jsx-no-duplicate-props": 1, 30 | "react/jsx-no-undef": 1, 31 | "react/jsx-quotes": 1, 32 | "react/jsx-sort-prop-types": 0, 33 | "react/jsx-sort-props": 0, 34 | "react/jsx-uses-react": 1, 35 | "react/jsx-uses-vars": 1, 36 | "react/no-danger": 0, 37 | "react/no-did-mount-set-state": 1, 38 | "react/no-did-update-set-state": 1, 39 | "react/no-multi-comp": 1, 40 | "react/no-unknown-property": 1, 41 | "react/prop-types": 1, 42 | "react/react-in-jsx-scope": 1, 43 | "react/require-extension": 1, 44 | "react/self-closing-comp": 1, 45 | "react/sort-comp": 1, 46 | "react/wrap-multilines": 1 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /step4/frontend/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build 3 | .*/config 4 | .*/node_modules 5 | .*/gulpfile.js 6 | 7 | [include] 8 | -------------------------------------------------------------------------------- /step4/frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.less text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.txt text eol=lf 19 | *.xml text eol=lf 20 | -------------------------------------------------------------------------------- /step4/frontend/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "validateIndentation": 2, 6 | "excludeFiles": ["build/**", "node_modules/**"], 7 | "esprima": "esprima-fb" 8 | } 9 | -------------------------------------------------------------------------------- /step4/frontend/.npmignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /step4/frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | - '0.12' 5 | - '0.10' 6 | matrix: 7 | allow_failures: 8 | - node_js: iojs 9 | - node_js: '0.12' 10 | -------------------------------------------------------------------------------- /step4/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN cd api && npm install --ignore-scripts 4 | CMD node api/index.js 5 | -------------------------------------------------------------------------------- /step4/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var seneca = require('seneca')(); 5 | var app = express(); 6 | var http = require('http').Server(app); 7 | var webStream = require('./webStream')(http); 8 | var moment = require('moment'); 9 | var _ = require('lodash'); 10 | 11 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'read'}}); 12 | app.use('/', express.static(__dirname + '/../public')); 13 | 14 | var lastEmitted = 0; 15 | setInterval(function() { 16 | seneca.act({role: 'serialize', cmd: 'read', sensorId: '1', start: moment().subtract(10, 'minutes').utc().format(), end: moment().utc().format()}, function(err, data) { 17 | var toEmit = []; 18 | 19 | _.each(data[0], function(point) { 20 | if (moment(point.time).unix() > lastEmitted) { 21 | lastEmitted = moment(point.time).unix(); 22 | point.time = (new Date(point.time)).getTime(); 23 | toEmit.push(point); 24 | } 25 | }); 26 | if (toEmit.length > 0) { 27 | console.log('will emit'); 28 | console.log(toEmit); 29 | webStream.emit(toEmit); 30 | } 31 | }); 32 | }, 1000); 33 | 34 | 35 | 36 | http.listen(process.env.frontend_PORT, function(){ 37 | console.log('listening on *:' + process.env.frontend_PORT); 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /step4/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "end-of-stream": "1.1.x", 13 | "express": "4.13.x", 14 | "lodash": "4.2.x", 15 | "moment": "2.11.x", 16 | "seneca": "1.1.x", 17 | "websocket-stream": "3.1.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /step4/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | console.log('data sent'); 18 | }); 19 | }); 20 | }; 21 | 22 | 23 | 24 | var handleStream = function(stream) { 25 | stream.id = streamCounter++; 26 | streams[stream.id] = stream; 27 | 28 | eos(stream, function () { 29 | delete streams[stream.id]; 30 | }); 31 | }; 32 | 33 | 34 | 35 | websocket.createServer({server: http}, handleStream); 36 | 37 | return { 38 | emit: emit 39 | }; 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /step4/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /step4/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | $(document).ready(function() { 80 | initChart(); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /step4/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "websocket-stream": "3.1.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step4/frontend/public/lib/rickshaw.min.css: -------------------------------------------------------------------------------- 1 | .rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px} -------------------------------------------------------------------------------- /step4/fuge/compose-dev.yml: -------------------------------------------------------------------------------- 1 | influx: 2 | image: tutum/influxdb 3 | ports: 4 | - 8086:8086 5 | - 8083:8083 6 | serializer: 7 | build: ../services/serializer/ 8 | container_name: serializer 9 | frontend: 10 | build: ../frontend/ 11 | container_name: frontend 12 | -------------------------------------------------------------------------------- /step4/fuge/fuge-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // run docker containers if false containers with image attribute will not be run 4 | runDocker: true, 5 | 6 | // proxy settings - one of docker | process | all | none 7 | proxy: 'docker', 8 | 9 | // if true tail running process to the shell by default 10 | tail: true, 11 | 12 | // if true monitor running processes for changes by default 13 | monitor: true, 14 | 15 | // exclude these patterns from the monitor 16 | exclude: /node_modules|\.git|\.log/mgi, 17 | 18 | // override section. Allows the default build, run and debug commands 19 | // to be overriden on a service by service basis. These commands are 20 | // normally generated by inspecting the Dockerfile for a service 21 | /* 22 | overrides: { 23 | service1: { build: 'sh build.sh' } 24 | } 25 | */ 26 | }; 27 | -------------------------------------------------------------------------------- /step4/services/broker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node broker.js 5 | -------------------------------------------------------------------------------- /step4/services/broker/broker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mosca = require('mosca'); 4 | const Seneca = require('seneca'); 5 | const server = new mosca.Server({}); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'write'}}); 11 | 12 | 13 | 14 | function parse (body) { 15 | try { 16 | return JSON.parse(body); 17 | } 18 | catch (err) { 19 | return null; 20 | } 21 | } 22 | 23 | 24 | 25 | server.published = function (packet, client, cb) { 26 | var body; 27 | if (!packet.topic.match(/temperature\/[0-9]+\/read/)) { 28 | return cb(); 29 | } 30 | 31 | body = parse(packet.payload); 32 | 33 | body.role = 'serialize'; 34 | body.cmd = 'write'; 35 | seneca.act(body, cb); 36 | }; 37 | -------------------------------------------------------------------------------- /step4/services/broker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "broker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "broker.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mosca": "1.0.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step4/services/influx/run.bat: -------------------------------------------------------------------------------- 1 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 2 | -------------------------------------------------------------------------------- /step4/services/influx/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 3 | -------------------------------------------------------------------------------- /step4/services/sensor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node sensor.js 5 | -------------------------------------------------------------------------------- /step4/services/sensor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "driver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "sensor.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "1.7.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step4/services/sensor/sensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mqtt = require('mqtt'); 4 | 5 | const mqtt = Mqtt.connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 6 | let offset = 100; 7 | 8 | 9 | 10 | mqtt.on('connect', () => { 11 | mqtt.subscribe('temperature/1/set', () => { 12 | console.log('subscribed', arguments); 13 | }); 14 | }); 15 | 16 | 17 | 18 | mqtt.on('message', (topic, payload) => { 19 | console.log('message received'); 20 | try { 21 | offset = JSON.parse(payload).offset; 22 | console.log('new offset', offset); 23 | } 24 | catch (err) { 25 | console.error(err); 26 | } 27 | }); 28 | 29 | 30 | 31 | let i = 0; 32 | setInterval(() => { 33 | const randInt = Math.floor(Math.random() * 100); 34 | const temperature = Math.round((Math.sin(i++ / 40) + 4) * randInt + offset); 35 | 36 | mqtt.publish('temperature/1/read', JSON.stringify({sensorId: '1', temperature}), (err) => { 37 | if (err) { 38 | console.error(err); 39 | } 40 | }); 41 | }, 2000); 42 | -------------------------------------------------------------------------------- /step4/services/serializer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node serializer.js 5 | 6 | -------------------------------------------------------------------------------- /step4/services/serializer/influxUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (client) { 4 | 5 | const writePoint = function (sensorId, temperature, cb) { 6 | client.writePoint('temperature', {sensorId: sensorId, temperature: temperature}, {}, cb); 7 | }; 8 | 9 | 10 | 11 | const readPoints = function (sensorId, start, end, cb) { 12 | const query = `select * from temperature where sensorId='${sensorId}' and time > '${start}' and time < '${end}'`; 13 | client.query(query, cb); 14 | }; 15 | 16 | 17 | 18 | return { 19 | writePoint, 20 | readPoints 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /step4/services/serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "serializer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "influx": "4.1.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step4/services/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Seneca = require('seneca'); 4 | const influx = require('influx'); 5 | const influxUtil = require('./influxUtil'); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | var createDatabase = function (cb) { 11 | setTimeout(() => { 12 | const initDb = influx({host: process.env.PROXY_HOST, username: 'root', password: 'root'}); 13 | initDb.createDatabase('temperature', (err) => { 14 | if (err) { 15 | console.error(`ERROR: ${err}`); 16 | } 17 | 18 | cb(); 19 | }); 20 | }, 3000); 21 | }; 22 | 23 | 24 | 25 | createDatabase(() => { 26 | var db = influx({host: process.env.PROXY_HOST, username: 'root', password: 'root', database: 'temperature'}); 27 | var ifx = influxUtil(db); 28 | 29 | seneca.add({role: 'serialize', cmd: 'read'}, (args, cb) => { 30 | ifx.readPoints(args.sensorId, args.start, args.end, cb); 31 | }); 32 | 33 | 34 | seneca.add({role: 'serialize', cmd: 'write'}, (args, cb) => { 35 | ifx.writePoint(args.sensorId, args.temperature, cb); 36 | }); 37 | 38 | 39 | seneca.listen({port: process.env.serializer_PORT}); 40 | }); 41 | 42 | 43 | module.exports.seneca = seneca; 44 | -------------------------------------------------------------------------------- /step4/services/serializer/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SERVICE_HOST = 'localhost'; 4 | process.env.SERVICE_PORT = 3001; 5 | process.env.INFLUX_HOST = '192.168.59.103'; 6 | 7 | var assert = require('assert'); 8 | var ser = require('../serializer'); 9 | 10 | describe('read write test', function() { 11 | 12 | it('should write data to influx', function(done){ 13 | ser.seneca.act({role: 'serialize', cmd: 'write', sensorId: '1', temperature: 32}, function(err) { 14 | assert(!err); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /step4/services/serializer/testWrite.bat: -------------------------------------------------------------------------------- 1 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 123}" --header "Content-Type:application/json" http://localhost:10000/act 2 | -------------------------------------------------------------------------------- /step4/services/serializer/testWrite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": $RANDOM}" http://localhost:10000/act --header "Content-Type:application/json" 3 | 4 | -------------------------------------------------------------------------------- /step5/README.md: -------------------------------------------------------------------------------- 1 | # Step 5 2 | 3 | ## solution to step 4 4 | 5 | 1. the folder step5/fuge contains an updated fuge configuration 6 | 2. start fuge up by running `fuge shell fuge/compose-dev.yml` 7 | 3. start the system in the shell by running `start all` 8 | 4. open http://localhost:10001 to view the data points 9 | 5. data is now streaming from the sensor through the broker to the serialization 10 | service and being displayed on the front end 11 | 12 | 13 | ## Challenge 14 | ![image](../docs/step5.png) 15 | 16 | For the final service in our system your challenge is to wire up the actuator 17 | micro-service. The actuator service is provided for you in 18 | step5/services/actuator. 19 | 20 | The actuator service will send an offset message to the sensor via the MQTT 21 | broker. To enable the actuator you will need to add an entry into the fuge 22 | configuration file `compose-dev.yml` as before 23 | 24 | __Note__: A button and input field have been provided on the front end to call 25 | the `/set` route for you. 26 | 27 | Once you have your config ready, restart the system and point your browser to 28 | the front end. You should see data flowing as before but now you should be able 29 | to send an offset message and see the chart change in real time. 30 | 31 | ## Next Up [step6](../step6/README.md) 32 | -------------------------------------------------------------------------------- /step5/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /step5/frontend/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /step5/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /step5/frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "__DEV__": true, 13 | "__SERVER__": true 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "rules": { 19 | // Strict mode 20 | "strict": [2, "never"], 21 | 22 | // Code style 23 | "indent": [2, 2], 24 | "quotes": [2, "single"], 25 | 26 | // React 27 | "react/display-name": 0, 28 | "react/jsx-boolean-value": 1, 29 | "react/jsx-no-duplicate-props": 1, 30 | "react/jsx-no-undef": 1, 31 | "react/jsx-quotes": 1, 32 | "react/jsx-sort-prop-types": 0, 33 | "react/jsx-sort-props": 0, 34 | "react/jsx-uses-react": 1, 35 | "react/jsx-uses-vars": 1, 36 | "react/no-danger": 0, 37 | "react/no-did-mount-set-state": 1, 38 | "react/no-did-update-set-state": 1, 39 | "react/no-multi-comp": 1, 40 | "react/no-unknown-property": 1, 41 | "react/prop-types": 1, 42 | "react/react-in-jsx-scope": 1, 43 | "react/require-extension": 1, 44 | "react/self-closing-comp": 1, 45 | "react/sort-comp": 1, 46 | "react/wrap-multilines": 1 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /step5/frontend/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build 3 | .*/config 4 | .*/node_modules 5 | .*/gulpfile.js 6 | 7 | [include] 8 | -------------------------------------------------------------------------------- /step5/frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.less text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.txt text eol=lf 19 | *.xml text eol=lf 20 | -------------------------------------------------------------------------------- /step5/frontend/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "validateIndentation": 2, 6 | "excludeFiles": ["build/**", "node_modules/**"], 7 | "esprima": "esprima-fb" 8 | } 9 | -------------------------------------------------------------------------------- /step5/frontend/.npmignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /step5/frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | - '0.12' 5 | - '0.10' 6 | matrix: 7 | allow_failures: 8 | - node_js: iojs 9 | - node_js: '0.12' 10 | -------------------------------------------------------------------------------- /step5/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN cd api && npm install --ignore-scripts 4 | CMD node api/index.js 5 | -------------------------------------------------------------------------------- /step5/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var seneca = require('seneca')(); 5 | var app = express(); 6 | var http = require('http').Server(app); 7 | var webStream = require('./webStream')(http); 8 | var moment = require('moment'); 9 | var _ = require('lodash'); 10 | 11 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'read'}}); 12 | seneca.client({host: process.env.PROXY_HOST, port: process.env.actuator_PORT, pin: {role: 'actuate', cmd: 'set'}}); 13 | 14 | app.use('/', express.static(__dirname + '/../public')); 15 | 16 | 17 | 18 | app.get('/set', function (req, res) { 19 | seneca.act({role: 'actuate', cmd: 'set', offset: req.query.offset}, function(err) { 20 | if (err) { 21 | res.json({result: err}); 22 | } 23 | else { 24 | res.json({result: 'ok'}); 25 | } 26 | res.end(); 27 | }); 28 | }); 29 | 30 | 31 | 32 | var lastEmitted = 0; 33 | setInterval(function() { 34 | seneca.act({role: 'serialize', cmd: 'read', sensorId: '1', start: moment().subtract(10, 'minutes').utc().format(), end: moment().utc().format()}, function(err, data) { 35 | var toEmit = []; 36 | 37 | _.each(data[0], function(point) { 38 | if (moment(point.time).unix() > lastEmitted) { 39 | lastEmitted = moment(point.time).unix(); 40 | point.time = (new Date(point.time)).getTime(); 41 | toEmit.push(point); 42 | } 43 | }); 44 | if (toEmit.length > 0) { 45 | webStream.emit(toEmit); 46 | } 47 | }); 48 | }, 1000); 49 | 50 | 51 | 52 | http.listen(process.env.frontend_PORT, function(){ 53 | console.log('listening on *:' + process.env.frontend_PORT); 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /step5/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "end-of-stream": "1.1.x", 13 | "express": "4.13.x", 14 | "lodash": "4.2.x", 15 | "moment": "2.11.x", 16 | "seneca": "1.1.x", 17 | "websocket-stream": "3.1.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /step5/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | }); 18 | }); 19 | }; 20 | 21 | 22 | 23 | var handleStream = function(stream) { 24 | stream.id = streamCounter++; 25 | streams[stream.id] = stream; 26 | 27 | eos(stream, function () { 28 | delete streams[stream.id]; 29 | }); 30 | }; 31 | 32 | 33 | 34 | websocket.createServer({server: http}, handleStream); 35 | 36 | return { 37 | emit: emit 38 | }; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /step5/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /step5/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | var initControls = function() { 80 | $('#setOffset').click(function() { 81 | $.get('/set?offset=' + $('#offset').val(), function() { 82 | }); 83 | }); 84 | }; 85 | 86 | 87 | 88 | $(document).ready(function() { 89 | initChart(); 90 | initControls(); 91 | }); 92 | 93 | -------------------------------------------------------------------------------- /step5/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "websocket-stream": "3.1.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step5/fuge/compose-dev.yml: -------------------------------------------------------------------------------- 1 | influx: 2 | image: tutum/influxdb 3 | ports: 4 | - 8086:8086 5 | - 8083:8083 6 | serializer: 7 | build: ../services/serializer/ 8 | container_name: serializer 9 | frontend: 10 | build: ../frontend/ 11 | container_name: frontend 12 | broker: 13 | build: ../services/broker/ 14 | container_name: broker 15 | sensor: 16 | build: ../services/sensor/ 17 | container_name: sensor 18 | -------------------------------------------------------------------------------- /step5/fuge/fuge-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // run docker containers if false containers with image attribute will not be run 4 | runDocker: true, 5 | 6 | // proxy settings - one of docker | process | all | none 7 | proxy: 'docker', 8 | 9 | // if true tail running process to the shell by default 10 | tail: true, 11 | 12 | // if true monitor running processes for changes by default 13 | monitor: true, 14 | 15 | // exclude these patterns from the monitor 16 | exclude: /node_modules|\.git|\.log/mgi, 17 | 18 | // override section. Allows the default build, run and debug commands 19 | // to be overriden on a service by service basis. These commands are 20 | // normally generated by inspecting the Dockerfile for a service 21 | /* 22 | overrides: { 23 | service1: { build: 'sh build.sh' } 24 | } 25 | */ 26 | }; 27 | -------------------------------------------------------------------------------- /step5/services/actuator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node actuator.js 5 | -------------------------------------------------------------------------------- /step5/services/actuator/actuator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mqtt = require('mqtt'); 4 | const Seneca = require('seneca'); 5 | 6 | const mqtt = require('mqtt').connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 7 | const seneca = Seneca(); 8 | 9 | 10 | 11 | seneca.add({role: 'actuate', cmd: 'set'}, (args, cb) => { 12 | const payload = JSON.stringify({'offset': parseInt(args.offset, 10) }); 13 | mqtt.publish('temperature/1/set', new Buffer(payload), {qos: 0, retain: true}, cb); 14 | }); 15 | 16 | seneca.listen({port: process.env.actuator_PORT}); 17 | -------------------------------------------------------------------------------- /step5/services/actuator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actuator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "actuator.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "1.7.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step5/services/broker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node broker.js 5 | -------------------------------------------------------------------------------- /step5/services/broker/broker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mosca = require('mosca'); 4 | const Seneca = require('seneca'); 5 | const server = new mosca.Server({}); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'write'}}); 11 | 12 | 13 | 14 | function parse (body) { 15 | try { 16 | return JSON.parse(body); 17 | } 18 | catch (err) { 19 | return null; 20 | } 21 | } 22 | 23 | 24 | 25 | server.published = function (packet, client, cb) { 26 | var body; 27 | if (!packet.topic.match(/temperature\/[0-9]+\/read/)) { 28 | return cb(); 29 | } 30 | 31 | body = parse(packet.payload); 32 | 33 | body.role = 'serialize'; 34 | body.cmd = 'write'; 35 | seneca.act(body, cb); 36 | }; 37 | -------------------------------------------------------------------------------- /step5/services/broker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "broker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "broker.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mosca": "1.0.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step5/services/influx/run.bat: -------------------------------------------------------------------------------- 1 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 2 | -------------------------------------------------------------------------------- /step5/services/influx/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 3 | -------------------------------------------------------------------------------- /step5/services/sensor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node sensor.js 5 | -------------------------------------------------------------------------------- /step5/services/sensor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "driver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "sensor.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "1.7.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step5/services/sensor/sensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mqtt = require('mqtt'); 4 | 5 | const mqtt = Mqtt.connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 6 | let offset = 100; 7 | 8 | 9 | 10 | mqtt.on('connect', () => { 11 | mqtt.subscribe('temperature/1/set', () => { 12 | console.log('subscribed', arguments); 13 | }); 14 | }); 15 | 16 | 17 | 18 | mqtt.on('message', (topic, payload) => { 19 | console.log('message received'); 20 | try { 21 | offset = JSON.parse(payload).offset; 22 | console.log('new offset', offset); 23 | } 24 | catch (err) { 25 | console.error(err); 26 | } 27 | }); 28 | 29 | 30 | 31 | let i = 0; 32 | setInterval(() => { 33 | const randInt = Math.floor(Math.random() * 100); 34 | const temperature = Math.round((Math.sin(i++ / 40) + 4) * randInt + offset); 35 | 36 | mqtt.publish('temperature/1/read', JSON.stringify({sensorId: '1', temperature}), (err) => { 37 | if (err) { 38 | console.error(err); 39 | } 40 | }); 41 | }, 2000); 42 | -------------------------------------------------------------------------------- /step5/services/serializer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node serializer.js 5 | 6 | -------------------------------------------------------------------------------- /step5/services/serializer/influxUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (client) { 4 | 5 | const writePoint = function (sensorId, temperature, cb) { 6 | client.writePoint('temperature', {sensorId: sensorId, temperature: temperature}, {}, cb); 7 | }; 8 | 9 | 10 | 11 | const readPoints = function (sensorId, start, end, cb) { 12 | const query = `select * from temperature where sensorId='${sensorId}' and time > '${start}' and time < '${end}'`; 13 | client.query(query, cb); 14 | }; 15 | 16 | 17 | 18 | return { 19 | writePoint, 20 | readPoints 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /step5/services/serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "serializer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "influx": "4.1.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step5/services/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Seneca = require('seneca'); 4 | const influx = require('influx'); 5 | const influxUtil = require('./influxUtil'); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | var createDatabase = function (cb) { 11 | setTimeout(() => { 12 | const initDb = influx({host: process.env.PROXY_HOST, username: 'root', password: 'root'}); 13 | initDb.createDatabase('temperature', (err) => { 14 | if (err) { 15 | console.error(`ERROR: ${err}`); 16 | } 17 | 18 | cb(); 19 | }); 20 | }, 3000); 21 | }; 22 | 23 | 24 | 25 | createDatabase(() => { 26 | var db = influx({host: process.env.PROXY_HOST, username: 'root', password: 'root', database: 'temperature'}); 27 | var ifx = influxUtil(db); 28 | 29 | seneca.add({role: 'serialize', cmd: 'read'}, (args, cb) => { 30 | ifx.readPoints(args.sensorId, args.start, args.end, cb); 31 | }); 32 | 33 | 34 | seneca.add({role: 'serialize', cmd: 'write'}, (args, cb) => { 35 | ifx.writePoint(args.sensorId, args.temperature, cb); 36 | }); 37 | 38 | 39 | seneca.listen({port: process.env.serializer_PORT}); 40 | }); 41 | 42 | 43 | module.exports.seneca = seneca; 44 | -------------------------------------------------------------------------------- /step5/services/serializer/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SERVICE_HOST = 'localhost'; 4 | process.env.SERVICE_PORT = 3001; 5 | process.env.INFLUX_HOST = '192.168.59.103'; 6 | 7 | var assert = require('assert'); 8 | var ser = require('../serializer'); 9 | 10 | describe('read write test', function() { 11 | 12 | it('should write data to influx', function(done){ 13 | ser.seneca.act({role: 'serialize', cmd: 'write', sensorId: '1', temperature: 32}, function(err) { 14 | assert(!err); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /step5/services/serializer/testWrite.bat: -------------------------------------------------------------------------------- 1 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 123}" --header "Content-Type:application/json" http://localhost:10000/act 2 | -------------------------------------------------------------------------------- /step5/services/serializer/testWrite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": $RANDOM}" http://localhost:10000/act --header "Content-Type:application/json" 3 | 4 | -------------------------------------------------------------------------------- /step6/README.md: -------------------------------------------------------------------------------- 1 | # Step 6 2 | 3 | ## solution to step 5 4 | 1. the folder step6/fuge contains an updated fuge configuration 5 | 2. start fuge up by running `fuge shell fuge/compose-dev.yml` 6 | 3. start the system in the shell by running `start all` 7 | 4. open http://localhost:10001 to view the data points 8 | 5. data is now streaming from the sensor through the broker to the serialization 9 | service and being displayed on the front end 10 | 6. type a numeric value (say 1000) into the text box at the top of the screen 11 | and hit the button 12 | 7. The chart should change to reflect the new offset 13 | 14 | 15 | ## Challenge 16 | 17 | Fuge automatically watches your services for you and will detect any changes to your code and live restart them. To see this in action 18 | lets do a couple of things: 19 | 20 | 1. Firstly, the output from influx is a little verbose so lets fix that: edit the file step6/fuge/fuge-config.js and change the tail setting to false 21 | 2. next restart the fuge shell and start all services. 22 | 3. next lets tail the serializer service by running `tail serializer` 23 | 4. run `ps` to show the state of the system it should report that fuge is watching all processes for changes but only tailing the serializer service 24 | 5. open the file step6/services/serializer/serializer.js and add some trace output 25 | 26 | __hint__ if you're not familiar with node.js you can output to the console using `console.log('hello')` 27 | 28 | ## Next Up [step7](../step7/README.md) 29 | -------------------------------------------------------------------------------- /step6/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /step6/frontend/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /step6/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /step6/frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "__DEV__": true, 13 | "__SERVER__": true 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "rules": { 19 | // Strict mode 20 | "strict": [2, "never"], 21 | 22 | // Code style 23 | "indent": [2, 2], 24 | "quotes": [2, "single"], 25 | 26 | // React 27 | "react/display-name": 0, 28 | "react/jsx-boolean-value": 1, 29 | "react/jsx-no-duplicate-props": 1, 30 | "react/jsx-no-undef": 1, 31 | "react/jsx-quotes": 1, 32 | "react/jsx-sort-prop-types": 0, 33 | "react/jsx-sort-props": 0, 34 | "react/jsx-uses-react": 1, 35 | "react/jsx-uses-vars": 1, 36 | "react/no-danger": 0, 37 | "react/no-did-mount-set-state": 1, 38 | "react/no-did-update-set-state": 1, 39 | "react/no-multi-comp": 1, 40 | "react/no-unknown-property": 1, 41 | "react/prop-types": 1, 42 | "react/react-in-jsx-scope": 1, 43 | "react/require-extension": 1, 44 | "react/self-closing-comp": 1, 45 | "react/sort-comp": 1, 46 | "react/wrap-multilines": 1 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /step6/frontend/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build 3 | .*/config 4 | .*/node_modules 5 | .*/gulpfile.js 6 | 7 | [include] 8 | -------------------------------------------------------------------------------- /step6/frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.less text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.txt text eol=lf 19 | *.xml text eol=lf 20 | -------------------------------------------------------------------------------- /step6/frontend/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "validateIndentation": 2, 6 | "excludeFiles": ["build/**", "node_modules/**"], 7 | "esprima": "esprima-fb" 8 | } 9 | -------------------------------------------------------------------------------- /step6/frontend/.npmignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /step6/frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | - '0.12' 5 | - '0.10' 6 | matrix: 7 | allow_failures: 8 | - node_js: iojs 9 | - node_js: '0.12' 10 | -------------------------------------------------------------------------------- /step6/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN cd api && npm install --ignore-scripts 4 | CMD node api/index.js 5 | -------------------------------------------------------------------------------- /step6/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var seneca = require('seneca')(); 5 | var app = express(); 6 | var http = require('http').Server(app); 7 | var webStream = require('./webStream')(http); 8 | var moment = require('moment'); 9 | var _ = require('lodash'); 10 | 11 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'read'}}); 12 | seneca.client({host: process.env.PROXY_HOST, port: process.env.actuator_PORT, pin: {role: 'actuate', cmd: 'set'}}); 13 | 14 | app.use('/', express.static(__dirname + '/../public')); 15 | 16 | 17 | 18 | app.get('/set', function (req, res) { 19 | seneca.act({role: 'actuate', cmd: 'set', offset: req.query.offset}, function(err) { 20 | if (err) { 21 | res.json({result: err}); 22 | } 23 | else { 24 | res.json({result: 'ok'}); 25 | } 26 | res.end(); 27 | }); 28 | }); 29 | 30 | 31 | 32 | var lastEmitted = 0; 33 | setInterval(function() { 34 | seneca.act({role: 'serialize', cmd: 'read', sensorId: '1', start: moment().subtract(10, 'minutes').utc().format(), end: moment().utc().format()}, function(err, data) { 35 | var toEmit = []; 36 | 37 | _.each(data[0], function(point) { 38 | if (moment(point.time).unix() > lastEmitted) { 39 | lastEmitted = moment(point.time).unix(); 40 | point.time = (new Date(point.time)).getTime(); 41 | toEmit.push(point); 42 | } 43 | }); 44 | if (toEmit.length > 0) { 45 | webStream.emit(toEmit); 46 | } 47 | }); 48 | }, 1000); 49 | 50 | 51 | 52 | http.listen(process.env.frontend_PORT, function(){ 53 | console.log('listening on *:' + process.env.frontend_PORT); 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /step6/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "4.13.x", 13 | "lodash": "4.2.x", 14 | "moment": "2.11.x", 15 | "seneca": "1.1.x", 16 | "end-of-stream": "1.1.x", 17 | "websocket-stream": "3.1.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /step6/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | }); 18 | }); 19 | }; 20 | 21 | 22 | 23 | var handleStream = function(stream) { 24 | stream.id = streamCounter++; 25 | streams[stream.id] = stream; 26 | 27 | eos(stream, function () { 28 | delete streams[stream.id]; 29 | }); 30 | }; 31 | 32 | 33 | 34 | websocket.createServer({server: http}, handleStream); 35 | 36 | return { 37 | emit: emit 38 | }; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /step6/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /step6/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | var initControls = function() { 80 | $('#setOffset').click(function() { 81 | $.get('/set?offset=' + $('#offset').val(), function() { 82 | }); 83 | }); 84 | }; 85 | 86 | 87 | 88 | $(document).ready(function() { 89 | initChart(); 90 | initControls(); 91 | }); 92 | 93 | -------------------------------------------------------------------------------- /step6/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "websocket-stream": "3.1.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step6/fuge/compose-dev.yml: -------------------------------------------------------------------------------- 1 | influx: 2 | image: tutum/influxdb 3 | ports: 4 | - 8086:8086 5 | - 8083:8083 6 | serializer: 7 | build: ../services/serializer/ 8 | container_name: serializer 9 | frontend: 10 | build: ../frontend/ 11 | container_name: frontend 12 | broker: 13 | build: ../services/broker/ 14 | container_name: broker 15 | sensor: 16 | build: ../services/sensor/ 17 | container_name: sensor 18 | actuator: 19 | build: ../services/actuator/ 20 | container_name: actuator 21 | -------------------------------------------------------------------------------- /step6/fuge/fuge-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // run docker containers if false containers with image attribute will not be run 4 | runDocker: true, 5 | 6 | // proxy settings - one of docker | process | all | none 7 | proxy: 'docker', 8 | 9 | // if true tail running process to the shell by default 10 | tail: true, 11 | 12 | // if true monitor running processes for changes by default 13 | monitor: true, 14 | 15 | // exclude these patterns from the monitor 16 | exclude: /node_modules|\.git|\.log/mgi, 17 | 18 | // override section. Allows the default build, run and debug commands 19 | // to be overriden on a service by service basis. These commands are 20 | // normally generated by inspecting the Dockerfile for a service 21 | /* 22 | overrides: { 23 | service1: { build: 'sh build.sh' } 24 | } 25 | */ 26 | }; 27 | -------------------------------------------------------------------------------- /step6/services/actuator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node actuator.js 5 | -------------------------------------------------------------------------------- /step6/services/actuator/actuator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mqtt = require('mqtt'); 4 | const Seneca = require('seneca'); 5 | 6 | const mqtt = require('mqtt').connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 7 | const seneca = Seneca(); 8 | 9 | 10 | 11 | seneca.add({role: 'actuate', cmd: 'set'}, (args, cb) => { 12 | const payload = JSON.stringify({'offset': parseInt(args.offset, 10) }); 13 | mqtt.publish('temperature/1/set', new Buffer(payload), {qos: 0, retain: true}, cb); 14 | }); 15 | 16 | seneca.listen({port: process.env.actuator_PORT}); 17 | -------------------------------------------------------------------------------- /step6/services/actuator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actuator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "actuator.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "1.7.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step6/services/broker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node broker.js 5 | -------------------------------------------------------------------------------- /step6/services/broker/broker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mosca = require('mosca'); 4 | const Seneca = require('seneca'); 5 | const server = new mosca.Server({}); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'write'}}); 11 | 12 | 13 | 14 | function parse (body) { 15 | try { 16 | return JSON.parse(body); 17 | } 18 | catch (err) { 19 | return null; 20 | } 21 | } 22 | 23 | 24 | 25 | server.published = function (packet, client, cb) { 26 | var body; 27 | if (!packet.topic.match(/temperature\/[0-9]+\/read/)) { 28 | return cb(); 29 | } 30 | 31 | body = parse(packet.payload); 32 | 33 | body.role = 'serialize'; 34 | body.cmd = 'write'; 35 | seneca.act(body, cb); 36 | }; 37 | -------------------------------------------------------------------------------- /step6/services/broker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "broker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "broker.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mosca": "1.0.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step6/services/influx/run.bat: -------------------------------------------------------------------------------- 1 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 2 | -------------------------------------------------------------------------------- /step6/services/influx/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 3 | -------------------------------------------------------------------------------- /step6/services/sensor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node sensor.js 5 | -------------------------------------------------------------------------------- /step6/services/sensor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "driver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "sensor.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "1.7.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step6/services/sensor/sensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mqtt = require('mqtt'); 4 | 5 | const mqtt = Mqtt.connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 6 | let offset = 100; 7 | 8 | 9 | 10 | mqtt.on('connect', () => { 11 | mqtt.subscribe('temperature/1/set', () => { 12 | console.log('subscribed', arguments); 13 | }); 14 | }); 15 | 16 | 17 | 18 | mqtt.on('message', (topic, payload) => { 19 | console.log('message received'); 20 | try { 21 | offset = JSON.parse(payload).offset; 22 | console.log('new offset', offset); 23 | } 24 | catch (err) { 25 | console.error(err); 26 | } 27 | }); 28 | 29 | 30 | 31 | let i = 0; 32 | setInterval(() => { 33 | const randInt = Math.floor(Math.random() * 100); 34 | const temperature = Math.round((Math.sin(i++ / 40) + 4) * randInt + offset); 35 | 36 | mqtt.publish('temperature/1/read', JSON.stringify({sensorId: '1', temperature}), (err) => { 37 | if (err) { 38 | console.error(err); 39 | } 40 | }); 41 | }, 2000); 42 | -------------------------------------------------------------------------------- /step6/services/serializer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node serializer.js 5 | 6 | -------------------------------------------------------------------------------- /step6/services/serializer/influxUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (client) { 4 | 5 | const writePoint = function (sensorId, temperature, cb) { 6 | client.writePoint('temperature', {sensorId: sensorId, temperature: temperature}, {}, cb); 7 | }; 8 | 9 | 10 | 11 | const readPoints = function (sensorId, start, end, cb) { 12 | const query = `select * from temperature where sensorId='${sensorId}' and time > '${start}' and time < '${end}'`; 13 | client.query(query, cb); 14 | }; 15 | 16 | 17 | 18 | return { 19 | writePoint, 20 | readPoints 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /step6/services/serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "serializer.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "influx": "4.1.x", 13 | "seneca": "1.1.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step6/services/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Seneca = require('seneca'); 4 | const influx = require('influx'); 5 | const influxUtil = require('./influxUtil'); 6 | 7 | const seneca = Seneca(); 8 | 9 | 10 | var createDatabase = function (cb) { 11 | setTimeout(() => { 12 | const initDb = influx({host: process.env.PROXY_HOST, username: 'root', password: 'root'}); 13 | initDb.createDatabase('temperature', (err) => { 14 | if (err) { 15 | console.error(`ERROR: ${err}`); 16 | } 17 | 18 | cb(); 19 | }); 20 | }, 3000); 21 | }; 22 | 23 | 24 | 25 | createDatabase(() => { 26 | var db = influx({host: process.env.PROXY_HOST, username: 'root', password: 'root', database: 'temperature'}); 27 | var ifx = influxUtil(db); 28 | 29 | seneca.add({role: 'serialize', cmd: 'read'}, (args, cb) => { 30 | ifx.readPoints(args.sensorId, args.start, args.end, cb); 31 | }); 32 | 33 | 34 | seneca.add({role: 'serialize', cmd: 'write'}, (args, cb) => { 35 | ifx.writePoint(args.sensorId, args.temperature, cb); 36 | }); 37 | 38 | 39 | seneca.listen({port: process.env.serializer_PORT}); 40 | }); 41 | 42 | 43 | module.exports.seneca = seneca; 44 | -------------------------------------------------------------------------------- /step6/services/serializer/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SERVICE_HOST = 'localhost'; 4 | process.env.SERVICE_PORT = 3001; 5 | process.env.INFLUX_HOST = '192.168.59.103'; 6 | 7 | var assert = require('assert'); 8 | var ser = require('../serializer'); 9 | 10 | describe('read write test', function() { 11 | 12 | it('should write data to influx', function(done){ 13 | ser.seneca.act({role: 'serialize', cmd: 'write', sensorId: '1', temperature: 32}, function(err) { 14 | assert(!err); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /step6/services/serializer/testWrite.bat: -------------------------------------------------------------------------------- 1 | docker-machine ip default > dock 2 | set /p DOCKER_IP= < dock 3 | del dock 4 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 32}" --header "Content-Type:application/json" http://%DOCKER_IP%:10000/act 5 | -------------------------------------------------------------------------------- /step6/services/serializer/testWrite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export DOCKER_IP=$(docker-machine ip default) 3 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 32}" http://$DOCKER_IP:3001/act --header "Content-Type:application/json" 4 | 5 | -------------------------------------------------------------------------------- /step7/README.md: -------------------------------------------------------------------------------- 1 | # Step 7 2 | 3 | ## solution to step 6 4 | 5 | 1. The file step7/fuge/fuge-config.js has been updated to set tail to false 6 | 2. The file step7/services/serializer/serializer.js has some additional trace added to it 7 | 2. The file step7/frontend/api/index.js has some additional trace added to it 8 | 9 | ## Challenge 10 | 11 | Fuge is targeted at running an entire or part of a microservice system in 12 | development mode. However the format used is fully compatible with tools like 13 | docker-compose. Docker compose allows you to run sets of connected containers. 14 | Running containers in development can be slow, however if you are deploying 15 | using containers then building containers locally in order to check system 16 | validity is a good idea. 17 | 18 | A docker-compose file has been provided for you in step7/fuge/docker-compose.yml. Your challenge is to firstly run your system with fuge using this 19 | docker-compose file. Once you have validated this try running it using docker-compose. You can find documentation on docker-compose here: [https://docs.docker.com/compose/](https://docs.docker.com/compose/) 20 | 21 | ## Next Up [step8](../step8/README.md) 22 | -------------------------------------------------------------------------------- /step7/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /step7/frontend/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /step7/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /step7/frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "__DEV__": true, 13 | "__SERVER__": true 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "rules": { 19 | // Strict mode 20 | "strict": [2, "never"], 21 | 22 | // Code style 23 | "indent": [2, 2], 24 | "quotes": [2, "single"], 25 | 26 | // React 27 | "react/display-name": 0, 28 | "react/jsx-boolean-value": 1, 29 | "react/jsx-no-duplicate-props": 1, 30 | "react/jsx-no-undef": 1, 31 | "react/jsx-quotes": 1, 32 | "react/jsx-sort-prop-types": 0, 33 | "react/jsx-sort-props": 0, 34 | "react/jsx-uses-react": 1, 35 | "react/jsx-uses-vars": 1, 36 | "react/no-danger": 0, 37 | "react/no-did-mount-set-state": 1, 38 | "react/no-did-update-set-state": 1, 39 | "react/no-multi-comp": 1, 40 | "react/no-unknown-property": 1, 41 | "react/prop-types": 1, 42 | "react/react-in-jsx-scope": 1, 43 | "react/require-extension": 1, 44 | "react/self-closing-comp": 1, 45 | "react/sort-comp": 1, 46 | "react/wrap-multilines": 1 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /step7/frontend/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build 3 | .*/config 4 | .*/node_modules 5 | .*/gulpfile.js 6 | 7 | [include] 8 | -------------------------------------------------------------------------------- /step7/frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.less text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.txt text eol=lf 19 | *.xml text eol=lf 20 | -------------------------------------------------------------------------------- /step7/frontend/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "validateIndentation": 2, 6 | "excludeFiles": ["build/**", "node_modules/**"], 7 | "esprima": "esprima-fb" 8 | } 9 | -------------------------------------------------------------------------------- /step7/frontend/.npmignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /step7/frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | - '0.12' 5 | - '0.10' 6 | matrix: 7 | allow_failures: 8 | - node_js: iojs 9 | - node_js: '0.12' 10 | -------------------------------------------------------------------------------- /step7/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN cd api && npm install --ignore-scripts 4 | CMD node api/index.js 5 | -------------------------------------------------------------------------------- /step7/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var seneca = require('seneca')(); 5 | var app = express(); 6 | var http = require('http').Server(app); 7 | var webStream = require('./webStream')(http); 8 | var moment = require('moment'); 9 | var _ = require('lodash'); 10 | 11 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'read'}}); 12 | seneca.client({host: process.env.PROXY_HOST, port: process.env.actuator_PORT, pin: {role: 'actuate', cmd: 'set'}}); 13 | 14 | app.use('/', express.static(__dirname + '/../public')); 15 | 16 | 17 | 18 | app.get('/set', function (req, res) { 19 | seneca.act({role: 'actuate', cmd: 'set', offset: req.query.offset}, function(err) { 20 | if (err) { 21 | res.json({result: err}); 22 | } 23 | else { 24 | res.json({result: 'ok'}); 25 | } 26 | res.end(); 27 | }); 28 | }); 29 | 30 | 31 | 32 | var lastEmitted = 0; 33 | setInterval(function() { 34 | seneca.act({role: 'serialize', cmd: 'read', sensorId: '1', start: moment().subtract(10, 'minutes').utc().format(), end: moment().utc().format()}, function(err, data) { 35 | var toEmit = []; 36 | 37 | _.each(data[0], function(point) { 38 | if (moment(point.time).unix() > lastEmitted) { 39 | lastEmitted = moment(point.time).unix(); 40 | point.time = (new Date(point.time)).getTime(); 41 | toEmit.push(point); 42 | } 43 | }); 44 | if (toEmit.length > 0) { 45 | console.log('emit some stuff'); 46 | webStream.emit(toEmit); 47 | } 48 | else { 49 | console.log('nothing to emit'); 50 | } 51 | }); 52 | }, 1000); 53 | 54 | 55 | 56 | http.listen(process.env.frontend_PORT, function(){ 57 | console.log('listening on *:' + process.env.frontend_PORT); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /step7/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.13.3", 13 | "lodash": "^3.10.1", 14 | "moment": "^2.10.6", 15 | "seneca": "^0.6.4", 16 | "end-of-stream": "^1.1.0", 17 | "websocket-stream": "^2.0.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /step7/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | }); 18 | }); 19 | }; 20 | 21 | 22 | 23 | var handleStream = function(stream) { 24 | stream.id = streamCounter++; 25 | streams[stream.id] = stream; 26 | 27 | eos(stream, function () { 28 | delete streams[stream.id]; 29 | }); 30 | }; 31 | 32 | 33 | 34 | websocket.createServer({server: http}, handleStream); 35 | 36 | return { 37 | emit: emit 38 | }; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /step7/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /step7/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | var initControls = function() { 80 | $('#setOffset').click(function() { 81 | $.get('/set?offset=' + $('#offset').val(), function() { 82 | }); 83 | }); 84 | }; 85 | 86 | 87 | 88 | $(document).ready(function() { 89 | initChart(); 90 | initControls(); 91 | }); 92 | 93 | -------------------------------------------------------------------------------- /step7/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "websocket-stream": "^2.0.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step7/fuge/compose-dev.yml: -------------------------------------------------------------------------------- 1 | influx: 2 | image: tutum/influxdb 3 | ports: 4 | - 8086:8086 5 | - 8083:8083 6 | serializer: 7 | build: ../services/serializer/ 8 | container_name: serializer 9 | frontend: 10 | build: ../frontend/ 11 | container_name: frontend 12 | broker: 13 | build: ../services/broker/ 14 | container_name: broker 15 | sensor: 16 | build: ../services/sensor/ 17 | container_name: sensor 18 | actuator: 19 | build: ../services/actuator/ 20 | container_name: actuator 21 | -------------------------------------------------------------------------------- /step7/fuge/docker-compose.yml: -------------------------------------------------------------------------------- 1 | influx: 2 | image: tutum/influxdb 3 | container_name: influx 4 | ports: 5 | - 8086:8086 6 | - 8083:8083 7 | serializer: 8 | env_file: env 9 | build: ../services/serializer/ 10 | container_name: serializer 11 | ports: 12 | - 10000:10000 13 | frontend: 14 | env_file: env 15 | build: ../frontend/ 16 | container_name: frontend 17 | ports: 18 | - 10001:10001 19 | broker: 20 | env_file: env 21 | build: ../services/broker/ 22 | container_name: broker 23 | ports: 24 | - 10002:10002 25 | - 1883:1883 26 | - 8883:8883 27 | sensor: 28 | env_file: env 29 | build: ../services/sensor/ 30 | container_name: sensor 31 | ports: 32 | - 10003:10003 33 | actuator: 34 | env_file: env 35 | build: ../services/actuator/ 36 | container_name: actuator 37 | ports: 38 | - 10004:10004 39 | 40 | -------------------------------------------------------------------------------- /step7/fuge/env: -------------------------------------------------------------------------------- 1 | PROXY_HOST=192.168.99.100 2 | influx_PORT_8086=8086 3 | influx_PORT_8083=8086 4 | serializer_PORT=10000 5 | frontend_PORT=10001 6 | broker_PORT=10002 7 | sensor_PORT=10003 8 | actuator_PORT=10004 9 | 10 | -------------------------------------------------------------------------------- /step7/fuge/fuge-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // run docker containers if false containers with image attribute will not be run 4 | runDocker: true, 5 | 6 | // proxy settings - one of docker | process | all | none 7 | proxy: 'docker', 8 | 9 | // if true tail running process to the shell by default 10 | tail: false, 11 | 12 | // if true monitor running processes for changes by default 13 | monitor: true, 14 | 15 | // exclude these patterns from the monitor 16 | exclude: /node_modules|\.git|\.log/mgi, 17 | 18 | // override section. Allows the default build, run and debug commands 19 | // to be overriden on a service by service basis. These commands are 20 | // normally generated by inspecting the Dockerfile for a service 21 | /* 22 | overrides: { 23 | service1: { build: 'sh build.sh' } 24 | } 25 | */ 26 | }; 27 | -------------------------------------------------------------------------------- /step7/services/actuator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node actuator.js 5 | -------------------------------------------------------------------------------- /step7/services/actuator/actuator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mqtt = require('mqtt').connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 4 | var seneca = require('seneca')(); 5 | 6 | 7 | 8 | seneca.add({role: 'actuate', cmd: 'set'}, function(args, callback) { 9 | var payload = JSON.stringify({'offset': parseInt(args.offset, 10) }); 10 | mqtt.publish('temperature/1/set', new Buffer(payload), {qos: 0, retain: true}, function (err) { 11 | callback(err); 12 | }); 13 | }); 14 | 15 | seneca.listen({port: process.env.actuator_PORT}); 16 | 17 | -------------------------------------------------------------------------------- /step7/services/actuator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actuator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "^1.4.0", 13 | "seneca": "^0.6.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step7/services/broker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node broker.js 5 | -------------------------------------------------------------------------------- /step7/services/broker/broker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mosca = require('mosca'); 4 | var seneca = require('seneca')(); 5 | var server = new mosca.Server({}); 6 | 7 | 8 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'write'}}); 9 | 10 | 11 | 12 | function parse (body) { 13 | try { 14 | return JSON.parse(body); 15 | } 16 | catch (err) { 17 | return null; 18 | } 19 | } 20 | 21 | 22 | 23 | server.published = function (packet, client, cb) { 24 | var body; 25 | if (packet.topic.match(/temperature\/[0-9]+\/read/)) { 26 | body = parse(packet.payload); 27 | 28 | body.role = 'serialize'; 29 | body.cmd = 'write'; 30 | seneca.act(body, cb); 31 | } 32 | else { 33 | cb(); 34 | } 35 | }; 36 | 37 | -------------------------------------------------------------------------------- /step7/services/broker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "broker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mosca": "^0.31.1", 13 | "seneca": "^0.6.4" 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /step7/services/influx/run.bat: -------------------------------------------------------------------------------- 1 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 2 | -------------------------------------------------------------------------------- /step7/services/influx/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 3 | -------------------------------------------------------------------------------- /step7/services/sensor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node sensor.js 5 | -------------------------------------------------------------------------------- /step7/services/sensor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "driver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "^1.4.0", 13 | "seneca": "^0.6.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step7/services/sensor/sensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mqtt = require('mqtt').connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 4 | var offset = 100; 5 | 6 | 7 | 8 | mqtt.on('connect', function () { 9 | mqtt.subscribe('temperature/1/set', function () { 10 | console.log('subscribed', arguments); 11 | }); 12 | }); 13 | 14 | 15 | 16 | mqtt.on('message', function (topic, payload) { 17 | console.log('message received'); 18 | try { 19 | offset = JSON.parse(payload).offset; 20 | console.log('new offset', offset); 21 | } 22 | catch (err) { 23 | console.log(err); 24 | return; 25 | } 26 | }); 27 | 28 | 29 | 30 | var i = 0; 31 | setInterval(function() { 32 | var randInt = Math.floor(Math.random()*100); 33 | var temp = Math.round((Math.sin(i++ / 40) + 4) * randInt + offset); 34 | 35 | mqtt.publish('temperature/1/read', JSON.stringify({sensorId: '1', temperature: temp}), function(err) { 36 | if (err) { console.log(err); } 37 | }); 38 | }, 2000); 39 | 40 | -------------------------------------------------------------------------------- /step7/services/serializer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node serializer.js 5 | 6 | -------------------------------------------------------------------------------- /step7/services/serializer/influxUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(client) { 4 | 5 | var writePoint = function(sensorId, temperature, cb) { 6 | client.writePoint('temperature', {sensorId: sensorId, temperature: temperature}, {}, function(err) { 7 | cb(err); 8 | }); 9 | }; 10 | 11 | 12 | 13 | var readPoints = function(sensorId, start, end, cb) { 14 | client.query('select * from temperature where sensorId=\'' + sensorId + '\' and time > \'' + start + '\' and time < \'' + end + '\'', function(err, data) { 15 | cb(err, data); 16 | }); 17 | }; 18 | 19 | 20 | 21 | return { 22 | writePoint: writePoint, 23 | readPoints: readPoints 24 | }; 25 | }; 26 | 27 | 28 | -------------------------------------------------------------------------------- /step7/services/serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "influx": "^4.0.1", 13 | "seneca": "^0.6.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step7/services/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var seneca = require('seneca')(); 4 | var influx= require('influx'); 5 | 6 | 7 | 8 | var createDatabase = function(cb) { 9 | setTimeout(function() { 10 | var initDb = influx({host: process.env.PROXY_HOST, username : 'root', password : 'root'}); 11 | initDb.createDatabase('temperature', function(err) { 12 | if (err) { console.log('ERROR: ' + err); } 13 | cb(); 14 | }); 15 | }, 3000); 16 | }; 17 | 18 | 19 | 20 | createDatabase(function() { 21 | var db = influx({host: process.env.PROXY_HOST, username : 'root', password : 'root', database : 'temperature'}); 22 | var ifx = require('./influxUtil')(db); 23 | 24 | seneca.add({role: 'serialize', cmd: 'read'}, function(args, callback) { 25 | ifx.readPoints(args.sensorId, args.start, args.end, function(err, data) { 26 | callback(err, data); 27 | }); 28 | }); 29 | 30 | 31 | seneca.add({role: 'serialize', cmd: 'write'}, function(args, callback) { 32 | ifx.writePoint(args.sensorId, args.temperature, function(err) { 33 | callback(err); 34 | }); 35 | }); 36 | 37 | 38 | seneca.listen({port: process.env.serializer_PORT}); 39 | }); 40 | 41 | 42 | module.exports.seneca = seneca; 43 | 44 | 45 | -------------------------------------------------------------------------------- /step7/services/serializer/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SERVICE_HOST = 'localhost'; 4 | process.env.SERVICE_PORT = 3001; 5 | process.env.INFLUX_HOST = '192.168.59.103'; 6 | 7 | var assert = require('assert'); 8 | var ser = require('../serializer'); 9 | 10 | describe('read write test', function() { 11 | 12 | it('should write data to influx', function(done){ 13 | ser.seneca.act({role: 'serialize', cmd: 'write', sensorId: '1', temperature: 32}, function(err) { 14 | assert(!err); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /step7/services/serializer/testWrite.bat: -------------------------------------------------------------------------------- 1 | docker-machine ip default > dock 2 | set /p DOCKER_IP= < dock 3 | del dock 4 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 32}" --header "Content-Type:application/json" http://%DOCKER_IP%:10000/act 5 | -------------------------------------------------------------------------------- /step7/services/serializer/testWrite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export DOCKER_IP=$(docker-machine ip default) 3 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 32}" http://$DOCKER_IP:3001/act --header "Content-Type:application/json" 4 | 5 | -------------------------------------------------------------------------------- /step8/README.md: -------------------------------------------------------------------------------- 1 | # Step 8 2 | 3 | ## solution to step 7 4 | 5 | 1. you can run the system using docker-compose by running: `docker-compose up` in fuge/ 6 | 2. To test that everything is working point your browser to http://\:10001 7 | 8 | If you issue a `docker ps` you should see that docker is now running our system 9 | as a set of containers. 10 | 11 | Thats it - congratulations - you have succeeded in building an IoT micro-service 12 | based system out of docker containers!! 13 | 14 | ## Next Up: Celebrate!! 15 | -------------------------------------------------------------------------------- /step8/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /step8/frontend/.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /step8/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /step8/frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "__DEV__": true, 13 | "__SERVER__": true 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "rules": { 19 | // Strict mode 20 | "strict": [2, "never"], 21 | 22 | // Code style 23 | "indent": [2, 2], 24 | "quotes": [2, "single"], 25 | 26 | // React 27 | "react/display-name": 0, 28 | "react/jsx-boolean-value": 1, 29 | "react/jsx-no-duplicate-props": 1, 30 | "react/jsx-no-undef": 1, 31 | "react/jsx-quotes": 1, 32 | "react/jsx-sort-prop-types": 0, 33 | "react/jsx-sort-props": 0, 34 | "react/jsx-uses-react": 1, 35 | "react/jsx-uses-vars": 1, 36 | "react/no-danger": 0, 37 | "react/no-did-mount-set-state": 1, 38 | "react/no-did-update-set-state": 1, 39 | "react/no-multi-comp": 1, 40 | "react/no-unknown-property": 1, 41 | "react/prop-types": 1, 42 | "react/react-in-jsx-scope": 1, 43 | "react/require-extension": 1, 44 | "react/self-closing-comp": 1, 45 | "react/sort-comp": 1, 46 | "react/wrap-multilines": 1 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /step8/frontend/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build 3 | .*/config 4 | .*/node_modules 5 | .*/gulpfile.js 6 | 7 | [include] 8 | -------------------------------------------------------------------------------- /step8/frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.less text eol=lf 16 | *.md text eol=lf 17 | *.sh text eol=lf 18 | *.txt text eol=lf 19 | *.xml text eol=lf 20 | -------------------------------------------------------------------------------- /step8/frontend/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "validateIndentation": 2, 6 | "excludeFiles": ["build/**", "node_modules/**"], 7 | "esprima": "esprima-fb" 8 | } 9 | -------------------------------------------------------------------------------- /step8/frontend/.npmignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /step8/frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - iojs 4 | - '0.12' 5 | - '0.10' 6 | matrix: 7 | allow_failures: 8 | - node_js: iojs 9 | - node_js: '0.12' 10 | -------------------------------------------------------------------------------- /step8/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN cd api && npm install --ignore-scripts 4 | CMD node api/index.js 5 | -------------------------------------------------------------------------------- /step8/frontend/api/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var seneca = require('seneca')(); 5 | var app = express(); 6 | var http = require('http').Server(app); 7 | var webStream = require('./webStream')(http); 8 | var moment = require('moment'); 9 | var _ = require('lodash'); 10 | 11 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'read'}}); 12 | seneca.client({host: process.env.PROXY_HOST, port: process.env.actuator_PORT, pin: {role: 'actuate', cmd: 'set'}}); 13 | 14 | app.use('/', express.static(__dirname + '/../public')); 15 | 16 | 17 | 18 | app.get('/set', function (req, res) { 19 | seneca.act({role: 'actuate', cmd: 'set', offset: req.query.offset}, function(err) { 20 | if (err) { 21 | res.json({result: err}); 22 | } 23 | else { 24 | res.json({result: 'ok'}); 25 | } 26 | res.end(); 27 | }); 28 | }); 29 | 30 | 31 | 32 | var lastEmitted = 0; 33 | setInterval(function() { 34 | console.log('calling read'); 35 | seneca.act({role: 'serialize', cmd: 'read', sensorId: '1', start: moment().subtract(10, 'minutes').utc().format(), end: moment().utc().format()}, function(err, data) { 36 | if (err) { return console.log(err); } 37 | if (!data) { return console.log('no data!!'); } 38 | var toEmit = []; 39 | 40 | _.each(data[0], function(point) { 41 | if (moment(point.time).unix() > lastEmitted) { 42 | lastEmitted = moment(point.time).unix(); 43 | point.time = (new Date(point.time)).getTime(); 44 | toEmit.push(point); 45 | } 46 | }); 47 | if (toEmit.length > 0) { 48 | console.log('emitting'); 49 | webStream.emit(toEmit); 50 | } 51 | else { 52 | console.log('no emit'); 53 | } 54 | }); 55 | }, 1000); 56 | 57 | 58 | 59 | http.listen(process.env.frontend_PORT, function(){ 60 | console.log('listening on *:' + process.env.frontend_PORT); 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /step8/frontend/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.13.3", 13 | "lodash": "^3.10.1", 14 | "moment": "^2.10.6", 15 | "seneca": "^0.6.4", 16 | "end-of-stream": "^1.1.0", 17 | "websocket-stream": "^2.0.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /step8/frontend/api/webStream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var websocket = require('websocket-stream'); 5 | var eos = require('end-of-stream'); 6 | 7 | 8 | 9 | module.exports = function(http) { 10 | var streamCounter = 0; 11 | var streams = {}; 12 | 13 | 14 | var emit = function(data) { 15 | _.forEach(streams, function (stream) { 16 | stream.write(JSON.stringify(data), function () { 17 | }); 18 | }); 19 | }; 20 | 21 | 22 | 23 | var handleStream = function(stream) { 24 | stream.id = streamCounter++; 25 | streams[stream.id] = stream; 26 | 27 | eos(stream, function () { 28 | delete streams[stream.id]; 29 | }); 30 | }; 31 | 32 | 33 | 34 | websocket.createServer({server: http}, handleStream); 35 | 36 | return { 37 | emit: emit 38 | }; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /step8/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Charting frontend 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /step8/frontend/public/js/chart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var graph; 4 | var websocket = require('websocket-stream'); 5 | 6 | 7 | 8 | var initChart = function() { 9 | graph = new Rickshaw.Graph( { 10 | element: document.getElementById('chart'), 11 | width: 700, 12 | height: 300, 13 | renderer: 'line', 14 | stroke: true, 15 | series: [{ 16 | data: [], 17 | color: '#6060c0', 18 | name: 'sensor1' 19 | }] 20 | }); 21 | graph.render(); 22 | 23 | var hoverDetail = new Rickshaw.Graph.HoverDetail({ 24 | graph: graph, 25 | xFormatter: function(x) { 26 | return new Date(x * 1000).toString(); 27 | } 28 | }); 29 | 30 | var annotator = new Rickshaw.Graph.Annotate({ 31 | graph: graph, 32 | element: document.getElementById('timeline') 33 | }); 34 | 35 | var ticksTreatment = 'glow'; 36 | 37 | var xAxis = new Rickshaw.Graph.Axis.Time({ 38 | graph: graph, 39 | ticksTreatment: ticksTreatment, 40 | timeFixture: new Rickshaw.Fixtures.Time.Local() 41 | }); 42 | xAxis.render(); 43 | 44 | var yAxis = new Rickshaw.Graph.Axis.Y({ 45 | graph: graph, 46 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT, 47 | ticksTreatment: ticksTreatment 48 | }); 49 | yAxis.render(); 50 | 51 | var start = (new Date().getTime() / 1000) - 50; 52 | for (var idx = 0; idx <= 50; ++idx) { 53 | graph.series[0].data.push({x: start + idx, y: 0}); 54 | } 55 | graph.render(); 56 | 57 | var stream = websocket(document.URL.replace('http', 'ws')); 58 | stream.on('data', function(data) { 59 | updateChart(graph, JSON.parse(data)); 60 | }); 61 | }; 62 | 63 | 64 | 65 | var updateChart = function(graph, data) { 66 | if (graph.series[0].data.length + data.length > 50) { 67 | graph.series[0].data = _.drop(graph.series[0].data, graph.series[0].data.length + data.length - 50); 68 | } 69 | data.forEach(function(point) { 70 | if (point.sensorId === '1') { 71 | graph.series[0].data.push({x: Math.round(point.time/1000), y: point.temperature}); 72 | } 73 | }); 74 | graph.render(); 75 | }; 76 | 77 | 78 | 79 | var initControls = function() { 80 | $('#setOffset').click(function() { 81 | $.get('/set?offset=' + $('#offset').val(), function() { 82 | }); 83 | }); 84 | }; 85 | 86 | 87 | 88 | $(document).ready(function() { 89 | initChart(); 90 | initControls(); 91 | }); 92 | 93 | -------------------------------------------------------------------------------- /step8/frontend/public/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "built.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "websocket-stream": "^2.0.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /step8/fuge/compose-dev.yml: -------------------------------------------------------------------------------- 1 | influx: 2 | image: tutum/influxdb 3 | ports: 4 | - 8086:8086 5 | - 8083:8083 6 | serializer: 7 | build: ../services/serializer/ 8 | container_name: serializer 9 | frontend: 10 | build: ../frontend/ 11 | container_name: frontend 12 | broker: 13 | build: ../services/broker/ 14 | container_name: broker 15 | sensor: 16 | build: ../services/sensor/ 17 | container_name: sensor 18 | actuator: 19 | build: ../services/actuator/ 20 | container_name: actuator 21 | -------------------------------------------------------------------------------- /step8/fuge/docker-compose.yml: -------------------------------------------------------------------------------- 1 | influx: 2 | image: tutum/influxdb 3 | container_name: influx 4 | ports: 5 | - 8086:8086 6 | - 8083:8083 7 | serializer: 8 | env_file: env 9 | build: ../services/serializer/ 10 | container_name: serializer 11 | ports: 12 | - 10000:10000 13 | frontend: 14 | env_file: env 15 | build: ../frontend/ 16 | container_name: frontend 17 | ports: 18 | - 10001:10001 19 | broker: 20 | env_file: env 21 | build: ../services/broker/ 22 | container_name: broker 23 | ports: 24 | - 10002:10002 25 | - 1883:1883 26 | - 8883:8883 27 | sensor: 28 | env_file: env 29 | build: ../services/sensor/ 30 | container_name: sensor 31 | ports: 32 | - 10003:10003 33 | actuator: 34 | env_file: env 35 | build: ../services/actuator/ 36 | container_name: actuator 37 | ports: 38 | - 10004:10004 39 | 40 | -------------------------------------------------------------------------------- /step8/fuge/env: -------------------------------------------------------------------------------- 1 | PROXY_HOST=192.168.99.100 2 | influx_PORT_8086=8086 3 | influx_PORT_8083=8086 4 | serializer_PORT=10000 5 | frontend_PORT=10001 6 | broker_PORT=10002 7 | sensor_PORT=10003 8 | actuator_PORT=10004 9 | 10 | -------------------------------------------------------------------------------- /step8/fuge/fuge-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // run docker containers if false containers with image attribute will not be run 4 | runDocker: true, 5 | 6 | // proxy settings - one of docker | process | all | none 7 | proxy: 'docker', 8 | 9 | // if true tail running process to the shell by default 10 | tail: false, 11 | 12 | // if true monitor running processes for changes by default 13 | monitor: true, 14 | 15 | // exclude these patterns from the monitor 16 | exclude: /node_modules|\.git|\.log/mgi, 17 | 18 | // override section. Allows the default build, run and debug commands 19 | // to be overriden on a service by service basis. These commands are 20 | // normally generated by inspecting the Dockerfile for a service 21 | overrides: { 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /step8/services/actuator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node actuator.js 5 | -------------------------------------------------------------------------------- /step8/services/actuator/actuator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mqtt = require('mqtt').connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 4 | var seneca = require('seneca')(); 5 | 6 | 7 | 8 | seneca.add({role: 'actuate', cmd: 'set'}, function(args, callback) { 9 | var payload = JSON.stringify({'offset': parseInt(args.offset, 10) }); 10 | mqtt.publish('temperature/1/set', new Buffer(payload), {qos: 0, retain: true}, function (err) { 11 | callback(err); 12 | }); 13 | }); 14 | 15 | seneca.listen({port: process.env.actuator_PORT}); 16 | 17 | -------------------------------------------------------------------------------- /step8/services/actuator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actuator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "^1.4.0", 13 | "seneca": "^0.6.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step8/services/broker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node broker.js 5 | -------------------------------------------------------------------------------- /step8/services/broker/broker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mosca = require('mosca'); 4 | var seneca = require('seneca')(); 5 | var server = new mosca.Server({}); 6 | 7 | 8 | seneca.client({host: process.env.PROXY_HOST, port: process.env.serializer_PORT, pin: {role: 'serialize', cmd: 'write'}}); 9 | 10 | 11 | 12 | function parse (body) { 13 | try { 14 | return JSON.parse(body); 15 | } 16 | catch (err) { 17 | return null; 18 | } 19 | } 20 | 21 | 22 | 23 | server.published = function (packet, client, cb) { 24 | var body; 25 | if (packet.topic.match(/temperature\/[0-9]+\/read/)) { 26 | body = parse(packet.payload); 27 | 28 | body.role = 'serialize'; 29 | body.cmd = 'write'; 30 | seneca.act(body, cb); 31 | } 32 | else { 33 | cb(); 34 | } 35 | }; 36 | 37 | -------------------------------------------------------------------------------- /step8/services/broker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "broker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mosca": "^0.31.1", 13 | "seneca": "^0.6.4" 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /step8/services/influx/run.bat: -------------------------------------------------------------------------------- 1 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 2 | -------------------------------------------------------------------------------- /step8/services/influx/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run -d -p 8083:8083 -p 8086:8086 --expose 8090 --expose 8099 tutum/influxdb 3 | -------------------------------------------------------------------------------- /step8/services/sensor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node sensor.js 5 | -------------------------------------------------------------------------------- /step8/services/sensor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "driver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mqtt": "^1.4.0", 13 | "seneca": "^0.6.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step8/services/sensor/sensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mqtt = require('mqtt').connect('mqtt://' + process.env.PROXY_HOST + ':1883'); 4 | var offset = 100; 5 | 6 | 7 | 8 | mqtt.on('connect', function () { 9 | mqtt.subscribe('temperature/1/set', function () { 10 | console.log('subscribed', arguments); 11 | }); 12 | }); 13 | 14 | 15 | 16 | mqtt.on('message', function (topic, payload) { 17 | console.log('message received'); 18 | try { 19 | offset = JSON.parse(payload).offset; 20 | console.log('new offset', offset); 21 | } 22 | catch (err) { 23 | console.log(err); 24 | return; 25 | } 26 | }); 27 | 28 | 29 | 30 | var i = 0; 31 | setInterval(function() { 32 | var randInt = Math.floor(Math.random()*100); 33 | var temp = Math.round((Math.sin(i++ / 40) + 4) * randInt + offset); 34 | 35 | mqtt.publish('temperature/1/read', JSON.stringify({sensorId: '1', temperature: temp}), function(err) { 36 | if (err) { console.log(err); } 37 | }); 38 | }, 2000); 39 | 40 | -------------------------------------------------------------------------------- /step8/services/serializer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4 2 | ADD . / 3 | RUN npm install --ignore-scripts 4 | CMD node serializer.js 5 | 6 | -------------------------------------------------------------------------------- /step8/services/serializer/influxUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(client) { 4 | 5 | var writePoint = function(sensorId, temperature, cb) { 6 | client.writePoint('temperature', {sensorId: sensorId, temperature: temperature}, {}, function(err) { 7 | cb(err); 8 | }); 9 | }; 10 | 11 | 12 | 13 | var readPoints = function(sensorId, start, end, cb) { 14 | client.query('select * from temperature where sensorId=\'' + sensorId + '\' and time > \'' + start + '\' and time < \'' + end + '\'', function(err, data) { 15 | cb(err, data); 16 | }); 17 | }; 18 | 19 | 20 | 21 | return { 22 | writePoint: writePoint, 23 | readPoints: readPoints 24 | }; 25 | }; 26 | 27 | 28 | -------------------------------------------------------------------------------- /step8/services/serializer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serializer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "influx": "^4.0.1", 13 | "seneca": "^0.6.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /step8/services/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var seneca = require('seneca')(); 4 | var influx= require('influx'); 5 | 6 | 7 | 8 | var createDatabase = function(cb) { 9 | setTimeout(function() { 10 | var initDb = influx({host: process.env.PROXY_HOST, username : 'root', password : 'root'}); 11 | initDb.createDatabase('temperature', function(err) { 12 | if (err) { console.log('ERROR: ' + err); } 13 | cb(); 14 | }); 15 | }, 3000); 16 | }; 17 | 18 | 19 | 20 | createDatabase(function() { 21 | var db = influx({host: process.env.PROXY_HOST, username : 'root', password : 'root', database : 'temperature'}); 22 | var ifx = require('./influxUtil')(db); 23 | 24 | seneca.add({role: 'serialize', cmd: 'read'}, function(args, callback) { 25 | ifx.readPoints(args.sensorId, args.start, args.end, function(err, data) { 26 | callback(err, data); 27 | }); 28 | }); 29 | 30 | 31 | seneca.add({role: 'serialize', cmd: 'write'}, function(args, callback) { 32 | ifx.writePoint(args.sensorId, args.temperature, function(err) { 33 | callback(err); 34 | }); 35 | }); 36 | 37 | 38 | seneca.listen({port: process.env.serializer_PORT}); 39 | }); 40 | 41 | 42 | module.exports.seneca = seneca; 43 | 44 | 45 | -------------------------------------------------------------------------------- /step8/services/serializer/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.SERVICE_HOST = 'localhost'; 4 | process.env.SERVICE_PORT = 3001; 5 | process.env.INFLUX_HOST = '192.168.59.103'; 6 | 7 | var assert = require('assert'); 8 | var ser = require('../serializer'); 9 | 10 | describe('read write test', function() { 11 | 12 | it('should write data to influx', function(done){ 13 | ser.seneca.act({role: 'serialize', cmd: 'write', sensorId: '1', temperature: 32}, function(err) { 14 | assert(!err); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /step8/services/serializer/testWrite.bat: -------------------------------------------------------------------------------- 1 | docker-machine ip default > dock 2 | set /p DOCKER_IP= < dock 3 | del dock 4 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 32}" --header "Content-Type:application/json" http://%DOCKER_IP%:10000/act 5 | -------------------------------------------------------------------------------- /step8/services/serializer/testWrite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export DOCKER_IP=$(docker-machine ip default) 3 | curl -X POST -d "{\"role\": \"serialize\", \"cmd\": \"write\", \"sensorId\": \"1\", \"temperature\": 32}" http://$DOCKER_IP:3001/act --header "Content-Type:application/json" 4 | 5 | --------------------------------------------------------------------------------