├── .dockerignore
├── .gitignore
├── .travis.yml
├── Dockerfile
├── Dockerfile-raspberrypi3
├── README.md
├── assets
├── dictionary-plugin.js
├── historical-telemetry-plugin.js
├── lib
│ └── http.js
└── realtime-telemetry-plugin.js
├── config
├── bitraf.js
├── c-base.js
└── eva.js
├── docker-compose-raspberrypi3.yml
├── docker-compose.yml
├── package.json
├── patched-openmct-package.json
├── server.sh
├── server
├── app.js
├── cbeam.coffee
├── config.coffee
├── dictionary.coffee
├── history.coffee
└── server.coffee
├── systemd
└── cbeam-telemetry-server.service
└── views
└── index.ejs
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /openmct/
3 | npm-debug.log
4 | package-lock.json
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | dist: trusty
3 | sudo: false
4 | node_js:
5 | - lts/*
6 | services:
7 | - docker
8 | install:
9 | # Install dependencies and build OpenMCT
10 | - npm install
11 | - npm run build
12 | script:
13 | # Copy OpenMCT assets into correct location
14 | - cp -R node_modules/openmct/dist openmct
15 | # Prepare cross-compiling environment if targeting ARM
16 | - if [ "$TARGET" == "raspberrypi3" ]; then docker run --rm --privileged multiarch/qemu-user-static:register --reset; fi
17 | # Build the Docker image
18 | - docker build -t $DOCKER_IMAGE -f $DOCKER_FILE .
19 | # Start the whole service
20 | - docker-compose up -d
21 | deploy:
22 | -
23 | provider: script
24 | script: docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" && docker push "$DOCKER_IMAGE"
25 | on:
26 | branch: master
27 | env:
28 | global:
29 | - secure: ZbH41UJz5qvJqA8nr0ijY4YRT4n23s+j5YS0THUkMtH2AM8S+RIjqLm7ZTi1iGJCzPHh2Pl7NdC9qcZ/f2Cy64ZLRbWaZ46TAkzbevqrOKvDoGNa5cPvx2NYtDGbyxSSrRjo1+nTx4TVkPPYyq6AYZ4Z9GIvIUDoZIo/v4ie5I9hQhjTR6r777AHqLcjWWpAYPkPhqRPHoEldaQu2XV+A4J4/eIBAbUipkDBtBpXg+Xv05mlQz+srWv5BQizYhxlyZaaeFb5BMp2dCofXBvM0daWdS0Iaeceuid/FRe8vyp0AOUEWh/knr0svNY1SI1yauXxfeEbqz0sTrrR/PzGy0l1a5+q69l47vvsXE49n09/Rr0msakUFixnd0XLYEEDzEdrpgWk0BBL0KBrVouCv6l1+5Sxka9juEh81en059dJWaIYUqeiMGbkyYnYdYIT2L0sUMDVFw7vRaP2Z3e+g5fMrS2LhVi5ua6oMQo5qxqqbc9KbG1qfOel0oKYG6vszlTfDHPC46aO95BZdTpxkgz8l7noz74wa30rcSTRiwmEiCN5Cp5MwyJHKkJuWaEj9/eHcTI8HTWoIeiwuTty6E0V5cu0V+cHaCRPS2nZQgefBzVdGIfyyZrVLw2TEvoUmYxh5JZWAyUZus+ZpqoV5eBIWquBe95zajGHd/0C5Nc=
30 | - secure: 3FSWAq3lxmiVWxfBaJ+fzoaYCiJwDViWTTLjP8f3P0qAp27aY4CvG/i0UJ9IHYlnwqfYi/JNkP914RTPVspVUDcVnep0Lpkg0xuuAxqFRGnYy/evC/UprRNVVd1asduGeFSEKfFiT1ka2NsPLEKtsrrKDHQeoCxinsPTbRauVYq37TCSSft0NObmkvSLwZ7nq5+l8x7guL+3xRNA62zSRbXfbhiXsR8QPPZd+885qPSOYcAwvIujXVn5IVyd8Ni4DFUEpizPYgx5+PEekuTbErjHin6qV7VWQT1nvrLqy6B6SEjFI9Hp9QHdD48/1Sp3Obv6WOMVbwA/S5QKNdMUMNwwZ7De+pfqDlH6PQWOnA6N/CEySOYkV20zpwL9EY0hix9R44M6bWYijgJZC9qD6xnDhBwRrNiPacqPPSneTimsQTtMIFOVleWZ+Dm6gw8rCVRitzN+C0iT/JCLQ17oeSSaOtQPOkW6z5uYAVdAv3k0IOesWk/EmEvYStbErDKboLe8MCFtidup9NgCQQ5AWkzSsvVGjWQYCL26jdMMBSOP+aJ0lKoUR3u9JUmaVkkJewb0CJg0xfECnG1OcP2R75xYXi1uK59hCK4fajFQ0W8kWt+eLBN5gNu6yeGViLv9/JJMUimXLiVvUt9sgDxSqX3z3vVERDr3pm+lGvUU1dM=
31 | matrix:
32 | include:
33 | - env: TARGET=x86 DOCKER_IMAGE=cbase/cbeam-telemetry-server DOCKER_FILE=Dockerfile
34 | - env: TARGET=raspberrypi3 DOCKER_IMAGE=cbase/raspberrypi3-cbeam-telemetry-server bower_allow_root=true COMPOSE_FILE=docker-compose-raspberrypi3.yml DOCKER_FILE=Dockerfile-raspberrypi3
35 | sudo: true
36 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8
2 |
3 | # Expose the HTTP port for OpenMCT
4 | EXPOSE 8080
5 | # Export the Websocket port for OpenMCT live telemetry
6 | EXPOSE 8082
7 |
8 | # Reduce npm install verbosity, overflows Travis CI log view
9 | ENV NPM_CONFIG_LOGLEVEL warn
10 | # Install only production dependencies since we're copying OpenMCT from outside
11 | ENV NODE_ENV production
12 |
13 | RUN mkdir -p /var/cbeam-telemetry-server
14 | WORKDIR /var/cbeam-telemetry-server
15 |
16 | COPY . /var/cbeam-telemetry-server
17 |
18 | # Install MsgFlo and dependencies
19 | RUN npm install --only=production
20 | RUN npm run build
21 | RUN cp -rf ./node_modules/openmct/dist ./openmct
22 |
23 | # Set OpenMCT location
24 | ENV OPENMCT_ROOT openmct
25 |
26 | # Map the volumes
27 | VOLUME /var/cbeam-telemetry-server/config
28 |
29 | CMD ./server.sh
30 |
--------------------------------------------------------------------------------
/Dockerfile-raspberrypi3:
--------------------------------------------------------------------------------
1 | FROM resin/raspberrypi3-node:8
2 |
3 | # Expose the HTTP port for OpenMCT
4 | EXPOSE 8080
5 | # Export the Websocket port for OpenMCT live telemetry
6 | EXPOSE 8082
7 |
8 | # Reduce npm install verbosity, overflows Travis CI log view
9 | ENV NPM_CONFIG_LOGLEVEL warn
10 | # Install only production dependencies since we're copying OpenMCT from outside
11 | ENV NODE_ENV production
12 |
13 | RUN mkdir -p /var/cbeam-telemetry-server
14 | WORKDIR /var/cbeam-telemetry-server
15 |
16 | COPY . /var/cbeam-telemetry-server
17 |
18 | # Install MsgFlo and dependencies
19 | RUN npm install --only=production
20 |
21 | # Set OpenMCT location
22 | ENV OPENMCT_ROOT openmct
23 |
24 | # Map the volumes
25 | VOLUME /var/cbeam-telemetry-server/config
26 |
27 | CMD ./server.sh
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | c-beam telemetry server [](https://hub.docker.com/r/cbase/cbeam-telemetry-server/) [](https://hub.docker.com/r/cbase/raspberrypi3-cbeam-telemetry-server/) [](https://travis-ci.org/c-base/cbeam-telemetry-server) [](https://greenkeeper.io/)
2 | =======================
3 |
4 | This is a telemetry server for connecting the NASA [OpenMCT](https://nasa.github.io/openmct/) with information sources on [c-base's](https://c-base.org/) c-beam telemetry network. It is based on the OpenMCT [telemetry adapter tutorial](http://nasa.github.io/openmct/docs/tutorials/#telemetry-adapter).
5 |
6 | 
7 |
8 | ## Installation
9 |
10 | * Install the dependencies with `npm install`
11 | * Build OpenMCT with `npm run build`
12 |
13 | ## Running
14 |
15 | * Start the service with `npm start`
16 |
17 | If you want to change the MQTT broker address, set the `MSGFLO_BROKER` environment variable before starting the service.
18 |
19 | Read more in .
20 |
21 | ## Adding information sources
22 |
23 | c-beam topics are mapped to OpenMCT data in the installation's runner file in `config/`.
24 |
25 | ## TODOs
26 |
27 | * Mapping more c-beam data
28 | * Custom displays combining different data points (like a green/red bar status UI)
29 | * UIs for station functionality
30 |
--------------------------------------------------------------------------------
/assets/dictionary-plugin.js:
--------------------------------------------------------------------------------
1 | function getDictionary(name) {
2 | return http.get('/dictionary/' + name + '.json')
3 | .then(function (result) {
4 | return result.data;
5 | });
6 | }
7 |
8 | var objectProvider = function (config) {
9 | return {
10 | get: function (identifier) {
11 | return getDictionary(config.key).then(function (dictionary) {
12 | if (identifier.key === config.key) {
13 | return {
14 | identifier: identifier,
15 | name: dictionary.name,
16 | type: 'folder',
17 | location: 'ROOT'
18 | };
19 | } else {
20 | var measurement = dictionary.measurements.filter(function (m) {
21 | return m.key === identifier.key;
22 | })[0];
23 | return {
24 | identifier: identifier,
25 | name: measurement.name,
26 | type: config.type,
27 | telemetry: {
28 | values: measurement.values
29 | },
30 | location: config.namespace + ':' + config.key
31 | };
32 | }
33 | });
34 | }
35 | }
36 | };
37 |
38 | var compositionProvider = function (config ) {
39 | return {
40 | appliesTo: function (domainObject) {
41 | return domainObject.identifier.namespace === config.namespace &&
42 | domainObject.type === 'folder';
43 | },
44 | load: function (domainObject) {
45 | return getDictionary(config.key)
46 | .then(function (dictionary) {
47 | return dictionary.measurements.map(function (m) {
48 | return {
49 | namespace: config.namespace,
50 | key: m.key
51 | };
52 | });
53 | });
54 | }
55 | }
56 | };
57 |
58 | var DictionaryPlugin = function (dictionary) {
59 | return function install(openmct) {
60 | openmct.objects.addRoot({
61 | namespace: dictionary.namespace,
62 | key: dictionary.key
63 | });
64 |
65 | openmct.objects.addProvider(dictionary.namespace, objectProvider(dictionary));
66 |
67 | openmct.composition.addProvider(compositionProvider(dictionary));
68 |
69 | openmct.types.addType(dictionary.type, {
70 | name: dictionary.name,
71 | description: dictionary.description,
72 | cssClass: 'icon-telemetry'
73 | });
74 | };
75 | };
76 |
--------------------------------------------------------------------------------
/assets/historical-telemetry-plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Basic historical telemetry plugin.
3 | */
4 |
5 | function HistoricalTelemetryPlugin(namespaces) {
6 | return function install (openmct) {
7 | var provider = {
8 | supportsRequest: function (domainObject) {
9 | if (namespaces.indexOf(domainObject.type) === -1) {
10 | return false;
11 | }
12 | return true;
13 | },
14 | request: function (domainObject, options) {
15 | var url = '/telemetry/' +
16 | domainObject.identifier.key +
17 | '?start=' + options.start +
18 | '&end=' + options.end;
19 |
20 | return http.get(url)
21 | .then(function (resp) {
22 | return resp.data;
23 | });
24 | }
25 | };
26 |
27 | openmct.telemetry.addProvider(provider);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/assets/lib/http.js:
--------------------------------------------------------------------------------
1 | (function (root, factory) {
2 | if (typeof define === 'function' && define.amd) {
3 | define(factory);
4 | } else if (typeof exports === 'object') {
5 | module.exports = factory;
6 | } else {
7 | root.http = factory(root);
8 | }
9 | })(this, function (root) {
10 |
11 | 'use strict';
12 |
13 | var exports = {};
14 |
15 | var generateResponse = function (req) {
16 | var response = {
17 | data: req.responseText,
18 | status: req.status,
19 | request: req
20 | };
21 | var contentType = req.getResponseHeader('Content-Type');
22 | if (contentType && contentType.indexOf('application/json') !== -1) {
23 | response.data = JSON.parse(response.data);
24 | }
25 | return response;
26 | };
27 |
28 | var xhr = function (type, url, data) {
29 | var promise = new Promise(function (resolve, reject) {
30 | var XHR = XMLHttpRequest || ActiveXObject;
31 | var request = new XHR('MSXML2.XMLHTTP.3.0');
32 |
33 | request.open(type, url, true);
34 | request.onreadystatechange = function () {
35 | var req;
36 | if (request.readyState === 4) {
37 | req = generateResponse(request);
38 | if (request.status >= 200 && request.status < 300) {
39 | resolve(req);
40 | } else {
41 | reject(req);
42 | }
43 | }
44 | };
45 | request.send(data);
46 | });
47 | return promise;
48 | };
49 |
50 | exports.get = function (src) {
51 | return xhr('GET', src);
52 | };
53 |
54 | exports.put = function (url, data) {
55 | return xhr('PUT', url, data);
56 | };
57 |
58 | exports.post= function (url, data) {
59 | return xhr('POST', url, data);
60 | };
61 |
62 | exports.delete = function (url) {
63 | return xhr('DELETE', url);
64 | };
65 |
66 | return exports;
67 | });
68 |
--------------------------------------------------------------------------------
/assets/realtime-telemetry-plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Basic Realtime telemetry plugin using websockets.
3 | */
4 | function RealtimeTelemetryPlugin(namespaces) {
5 | return function (openmct) {
6 | var socket = new WebSocket('ws://' + window.location.hostname + ':8082');
7 | var listeners = {};
8 |
9 | socket.onmessage = function (event) {
10 | point = JSON.parse(event.data);
11 | if (listeners[point.id]) {
12 | listeners[point.id].forEach(function (l) {
13 | l(point);
14 | });
15 | }
16 | };
17 |
18 | var provider = {
19 | supportsSubscribe: function (domainObject) {
20 | if (namespaces.indexOf(domainObject.type) === -1) {
21 | return false;
22 | }
23 | return true;
24 | },
25 | subscribe: function (domainObject, callback, options) {
26 | if (!listeners[domainObject.identifier.key]) {
27 | listeners[domainObject.identifier.key] = [];
28 | }
29 | if (!listeners[domainObject.identifier.key].length) {
30 | socket.send('subscribe ' + domainObject.identifier.key);
31 | }
32 | listeners[domainObject.identifier.key].push(callback);
33 | return function () {
34 | listeners[domainObject.identifier.key] =
35 | listeners[domainObject.identifier.key].filter(function (c) {
36 | return c !== callback;
37 | });
38 |
39 | if (!listeners[domainObject.identifier.key].length) {
40 | socket.send('unsubscribe ' + domainObject.identifier.key);
41 | }
42 | };
43 | }
44 | };
45 | openmct.telemetry.addProvider(provider);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/config/bitraf.js:
--------------------------------------------------------------------------------
1 | var app = require('../server/app');
2 |
3 | // Configure Bitraf
4 | var floor2 = new app.Dictionary('2nd floor', 'floor2');
5 | floor2.addMeasurement('temperature', 'floor2_temperature', [
6 | {
7 | units: 'degrees',
8 | format: 'float',
9 | min: 0,
10 | max: 100
11 | }
12 | ], {
13 | topic: 'bitraf/temperature/1',
14 | timeseries: 'Temperature1',
15 | persist: true
16 | });
17 | floor2.addMeasurement('humidity', 'floor2_humidity', [
18 | {
19 | units: 'percentage',
20 | format: 'float',
21 | min: 0,
22 | max: 100
23 | }
24 | ], {
25 | topic: 'bitraf/humidity/1',
26 | timeseries: 'Humidity1',
27 | persist: true
28 | });
29 | floor2.addMeasurement('isopen', 'floor2_is_open', [
30 | {
31 | format: 'boolean'
32 | }
33 | ], {
34 | topic: '/bitraf/door/2floor/isopen',
35 | timeseries: 'Floor2IsOpen',
36 | persist: true
37 | });
38 | floor2.addMeasurement('motion', 'floor2_motion', [
39 | {
40 | format: 'boolean'
41 | }
42 | ], {
43 | topic: 'public/motionsensor/lab/motion',
44 | timeseries: 'Floor2Motion',
45 | persist: true
46 | });
47 | /*
48 | floor2.addMeasurement('current', 'floor2_current', [
49 | {
50 | format: 'boolean'
51 | }
52 | ], {
53 | topic: 'public/currentsensor/current',
54 | timeseries: 'Floor2Current',
55 | persist: true
56 | });
57 | */
58 | var floor3 = new app.Dictionary('3rd floor', 'floor3');
59 | floor3.addMeasurement('window1', 'floor3_window1', [
60 | {
61 | format: 'boolean'
62 | }
63 | ], {
64 | topic: 'public/bitraf/windowsensor/workshop/sensor1',
65 | timeseries: 'Floor3Window1',
66 | persist: true
67 | });
68 | floor3.addMeasurement('window2', 'floor3_window2', [
69 | {
70 | format: 'boolean'
71 | }
72 | ], {
73 | topic: 'public/bitraf/windowsensor/workshop/sensor2',
74 | timeseries: 'Floor3Window2',
75 | persist: true
76 | });
77 | floor3.addMeasurement('window3', 'floor3_window3', [
78 | {
79 | format: 'boolean'
80 | }
81 | ], {
82 | topic: 'public/bitraf/windowsensor/workshop/sensor3',
83 | timeseries: 'Floor3Window3',
84 | persist: true
85 | });
86 | var floor4 = new app.Dictionary('4th floor', 'floor4');
87 | floor4.addMeasurement('temperature', 'floor4_temperature', [
88 | {
89 | units: 'degrees',
90 | format: 'float',
91 | min: 0,
92 | max: 100
93 | }
94 | ], {
95 | topic: 'bitraf/temperature/2/value',
96 | timeseries: 'Temperature2',
97 | persist: true
98 | });
99 | floor4.addMeasurement('humidity', 'floor4_humidity', [
100 | {
101 | units: 'percentage',
102 | format: 'float',
103 | min: 0,
104 | max: 100
105 | }
106 | ], {
107 | topic: 'bitraf/humidity/2/value',
108 | timeseries: 'Humidity2',
109 | persist: true
110 | });
111 | floor4.addMeasurement('isopen', 'floor4_is_open', [
112 | {
113 | format: 'boolean'
114 | }
115 | ], {
116 | topic: '/bitraf/door/4floor/isopen',
117 | timeseries: 'Floor4IsOpen',
118 | persist: true
119 | });
120 | var outside = new app.Dictionary('Outside', 'outside');
121 | outside.addMeasurement('temperature', 'outside_temperature', [
122 | {
123 | units: 'degrees',
124 | format: 'float',
125 | min: 0,
126 | max: 100
127 | }
128 | ], {
129 | topic: 'bitraf/temperature/3/value',
130 | timeseries: 'Temperature3',
131 | persist: true
132 | });
133 | outside.addMeasurement('isopen', 'frontdoor_is_open', [
134 | {
135 | format: 'boolean'
136 | }
137 | ], {
138 | topic: '/bitraf/door/frontdoor/isopen',
139 | timeseries: 'FrontDoorIsOpen',
140 | persist: true
141 | });
142 |
143 | // Start the server
144 | var server = new app.Server({
145 | host: process.env.HOST || 'localhost',
146 | port: process.env.PORT || 8080,
147 | wss_port: process.env.WSS_PORT || 8082,
148 | broker: process.env.MSGFLO_BROKER || 'mqtt://localhost',
149 | dictionaries: [floor2, floor3, floor4, outside],
150 | theme: 'Snow',
151 | timeWindow: 24 * 60 * 60 * 1000,
152 | history: {
153 | host: process.env.INFLUX_HOST || 'localhost',
154 | db: process.env.INFLUX_DB || 'openhab'
155 | }
156 | });
157 | server.start(function (err) {
158 | if (err) {
159 | console.error(err);
160 | process.exit(1);
161 | }
162 | console.log('Server listening in ' + server.config.port);
163 | });
164 |
--------------------------------------------------------------------------------
/config/c-base.js:
--------------------------------------------------------------------------------
1 | var app = require('../server/app');
2 |
3 | // Configure EVA unit 1
4 | var crew = new app.Dictionary('Crew', 'crewtracker');
5 | crew.addMeasurement('members', 'crew.members', [
6 | {
7 | units: 'members',
8 | format: 'integer',
9 | min: 0,
10 | max: 1000
11 | }
12 | ], {
13 | topic: 'c-base/crew/members'
14 | });
15 | crew.addMeasurement('passive', 'crew.passive', [
16 | {
17 | units: 'members',
18 | format: 'integer',
19 | min: 0,
20 | max: 1000
21 | }
22 | ], {
23 | topic: 'c-base/crew/passive'
24 | });
25 | crew.addMeasurement('online', 'crew.online', [
26 | {
27 | units: 'members',
28 | format: 'integer',
29 | min: 0,
30 | max: 100
31 | }
32 | ], {
33 | topic: 'CrewOnline/out'
34 | }, function (online) {
35 | return online.length
36 | });
37 | var bar = new app.Dictionary('Bar', 'bartracker');
38 | bar.addMeasurement('open', 'bar.open', [
39 | {
40 | units: 'barbot',
41 | format: 'boolean',
42 | min: 0,
43 | max: 1
44 | }
45 | ], {
46 | topic: 'bar/state'
47 | }, function (state) {
48 | return state == 'opening' || state == 'open' || state == 'closing';
49 | });
50 | var replicatorValue = function (dict, key, name) {
51 | dict.addMeasurement('replicator_' + name, 'bar.replicator.' + key, [
52 | {
53 | units: 'available',
54 | format: 'boolean',
55 | min: 0,
56 | max: 1
57 | }
58 | ], {
59 | topic: 'Replicator/out'
60 | }, function (state) {
61 | var keys = Object.keys(state).sort();
62 | var stateKey = keys[key - 1];
63 | return state[stateKey];
64 | });
65 | };
66 | replicatorValue(bar, 1, 'clubmate');
67 | replicatorValue(bar, 2, 'berliner1');
68 | replicatorValue(bar, 3, 'berliner2');
69 | replicatorValue(bar, 4, 'flora');
70 | replicatorValue(bar, 5, 'premiumcola');
71 | replicatorValue(bar, 6, 'spezi');
72 | replicatorValue(bar, 7, 'kraftmalz');
73 |
74 | var station = new app.Dictionary('Station', 'stationtracker');
75 | station.addMeasurement('load', 'powermon.load', [
76 | {
77 | units: 'Watts',
78 | format: 'integer',
79 | min: 0,
80 | max: 15000
81 | }
82 | ], {
83 | topic: 'system/powermon/load'
84 | });
85 | station.addMeasurement('load_low', 'powermon.load_low', [
86 | {
87 | units: 'Watts',
88 | format: 'integer',
89 | min: 0,
90 | max: 15000
91 | }
92 | ], {
93 | topic: 'system/powermon/load_low'
94 | });
95 | station.addMeasurement('load_high', 'powermon.load_high', [
96 | {
97 | units: 'Watts',
98 | format: 'integer',
99 | min: 0,
100 | max: 15000
101 | }
102 | ], {
103 | topic: 'system/powermon/load_high'
104 | });
105 | /*
106 | station.addMeasurement('kdg_rx', 'echelon.kdg.rx', [
107 | {
108 | units: 'bytes',
109 | format: 'integer',
110 | min: 0,
111 | max: 2000000
112 | }
113 | ], {
114 | topic: 'system/echelon/traffic'
115 | }, function (traffic) {
116 | return traffic.interfaces[0].rx;
117 | });
118 | station.addMeasurement('kdg_tx', 'echelon.kdg.tx', [
119 | {
120 | units: 'bytes',
121 | format: 'integer',
122 | min: 0,
123 | max: 2000000
124 | }
125 | ], {
126 | topic: 'system/echelon/traffic'
127 | }, function (traffic) {
128 | return traffic.interfaces[0].tx;
129 | });
130 | station.addMeasurement('ipb_rx', 'echelon.ipb.rx', [
131 | {
132 | units: 'bytes',
133 | format: 'integer',
134 | min: 0,
135 | max: 2000000
136 | }
137 | ], {
138 | topic: 'system/echelon/traffic'
139 | }, function (traffic) {
140 | return traffic.interfaces[1].rx;
141 | });
142 | station.addMeasurement('ipb_tx', 'echelon.ipb.tx', [
143 | {
144 | units: 'bytes',
145 | format: 'integer',
146 | min: 0,
147 | max: 2000000
148 | }
149 | ], {
150 | topic: 'system/echelon/traffic'
151 | }, function (traffic) {
152 | return traffic.interfaces[1].tx;
153 | });
154 | */
155 | station.addMeasurement('vacuum', 'device.vacuum', [
156 | {
157 | units: 'on',
158 | format: 'boolean',
159 | min: 0,
160 | max: 1
161 | }
162 | ], {
163 | topic: 'c-base/vacuum/on'
164 | });
165 | station.addMeasurement('disco', 'disco.mainhall', [
166 | {
167 | units: 'on',
168 | format: 'boolean',
169 | min: 0,
170 | max: 1
171 | }
172 | ], {
173 | topic: 'DiscoAnimation/running'
174 | });
175 | station.addMeasurement('mainhall_motion', 'motion.mainhall', [
176 | {
177 | units: 'motion',
178 | format: 'float',
179 | min: 0,
180 | max: 1
181 | }
182 | ], {
183 | topic: 'sensor/mainhallsensor/motion'
184 | });
185 | station.addMeasurement('workshop_motion', 'motion.workshop', [
186 | {
187 | units: 'motion',
188 | format: 'float',
189 | min: 0,
190 | max: 1
191 | }
192 | ], {
193 | topic: 'sensor/workshop/motion'
194 | });
195 | station.addMeasurement('weltenbau_motion', 'motion.weltenbaulab', [
196 | {
197 | units: 'motion',
198 | format: 'float',
199 | min: 0,
200 | max: 1
201 | }
202 | ], {
203 | topic: 'sensor/weltenbausensor/motion'
204 | });
205 | station.addMeasurement('announcement', 'c_out.announcement', [
206 | {
207 | units: 'Message',
208 | format: 'string'
209 | }
210 | ], {
211 | topic: 'c_out/announce_en'
212 | });
213 | station.addMeasurement('fbp', 'c-flo.heartbeat', [
214 | {
215 | units: 'Message',
216 | format: 'string'
217 | }
218 | ], {
219 | topic: 'fbp'
220 | }, function (discovery) {
221 | if (discovery.payload) {
222 | return discovery.payload.role;
223 | }
224 | return 'unknown';
225 | });
226 | var microclimate = new app.Dictionary('Microclimate', 'climatetracker');
227 | microclimate.addMeasurement('mainhall_temperature', 'clima.temperature.mainhall', [
228 | {
229 | units: 'degrees',
230 | format: 'float',
231 | min: 0,
232 | max: 100
233 | }
234 | ], {
235 | topic: 'sensor/mainhallsensor/temperature'
236 | });
237 | microclimate.addMeasurement('mainhall_humidity', 'clima.humidity.mainhall', [
238 | {
239 | units: 'percentage',
240 | format: 'float',
241 | min: 0,
242 | max: 100
243 | }
244 | ], {
245 | topic: 'sensor/mainhallsensor/humidity'
246 | });
247 | microclimate.addMeasurement('mainhall_pm10', 'clima.pm10.mainhall', [
248 | {
249 | units: 'ppm',
250 | format: 'float',
251 | min: 0,
252 | max: 2000
253 | }
254 | ], {
255 | topic: 'staub/mainhall/pm10'
256 | });
257 | microclimate.addMeasurement('mainhall_pm25', 'clima.pm25.mainhall', [
258 | {
259 | units: 'ppm',
260 | format: 'float',
261 | min: 0,
262 | max: 2000
263 | }
264 | ], {
265 | topic: 'staub/mainhall/pm25'
266 | });
267 | microclimate.addMeasurement('workshop_temperature', 'clima.temperature.workshop', [
268 | {
269 | units: 'degrees',
270 | format: 'float',
271 | min: 0,
272 | max: 100
273 | }
274 | ], {
275 | topic: 'sensor/workshop/temperature'
276 | });
277 | microclimate.addMeasurement('workshop_humidity', 'clima.humidity.workshop', [
278 | {
279 | units: 'percentage',
280 | format: 'float',
281 | min: 0,
282 | max: 100
283 | }
284 | ], {
285 | topic: 'sensor/workshop/humidity'
286 | });
287 | microclimate.addMeasurement('workshop_sound', 'clima.sound.workshop', [
288 | {
289 | units: 'degrees',
290 | format: 'int',
291 | min: 0,
292 | max: 255
293 | }
294 | ], {
295 | topic: 'sensor/workshop/sound'
296 | });
297 | station.addMeasurement('workshop_onair', 'onair.workshop', [
298 | {
299 | units: 'onair',
300 | format: 'boolean',
301 | min: 0,
302 | max: 1
303 | }
304 | ], {
305 | topic: 'c-base/werkstattonair/onair'
306 | });
307 | station.addMeasurement('workshop_cnancy_running', 'cnc.workshop.running', [
308 | {
309 | units: 'running',
310 | format: 'boolean',
311 | min: 0,
312 | max: 1
313 | }
314 | ], {
315 | topic: 'werkstatt/c_nancy/milling'
316 | });
317 | microclimate.addMeasurement('weltenbau_temperature', 'clima.temperature.weltenbaulab', [
318 | {
319 | units: 'degrees',
320 | format: 'float',
321 | min: 0,
322 | max: 100
323 | }
324 | ], {
325 | topic: 'sensor/weltenbausensor/temperature'
326 | });
327 | microclimate.addMeasurement('weltenbau_humidity', 'clima.humidity.weltenbaulab', [
328 | {
329 | units: 'percentage',
330 | format: 'float',
331 | min: 0,
332 | max: 100
333 | }
334 | ], {
335 | topic: 'sensor/weltenbausensor/humidity'
336 | });
337 | microclimate.addMeasurement('soundlab_temperature', 'clima.temperature.soundlab', [
338 | {
339 | units: 'degrees',
340 | format: 'float',
341 | min: 0,
342 | max: 100
343 | }
344 | ], {
345 | topic: 'sensor/soundlab/temperature'
346 | });
347 | microclimate.addMeasurement('soundlab_humidity', 'clima.humidity.soundlab', [
348 | {
349 | units: 'percentage',
350 | format: 'float',
351 | min: 0,
352 | max: 100
353 | }
354 | ], {
355 | topic: 'sensor/soundlab/humidity'
356 | });
357 | microclimate.addMeasurement('soundlab_sound', 'clima.sound.soundlab', [
358 | {
359 | units: 'db',
360 | format: 'float',
361 | min: 0,
362 | max: 255
363 | }
364 | ], {
365 | topic: 'sensor/soundlab/sound'
366 | });
367 | microclimate.addMeasurement('c_lab_light', 'clima.light.c_lab', [
368 | {
369 | units: 'lumens',
370 | format: 'int',
371 | min: 0,
372 | max: 255
373 | }
374 | ], {
375 | topic: 'sensor/c-lab/light'
376 | });
377 |
378 | var arboretum = new app.Dictionary('Arboretum', 'arboretumtracker');
379 | arboretum.addMeasurement('leftdoor', 'arboretum.leftdoor', [
380 | {
381 | units: 'opened',
382 | format: 'boolean',
383 | min: 0,
384 | max: 1
385 | }
386 | ], {
387 | topic: 'arboretum/door/leftdooropen'
388 | });
389 | arboretum.addMeasurement('rightdoor', 'arboretum.rightdoor', [
390 | {
391 | units: 'opened',
392 | format: 'boolean',
393 | min: 0,
394 | max: 1
395 | }
396 | ], {
397 | topic: 'arboretum/door/rightdooropen'
398 | });
399 | arboretum.addMeasurement('arboretum_motion', 'motion.arboretum', [
400 | {
401 | units: 'motion',
402 | format: 'float',
403 | min: 0,
404 | max: 1
405 | }
406 | ], {
407 | topic: 'sensor/arboretum/motion'
408 | }, function (motion) {
409 | if (typeof motion === 'boolean') {
410 | if (motion) {
411 | return 1.0;
412 | } else {
413 | return 0.0;
414 | }
415 | }
416 | return motion;
417 | });
418 | arboretum.addMeasurement('arboretum_temperature', 'clima.temperature.arboretum', [
419 | {
420 | units: 'degrees',
421 | format: 'float',
422 | min: 0,
423 | max: 100
424 | }
425 | ], {
426 | topic: 'staub/arboretum/temperature'
427 | });
428 | arboretum.addMeasurement('arboretum_humidity', 'clima.humidity.arboretum', [
429 | {
430 | units: 'percentage',
431 | format: 'float',
432 | min: 0,
433 | max: 100
434 | }
435 | ], {
436 | topic: 'staub/arboretum/humidity'
437 | });
438 | arboretum.addMeasurement('arboretum_sound', 'clima.sound.arboretum', [
439 | {
440 | units: 'db',
441 | format: 'float',
442 | min: 0,
443 | max: 255
444 | }
445 | ], {
446 | topic: 'staub/arboretum/sound'
447 | });
448 | arboretum.addMeasurement('arboretum_pm10', 'clima.pm10.arboretum', [
449 | {
450 | units: 'ppm',
451 | format: 'float',
452 | min: 0,
453 | max: 2000
454 | }
455 | ], {
456 | topic: 'staub/arboretum/pm10'
457 | });
458 | arboretum.addMeasurement('arboretum_pm25', 'clima.pm25.arboretum', [
459 | {
460 | units: 'ppm',
461 | format: 'float',
462 | min: 0,
463 | max: 2000
464 | }
465 | ], {
466 | topic: 'staub/arboretum/pm25'
467 | });
468 | arboretum.addMeasurement('duckbox_pm10', 'clima.pm10.duckbox', [
469 | {
470 | units: 'ppm',
471 | format: 'float',
472 | min: 0,
473 | max: 2000
474 | }
475 | ], {
476 | topic: 'LuftJetzt/pm10'
477 | });
478 | arboretum.addMeasurement('duckbox_o3', 'clima.o3.duckbox', [
479 | {
480 | units: 'ppm',
481 | format: 'float',
482 | min: 0,
483 | max: 2000
484 | }
485 | ], {
486 | topic: 'LuftJetzt/o3'
487 | });
488 | arboretum.addMeasurement('duckbox_no2', 'clima.no2.duckbox', [
489 | {
490 | units: 'ppm',
491 | format: 'float',
492 | min: 0,
493 | max: 2000
494 | }
495 | ], {
496 | topic: 'LuftJetzt/no2'
497 | });
498 | arboretum.addMeasurement('duckbox_so2', 'clima.so2.duckbox', [
499 | {
500 | units: 'ppm',
501 | format: 'float',
502 | min: 0,
503 | max: 2000
504 | }
505 | ], {
506 | topic: 'LuftJetzt/so2'
507 | });
508 | arboretum.addMeasurement('duckbox_co', 'clima.co.duckbox', [
509 | {
510 | units: 'ppm',
511 | format: 'float',
512 | min: 0,
513 | max: 20000
514 | }
515 | ], {
516 | topic: 'LuftJetzt/co'
517 | });
518 | arboretum.addMeasurement('txl_temperature', 'clima.temperature.txl', [
519 | {
520 | units: 'degrees',
521 | format: 'float',
522 | min: 0,
523 | max: 100
524 | }
525 | ], {
526 | topic: 'airportweather.TEMPERATURE'
527 | });
528 | arboretum.addMeasurement('txl_humidity', 'clima.humidity.txl', [
529 | {
530 | units: 'percentage',
531 | format: 'float',
532 | min: 0,
533 | max: 100
534 | }
535 | ], {
536 | topic: 'airportweather.HUMIDITY'
537 | });
538 | arboretum.addMeasurement('txl_pressure', 'clima.pressure.txl', [
539 | {
540 | units: 'hPa',
541 | format: 'float',
542 | min: 0,
543 | max: 1050
544 | }
545 | ], {
546 | topic: 'airportweather.PRESSURE'
547 | });
548 | arboretum.addMeasurement('spree_temperature', 'clima.temperature.spree', [
549 | {
550 | units: 'degrees',
551 | format: 'float',
552 | min: 0,
553 | max: 100
554 | }
555 | ], {
556 | topic: 'c-base/spreesensor/temperature'
557 | });
558 | var ingress = new app.Dictionary('Ingress', 'ingresstracker');
559 | ingress.addMeasurement('cbase', 'ingress.cbase.portal', [
560 | {
561 | units: 'Level',
562 | format: 'integer',
563 | min: -8,
564 | max: 8
565 | }
566 | ], {
567 | topic: 'ingress/status/a6301120831b46f1be00fa2cb0bce195.16'
568 | }, function (state) {
569 | var level = state.level;
570 | if (state.team !== 'RESISTANCE') {
571 | level = level * -1;
572 | }
573 | return level;
574 | });
575 | ingress.addMeasurement('winning', 'ingress.winning', [
576 | {
577 | units: 'Blue',
578 | format: 'boolean',
579 | min: 0,
580 | max: 1
581 | }
582 | ], {
583 | topic: 'ingress-data.FLOOR'
584 | }, function (state) {
585 | if (state[2] > 0) {
586 | return true;
587 | }
588 | return false;
589 | });
590 |
591 | // Start the server
592 | var server = new app.Server({
593 | host: process.env.HOST || 'localhost',
594 | port: process.env.PORT || 8080,
595 | wss_port: process.env.WSS_PORT || 8082,
596 | broker: process.env.MSGFLO_BROKER || 'mqtt://c-beam.cbrp3.c-base.org',
597 | dictionaries: [
598 | bar,
599 | station,
600 | microclimate,
601 | arboretum,
602 | crew,
603 | ingress
604 | ],
605 | history: {
606 | host: process.env.INFLUX_HOST || 'localhost',
607 | db: process.env.INFLUX_DB || 'cbeam'
608 | },
609 | persistence: 'openmct.plugins.CouchDB("http://openmct.cbrp3.c-base.org:5984/openmct")'
610 | });
611 | server.start(function (err) {
612 | if (err) {
613 | console.error(err);
614 | process.exit(1);
615 | }
616 | console.log('Server listening in ' + server.config.port);
617 | });
618 |
--------------------------------------------------------------------------------
/config/eva.js:
--------------------------------------------------------------------------------
1 | var app = require('../server/app');
2 |
3 | // Configure EVA unit 1
4 | var eva = new app.Dictionary('EVA1', 'eva1');
5 | eva.addMeasurement('temperature', 'EVA1.temp', [
6 | {
7 | units: 'degrees',
8 | format: 'integer',
9 | min: 0,
10 | max: 100
11 | }
12 | ]);
13 | eva.addMeasurement('luminosity', 'EVA1.lum', [
14 | {
15 | units: 'points',
16 | format: 'integer',
17 | min: 0,
18 | max: 255
19 | }
20 | ]);
21 | eva.addMeasurement('hall', 'EVA1.hall', [
22 | {
23 | units: 'points',
24 | format: 'integer',
25 | min: 0,
26 | max: 255
27 | }
28 | ]);
29 | eva.addMeasurement('counter', 'EVA1.cnt', [
30 | {
31 | units: 'points',
32 | format: 'integer',
33 | min: 0,
34 | max: 255
35 | }
36 | ]);
37 |
38 | // Start the server
39 | var server = new app.Server({
40 | host: process.env.HOST || 'localhost',
41 | port: process.env.PORT || 8080,
42 | wss_port: process.env.WSS_PORT || 8082,
43 | broker: process.env.MSGFLO_BROKER || 'mqtt://localhost',
44 | dictionaries: [eva],
45 | history: {
46 | host: process.env.INFLUX_HOST || 'localhost',
47 | db: process.env.INFLUX_DB || 'cbeam'
48 | }
49 | });
50 | server.start(function (err) {
51 | if (err) {
52 | console.error(err);
53 | process.exit(1);
54 | }
55 | console.log('Server listening in ' + server.config.port);
56 | });
57 |
--------------------------------------------------------------------------------
/docker-compose-raspberrypi3.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | mqtt:
4 | image: ansi/raspberrypi3-mosquitto
5 | container_name: cbeam-telemetry-server-mosquitto
6 | ports:
7 | - '127.0.0.1:1883:1883'
8 | entrypoint: /usr/local/sbin/mosquitto
9 | influxdb:
10 | image: hypriot/rpi-influxdb
11 | container_name: influxdb
12 | ports:
13 | - '127.0.0.1:8086:8086'
14 | openmct:
15 | image: cbase/raspberrypi3-cbeam-telemetry-server
16 | build:
17 | context: .
18 | dockerfile: Dockerfile-raspberrypi3
19 | container_name: cbeam-telemetry-server
20 | environment:
21 | MSGFLO_BROKER: 'mqtt://mqtt:1883'
22 | INFLUX_HOST: influxdb
23 | OPENMCT_CONFIG: eva.js
24 | ports:
25 | - '127.0.0.1:8080:8080'
26 | - '127.0.0.1:8082:8082'
27 | links:
28 | - influxdb
29 | - mqtt
30 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | mqtt:
4 | image: ansi/mosquitto
5 | container_name: cbeam-telemetry-server-mosquitto
6 | ports:
7 | - '127.0.0.1:1883:1883'
8 | entrypoint: /usr/local/sbin/mosquitto
9 | influxdb:
10 | image: influxdb
11 | container_name: influxdb
12 | ports:
13 | - '127.0.0.1:8086:8086'
14 | openmct:
15 | image: cbase/cbeam-telemetry-server
16 | build: .
17 | container_name: cbeam-telemetry-server
18 | environment:
19 | MSGFLO_BROKER: 'mqtt://mqtt:1883'
20 | INFLUX_HOST: influxdb
21 | OPENMCT_CONFIG: eva.js
22 | ports:
23 | - '127.0.0.1:8080:8080'
24 | - '127.0.0.1:8082:8082'
25 | links:
26 | - influxdb
27 | - mqtt
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cbeam-telemetry-server",
3 | "version": "0.0.1",
4 | "description": "OpenMCT Telemetry Server for c-beam",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "grunt test",
8 | "build": "cp patched-openmct-package.json node_modules/openmct/package.json && cd node_modules/openmct && npm install && npm install --dev && npm run prepare",
9 | "start": "node config/eva.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git://github.com/c-base/cbeam-telemetry-server.git"
14 | },
15 | "keywords": [
16 | "openmct",
17 | "cbase",
18 | "mqtt"
19 | ],
20 | "author": "Henri Bergius ",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/c-base/cbeam-telemetry-server/issues"
24 | },
25 | "homepage": "https://bergie.iki.fi/blog/nasa-openmct-iot-dashboard/",
26 | "dependencies": {
27 | "coffeescript": "^2.3.1",
28 | "cors": "^2.8.4",
29 | "debug": "^3.1.0",
30 | "ejs": "^2.5.6",
31 | "express": "^4.15.3",
32 | "influx": "^5.0.7",
33 | "mqtt": "^2.3.0",
34 | "websocket": "^1.0.26",
35 | "openmct": "git+https://github.com/nasa/openmct.git#v0.13.3"
36 | },
37 | "devDependencies": {
38 | },
39 | "config": {
40 | "unsafe-perm": true
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/patched-openmct-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "_from": "git+https://github.com/nasa/openmct.git#v0.13.3",
3 | "_id": "openmct@0.13.3",
4 | "_inBundle": false,
5 | "_integrity": "",
6 | "_location": "/openmct",
7 | "_phantomChildren": {},
8 | "_requested": {
9 | "type": "git",
10 | "raw": "openmct@git+https://github.com/nasa/openmct.git#v0.13.3",
11 | "name": "openmct",
12 | "escapedName": "openmct",
13 | "rawSpec": "git+https://github.com/nasa/openmct.git#v0.13.3",
14 | "saveSpec": "git+https://github.com/nasa/openmct.git#v0.13.3",
15 | "fetchSpec": "https://github.com/nasa/openmct.git",
16 | "gitCommittish": "v0.13.3"
17 | },
18 | "_requiredBy": [
19 | "/"
20 | ],
21 | "_resolved": "git+https://github.com/nasa/openmct.git#8da6f05825b42e9d85cb82e05a122aa7d75f1846",
22 | "_spec": "openmct@git+https://github.com/nasa/openmct.git#v0.13.3",
23 | "_where": "/var/cbeam-telemetry-server",
24 | "author": "",
25 | "bugs": {
26 | "url": "https://github.com/nasa/openmct/issues"
27 | },
28 | "bundleDependencies": false,
29 | "dependencies": {
30 | "d3-array": "1.2.0",
31 | "d3-axis": "1.0.0",
32 | "d3-collection": "1.0.0",
33 | "d3-color": "1.0.0",
34 | "d3-format": "1.2.0",
35 | "d3-interpolate": "1.1.0",
36 | "d3-scale": "1.0.0",
37 | "d3-selection": "1.3.0",
38 | "d3-time": "1.0.0",
39 | "d3-time-format": "2.1.0",
40 | "express": "^4.13.1",
41 | "minimist": "^1.1.1",
42 | "request": "^2.69.0",
43 | "vue": "^2.5.6"
44 | },
45 | "deprecated": false,
46 | "description": "The Open MCT core platform",
47 | "devDependencies": {
48 | "bower": "^1.7.7",
49 | "git-rev-sync": "^1.4.0",
50 | "glob": ">= 3.0.0",
51 | "gulp": "^3.9.1",
52 | "gulp-jscs": "^3.0.2",
53 | "gulp-jshint": "^2.0.0",
54 | "gulp-jshint-html-reporter": "^0.1.3",
55 | "gulp-rename": "^1.2.2",
56 | "gulp-requirejs-optimize": "^0.3.1",
57 | "gulp-sass": "^3.1.0",
58 | "gulp-sourcemaps": "^1.6.0",
59 | "jasmine-core": "^2.3.0",
60 | "jscs-html-reporter": "^0.1.0",
61 | "jsdoc": "^3.3.2",
62 | "jshint": "^2.7.0",
63 | "karma": "^0.13.3",
64 | "karma-chrome-launcher": "^0.1.12",
65 | "karma-cli": "0.0.4",
66 | "karma-coverage": "^0.5.3",
67 | "karma-html-reporter": "^0.2.7",
68 | "karma-jasmine": "^0.1.5",
69 | "karma-junit-reporter": "^0.3.8",
70 | "karma-requirejs": "^0.2.2",
71 | "lodash": "^3.10.1",
72 | "markdown-toc": "^0.11.7",
73 | "marked": "^0.3.5",
74 | "merge-stream": "^1.0.0",
75 | "mkdirp": "^0.5.1",
76 | "moment": "^2.11.1",
77 | "node-bourbon": "^4.2.3",
78 | "requirejs": "2.1.x",
79 | "split": "^1.0.0",
80 | "v8-compile-cache": "^1.1.0"
81 | },
82 | "homepage": "https://github.com/nasa/openmct#readme",
83 | "license": "Apache-2.0",
84 | "name": "openmct",
85 | "private": true,
86 | "repository": {
87 | "type": "git",
88 | "url": "git+https://github.com/nasa/openmct.git"
89 | },
90 | "scripts": {
91 | "docs": "npm run jsdoc ; npm run otherdoc",
92 | "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
93 | "jshint": "jshint platform example",
94 | "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
95 | "prepare": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install",
96 | "start": "node app.js",
97 | "test": "karma start --single-run",
98 | "watch": "karma start"
99 | },
100 | "version": "0.13.3"
101 | }
--------------------------------------------------------------------------------
/server.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | CONFIGFILE=${OPENMCT_CONFIG:=eva.js}
3 | OPENMCT_ROOT=${OPENMCT_ROOT:=node_modules/openmct/dist}
4 | echo "Starting OpenMCT with config $OPENMCT_CONFIG at $OPENMCT_ROOT"
5 | node config/${OPENMCT_CONFIG}
6 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | require('coffeescript/register');
2 | module.exports = {
3 | Dictionary: require('./dictionary'),
4 | Server: require('./server')
5 | };
6 |
--------------------------------------------------------------------------------
/server/cbeam.coffee:
--------------------------------------------------------------------------------
1 | mqtt = require 'mqtt'
2 |
3 | booleanStates = {}
4 |
5 | exports.latestState = (id) ->
6 | if typeof booleanStates[id] is 'undefined'
7 | return null
8 | booleanStates[id]
9 |
10 | exports.connect = (config, callback) ->
11 | booleanStates = {}
12 | client = mqtt.connect config.broker
13 | client.on 'connect', ->
14 | return unless callback
15 | callback null, client
16 | callback = null
17 | client.on 'error', (err) ->
18 | return unless callback
19 | callback err
20 | callback = null
21 |
22 | unhandled = []
23 |
24 | exports.announce = (client, dictionaries, callback) ->
25 | for dictionary in dictionaries
26 | continue unless dictionary.options.announce
27 | def =
28 | protocol: 'discovery'
29 | command: 'participant'
30 | payload:
31 | id: "openmct-#{dictionary.key}"
32 | component: "openmct/#{dictionary.key}"
33 | icon: dictionary.options.icon
34 | label: "OpenMCT logger for #{dictionary.name}"
35 | role: dictionary.key
36 | inports: []
37 | outports: []
38 | for key, val of dictionary.measurements
39 | def.payload.inports.push
40 | id: val.name
41 | type: val.values[0].format
42 | hidden: val.options.hidden
43 | queue: val.options.topic
44 | client.publish 'fbp', JSON.stringify def
45 | do callback
46 |
47 | exports.filterMessages = (topic, msg, dictionaries, callback) ->
48 | try
49 | value = JSON.parse msg.toString()
50 | catch e
51 | value = msg.toString()
52 | handlers = []
53 | for dictionary in dictionaries
54 | for key, val of dictionary.measurements
55 | continue unless val.options.topic is topic
56 | handlers.push val
57 | unless handlers.length
58 | return callback [] unless unhandled.indexOf(topic) is -1
59 | unhandled.push topic
60 | console.log "Unhandled key #{topic}"
61 | return callback []
62 | points = handlers.map (handler) ->
63 | return message =
64 | id: handler.key
65 | value: handler.callback value
66 | timestamp: Date.now()
67 | seenBooleans = points.filter((point) ->
68 | # We're only interested in boolean values
69 | return false unless typeof point.value is 'boolean'
70 | # We're only interested in booleans we have a previous state for
71 | if typeof booleanStates[point.id] is 'undefined'
72 | booleanStates[point.id] =
73 | value: point.value
74 | timestamp: point.timestamp
75 | return false
76 | # We're only interested in booleans that change state
77 | return false if booleanStates[point.id].value is point.value
78 | true
79 | ).map (point) ->
80 | prevState = booleanStates[point.id].value
81 | booleanStates[point.id] =
82 | value: point.value
83 | timestamp: point.timestamp
84 | return prevPoint =
85 | id: point.id
86 | value: prevState
87 | timestamp: point.timestamp - 1
88 |
89 | callback seenBooleans.concat points
90 |
91 | main = ->
92 | exports.connect (err, client) ->
93 | if err
94 | console.error err
95 | process.exit 1
96 | client.subscribe '#'
97 | client.on 'message', (topic, msg) ->
98 | message = msg.toString()
99 | console.log "#{topic}: #{message}"
100 |
101 | main() unless module.parent
102 |
--------------------------------------------------------------------------------
/server/config.coffee:
--------------------------------------------------------------------------------
1 | module.exports =
2 | host: process.env.HOST or 'localhost'
3 | port: process.env.PORT or 8080
4 | wss_port: process.env.PORT or 8082
5 | broker: process.env.MSGFLO_BROKER or 'mqtt://localhost'
6 | dictionary: './dictionary.json'
7 |
--------------------------------------------------------------------------------
/server/dictionary.coffee:
--------------------------------------------------------------------------------
1 | class Dictionary
2 | constructor: (@name, @key, @options) ->
3 | @measurements = {}
4 | @options = {} unless @options
5 | unless typeof @options.announce is 'boolean'
6 | @options.announce = true
7 | unless @options.icon
8 | @options.icon = 'line-chart'
9 | unless @options.description
10 | @options.description = @name
11 |
12 | addMeasurement: (name, key, values, options, callback) ->
13 | if typeof options is 'function'
14 | callback = options
15 | unless options
16 | options = {}
17 | unless typeof options.persist is 'boolean'
18 | options.persist = true
19 | unless typeof options.hidden is 'boolean'
20 | options.hidden = false
21 | unless options.timeseries
22 | options.timeseries = key
23 | unless options.topic
24 | options.topic = key
25 |
26 | if values.length
27 | values[0].name = 'Value'
28 | values[0].key = 'value'
29 | values[0].hints =
30 | range: 1
31 | if values[0].format and not callback
32 | callback = @formatToCallback values[0].format
33 | unless callback
34 | callback = (val) -> val
35 |
36 | values.push
37 | key: 'utc'
38 | source: 'timestamp'
39 | name: 'Timestamp'
40 | format: 'utc'
41 | hints:
42 | domain: 1
43 |
44 | @measurements[key] =
45 | name: name
46 | key: key
47 | values: values
48 | callback: callback
49 | options: options
50 |
51 | formatToCallback: (format) ->
52 | switch format
53 | when 'integer'
54 | return (val) -> parseInt val
55 | when 'float'
56 | return (val) -> parseFloat val
57 | when 'boolean'
58 | return (val) -> String(val) is 'true'
59 | else
60 | return (val) -> val
61 |
62 | toJSON: ->
63 | def =
64 | name: @name
65 | key: @key
66 | measurements: []
67 | for key, val of @measurements
68 | def.measurements.push
69 | name: val.name
70 | key: val.key
71 | values: val.values
72 | def
73 |
74 | module.exports = Dictionary
75 |
--------------------------------------------------------------------------------
/server/history.coffee:
--------------------------------------------------------------------------------
1 | influx = require 'influx'
2 |
3 | class History
4 | constructor: (@config) ->
5 |
6 | connect: (callback) ->
7 | @client = new influx.InfluxDB
8 | host: @config.history.host
9 | database: @config.history.db
10 | schema: @prepareSchema()
11 | @client.getDatabaseNames()
12 | .then (names) =>
13 | unless names.includes @config.history.db
14 | @client.createDatabase @config.history.db
15 | .then ->
16 | do callback
17 | .catch (e) ->
18 | callback e
19 |
20 | prepareSchema: ->
21 | schema = []
22 | for dictionary in @config.dictionaries
23 | for k, val of dictionary.measurements
24 | table =
25 | measurement: @prepareId k
26 | fields:
27 | value: @toInfluxType val.values[0].format
28 | tags: []
29 | schema.push table
30 | return schema
31 |
32 | toInfluxType: (format) ->
33 | switch format
34 | when 'integer'
35 | return influx.FieldType.INTEGER
36 | when 'float'
37 | return influx.FieldType.FLOAT
38 | when 'boolean'
39 | return influx.FieldType.BOOLEAN
40 | else
41 | return influx.FieldType.STRING
42 |
43 | record: (point, callback) ->
44 | unless @client
45 | return callback new Error 'Not connected to InfluxDB'
46 | measurement = @getMeasurement point.id
47 | unless measurement
48 | return callback new Error "Measurement #{point.id} not defined"
49 | return callback() unless measurement.options.persist
50 | return callback() if Number.isNaN point?.value
51 |
52 | @client.writePoints([
53 | measurement: @prepareId point.id
54 | timestamp: new Date point.timestamp
55 | fields:
56 | value: point.value
57 | ])
58 | .then ->
59 | do callback
60 | .catch (e) ->
61 | callback e
62 |
63 | recordBatch: (points, callback) ->
64 | unless @client
65 | return callback new Error 'Not connected to InfluxDB'
66 | pts = points.map((point) =>
67 | measurement = @getMeasurement point.id
68 | return null unless measurement
69 | return null unless measurement.options.persist
70 | return pt =
71 | measurement: @prepareId point.id
72 | timestamp: new Date point.timestamp
73 | fields:
74 | value: point.value
75 | ).filter (point) ->
76 | return false unless point?.measurement
77 | return false if Number.isNaN point?.fields?.value
78 | true
79 | @client.writePoints(pts)
80 | .then ->
81 | do callback
82 | .catch (e) ->
83 | callback e
84 |
85 | getMeasurement: (key) ->
86 | for dictionary in @config.dictionaries
87 | continue unless dictionary.measurements[key]
88 | return dictionary.measurements[key]
89 | null
90 |
91 | prepareId: (key) ->
92 | measurement = @getMeasurement key
93 | if measurement
94 | id = measurement.options.timeseries
95 | else
96 | id = key
97 | id = id.replace /\./g, '_'
98 | id = id.replace /\//g, '_'
99 | return id
100 |
101 | query: (id, start, end, callback) ->
102 | unless @client
103 | return callback new Error 'Not connected to InfluxDB'
104 | measurement = @getMeasurement id
105 | unless measurement
106 | return callback new Error "Measurement #{id} not available"
107 | startString = new Date(start).toISOString()
108 | endString = new Date(end).toISOString()
109 | query = "
110 | select value from #{@prepareId(id)}
111 | where time > '#{startString}' and time < '#{endString}'
112 | order by time asc;
113 | "
114 | @client.query(query)
115 | .then (result) ->
116 | points = result.map (r) ->
117 | return res =
118 | id: id
119 | value: r.value
120 | timestamp: new Date(r.time).getTime()
121 | callback null, points
122 | .catch (e) ->
123 | callback e
124 |
125 | module.exports = History
126 |
--------------------------------------------------------------------------------
/server/server.coffee:
--------------------------------------------------------------------------------
1 | cbeam = require './cbeam'
2 | history = require './history'
3 | express = require 'express'
4 | cors = require 'cors'
5 | http = require 'http'
6 | ws = require 'websocket'
7 |
8 | class Server
9 | listeners: []
10 | points: []
11 | chunkSaver: null
12 | constructor: (@config) ->
13 | @config.theme = 'Espresso' unless @config.theme
14 | @config.timeWindow = 24 * 60 * 60 * 1000 unless @config.timeWindow
15 | unless @config.persistence
16 | @config.persistence = 'openmct.plugins.LocalStorage()'
17 | unless @config.openmctRoot
18 | @config.openmctRoot = process.env.OPENMCT_ROOT or 'node_modules/openmct/dist'
19 | @history = new history @config
20 | @app = express()
21 |
22 | @app.use express.static 'assets'
23 | @app.use "/#{@config.openmctRoot}", express.static @config.openmctRoot
24 | @app.set 'view engine', 'ejs'
25 | @app.get '/', (req, res) =>
26 | res.render 'index', @config
27 | @app.get '/index.html', (req, res) =>
28 | res.render 'index', @config
29 | @app.get '/dictionary/:dict.json', (req, res) =>
30 | for dict in @config.dictionaries
31 | if dict.key is req.params.dict
32 | res.json dict.toJSON()
33 | return
34 | res.status(404).end()
35 | @app.get '/telemetry/latest/:pointId', cors(), (req, res) =>
36 | unless @history.getMeasurement req.params.pointId
37 | res.status(404).end()
38 | return
39 | if req.query.timestamp
40 | res.json cbeam.latestState req.params.pointId
41 | return
42 | state = cbeam.latestState req.params.pointId
43 | if typeof state?.value isnt 'undefined'
44 | res.json state.value
45 | return
46 | res.json null
47 | @app.get '/telemetry/:pointId', cors(), (req, res) =>
48 | start = parseInt req.query.start
49 | end = parseInt req.query.end
50 | ids = req.params.pointId.split ','
51 | @history.query ids[0], start, end, (err, response) ->
52 | if err
53 | console.log err
54 | res.status(500).end()
55 | return
56 | res.json response
57 | return
58 |
59 | start: (callback) ->
60 | @wssServer = http.createServer (req, res) ->
61 | res.writeHead 404
62 | res.end()
63 | @wssServer.listen @config.wss_port, (err) =>
64 | @wss = new ws.server
65 | httpServer: @wssServer
66 | autoAcceptConnections: false
67 | @app.listen @config.port, (err) =>
68 | return callback err if err
69 | cbeam.connect @config, (err, client) =>
70 | return callback err if err
71 | @history.connect (err) =>
72 | return callback err if err
73 | @cbeam = client
74 | do @subscribe
75 | cbeam.announce @cbeam, @config.dictionaries, callback
76 | setInterval =>
77 | cbeam.announce @cbeam, @config.dictionaries, ->
78 | , 30000
79 |
80 | subscribe: ->
81 | # Subscribe to all messages
82 | @cbeam.subscribe '#'
83 | @cbeam.on 'message', (topic, msg) =>
84 | cbeam.filterMessages topic, msg, @config.dictionaries, (points) =>
85 | return unless points.length
86 | for point in points
87 | @points.push point
88 | @listeners.forEach (listener) ->
89 | listener point
90 | unless @chunkSaver
91 | @chunkSaver = setTimeout =>
92 | savePoints = @points.slice 0
93 | @history.recordBatch savePoints, (err) ->
94 | if err
95 | console.log err
96 | # Save failed, put the failed data points back to the list
97 | @points = savePoints.concat @points
98 | @points = []
99 | @chunkSaver = null
100 | , 10000
101 | @wss.on 'request', (request) =>
102 | socket = request.accept null, request.origin
103 | exports.handleConnection @, socket
104 |
105 | exports.handleConnection = (server, socket) ->
106 | # Topics subscribed by this connection
107 | subscriptions = {}
108 |
109 | handlers =
110 | subscribe: (id) ->
111 | subscriptions[id] = true
112 | unsubscribe: (id) ->
113 | delete subscriptions[id]
114 |
115 | notify = (msg) ->
116 | for id, value of subscriptions
117 | continue unless msg.id is id
118 | socket.sendUTF JSON.stringify msg
119 |
120 | # Listen for requests
121 | socket.on 'message', (message) ->
122 | parts = message.utf8Data.split ' '
123 | handler = handlers[parts[0]]
124 | unless handler
125 | console.log "No handler for #{parts[0]}"
126 | return
127 | handler.apply handlers, parts.slice 1
128 |
129 | # Remove subscription when connection closes
130 | socket.on 'close', ->
131 | server.listeners = server.listeners.filter (l) ->
132 | l isnt notify
133 | socket.on 'error', ->
134 | server.listeners = server.listeners.filter (l) ->
135 | l isnt notify
136 |
137 | # Register listener
138 | server.listeners.push notify
139 |
140 | main = (config) ->
141 | exports.initialize config, (err, server) ->
142 | if err
143 | console.error err
144 | process.exit 1
145 | console.log "c-beam telemetry server running on port #{config.port}"
146 | console.log "c-beam telemetry provider running on port #{config.wss_port}"
147 | console.log "Open c-beam telemetry server at http://#{config.host}:#{config.port}"
148 |
149 |
150 | module.exports = Server
151 |
--------------------------------------------------------------------------------
/systemd/cbeam-telemetry-server.service:
--------------------------------------------------------------------------------
1 | [Service]
2 | WorkingDirectory=/opt/cbeam-telemetry-server
3 | ExecStart=/usr/bin/node ./config/c-base.js
4 | Restart=always
5 | User=bergie
6 | Environment=PORT=80
7 |
8 | [Unit]
9 | After=network-online.target influxd.service couchdb.service ntpd.service
10 |
11 | [Install]
12 | WantedBy=multi-user.target
13 |
--------------------------------------------------------------------------------
/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | c-beam telemetry server
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
17 |
62 |
63 |
64 |
--------------------------------------------------------------------------------