├── .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 [![Docker Hub x86](https://img.shields.io/docker/pulls/cbase/cbeam-telemetry-server.svg)](https://hub.docker.com/r/cbase/cbeam-telemetry-server/) [![Docker Hub Raspberry Pi3](https://img.shields.io/docker/pulls/cbase/raspberrypi3-cbeam-telemetry-server.svg)](https://hub.docker.com/r/cbase/raspberrypi3-cbeam-telemetry-server/) [![Build Status](https://travis-ci.org/c-base/cbeam-telemetry-server.svg?branch=master)](https://travis-ci.org/c-base/cbeam-telemetry-server) [![Greenkeeper badge](https://badges.greenkeeper.io/c-base/cbeam-telemetry-server.svg)](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 | ![Screenshot of OpenMCT with c-beam data](https://pbs.twimg.com/media/CotctAfXYAAKCh0.jpg) 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 | --------------------------------------------------------------------------------