├── .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 | 
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 |
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 | 
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 |
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 | 
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 |
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 | 
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 |
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 | 
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 |
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 | 
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------