From 1b17c88f1fd76edcb17ccab341c72fc9464aa359 Mon Sep 17 00:00:00 2001
From: Michael Joyce <Michael.J.Joyce@jpl.nasa.gov>
Date: Thu, 27 Sep 2018 16:29:11 -0700
Subject: [PATCH] Issue #88 - Add support for integration with OpenMCT

---
 ait/gui/__init__.py                           |  43 ++++
 ait/gui/openmct_adapter/ait_integration.js    | 188 ++++++++++++++++++
 .../example_ait_telem_fetch.js                |  76 +++++++
 3 files changed, 307 insertions(+)
 create mode 100644 ait/gui/openmct_adapter/ait_integration.js
 create mode 100644 ait/gui/openmct_adapter/example_ait_telem_fetch.js

diff --git a/ait/gui/__init__.py b/ait/gui/__init__.py
index 774c7a74..ece8c1c8 100644
--- a/ait/gui/__init__.py
+++ b/ait/gui/__init__.py
@@ -732,6 +732,49 @@ def handle():
             __setResponseToEventStream()
             yield 'data: %s\n\n' % json.dumps(msg)
 
+@App.route('/tlm/realtime/openmct')
+def handle():
+    """Return telemetry packets in realtime to client"""
+    session = Sessions.create()
+    pad   = bytearray(1)
+    wsock = bottle.request.environ.get('wsgi.websocket')
+
+    if not wsock:
+        bottle.abort(400, 'Expected WebSocket request.')
+
+    try:
+        tlmdict = ait.core.tlm.getDefaultDict()
+        while not wsock.closed:
+            try:
+                uid, data = session.telemetry.popleft(timeout=30)
+                pkt_defn = None
+                for k, v in tlmdict.iteritems():
+                    if v.uid == uid:
+                        pkt_defn = v
+                        break
+                else:
+                    continue
+
+                wsock.send(json.dumps({
+                    'packet': pkt_defn.name,
+                    'data': ait.core.tlm.Packet(pkt_defn, data=data).toJSON()
+                }))
+
+            except IndexError:
+                # If no telemetry has been received by the GUI
+                # server after timeout seconds, "probe" the client
+                # websocket connection to make sure it's still
+                # active and if so, keep it alive.  This is
+                # accomplished by sending a packet with an ID of
+                # zero and no packet data.  Packet ID zero with no
+                # data is ignored by AIT GUI client-side
+                # Javascript code.
+
+                if not wsock.closed:
+                    wsock.send(pad + struct.pack('>I', 0))
+    except geventwebsocket.WebSocketError:
+        pass
+
 
 @App.route('/tlm/realtime')
 def handle():
diff --git a/ait/gui/openmct_adapter/ait_integration.js b/ait/gui/openmct_adapter/ait_integration.js
new file mode 100644
index 00000000..f7fb837a
--- /dev/null
+++ b/ait/gui/openmct_adapter/ait_integration.js
@@ -0,0 +1,188 @@
+/*
+ * Advanced Multi-Mission Operations System (AMMOS) Instrument Toolkit (AIT)
+ * Bespoke Link to Instruments and Small Satellites (BLISS)
+ *
+ * Copyright 2018, by the California Institute of Technology. ALL RIGHTS
+ * RESERVED. United States Government Sponsorship acknowledged. Any
+ * commercial use must be negotiated with the Office of Technology Transfer
+ * at the California Institute of Technology.
+ *
+ * This software may be subject to U.S. export control laws. By accepting
+ * this software, the user agrees to comply with all applicable U.S. export
+ * laws and regulations. User has the responsibility to obtain export licenses,
+ * or other export authority as may be required before exporting such
+ * information to foreign countries or providing access to foreign persons.
+ */
+
+/*
+ * Example functions for AIT integration with OpenMCT. The below code can be
+ * used in place of the examples provided in the OpenMCT Tutorial so that
+ * AIT telemetry can be viewed in real time.
+ *
+ * https://github.com/nasa/openmct-tutorial
+ *
+ * Important Notes and Setup:
+ *  - The current AIT backend implementation does not support the OpenMCT
+ *    historical data query implementation. To work around this, ensure
+ *    that the historical data endpoint in OpenMCT returns an empty
+ *    dataset for any queried point. The AITHistoricalTelemetryPlugin
+ *    function calls the default OpenMCT Tutorial historical endpoint.
+ *  - The below example code queries for the AIT telemetry dictionary
+ *    in the getDictionary call so the example implementation follows along
+ *    with the OpenMCT tutorial. The endpoint that it calls can be added
+ *    to the example OpenMCT server file (server.js). The code for this
+ *    can be found in example_ait_telem_fetch.js.
+ *  - Include the below code into OpenMCT by including this file in the
+ *    example index.html file and installing the various components.
+ *
+ *    <script src="ait_integration.js"></script>
+ *
+ *    openmct.install(AITIntegration());
+ *    openmct.install(AITHistoricalTelemetryPlugin());
+ *    openmct.install(AITRealtimeTelemetryPlugin());
+ *
+ * How to run:
+ *  Assuming the above "Important Notes and Setup" have been handled and
+ *  the installation instructions in the OpenMCT Tutorial have been run:
+ *
+ *  1) Run `ait-gui 8081` to start the AIT backend
+ *  2) Run `npm start` to run the OpenMCT server
+ *  3) Point your browser to localhost:8080
+ */
+
+let AIT_HOST = 'localhost'
+let AIT_PORT = 8081
+let tlmdict = getDictionary()
+
+function getDictionary() {
+    return http.get('/telemdict')
+    .then(function (result) {
+        return result.data
+    });
+};
+
+function AITIntegration() {
+    let objectProvider = {
+        get: function (identifier) {
+            return tlmdict.then(function (dictionary) {
+                if (identifier.key === 'spacecraft') {
+                    return {
+                        identifier: identifier,
+                        name: dictionary.name,
+                        type: 'folder',
+                        location: 'ROOT'
+                    };
+                } else {
+                    let measurement = dictionary.measurements.filter(function (m) {
+                        return m.key === identifier.key;
+                    })[0];
+                    return {
+                        identifier: identifier,
+                        name: measurement.name,
+                        type: 'telemetry',
+                        telemetry: {
+                            values: measurement.values
+                        },
+                        location: 'taxonomy:spacecraft'
+                    };
+                }
+            });
+        }
+    };
+
+    let compositionProvider = {
+        appliesTo: function (domainObject) {
+            return domainObject.identifier.namespace === 'taxonomy' &&
+                   domainObject.type === 'folder';
+        },
+        load: function (domainObject) {
+            return tlmdict
+                .then(function (dictionary) {
+                    return dictionary.measurements.map(function (m) {
+                        return {
+                            namespace: 'taxonomy',
+                            key: m.key
+                        };
+                    });
+                });
+        }
+    };
+
+    return function install(openmct) {
+        openmct.objects.addRoot({
+            namespace: 'taxonomy',
+            key: 'spacecraft'
+        });
+    
+        openmct.objects.addProvider('taxonomy', objectProvider);
+    
+        openmct.composition.addProvider(compositionProvider);
+    
+        openmct.types.addType('telemetry', {
+            name: 'Telemetry Point',
+            description: 'Spacecraft Telemetry point',
+            cssClass: 'icon-telemetry'
+        });
+    };
+};
+
+function AITHistoricalTelemetryPlugin() {
+    return function install (openmct) {
+        let provider = {
+            supportsRequest: function (domainObject) {
+                return domainObject.type === 'telemetry';
+            },
+            request: function (domainObject, options) {
+                let url = '/history/' + domainObject.identifier.key +
+                    '?start=' + options.start + '&end=' + options.end;
+    
+                return http.get(url)
+                    .then(function (resp) {
+                        return resp.data
+                    });
+            }
+        };
+    
+        openmct.telemetry.addProvider(provider);
+    }
+};
+
+function AITRealtimeTelemetryPlugin() {
+    return function install(openmct) {
+        let socket = new WebSocket(
+            'ws://' + AIT_HOST + ':' + AIT_PORT + '/tlm/realtime/openmct');
+        let listener = {};
+    
+        socket.onmessage = function (event) {
+            let now = Date.now()
+
+            let data = JSON.parse(event.data)
+            let packet = data['packet']
+            for (let p in data['data'])  {
+                let point = {
+                    'id': packet + '.' + p,
+                    'timestamp': Date.now(),
+                    'value': data['data'][p]
+                }
+
+                if (listener[point.id]) {
+                    listener[point.id](point);
+                }
+            }
+        };
+        
+        let provider = {
+            supportsSubscribe: function (domainObject) {
+                return domainObject.type === 'telemetry';
+            },
+            subscribe: function (domainObject, callback) {
+                listener[domainObject.identifier.key] = callback;
+                return function unsubscribe() {
+                    delete listener[domainObject.identifier.key];
+                };
+            }
+        };
+        
+        openmct.telemetry.addProvider(provider);
+    }
+};
diff --git a/ait/gui/openmct_adapter/example_ait_telem_fetch.js b/ait/gui/openmct_adapter/example_ait_telem_fetch.js
new file mode 100644
index 00000000..01913f92
--- /dev/null
+++ b/ait/gui/openmct_adapter/example_ait_telem_fetch.js
@@ -0,0 +1,76 @@
+/*
+ * Advanced Multi-Mission Operations System (AMMOS) Instrument Toolkit (AIT)
+ * Bespoke Link to Instruments and Small Satellites (BLISS)
+ *
+ * Copyright 2018, by the California Institute of Technology. ALL RIGHTS
+ * RESERVED. United States Government Sponsorship acknowledged. Any
+ * commercial use must be negotiated with the Office of Technology Transfer
+ * at the California Institute of Technology.
+ *
+ * This software may be subject to U.S. export control laws. By accepting
+ * this software, the user agrees to comply with all applicable U.S. export
+ * laws and regulations. User has the responsibility to obtain export licenses,
+ * or other export authority as may be required before exporting such
+ * information to foreign countries or providing access to foreign persons.
+ */
+
+let AIT_HOST = 'localhost'
+let AIT_PORT = 8081
+
+function AITTlmDict(body) {
+    let retJSON = JSON.parse(body);
+    let dict = {}
+    dict['name'] = 'AIT Telemetry'
+    dict['key'] = 'ait_telemetry_dictionary'
+    dict['measurements'] = []
+
+    Object.keys(retJSON).forEach((packet) => {
+        let fieldDefns = retJSON[packet]["fields"]
+        Object.keys(fieldDefns).forEach((key) => {
+            let fDict = {
+                'key': packet + '.' + key,
+                'name': packet + ' ' + fieldDefns[key]['name']
+            }
+
+            let vals = [
+                {
+                    "key": "value",
+                    "name": "Value",
+                    "hints": {
+                        "range": 1
+                    }
+                },
+                {
+                    "key": "utc",
+                    "source": "timestamp",
+                    "name": "Timestamp",
+                    "format": "utc",
+                    "hints": {
+                        "domain": 1
+                    }
+                }
+            ]
+            fDict['values'] = vals
+            dict['measurements'].push(fDict)
+        })
+    })
+    return dict
+};
+
+app.get('/telemdict', function (req, res) {
+        let url = 'http://' + AIT_HOST + ':' + AIT_PORT + '/tlm/dict'
+        http.get(url, function(response){
+            var body = '';
+
+            response.on('data', function(chunk){
+                body += chunk;
+            });
+
+            response.on('end', function() {
+                let dict = AITTlmDict(body)
+                res.json(dict)
+            });
+        }).on('error', function(e){
+              console.log("Got an error: ", e);
+        });
+});